refactor(web): webauthn references (#5244)

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/5246/head
James Elliott 2023-04-15 02:54:24 +10:00 committed by GitHub
parent 2733fc040c
commit 370585d1de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 133 additions and 71 deletions

View File

@ -52,7 +52,7 @@ func TestWebAuthnGetUser(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, user)
assert.Equal(t, []byte("john"), user.WebAuthnID())
assert.Equal(t, []byte{}, user.WebAuthnID())
assert.Equal(t, "john", user.WebAuthnName())
assert.Equal(t, "john", user.Username)
@ -109,7 +109,7 @@ func TestWebAuthnGetUserWithoutDisplayName(t *testing.T) {
ctx.StorageMock.EXPECT().LoadWebAuthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{
{
ID: 1,
RPID: "https://example.com",
RPID: "example.com",
Username: "john",
Description: "Primary",
KID: model.NewBase64([]byte("abc123")),

View File

@ -1,10 +0,0 @@
package model
// U2FDevice represents a users U2F device row in the database.
type U2FDevice struct {
ID int `db:"id"`
Username string `db:"username"`
Description string `db:"description"`
KeyHandle []byte `db:"key_handle"`
PublicKey []byte `db:"public_key"`
}

View File

@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/base64"
"encoding/hex"
"encoding/json"
"strings"
"time"
@ -19,9 +20,13 @@ const (
// WebAuthnUser is an object to represent a user for the WebAuthn lib.
type WebAuthnUser struct {
Username string
DisplayName string
Devices []WebAuthnDevice
ID int `db:"id"`
RPID string `db:"rpid"`
Username string `db:"username"`
UserID string `db:"userid"`
DisplayName string `db:"-"`
Devices []WebAuthnDevice `db:"-"`
}
// HasFIDOU2F returns true if the user has any attestation type `fido-u2f` devices.
@ -37,7 +42,7 @@ func (w WebAuthnUser) HasFIDOU2F() bool {
// WebAuthnID implements the webauthn.User interface.
func (w WebAuthnUser) WebAuthnID() []byte {
return []byte(w.Username)
return []byte(w.UserID)
}
// WebAuthnName implements the webauthn.User interface.
@ -122,11 +127,11 @@ func NewWebAuthnDeviceFromCredential(rpid, username, description string, credent
CreatedAt: time.Now(),
Description: description,
KID: NewBase64(credential.ID),
PublicKey: credential.PublicKey,
AttestationType: credential.AttestationType,
Transport: strings.Join(transport, ","),
SignCount: credential.Authenticator.SignCount,
CloneWarning: credential.Authenticator.CloneWarning,
Transport: strings.Join(transport, ","),
PublicKey: credential.PublicKey,
}
aaguid, err := uuid.Parse(hex.EncodeToString(credential.Authenticator.AAGUID))
@ -146,12 +151,12 @@ type WebAuthnDevice struct {
Username string `db:"username"`
Description string `db:"description"`
KID Base64 `db:"kid"`
PublicKey []byte `db:"public_key"`
AAGUID uuid.NullUUID `db:"aaguid"`
AttestationType string `db:"attestation_type"`
Transport string `db:"transport"`
AAGUID uuid.NullUUID `db:"aaguid"`
SignCount uint32 `db:"sign_count"`
CloneWarning bool `db:"clone_warning"`
PublicKey []byte `db:"public_key"`
}
// UpdateSignInInfo adjusts the values of the WebAuthnDevice after a sign in.
@ -172,8 +177,8 @@ func (d *WebAuthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time
}
}
// LastUsed provides LastUsedAt as a *time.Time instead of sql.NullTime.
func (d *WebAuthnDevice) LastUsed() *time.Time {
// DataValueLastUsedAt provides LastUsedAt as a *time.Time instead of sql.NullTime.
func (d *WebAuthnDevice) DataValueLastUsedAt() *time.Time {
if d.LastUsedAt.Valid {
value := time.Unix(d.LastUsedAt.Time.Unix(), int64(d.LastUsedAt.Time.Nanosecond()))
@ -183,22 +188,43 @@ func (d *WebAuthnDevice) LastUsed() *time.Time {
return nil
}
// ToData converts this WebAuthnDevice into the data format for exporting etc.
// DataValueAAGUID provides AAGUID as a *string instead of uuid.NullUUID.
func (d *WebAuthnDevice) DataValueAAGUID() *string {
if d.AAGUID.Valid {
value := d.AAGUID.UUID.String()
return &value
}
return nil
}
func (d *WebAuthnDevice) ToData() WebAuthnDeviceData {
return WebAuthnDeviceData{
o := WebAuthnDeviceData{
ID: d.ID,
CreatedAt: d.CreatedAt,
LastUsedAt: d.LastUsed(),
LastUsedAt: d.DataValueLastUsedAt(),
RPID: d.RPID,
Username: d.Username,
Description: d.Description,
KID: d.KID.String(),
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
AAGUID: d.DataValueAAGUID(),
AttestationType: d.AttestationType,
Transport: d.Transport,
AAGUID: d.AAGUID.UUID.String(),
SignCount: d.SignCount,
CloneWarning: d.CloneWarning,
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
}
if d.Transport != "" {
o.Transports = strings.Split(d.Transport, ",")
}
return o
}
// MarshalJSON returns the WebAuthnDevice in a JSON friendly manner.
func (d *WebAuthnDevice) MarshalJSON() (data []byte, err error) {
return json.Marshal(d.ToData())
}
// MarshalYAML marshals this model into YAML.
@ -220,12 +246,14 @@ func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
var aaguid uuid.UUID
if aaguid, err = uuid.Parse(o.AAGUID); err != nil {
return err
}
if o.AAGUID != nil {
if aaguid, err = uuid.Parse(*o.AAGUID); err != nil {
return err
}
if aaguid.ID() != 0 {
d.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
if aaguid.ID() != 0 {
d.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
}
}
var kid []byte
@ -241,7 +269,7 @@ func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
d.Username = o.Username
d.Description = o.Description
d.AttestationType = o.AttestationType
d.Transport = o.Transport
d.Transport = strings.Join(o.Transports, ",")
d.SignCount = o.SignCount
d.CloneWarning = o.CloneWarning
@ -254,18 +282,62 @@ func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
// WebAuthnDeviceData represents a WebAuthn Device in the database storage.
type WebAuthnDeviceData struct {
CreatedAt time.Time `yaml:"created_at"`
LastUsedAt *time.Time `yaml:"last_used_at"`
RPID string `yaml:"rpid"`
Username string `yaml:"username"`
Description string `yaml:"description"`
KID string `yaml:"kid"`
PublicKey string `yaml:"public_key"`
AttestationType string `yaml:"attestation_type"`
Transport string `yaml:"transport"`
AAGUID string `yaml:"aaguid"`
SignCount uint32 `yaml:"sign_count"`
CloneWarning bool `yaml:"clone_warning"`
ID int `json:"id" yaml:"-"`
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty" yaml:"last_used_at,omitempty"`
RPID string `json:"rpid" yaml:"rpid"`
Username string `json:"-" yaml:"username"`
Description string `json:"description" yaml:"description"`
KID string `json:"kid" yaml:"kid"`
AAGUID *string `json:"aaguid,omitempty" yaml:"aaguid,omitempty"`
AttestationType string `json:"attestation_type" yaml:"attestation_type"`
Transports []string `json:"transports" yaml:"transports"`
SignCount uint32 `json:"sign_count" yaml:"sign_count"`
CloneWarning bool `json:"clone_warning" yaml:"clone_warning"`
PublicKey string `json:"public_key" yaml:"public_key"`
}
func (d *WebAuthnDeviceData) ToDevice() (device *WebAuthnDevice, err error) {
device = &WebAuthnDevice{
CreatedAt: d.CreatedAt,
RPID: d.RPID,
Username: d.Username,
Description: d.Description,
AttestationType: d.AttestationType,
Transport: strings.Join(d.Transports, ","),
SignCount: d.SignCount,
CloneWarning: d.CloneWarning,
}
if device.PublicKey, err = base64.StdEncoding.DecodeString(d.PublicKey); err != nil {
return nil, err
}
var aaguid uuid.UUID
if d.AAGUID != nil {
if aaguid, err = uuid.Parse(*d.AAGUID); err != nil {
return nil, err
}
if aaguid.ID() != 0 {
device.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
}
}
var kid []byte
if kid, err = base64.StdEncoding.DecodeString(d.KID); err != nil {
return nil, err
}
device.KID = NewBase64(kid)
if d.LastUsedAt != nil {
device.LastUsedAt = sql.NullTime{Valid: true, Time: *d.LastUsedAt}
}
return device, nil
}
// WebAuthnDeviceExport represents a WebAuthnDevice export file.

View File

@ -1,5 +1,5 @@
export enum SecondFactorMethod {
TOTP = 1,
Webauthn,
WebAuthn,
MobilePush,
}

View File

@ -89,7 +89,7 @@ export enum AttestationResult {
FailureSyntax,
FailureSupport,
FailureUnknown,
FailureWebauthnNotSupported,
FailureWebAuthnNotSupported,
FailureToken,
}
@ -111,7 +111,7 @@ export enum AssertionResult {
FailureSyntax,
FailureUnknown,
FailureUnknownSecurity,
FailureWebauthnNotSupported,
FailureWebAuthnNotSupported,
FailureChallenge,
}

View File

@ -22,7 +22,7 @@ export function toEnum(method: Method2FA): SecondFactorMethod {
case "totp":
return SecondFactorMethod.TOTP;
case "webauthn":
return SecondFactorMethod.Webauthn;
return SecondFactorMethod.WebAuthn;
case "mobile_push":
return SecondFactorMethod.MobilePush;
}
@ -32,7 +32,7 @@ export function toString(method: SecondFactorMethod): Method2FA {
switch (method) {
case SecondFactorMethod.TOTP:
return "totp";
case SecondFactorMethod.Webauthn:
case SecondFactorMethod.WebAuthn:
return "webauthn";
case SecondFactorMethod.MobilePush:
return "mobile_push";

View File

@ -16,7 +16,7 @@ import {
PublicKeyCredentialJSON,
PublicKeyCredentialRequestOptionsJSON,
PublicKeyCredentialRequestOptionsStatus,
} from "@models/Webauthn";
} from "@models/WebAuthn";
import {
OptionalDataServiceResponse,
ServiceResponse,

View File

@ -9,9 +9,9 @@ import { IdentityToken } from "@constants/SearchParams";
import { useNotifications } from "@hooks/NotificationsContext";
import { useQueryParam } from "@hooks/QueryParam";
import LoginLayout from "@layouts/LoginLayout";
import { AttestationResult } from "@models/Webauthn";
import { AttestationResult } from "@models/WebAuthn";
import { FirstFactorPath } from "@services/Api";
import { performAttestationCeremony } from "@services/Webauthn";
import { performAttestationCeremony } from "@services/WebAuthn";
const RegisterWebAuthn = function () {
const styles = useStyles();
@ -53,7 +53,7 @@ const RegisterWebAuthn = function () {
"The attestation challenge was rejected as malformed or incompatible by your browser.",
);
break;
case AttestationResult.FailureWebauthnNotSupported:
case AttestationResult.FailureWebAuthnNotSupported:
createErrorNotification("Your browser does not support the WebAuthN protocol.");
break;
case AttestationResult.FailureUserConsent:

View File

@ -143,7 +143,7 @@ const LoginPortal = function (props: Props) {
if (configuration.available_methods.size === 0) {
redirect(AuthenticatedRoute, false);
} else {
if (userInfo.method === SecondFactorMethod.Webauthn) {
if (userInfo.method === SecondFactorMethod.WebAuthn) {
redirect(`${SecondFactorRoute}${SecondFactorWebAuthnSubRoute}`);
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);

View File

@ -39,12 +39,12 @@ const MethodSelectionDialog = function (props: Props) {
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
/>
) : null}
{props.methods.has(SecondFactorMethod.Webauthn) && props.webauthnSupported ? (
{props.methods.has(SecondFactorMethod.WebAuthn) && props.webauthnSupported ? (
<MethodItem
id="webauthn-option"
method={translate("Security Key - WebAuthN")}
icon={<FingerTouchIcon size={32} />}
onClick={() => props.onClick(SecondFactorMethod.Webauthn)}
onClick={() => props.onClick(SecondFactorMethod.WebAuthn)}
/>
) : null}
{props.methods.has(SecondFactorMethod.MobilePush) ? (

View File

@ -19,11 +19,11 @@ import { UserInfo } from "@models/UserInfo";
import { initiateTOTPRegistrationProcess, initiateWebAuthnRegistrationProcess } from "@services/RegisterDevice";
import { AuthenticationLevel } from "@services/State";
import { setPreferred2FAMethod } from "@services/UserInfo";
import { isWebAuthnSupported } from "@services/Webauthn";
import { isWebAuthnSupported } from "@services/WebAuthn";
import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog";
import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod";
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
import WebauthnMethod from "@views/LoginPortal/SecondFactor/WebauthnMethod";
import WebAuthnMethod from "@views/LoginPortal/SecondFactor/WebAuthnMethod";
export interface Props {
authenticationLevel: AuthenticationLevel;
@ -41,12 +41,12 @@ const SecondFactorForm = function (props: Props) {
const [methodSelectionOpen, setMethodSelectionOpen] = useState(false);
const { createInfoNotification, createErrorNotification } = useNotifications();
const [registrationInProgress, setRegistrationInProgress] = useState(false);
const [webauthnSupported, setWebauthnSupported] = useState(false);
const [stateWebAuthnSupported, setStateWebAuthnSupported] = useState(false);
const { t: translate } = useTranslation();
useEffect(() => {
setWebauthnSupported(isWebAuthnSupported());
}, [setWebauthnSupported]);
setStateWebAuthnSupported(isWebAuthnSupported());
}, [setStateWebAuthnSupported]);
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>) => {
return async () => {
@ -90,7 +90,7 @@ const SecondFactorForm = function (props: Props) {
<MethodSelectionDialog
open={methodSelectionOpen}
methods={props.configuration.available_methods}
webauthnSupported={webauthnSupported}
webauthnSupported={stateWebAuthnSupported}
onClose={() => setMethodSelectionOpen(false)}
onClick={handleMethodSelected}
/>
@ -126,10 +126,10 @@ const SecondFactorForm = function (props: Props) {
<Route
path={SecondFactorWebAuthnSubRoute}
element={
<WebauthnMethod
<WebAuthnMethod
id="webauthn-method"
authenticationLevel={props.authenticationLevel}
// Whether the user has a Webauthn device registered already
// Whether the user has a WebAuthn device registered already
registered={props.userInfo.has_webauthn}
onRegisterClick={initiateRegistration(initiateWebAuthnRegistrationProcess)}
onSignInError={(err) => createErrorNotification(err.message)}

View File

@ -11,13 +11,13 @@ import { useIsMountedRef } from "@hooks/Mounted";
import { useQueryParam } from "@hooks/QueryParam";
import { useTimer } from "@hooks/Timer";
import { useWorkflow } from "@hooks/Workflow";
import { AssertionResult } from "@models/Webauthn";
import { AssertionResult } from "@models/WebAuthn";
import { AuthenticationLevel } from "@services/State";
import {
getAssertionPublicKeyCredentialResult,
getAssertionRequestOptions,
postAssertionPublicKeyCredentialResult,
} from "@services/Webauthn";
} from "@services/WebAuthn";
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
@ -37,7 +37,7 @@ export interface Props {
onSignInSuccess: (redirectURL: string | undefined) => void;
}
const WebauthnMethod = function (props: Props) {
const WebAuthnMethod = function (props: Props) {
const signInTimeout = 30;
const [state, setState] = useState(State.WaitTouch);
const styles = useStyles();
@ -86,7 +86,7 @@ const WebauthnMethod = function (props: Props) {
),
);
break;
case AssertionResult.FailureWebauthnNotSupported:
case AssertionResult.FailureWebAuthnNotSupported:
onSignInErrorCallback(new Error("Your browser does not support the WebAuthN protocol."));
break;
case AssertionResult.FailureUnknownSecurity:
@ -179,7 +179,7 @@ const WebauthnMethod = function (props: Props) {
);
};
export default WebauthnMethod;
export default WebAuthnMethod;
const useStyles = makeStyles((theme: Theme) => ({
icon: {