refactor(web): webauthn references (#5244)
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>pull/5246/head
parent
2733fc040c
commit
370585d1de
|
@ -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")),
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -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,13 +246,15 @@ func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
|||
|
||||
var aaguid uuid.UUID
|
||||
|
||||
if aaguid, err = uuid.Parse(o.AAGUID); err != nil {
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum SecondFactorMethod {
|
||||
TOTP = 1,
|
||||
Webauthn,
|
||||
WebAuthn,
|
||||
MobilePush,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
PublicKeyCredentialJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsStatus,
|
||||
} from "@models/Webauthn";
|
||||
} from "@models/WebAuthn";
|
||||
import {
|
||||
OptionalDataServiceResponse,
|
||||
ServiceResponse,
|
|
@ -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:
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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) ? (
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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: {
|
Loading…
Reference in New Issue