build(deps): use @simplewebauthn/browser
parent
a36c45f1e1
commit
3af20a7daf
|
@ -30,10 +30,15 @@ var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
|
||||||
middlewares.IdentityVerificationFinishArgs{
|
middlewares.IdentityVerificationFinishArgs{
|
||||||
ActionClaim: ActionWebauthnRegistration,
|
ActionClaim: ActionWebauthnRegistration,
|
||||||
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
|
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
|
||||||
}, SecondFactorWebauthnAttestationGET)
|
}, WebauthnAttestationGET)
|
||||||
|
|
||||||
// SecondFactorWebauthnAttestationGET returns the attestation challenge from the server.
|
// WebauthnAttestationGET returns the attestation challenge from the server.
|
||||||
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
func WebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
||||||
|
WebauthnRegistrationGET(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebauthnRegistrationGET returns the attestation challenge from the server.
|
||||||
|
func WebauthnRegistrationGET(ctx *middlewares.AutheliaCtx) {
|
||||||
var (
|
var (
|
||||||
w *webauthn.WebAuthn
|
w *webauthn.WebAuthn
|
||||||
user *model.WebauthnUser
|
user *model.WebauthnUser
|
||||||
|
@ -92,13 +97,8 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebauthnAttestationPOST processes the attestation challenge response from the client.
|
// WebauthnRegistrationPOST processes the attestation challenge response from the client.
|
||||||
func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
type requestPostData struct {
|
|
||||||
Credential json.RawMessage `json:"credential"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
w *webauthn.WebAuthn
|
w *webauthn.WebAuthn
|
||||||
|
@ -106,9 +106,10 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
|
|
||||||
userSession session.UserSession
|
userSession session.UserSession
|
||||||
|
|
||||||
attestationResponse *protocol.ParsedCredentialCreationData
|
response *protocol.ParsedCredentialCreationData
|
||||||
|
|
||||||
credential *webauthn.Credential
|
credential *webauthn.Credential
|
||||||
postData *requestPostData
|
bodyJSON bodyRegisterWebauthnRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
if userSession, err = ctx.GetSession(); err != nil {
|
if userSession, err = ctx.GetSession(); err != nil {
|
||||||
|
@ -135,8 +136,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(ctx.PostBody(), &postData)
|
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to parse %s assertion request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to parse %s assertion request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -144,7 +144,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if attestationResponse, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(postData.Credential)); err != nil {
|
if response, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to parse %s assertion for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to parse %s assertion for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -152,6 +152,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Logger.WithField("att_format", response.Response.AttestationObject.Format).Debug("Response Data")
|
||||||
|
|
||||||
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
|
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
|
@ -160,7 +162,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if credential, err = w.CreateCredential(user, *userSession.Webauthn, attestationResponse); err != nil {
|
if credential, err = w.CreateCredential(user, *userSession.Webauthn, response); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -168,6 +170,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Logger.WithField("att_type", credential.AttestationType).Debug("Credential Data")
|
||||||
|
|
||||||
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username)
|
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username)
|
||||||
if err != nil && err != storage.ErrNoWebauthnDevice {
|
if err != nil && err != storage.ErrNoWebauthnDevice {
|
||||||
ctx.Logger.Errorf("Unable to load existing %s devices for for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to load existing %s devices for for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
@ -178,8 +182,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, existingDevice := range devices {
|
for _, existingDevice := range devices {
|
||||||
if existingDevice.Description == postData.Description {
|
if existingDevice.Description == bodyJSON.Description {
|
||||||
ctx.Logger.Errorf("%s device for for user '%s' with name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, postData.Description)
|
ctx.Logger.Errorf("%s device for for user '%s' with name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, bodyJSON.Description)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||||
ctx.SetStatusCode(fasthttp.StatusConflict)
|
ctx.SetStatusCode(fasthttp.StatusConflict)
|
||||||
|
@ -189,7 +193,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, postData.Description, credential)
|
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, bodyJSON.Description, credential)
|
||||||
|
|
||||||
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
@ -207,5 +211,5 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
ctx.SetStatusCode(fasthttp.StatusCreated)
|
ctx.SetStatusCode(fasthttp.StatusCreated)
|
||||||
|
|
||||||
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Credential Description": postData.Description})
|
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Credential Description": bodyJSON.Description})
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
user *model.WebauthnUser
|
user *model.WebauthnUser
|
||||||
)
|
)
|
||||||
|
|
||||||
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
|
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
@ -35,6 +36,14 @@ type bodySignWebauthnRequest struct {
|
||||||
TargetURL string `json:"targetURL"`
|
TargetURL string `json:"targetURL"`
|
||||||
Workflow string `json:"workflow"`
|
Workflow string `json:"workflow"`
|
||||||
WorkflowID string `json:"workflowID"`
|
WorkflowID string `json:"workflowID"`
|
||||||
|
|
||||||
|
Response json.RawMessage `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type bodyRegisterWebauthnRequest struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
|
||||||
|
Response json.RawMessage `json:"response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type bodyEditWebauthnDeviceRequest struct {
|
type bodyEditWebauthnDeviceRequest struct {
|
||||||
|
|
|
@ -96,23 +96,21 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
||||||
}
|
}
|
||||||
|
|
||||||
func identityVerificationValidateToken(ctx *AutheliaCtx) (*jwt.Token, error) {
|
func identityVerificationValidateToken(ctx *AutheliaCtx) (*jwt.Token, error) {
|
||||||
var finishBody IdentityVerificationFinishBody
|
var bodyJSON IdentityVerificationFinishBody
|
||||||
|
|
||||||
b := ctx.PostBody()
|
err := json.Unmarshal(ctx.PostBody(), &bodyJSON)
|
||||||
|
|
||||||
err := json.Unmarshal(b, &finishBody)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err, messageOperationFailed)
|
ctx.Error(err, messageOperationFailed)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if finishBody.Token == "" {
|
if bodyJSON.Token == "" {
|
||||||
ctx.Error(fmt.Errorf("No token provided"), messageOperationFailed)
|
ctx.Error(fmt.Errorf("No token provided"), messageOperationFailed)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := jwt.ParseWithClaims(finishBody.Token, &model.IdentityVerificationClaim{},
|
token, err := jwt.ParseWithClaims(bodyJSON.Token, &model.IdentityVerificationClaim{},
|
||||||
func(token *jwt.Token) (any, error) {
|
func(token *jwt.Token) (any, error) {
|
||||||
return []byte(ctx.Configuration.JWTSecret), nil
|
return []byte(ctx.Configuration.JWTSecret), nil
|
||||||
})
|
})
|
||||||
|
@ -185,8 +183,7 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ctx.Providers.StorageProvider.ConsumeIdentityVerification(ctx, claims.ID, model.NewNullIP(ctx.RemoteIP()))
|
if err = ctx.Providers.StorageProvider.ConsumeIdentityVerification(ctx, claims.ID, model.NewNullIP(ctx.RemoteIP())); err != nil {
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err, messageOperationFailed)
|
ctx.Error(err, messageOperationFailed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,7 +238,11 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
||||||
// Webauthn Endpoints.
|
// Webauthn Endpoints.
|
||||||
r.POST("/api/secondfactor/webauthn/identity/start", middleware1FA(handlers.WebauthnIdentityStart))
|
r.POST("/api/secondfactor/webauthn/identity/start", middleware1FA(handlers.WebauthnIdentityStart))
|
||||||
r.POST("/api/secondfactor/webauthn/identity/finish", middleware1FA(handlers.WebauthnIdentityFinish))
|
r.POST("/api/secondfactor/webauthn/identity/finish", middleware1FA(handlers.WebauthnIdentityFinish))
|
||||||
r.POST("/api/secondfactor/webauthn/attestation", middleware1FA(handlers.WebauthnAttestationPOST))
|
|
||||||
|
r.GET("/api/secondfactor/register/webauthn", middleware1FA(handlers.WebauthnRegistrationGET))
|
||||||
|
r.POST("/api/secondfactor/register/webauthn", middleware1FA(handlers.WebauthnRegistrationPOST))
|
||||||
|
|
||||||
|
r.POST("/api/secondfactor/webauthn/attestation", middleware1FA(handlers.WebauthnRegistrationPOST))
|
||||||
|
|
||||||
r.GET("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionGET))
|
r.GET("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionGET))
|
||||||
r.POST("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionPOST))
|
r.POST("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionPOST))
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"@mui/icons-material": "5.11.0",
|
"@mui/icons-material": "5.11.0",
|
||||||
"@mui/material": "5.11.6",
|
"@mui/material": "5.11.6",
|
||||||
"@mui/styles": "5.11.2",
|
"@mui/styles": "5.11.2",
|
||||||
|
"@simplewebauthn/browser": "^7.0.1",
|
||||||
|
"@simplewebauthn/typescript-types": "^7.0.0",
|
||||||
"axios": "1.2.6",
|
"axios": "1.2.6",
|
||||||
"broadcast-channel": "4.20.2",
|
"broadcast-channel": "4.20.2",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
|
|
|
@ -14,6 +14,8 @@ specifiers:
|
||||||
'@mui/icons-material': 5.11.0
|
'@mui/icons-material': 5.11.0
|
||||||
'@mui/material': 5.11.6
|
'@mui/material': 5.11.6
|
||||||
'@mui/styles': 5.11.2
|
'@mui/styles': 5.11.2
|
||||||
|
'@simplewebauthn/browser': ^7.0.1
|
||||||
|
'@simplewebauthn/typescript-types': ^7.0.0
|
||||||
'@testing-library/jest-dom': 5.16.5
|
'@testing-library/jest-dom': 5.16.5
|
||||||
'@testing-library/react': 13.4.0
|
'@testing-library/react': 13.4.0
|
||||||
'@types/jest': 29.4.0
|
'@types/jest': 29.4.0
|
||||||
|
@ -76,6 +78,8 @@ dependencies:
|
||||||
'@mui/icons-material': 5.11.0_j5wvuqirnhynb4halegp2mqooy
|
'@mui/icons-material': 5.11.0_j5wvuqirnhynb4halegp2mqooy
|
||||||
'@mui/material': 5.11.6_rqh7qj4464ntrqrt6banhaqg4q
|
'@mui/material': 5.11.6_rqh7qj4464ntrqrt6banhaqg4q
|
||||||
'@mui/styles': 5.11.2_3stiutgnnbnfnf3uowm5cip22i
|
'@mui/styles': 5.11.2_3stiutgnnbnfnf3uowm5cip22i
|
||||||
|
'@simplewebauthn/browser': 7.0.1
|
||||||
|
'@simplewebauthn/typescript-types': 7.0.0
|
||||||
axios: 1.2.6
|
axios: 1.2.6
|
||||||
broadcast-channel: 4.20.2
|
broadcast-channel: 4.20.2
|
||||||
classnames: 2.3.2
|
classnames: 2.3.2
|
||||||
|
@ -3395,6 +3399,14 @@ packages:
|
||||||
resolution: {integrity: sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==}
|
resolution: {integrity: sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@simplewebauthn/browser/7.0.1:
|
||||||
|
resolution: {integrity: sha512-zFTTQojH8FWAvzBeB9WHMkZz5XrHaNQwKmdpGDfyRtXHJSLlWUjZN1egD2vWqU+gE0CKl0NRZJ56uLugBs9AUg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@simplewebauthn/typescript-types/7.0.0:
|
||||||
|
resolution: {integrity: sha512-bV+xACCFTsrLR/23ozHO06ZllHZaxC8LlI5YCo79GvU2BrN+rePDU2yXwZIYndNWcMQwRdndRdAhpafOh9AC/g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@sinclair/typebox/0.25.21:
|
/@sinclair/typebox/0.25.21:
|
||||||
resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==}
|
resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { WebauthnTouchState } from "@models/Webauthn";
|
||||||
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
|
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
timer: number;
|
|
||||||
onRetryClick: () => void;
|
onRetryClick: () => void;
|
||||||
webauthnTouchState: WebauthnTouchState;
|
webauthnTouchState: WebauthnTouchState;
|
||||||
}
|
}
|
||||||
|
@ -61,7 +60,7 @@ export default function WebauthnTryIcon(props: Props) {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.icon} sx={{ minHeight: 101 }} align="middle">
|
<Box className={styles.icon} sx={{ minHeight: 101 }}>
|
||||||
{touch}
|
{touch}
|
||||||
{failure}
|
{failure}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
|
import {
|
||||||
|
AuthenticationResponseJSON,
|
||||||
|
PublicKeyCredentialCreationOptionsJSON,
|
||||||
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
|
RegistrationResponseJSON,
|
||||||
|
} from "@simplewebauthn/typescript-types";
|
||||||
|
|
||||||
export interface PublicKeyCredentialCreationOptionsStatus {
|
export interface PublicKeyCredentialCreationOptionsStatus {
|
||||||
options?: PublicKeyCredentialCreationOptions;
|
options?: PublicKeyCredentialCreationOptionsJSON;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,15 +14,8 @@ export interface CredentialCreation {
|
||||||
publicKey: PublicKeyCredentialCreationOptionsJSON;
|
publicKey: PublicKeyCredentialCreationOptionsJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicKeyCredentialCreationOptionsJSON
|
|
||||||
extends Omit<PublicKeyCredentialCreationOptions, "challenge" | "excludeCredentials" | "user"> {
|
|
||||||
challenge: string;
|
|
||||||
excludeCredentials?: PublicKeyCredentialDescriptorJSON[];
|
|
||||||
user: PublicKeyCredentialUserEntityJSON;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublicKeyCredentialRequestOptionsStatus {
|
export interface PublicKeyCredentialRequestOptionsStatus {
|
||||||
options?: PublicKeyCredentialRequestOptions;
|
options?: PublicKeyCredentialRequestOptionsJSON;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,63 +23,6 @@ export interface CredentialRequest {
|
||||||
publicKey: PublicKeyCredentialRequestOptionsJSON;
|
publicKey: PublicKeyCredentialRequestOptionsJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicKeyCredentialRequestOptionsJSON
|
|
||||||
extends Omit<PublicKeyCredentialRequestOptions, "allowCredentials" | "challenge"> {
|
|
||||||
allowCredentials?: PublicKeyCredentialDescriptorJSON[];
|
|
||||||
challenge: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublicKeyCredentialDescriptorJSON extends Omit<PublicKeyCredentialDescriptor, "id"> {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublicKeyCredentialUserEntityJSON extends Omit<PublicKeyCredentialUserEntity, "id"> {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthenticatorAssertionResponseJSON
|
|
||||||
extends Omit<AuthenticatorAssertionResponse, "authenticatorData" | "clientDataJSON" | "signature" | "userHandle"> {
|
|
||||||
authenticatorData: string;
|
|
||||||
clientDataJSON: string;
|
|
||||||
signature: string;
|
|
||||||
userHandle: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAttestationResponse {
|
|
||||||
getTransports?: () => AuthenticatorTransport[];
|
|
||||||
getAuthenticatorData?: () => ArrayBuffer;
|
|
||||||
getPublicKey?: () => ArrayBuffer;
|
|
||||||
getPublicKeyAlgorithm?: () => COSEAlgorithmIdentifier[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AttestationPublicKeyCredential extends PublicKeyCredential {
|
|
||||||
response: AuthenticatorAttestationResponseFuture;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthenticatorAttestationResponseJSON
|
|
||||||
extends Omit<AuthenticatorAttestationResponseFuture, "clientDataJSON" | "attestationObject"> {
|
|
||||||
clientDataJSON: string;
|
|
||||||
attestationObject: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AttestationPublicKeyCredentialJSON
|
|
||||||
extends Omit<AttestationPublicKeyCredential, "response" | "rawId" | "getClientExtensionResults"> {
|
|
||||||
rawId: string;
|
|
||||||
response: AuthenticatorAttestationResponseJSON;
|
|
||||||
clientExtensionResults: AuthenticationExtensionsClientOutputs;
|
|
||||||
transports?: AuthenticatorTransport[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublicKeyCredentialJSON
|
|
||||||
extends Omit<PublicKeyCredential, "rawId" | "response" | "getClientExtensionResults"> {
|
|
||||||
rawId: string;
|
|
||||||
clientExtensionResults: AuthenticationExtensionsClientOutputs;
|
|
||||||
response: AuthenticatorAssertionResponseJSON;
|
|
||||||
targetURL?: string;
|
|
||||||
workflow?: string;
|
|
||||||
workflowID?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AttestationResult {
|
export enum AttestationResult {
|
||||||
Success = 1,
|
Success = 1,
|
||||||
Failure,
|
Failure,
|
||||||
|
@ -93,21 +36,11 @@ export enum AttestationResult {
|
||||||
FailureToken,
|
FailureToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttestationPublicKeyCredentialResult {
|
export interface RegistrationResult {
|
||||||
credential?: AttestationPublicKeyCredential;
|
response?: RegistrationResponseJSON;
|
||||||
result: AttestationResult;
|
result: AttestationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttestationPublicKeyCredentialResultJSON {
|
|
||||||
credential?: AttestationPublicKeyCredentialJSON;
|
|
||||||
result: AttestationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AttestationFinishResult {
|
|
||||||
result: AttestationResult;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AssertionResult {
|
export enum AssertionResult {
|
||||||
Success = 1,
|
Success = 1,
|
||||||
Failure,
|
Failure,
|
||||||
|
@ -121,18 +54,54 @@ export enum AssertionResult {
|
||||||
FailureUnrecognized,
|
FailureUnrecognized,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscoverableAssertionResult {
|
export function AssertionResultFailureString(result: AssertionResult) {
|
||||||
result: AssertionResult;
|
switch (result) {
|
||||||
username: string;
|
case AssertionResult.Success:
|
||||||
|
return "";
|
||||||
|
case AssertionResult.FailureUserConsent:
|
||||||
|
return "You cancelled the assertion request.";
|
||||||
|
case AssertionResult.FailureU2FFacetID:
|
||||||
|
return "The server responded with an invalid Facet ID for the URL.";
|
||||||
|
case AssertionResult.FailureSyntax:
|
||||||
|
return "The assertion challenge was rejected as malformed or incompatible by your browser.";
|
||||||
|
case AssertionResult.FailureWebauthnNotSupported:
|
||||||
|
return "Your browser does not support the WebAuthN protocol.";
|
||||||
|
case AssertionResult.FailureUnrecognized:
|
||||||
|
return "This device is not registered.";
|
||||||
|
case AssertionResult.FailureUnknownSecurity:
|
||||||
|
return "An unknown security error occurred.";
|
||||||
|
case AssertionResult.FailureUnknown:
|
||||||
|
return "An unknown error occurred.";
|
||||||
|
default:
|
||||||
|
return "An unexpected error occurred.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssertionPublicKeyCredentialResult {
|
export function AttestationResultFailureString(result: AttestationResult) {
|
||||||
credential?: PublicKeyCredential;
|
switch (result) {
|
||||||
result: AssertionResult;
|
case AttestationResult.FailureToken:
|
||||||
|
return "You must open the link from the same device and browser that initiated the registration process.";
|
||||||
|
case AttestationResult.FailureSupport:
|
||||||
|
return "Your browser does not appear to support the configuration.";
|
||||||
|
case AttestationResult.FailureSyntax:
|
||||||
|
return "The attestation challenge was rejected as malformed or incompatible by your browser.";
|
||||||
|
case AttestationResult.FailureWebauthnNotSupported:
|
||||||
|
return "Your browser does not support the WebAuthN protocol.";
|
||||||
|
case AttestationResult.FailureUserConsent:
|
||||||
|
return "You cancelled the attestation request.";
|
||||||
|
case AttestationResult.FailureUserVerificationOrResidentKey:
|
||||||
|
return "Your device does not support user verification or resident keys but this was required.";
|
||||||
|
case AttestationResult.FailureExcluded:
|
||||||
|
return "You have registered this device already.";
|
||||||
|
case AttestationResult.FailureUnknown:
|
||||||
|
return "An unknown error occurred.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssertionPublicKeyCredentialResultJSON {
|
export interface AuthenticationResult {
|
||||||
credential?: PublicKeyCredentialJSON;
|
response?: AuthenticationResponseJSON;
|
||||||
result: AssertionResult;
|
result: AssertionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +125,3 @@ export enum WebauthnTouchState {
|
||||||
InProgress = 2,
|
InProgress = 2,
|
||||||
Failure = 3,
|
Failure = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebauthnDeviceUpdateRequest {
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,8 +12,7 @@ export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/i
|
||||||
export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
|
export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
|
||||||
|
|
||||||
export const WebauthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start";
|
export const WebauthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start";
|
||||||
export const WebauthnIdentityFinishPath = basePath + "/api/secondfactor/webauthn/identity/finish";
|
export const WebauthnRegistrationPath = basePath + "/api/secondfactor/register/webauthn";
|
||||||
export const WebauthnAttestationPath = basePath + "/api/secondfactor/webauthn/attestation";
|
|
||||||
|
|
||||||
export const WebauthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion";
|
export const WebauthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion";
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,32 @@
|
||||||
|
import { startAuthentication, startRegistration } from "@simplewebauthn/browser";
|
||||||
|
import {
|
||||||
|
AuthenticationResponseJSON,
|
||||||
|
PublicKeyCredentialCreationOptionsJSON,
|
||||||
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
|
RegistrationResponseJSON,
|
||||||
|
} from "@simplewebauthn/typescript-types";
|
||||||
import axios, { AxiosError, AxiosResponse } from "axios";
|
import axios, { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AssertionPublicKeyCredentialResult,
|
|
||||||
AssertionResult,
|
AssertionResult,
|
||||||
AttestationFinishResult,
|
|
||||||
AttestationPublicKeyCredential,
|
|
||||||
AttestationPublicKeyCredentialJSON,
|
|
||||||
AttestationPublicKeyCredentialResult,
|
|
||||||
AttestationResult,
|
AttestationResult,
|
||||||
AuthenticatorAttestationResponseFuture,
|
AuthenticationResult,
|
||||||
CredentialCreation,
|
CredentialCreation,
|
||||||
CredentialRequest,
|
CredentialRequest,
|
||||||
PublicKeyCredentialCreationOptionsJSON,
|
|
||||||
PublicKeyCredentialCreationOptionsStatus,
|
PublicKeyCredentialCreationOptionsStatus,
|
||||||
PublicKeyCredentialDescriptorJSON,
|
|
||||||
PublicKeyCredentialJSON,
|
|
||||||
PublicKeyCredentialRequestOptionsJSON,
|
|
||||||
PublicKeyCredentialRequestOptionsStatus,
|
PublicKeyCredentialRequestOptionsStatus,
|
||||||
|
RegistrationResult,
|
||||||
} from "@models/Webauthn";
|
} from "@models/Webauthn";
|
||||||
import {
|
import {
|
||||||
AuthenticationOKResponse,
|
AuthenticationOKResponse,
|
||||||
OptionalDataServiceResponse,
|
OptionalDataServiceResponse,
|
||||||
ServiceResponse,
|
ServiceResponse,
|
||||||
WebauthnAssertionPath,
|
WebauthnAssertionPath,
|
||||||
WebauthnAttestationPath,
|
|
||||||
WebauthnDevicePath,
|
WebauthnDevicePath,
|
||||||
WebauthnIdentityFinishPath,
|
WebauthnRegistrationPath,
|
||||||
validateStatusAuthentication,
|
validateStatusAuthentication,
|
||||||
} from "@services/Api";
|
} from "@services/Api";
|
||||||
import { SignInResponse } from "@services/SignIn";
|
import { SignInResponse } from "@services/SignIn";
|
||||||
import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from "@utils/Base64";
|
|
||||||
|
|
||||||
export function isWebauthnSecure(): boolean {
|
export function isWebauthnSecure(): boolean {
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
|
@ -51,120 +48,6 @@ export async function isWebauthnPlatformAuthenticatorAvailable(): Promise<boolea
|
||||||
return window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
return window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferEncode(value: ArrayBuffer): string {
|
|
||||||
return getBase64WebEncodingFromBytes(new Uint8Array(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayBufferDecode(value: string): ArrayBuffer {
|
|
||||||
return getBytesFromBase64(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodePublicKeyCredentialDescriptor(
|
|
||||||
descriptor: PublicKeyCredentialDescriptorJSON,
|
|
||||||
): PublicKeyCredentialDescriptor {
|
|
||||||
return {
|
|
||||||
id: arrayBufferDecode(descriptor.id),
|
|
||||||
type: descriptor.type,
|
|
||||||
transports: descriptor.transports,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodePublicKeyCredentialCreationOptions(
|
|
||||||
options: PublicKeyCredentialCreationOptionsJSON,
|
|
||||||
): PublicKeyCredentialCreationOptions {
|
|
||||||
return {
|
|
||||||
attestation: options.attestation,
|
|
||||||
authenticatorSelection: options.authenticatorSelection,
|
|
||||||
challenge: arrayBufferDecode(options.challenge),
|
|
||||||
excludeCredentials: options.excludeCredentials?.map(decodePublicKeyCredentialDescriptor),
|
|
||||||
extensions: options.extensions,
|
|
||||||
pubKeyCredParams: options.pubKeyCredParams,
|
|
||||||
rp: options.rp,
|
|
||||||
timeout: options.timeout,
|
|
||||||
user: {
|
|
||||||
displayName: options.user.displayName,
|
|
||||||
id: arrayBufferDecode(options.user.id),
|
|
||||||
name: options.user.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodePublicKeyCredentialRequestOptions(
|
|
||||||
options: PublicKeyCredentialRequestOptionsJSON,
|
|
||||||
): PublicKeyCredentialRequestOptions {
|
|
||||||
let allowCredentials: PublicKeyCredentialDescriptor[] | undefined = undefined;
|
|
||||||
|
|
||||||
if (options.allowCredentials?.length !== 0) {
|
|
||||||
allowCredentials = options.allowCredentials?.map(decodePublicKeyCredentialDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowCredentials: allowCredentials,
|
|
||||||
challenge: arrayBufferDecode(options.challenge),
|
|
||||||
extensions: options.extensions,
|
|
||||||
rpId: options.rpId,
|
|
||||||
timeout: options.timeout,
|
|
||||||
userVerification: options.userVerification,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeAttestationPublicKeyCredential(
|
|
||||||
credential: AttestationPublicKeyCredential,
|
|
||||||
): AttestationPublicKeyCredentialJSON {
|
|
||||||
const response = credential.response as AuthenticatorAttestationResponseFuture;
|
|
||||||
|
|
||||||
let transports: AuthenticatorTransport[] | undefined;
|
|
||||||
|
|
||||||
if (response?.getTransports !== undefined && typeof response.getTransports === "function") {
|
|
||||||
transports = response.getTransports();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: credential.id,
|
|
||||||
type: credential.type,
|
|
||||||
rawId: arrayBufferEncode(credential.rawId),
|
|
||||||
clientExtensionResults: credential.getClientExtensionResults(),
|
|
||||||
response: {
|
|
||||||
attestationObject: arrayBufferEncode(response.attestationObject),
|
|
||||||
clientDataJSON: arrayBufferEncode(response.clientDataJSON),
|
|
||||||
},
|
|
||||||
transports: transports,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeAssertionPublicKeyCredential(
|
|
||||||
credential: PublicKeyCredential,
|
|
||||||
targetURL: string | undefined,
|
|
||||||
workflow: string | undefined,
|
|
||||||
workflowID: string | undefined,
|
|
||||||
): PublicKeyCredentialJSON {
|
|
||||||
const response = credential.response as AuthenticatorAssertionResponse;
|
|
||||||
|
|
||||||
let userHandle: string;
|
|
||||||
|
|
||||||
if (response.userHandle == null) {
|
|
||||||
userHandle = "";
|
|
||||||
} else {
|
|
||||||
userHandle = arrayBufferEncode(response.userHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: credential.id,
|
|
||||||
type: credential.type,
|
|
||||||
rawId: arrayBufferEncode(credential.rawId),
|
|
||||||
clientExtensionResults: credential.getClientExtensionResults(),
|
|
||||||
response: {
|
|
||||||
authenticatorData: arrayBufferEncode(response.authenticatorData),
|
|
||||||
clientDataJSON: arrayBufferEncode(response.clientDataJSON),
|
|
||||||
signature: arrayBufferEncode(response.signature),
|
|
||||||
userHandle: userHandle,
|
|
||||||
},
|
|
||||||
targetURL: targetURL,
|
|
||||||
workflow: workflow,
|
|
||||||
workflowID: workflowID,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAttestationResultFromDOMException(exception: DOMException): AttestationResult {
|
function getAttestationResultFromDOMException(exception: DOMException): AttestationResult {
|
||||||
// Docs for this section:
|
// Docs for this section:
|
||||||
// https://w3c.github.io/webauthn/#sctn-op-make-cred
|
// https://w3c.github.io/webauthn/#sctn-op-make-cred
|
||||||
|
@ -193,7 +76,7 @@ function getAttestationResultFromDOMException(exception: DOMException): Attestat
|
||||||
|
|
||||||
function getAssertionResultFromDOMException(
|
function getAssertionResultFromDOMException(
|
||||||
exception: DOMException,
|
exception: DOMException,
|
||||||
requestOptions: PublicKeyCredentialRequestOptions,
|
options: PublicKeyCredentialRequestOptionsJSON,
|
||||||
): AssertionResult {
|
): AssertionResult {
|
||||||
// Docs for this section:
|
// Docs for this section:
|
||||||
// https://w3c.github.io/webauthn/#sctn-op-get-assertion
|
// https://w3c.github.io/webauthn/#sctn-op-get-assertion
|
||||||
|
@ -209,7 +92,7 @@ function getAssertionResultFromDOMException(
|
||||||
// § 6.3.3 Step 6 and Step 7.
|
// § 6.3.3 Step 6 and Step 7.
|
||||||
return AssertionResult.FailureUserConsent;
|
return AssertionResult.FailureUserConsent;
|
||||||
case "SecurityError":
|
case "SecurityError":
|
||||||
if (requestOptions.extensions?.appid !== undefined) {
|
if (options.extensions?.appid !== undefined) {
|
||||||
// § 10.1 and 10.2 Step 3.
|
// § 10.1 and 10.2 Step 3.
|
||||||
return AssertionResult.FailureU2FFacetID;
|
return AssertionResult.FailureU2FFacetID;
|
||||||
} else {
|
} else {
|
||||||
|
@ -221,14 +104,10 @@ function getAssertionResultFromDOMException(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAttestationCreationOptions(
|
export async function getAttestationCreationOptions(): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
||||||
token: null | string,
|
|
||||||
): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
|
||||||
let response: AxiosResponse<ServiceResponse<CredentialCreation>>;
|
let response: AxiosResponse<ServiceResponse<CredentialCreation>>;
|
||||||
|
|
||||||
response = await axios.post<ServiceResponse<CredentialCreation>>(WebauthnIdentityFinishPath, {
|
response = await axios.get<ServiceResponse<CredentialCreation>>(WebauthnRegistrationPath);
|
||||||
token: token,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.status !== "OK" || response.data.data == null) {
|
if (response.data.status !== "OK" || response.data.data == null) {
|
||||||
return {
|
return {
|
||||||
|
@ -237,12 +116,12 @@ export async function getAttestationCreationOptions(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options: decodePublicKeyCredentialCreationOptions(response.data.data.publicKey),
|
options: response.data.data.publicKey,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> {
|
export async function getAuthenticationOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> {
|
||||||
let response: AxiosResponse<ServiceResponse<CredentialRequest>>;
|
let response: AxiosResponse<ServiceResponse<CredentialRequest>>;
|
||||||
|
|
||||||
response = await axios.get<ServiceResponse<CredentialRequest>>(WebauthnAssertionPath);
|
response = await axios.get<ServiceResponse<CredentialRequest>>(WebauthnAssertionPath);
|
||||||
|
@ -254,65 +133,55 @@ export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialR
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options: decodePublicKeyCredentialRequestOptions(response.data.data.publicKey),
|
options: response.data.data.publicKey,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAttestationPublicKeyCredentialResult(
|
export async function startWebauthnRegistration(options: PublicKeyCredentialCreationOptionsJSON) {
|
||||||
creationOptions: PublicKeyCredentialCreationOptions,
|
const result: RegistrationResult = {
|
||||||
): Promise<AttestationPublicKeyCredentialResult> {
|
result: AttestationResult.Failure,
|
||||||
const result: AttestationPublicKeyCredentialResult = {
|
|
||||||
result: AttestationResult.Success,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result.credential = (await navigator.credentials.create({
|
result.response = await startRegistration(options);
|
||||||
publicKey: creationOptions,
|
|
||||||
})) as AttestationPublicKeyCredential;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.result = AttestationResult.Failure;
|
|
||||||
|
|
||||||
const exception = e as DOMException;
|
const exception = e as DOMException;
|
||||||
if (exception !== undefined) {
|
if (exception !== undefined) {
|
||||||
result.result = getAttestationResultFromDOMException(exception);
|
result.result = getAttestationResultFromDOMException(exception);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
console.error(`Unhandled exception occurred during WebAuthN attestation: ${e}`);
|
console.error(`Unhandled exception occurred during WebAuthn attestation: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.credential != null) {
|
if (result.response != null) {
|
||||||
result.result = AttestationResult.Success;
|
result.result = AttestationResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssertionPublicKeyCredentialResult(
|
export async function getAuthenticationResult(options: PublicKeyCredentialRequestOptionsJSON) {
|
||||||
requestOptions: PublicKeyCredentialRequestOptions,
|
const result: AuthenticationResult = {
|
||||||
): Promise<AssertionPublicKeyCredentialResult> {
|
|
||||||
const result: AssertionPublicKeyCredentialResult = {
|
|
||||||
result: AssertionResult.Success,
|
result: AssertionResult.Success,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result.credential = (await navigator.credentials.get({ publicKey: requestOptions })) as PublicKeyCredential;
|
result.response = await startAuthentication(options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.result = AssertionResult.Failure;
|
|
||||||
|
|
||||||
const exception = e as DOMException;
|
const exception = e as DOMException;
|
||||||
if (exception !== undefined) {
|
if (exception !== undefined) {
|
||||||
result.result = getAssertionResultFromDOMException(exception, requestOptions);
|
result.result = getAssertionResultFromDOMException(exception, options);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
console.error(`Unhandled exception occurred during WebAuthN assertion: ${e}`);
|
console.error(`Unhandled exception occurred during WebAuthn authentication: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.credential == null) {
|
if (result.response == null) {
|
||||||
result.result = AssertionResult.Failure;
|
result.result = AssertionResult.Failure;
|
||||||
} else {
|
} else {
|
||||||
result.result = AssertionResult.Success;
|
result.result = AssertionResult.Success;
|
||||||
|
@ -321,84 +190,53 @@ export async function getAssertionPublicKeyCredentialResult(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postAttestationPublicKeyCredentialResult(
|
async function postRegistrationResponse(
|
||||||
credential: AttestationPublicKeyCredential,
|
response: RegistrationResponseJSON,
|
||||||
description: string,
|
description: string,
|
||||||
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
|
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
|
||||||
const credentialJSON = encodeAttestationPublicKeyCredential(credential);
|
return axios.post<OptionalDataServiceResponse<any>>(WebauthnRegistrationPath, {
|
||||||
const postBody = {
|
response: response,
|
||||||
credential: credentialJSON,
|
|
||||||
description: description,
|
description: description,
|
||||||
};
|
});
|
||||||
return axios.post<OptionalDataServiceResponse<any>>(WebauthnAttestationPath, postBody);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postAssertionPublicKeyCredentialResult(
|
export async function postAuthenticationResponse(
|
||||||
credential: PublicKeyCredential,
|
response: AuthenticationResponseJSON,
|
||||||
targetURL: string | undefined,
|
targetURL: string | undefined,
|
||||||
workflow?: string,
|
workflow?: string,
|
||||||
workflowID?: string,
|
workflowID?: string,
|
||||||
): Promise<AxiosResponse<ServiceResponse<SignInResponse>>> {
|
) {
|
||||||
const credentialJSON = encodeAssertionPublicKeyCredential(credential, targetURL, workflow, workflowID);
|
return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, {
|
||||||
return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, credentialJSON);
|
response: response,
|
||||||
|
targetURL: targetURL,
|
||||||
|
workflow: workflow,
|
||||||
|
workflowID: workflowID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function finishAttestationCeremony(
|
export async function finishRegistration(response: RegistrationResponseJSON, description: string) {
|
||||||
credential: AttestationPublicKeyCredential,
|
|
||||||
description: string,
|
|
||||||
): Promise<AttestationResult> {
|
|
||||||
let result = {
|
let result = {
|
||||||
status: AttestationResult.Failure,
|
status: AttestationResult.Failure,
|
||||||
message: "Device registration failed.",
|
message: "Device registration failed.",
|
||||||
} as AttestationResult;
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await postAttestationPublicKeyCredentialResult(credential, description);
|
const resp = await postRegistrationResponse(response, description);
|
||||||
if (response.data.status === "OK" && (response.status === 200 || response.status === 201)) {
|
if (resp.data.status === "OK" && (resp.status === 200 || resp.status === 201)) {
|
||||||
return {
|
return {
|
||||||
status: AttestationResult.Success,
|
status: AttestationResult.Success,
|
||||||
} as AttestationFinishResult;
|
message: "",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError && error.response !== undefined) {
|
||||||
result.message = error.response.data.message;
|
result.message = error.response.data.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function performAssertionCeremony(
|
|
||||||
targetURL?: string,
|
|
||||||
workflow?: string,
|
|
||||||
workflowID?: string,
|
|
||||||
): Promise<AssertionResult> {
|
|
||||||
const assertionRequestOpts = await getAssertionRequestOptions();
|
|
||||||
|
|
||||||
if (assertionRequestOpts.status !== 200 || assertionRequestOpts.options == null) {
|
|
||||||
return AssertionResult.FailureChallenge;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assertionResult = await getAssertionPublicKeyCredentialResult(assertionRequestOpts.options);
|
|
||||||
|
|
||||||
if (assertionResult.result !== AssertionResult.Success) {
|
|
||||||
return assertionResult.result;
|
|
||||||
} else if (assertionResult.credential == null) {
|
|
||||||
return AssertionResult.Failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await postAssertionPublicKeyCredentialResult(
|
|
||||||
assertionResult.credential,
|
|
||||||
targetURL,
|
|
||||||
workflow,
|
|
||||||
workflowID,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.status === "OK" && response.status === 200) {
|
|
||||||
return AssertionResult.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AssertionResult.Failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteDevice(deviceID: string) {
|
export async function deleteDevice(deviceID: string) {
|
||||||
return await axios<AuthenticationOKResponse>({
|
return await axios<AuthenticationOKResponse>({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
/*
|
|
||||||
|
|
||||||
This file is a work taken from the following location: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Egor Nepomnyaschih
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
// This constant can also be computed with the following algorithm:
|
|
||||||
const base64Chars = [],
|
|
||||||
A = "A".charCodeAt(0),
|
|
||||||
a = "a".charCodeAt(0),
|
|
||||||
n = "0".charCodeAt(0);
|
|
||||||
for (let i = 0; i < 26; ++i) {
|
|
||||||
base64Chars.push(String.fromCharCode(A + i));
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 26; ++i) {
|
|
||||||
base64Chars.push(String.fromCharCode(a + i));
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 10; ++i) {
|
|
||||||
base64Chars.push(String.fromCharCode(n + i));
|
|
||||||
}
|
|
||||||
base64Chars.push("+");
|
|
||||||
base64Chars.push("/");
|
|
||||||
*/
|
|
||||||
|
|
||||||
const base64Chars = [
|
|
||||||
"A",
|
|
||||||
"B",
|
|
||||||
"C",
|
|
||||||
"D",
|
|
||||||
"E",
|
|
||||||
"F",
|
|
||||||
"G",
|
|
||||||
"H",
|
|
||||||
"I",
|
|
||||||
"J",
|
|
||||||
"K",
|
|
||||||
"L",
|
|
||||||
"M",
|
|
||||||
"N",
|
|
||||||
"O",
|
|
||||||
"P",
|
|
||||||
"Q",
|
|
||||||
"R",
|
|
||||||
"S",
|
|
||||||
"T",
|
|
||||||
"U",
|
|
||||||
"V",
|
|
||||||
"W",
|
|
||||||
"X",
|
|
||||||
"Y",
|
|
||||||
"Z",
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
"c",
|
|
||||||
"d",
|
|
||||||
"e",
|
|
||||||
"f",
|
|
||||||
"g",
|
|
||||||
"h",
|
|
||||||
"i",
|
|
||||||
"j",
|
|
||||||
"k",
|
|
||||||
"l",
|
|
||||||
"m",
|
|
||||||
"n",
|
|
||||||
"o",
|
|
||||||
"p",
|
|
||||||
"q",
|
|
||||||
"r",
|
|
||||||
"s",
|
|
||||||
"t",
|
|
||||||
"u",
|
|
||||||
"v",
|
|
||||||
"w",
|
|
||||||
"x",
|
|
||||||
"y",
|
|
||||||
"z",
|
|
||||||
"0",
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5",
|
|
||||||
"6",
|
|
||||||
"7",
|
|
||||||
"8",
|
|
||||||
"9",
|
|
||||||
"+",
|
|
||||||
"/",
|
|
||||||
];
|
|
||||||
|
|
||||||
/*
|
|
||||||
// This constant can also be computed with the following algorithm:
|
|
||||||
const l = 256, base64codes = new Uint8Array(l);
|
|
||||||
for (let i = 0; i < l; ++i) {
|
|
||||||
base64codes[i] = 255; // invalid character
|
|
||||||
}
|
|
||||||
base64Chars.forEach((char, index) => {
|
|
||||||
base64codes[char.charCodeAt(0)] = index;
|
|
||||||
});
|
|
||||||
base64codes["=".charCodeAt(0)] = 0; // ignored anyway, so we just need to prevent an error
|
|
||||||
*/
|
|
||||||
|
|
||||||
const base64Codes = [
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255,
|
|
||||||
255, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
|
||||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, 29, 30, 31,
|
|
||||||
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
|
|
||||||
];
|
|
||||||
|
|
||||||
function getBase64Code(charCode: number) {
|
|
||||||
if (charCode >= base64Codes.length) {
|
|
||||||
throw new Error("Unable to parse base64 string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = base64Codes[charCode];
|
|
||||||
if (code === 255) {
|
|
||||||
throw new Error("Unable to parse base64 string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBase64FromBytes(bytes: number[] | Uint8Array): string {
|
|
||||||
let result = "",
|
|
||||||
i,
|
|
||||||
l = bytes.length;
|
|
||||||
|
|
||||||
for (i = 2; i < l; i += 3) {
|
|
||||||
result += base64Chars[bytes[i - 2] >> 2];
|
|
||||||
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
|
|
||||||
result += base64Chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)];
|
|
||||||
result += base64Chars[bytes[i] & 0x3f];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === l + 1) {
|
|
||||||
// 1 octet yet to write
|
|
||||||
result += base64Chars[bytes[i - 2] >> 2];
|
|
||||||
result += base64Chars[(bytes[i - 2] & 0x03) << 4];
|
|
||||||
result += "==";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === l) {
|
|
||||||
// 2 octets yet to write
|
|
||||||
result += base64Chars[bytes[i - 2] >> 2];
|
|
||||||
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
|
|
||||||
result += base64Chars[(bytes[i - 1] & 0x0f) << 2];
|
|
||||||
result += "=";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBase64WebEncodingFromBytes(bytes: number[] | Uint8Array): string {
|
|
||||||
return getBase64FromBytes(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBytesFromBase64(str: string): Uint8Array {
|
|
||||||
if (str.length % 4 !== 0) {
|
|
||||||
throw new Error("Unable to parse base64 string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = str.indexOf("=");
|
|
||||||
|
|
||||||
if (index !== -1 && index < str.length - 2) {
|
|
||||||
throw new Error("Unable to parse base64 string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0,
|
|
||||||
n = str.length,
|
|
||||||
result = new Uint8Array(3 * (n / 4)),
|
|
||||||
buffer;
|
|
||||||
|
|
||||||
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
|
|
||||||
buffer =
|
|
||||||
(getBase64Code(str.charCodeAt(i)) << 18) |
|
|
||||||
(getBase64Code(str.charCodeAt(i + 1)) << 12) |
|
|
||||||
(getBase64Code(str.charCodeAt(i + 2)) << 6) |
|
|
||||||
getBase64Code(str.charCodeAt(i + 3));
|
|
||||||
result[j] = buffer >> 16;
|
|
||||||
result[j + 1] = (buffer >> 8) & 0xff;
|
|
||||||
result[j + 2] = buffer & 0xff;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.subarray(0, result.length - missingOctets);
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ import React, { Fragment, MutableRefObject, useCallback, useEffect, useRef, useS
|
||||||
|
|
||||||
import { Box, Button, Grid, Stack, Step, StepLabel, Stepper, Theme, Typography } from "@mui/material";
|
import { Box, Button, Grid, Stack, Step, StepLabel, Stepper, Theme, Typography } from "@mui/material";
|
||||||
import makeStyles from "@mui/styles/makeStyles";
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
|
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
@ -10,16 +11,15 @@ import InformationIcon from "@components/InformationIcon";
|
||||||
import SuccessIcon from "@components/SuccessIcon";
|
import SuccessIcon from "@components/SuccessIcon";
|
||||||
import WebauthnTryIcon from "@components/WebauthnTryIcon";
|
import WebauthnTryIcon from "@components/WebauthnTryIcon";
|
||||||
import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
||||||
import { IdentityToken } from "@constants/SearchParams";
|
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { useQueryParam } from "@hooks/QueryParam";
|
|
||||||
import LoginLayout from "@layouts/LoginLayout";
|
import LoginLayout from "@layouts/LoginLayout";
|
||||||
import { AttestationPublicKeyCredential, AttestationResult, WebauthnTouchState } from "@models/Webauthn";
|
|
||||||
import {
|
import {
|
||||||
finishAttestationCeremony,
|
AttestationResult,
|
||||||
getAttestationCreationOptions,
|
AttestationResultFailureString,
|
||||||
getAttestationPublicKeyCredentialResult,
|
RegistrationResult,
|
||||||
} from "@services/Webauthn";
|
WebauthnTouchState,
|
||||||
|
} from "@models/Webauthn";
|
||||||
|
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn";
|
||||||
|
|
||||||
const steps = ["Confirm device", "Choose name"];
|
const steps = ["Confirm device", "Choose name"];
|
||||||
|
|
||||||
|
@ -33,83 +33,60 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
const { createErrorNotification } = useNotifications();
|
const { createErrorNotification } = useNotifications();
|
||||||
|
|
||||||
const [activeStep, setActiveStep] = React.useState(0);
|
const [activeStep, setActiveStep] = React.useState(0);
|
||||||
const [credential, setCredential] = React.useState(null as null | AttestationPublicKeyCredential);
|
const [result, setResult] = React.useState(null as null | RegistrationResult);
|
||||||
const [creationOptions, setCreationOptions] = useState(null as null | PublicKeyCredentialCreationOptions);
|
const [options, setOptions] = useState(null as null | PublicKeyCredentialCreationOptionsJSON);
|
||||||
const [deviceName, setName] = useState("");
|
const [deviceName, setName] = useState("");
|
||||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||||
const [nameError, setNameError] = useState(false);
|
const [nameError, setNameError] = useState(false);
|
||||||
|
|
||||||
const processToken = useQueryParam(IdentityToken);
|
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishAttestation = async () => {
|
const finishAttestation = async () => {
|
||||||
if (!credential) {
|
if (!result || !result.response) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deviceName.length) {
|
if (!deviceName.length) {
|
||||||
setNameError(true);
|
setNameError(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await finishAttestationCeremony(credential, deviceName);
|
|
||||||
switch (result.status) {
|
const res = await finishRegistration(result.response, deviceName);
|
||||||
|
switch (res.status) {
|
||||||
case AttestationResult.Success:
|
case AttestationResult.Success:
|
||||||
setActiveStep(2);
|
setActiveStep(2);
|
||||||
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
||||||
break;
|
break;
|
||||||
case AttestationResult.Failure:
|
case AttestationResult.Failure:
|
||||||
createErrorNotification(result.message);
|
createErrorNotification(res.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startAttestation = useCallback(async () => {
|
const startRegistration = useCallback(async () => {
|
||||||
|
if (options === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebauthnTouchState.WaitTouch);
|
||||||
setActiveStep(0);
|
setActiveStep(0);
|
||||||
|
|
||||||
const startResult = await getAttestationPublicKeyCredentialResult(creationOptions);
|
const res = await startWebauthnRegistration(options);
|
||||||
|
|
||||||
switch (startResult.result) {
|
if (res.result === AttestationResult.Success) {
|
||||||
case AttestationResult.Success:
|
if (res.response == null) {
|
||||||
if (startResult.credential == null) {
|
|
||||||
throw new Error("Attestation request succeeded but credential is empty");
|
throw new Error("Attestation request succeeded but credential is empty");
|
||||||
}
|
}
|
||||||
setCredential(startResult.credential);
|
|
||||||
|
setResult(res);
|
||||||
setActiveStep(1);
|
setActiveStep(1);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case AttestationResult.FailureToken:
|
|
||||||
createErrorNotification(
|
|
||||||
"You must open the link from the same device and browser that initiated the registration process.",
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AttestationResult.FailureSupport:
|
|
||||||
createErrorNotification("Your browser does not appear to support the configuration.");
|
|
||||||
break;
|
|
||||||
case AttestationResult.FailureSyntax:
|
|
||||||
createErrorNotification(
|
|
||||||
"The attestation challenge was rejected as malformed or incompatible by your browser.",
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AttestationResult.FailureWebauthnNotSupported:
|
|
||||||
createErrorNotification("Your browser does not support the WebAuthN protocol.");
|
|
||||||
break;
|
|
||||||
case AttestationResult.FailureUserConsent:
|
|
||||||
createErrorNotification("You cancelled the attestation request.");
|
|
||||||
break;
|
|
||||||
case AttestationResult.FailureUserVerificationOrResidentKey:
|
|
||||||
createErrorNotification(
|
|
||||||
"Your device does not support user verification or resident keys but this was required.",
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AttestationResult.FailureExcluded:
|
|
||||||
createErrorNotification("You have registered this device already.");
|
|
||||||
break;
|
|
||||||
case AttestationResult.FailureUnknown:
|
|
||||||
createErrorNotification("An unknown error occurred.");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createErrorNotification(AttestationResultFailureString(res.result));
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebauthnTouchState.Failure);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -117,26 +94,26 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
"Failed to register your device. The identity verification process might have timed out.",
|
"Failed to register your device. The identity verification process might have timed out.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [creationOptions, createErrorNotification]);
|
}, [options, createErrorNotification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (creationOptions !== null) {
|
if (options !== null) {
|
||||||
startAttestation();
|
startRegistration();
|
||||||
}
|
}
|
||||||
}, [creationOptions, startAttestation]);
|
}, [options, startRegistration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const result = await getAttestationCreationOptions(processToken);
|
const res = await getAttestationCreationOptions();
|
||||||
if (result.status !== 200 || !result.options) {
|
if (res.status !== 200 || !res.options) {
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
"You must open the link from the same device and browser that initiated the registration process.",
|
"You must open the link from the same device and browser that initiated the registration process.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCreationOptions(result.options);
|
setOptions(res.options);
|
||||||
})();
|
})();
|
||||||
}, [processToken, setCreationOptions, createErrorNotification]);
|
}, [setOptions, createErrorNotification]);
|
||||||
|
|
||||||
function renderStep(step: number) {
|
function renderStep(step: number) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
|
@ -144,10 +121,10 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={styles.icon}>
|
<div className={styles.icon}>
|
||||||
<WebauthnTryIcon onRetryClick={startAttestation} webauthnTouchState={state} />
|
<WebauthnTryIcon onRetryClick={startRegistration} webauthnTouchState={state} />
|
||||||
</div>
|
</div>
|
||||||
<Typography className={styles.instruction}>Touch the token on your security key</Typography>
|
<Typography className={styles.instruction}>Touch the token on your security key</Typography>
|
||||||
<Grid container align="center" spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Stack direction="row" spacing={1} justifyContent="center">
|
<Stack direction="row" spacing={1} justifyContent="center">
|
||||||
<Button color="primary" onClick={handleBackClick}>
|
<Button color="primary" onClick={handleBackClick}>
|
||||||
|
@ -196,7 +173,7 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Stack direction="row" spacing={1} justifyContent="center">
|
<Stack direction="row" spacing={1} justifyContent="center">
|
||||||
<Button color="primary" variant="outlined" onClick={startAttestation}>
|
<Button color="primary" variant="outlined" onClick={startRegistration}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" variant="contained" onClick={finishAttestation}>
|
<Button color="primary" variant="contained" onClick={finishAttestation}>
|
||||||
|
|
|
@ -5,13 +5,9 @@ import { RedirectionURL } from "@constants/SearchParams";
|
||||||
import { useIsMountedRef } from "@hooks/Mounted";
|
import { useIsMountedRef } from "@hooks/Mounted";
|
||||||
import { useQueryParam } from "@hooks/QueryParam";
|
import { useQueryParam } from "@hooks/QueryParam";
|
||||||
import { useWorkflow } from "@hooks/Workflow";
|
import { useWorkflow } from "@hooks/Workflow";
|
||||||
import { AssertionResult, WebauthnTouchState } from "@models/Webauthn";
|
import { AssertionResult, AssertionResultFailureString, WebauthnTouchState } from "@models/Webauthn";
|
||||||
import { AuthenticationLevel } from "@services/State";
|
import { AuthenticationLevel } from "@services/State";
|
||||||
import {
|
import { getAuthenticationOptions, getAuthenticationResult, postAuthenticationResponse } from "@services/Webauthn";
|
||||||
getAssertionPublicKeyCredentialResult,
|
|
||||||
getAssertionRequestOptions,
|
|
||||||
postAssertionPublicKeyCredentialResult,
|
|
||||||
} from "@services/Webauthn";
|
|
||||||
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
|
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -42,55 +38,28 @@ const WebauthnMethod = function (props: Props) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebauthnTouchState.WaitTouch);
|
||||||
const assertionRequestResponse = await getAssertionRequestOptions();
|
const optionsStatus = await getAuthenticationOptions();
|
||||||
|
|
||||||
if (assertionRequestResponse.status !== 200 || assertionRequestResponse.options == null) {
|
if (optionsStatus.status !== 200 || optionsStatus.options == null) {
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebauthnTouchState.Failure);
|
||||||
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getAssertionPublicKeyCredentialResult(assertionRequestResponse.options);
|
const result = await getAuthenticationResult(optionsStatus.options);
|
||||||
|
|
||||||
if (result.result !== AssertionResult.Success) {
|
if (result.result !== AssertionResult.Success) {
|
||||||
if (!mounted.current) return;
|
if (!mounted.current) return;
|
||||||
switch (result.result) {
|
|
||||||
case AssertionResult.FailureUserConsent:
|
|
||||||
onSignInErrorCallback(new Error("You cancelled the assertion request."));
|
|
||||||
break;
|
|
||||||
case AssertionResult.FailureU2FFacetID:
|
|
||||||
onSignInErrorCallback(new Error("The server responded with an invalid Facet ID for the URL."));
|
|
||||||
break;
|
|
||||||
case AssertionResult.FailureSyntax:
|
|
||||||
onSignInErrorCallback(
|
|
||||||
new Error(
|
|
||||||
"The assertion challenge was rejected as malformed or incompatible by your browser.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AssertionResult.FailureWebauthnNotSupported:
|
|
||||||
onSignInErrorCallback(new Error("Your browser does not support the WebAuthN protocol."));
|
|
||||||
break;
|
|
||||||
case AssertionResult.FailureUnrecognized:
|
|
||||||
onSignInErrorCallback(new Error("This device is not registered."));
|
|
||||||
break;
|
|
||||||
case AssertionResult.FailureUnknownSecurity:
|
|
||||||
onSignInErrorCallback(new Error("An unknown security error occurred."));
|
|
||||||
break;
|
|
||||||
case AssertionResult.FailureUnknown:
|
|
||||||
onSignInErrorCallback(new Error("An unknown error occurred."));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
onSignInErrorCallback(new Error("An unexpected error occurred."));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebauthnTouchState.Failure);
|
||||||
|
|
||||||
|
onSignInErrorCallback(new Error(AssertionResultFailureString(result.result)));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.credential == null) {
|
if (result.response == null) {
|
||||||
onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data."));
|
onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data."));
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebauthnTouchState.Failure);
|
||||||
|
|
||||||
|
@ -101,12 +70,7 @@ const WebauthnMethod = function (props: Props) {
|
||||||
|
|
||||||
setState(WebauthnTouchState.InProgress);
|
setState(WebauthnTouchState.InProgress);
|
||||||
|
|
||||||
const response = await postAssertionPublicKeyCredentialResult(
|
const response = await postAuthenticationResponse(result.response, redirectionURL, workflow, workflowID);
|
||||||
result.credential,
|
|
||||||
redirectionURL,
|
|
||||||
workflow,
|
|
||||||
workflowID,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.status === "OK" && response.status === 200) {
|
if (response.data.status === "OK" && response.status === 200) {
|
||||||
onSignInSuccessCallback(response.data.data ? response.data.data.redirect : undefined);
|
onSignInSuccessCallback(response.data.data ? response.data.data.redirect : undefined);
|
||||||
|
|
Loading…
Reference in New Issue