build(deps): use @simplewebauthn/browser
parent
a36c45f1e1
commit
3af20a7daf
|
@ -30,10 +30,15 @@ var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
|
|||
middlewares.IdentityVerificationFinishArgs{
|
||||
ActionClaim: ActionWebauthnRegistration,
|
||||
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
|
||||
}, SecondFactorWebauthnAttestationGET)
|
||||
}, WebauthnAttestationGET)
|
||||
|
||||
// SecondFactorWebauthnAttestationGET returns the attestation challenge from the server.
|
||||
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
||||
// WebauthnAttestationGET returns the attestation challenge from the server.
|
||||
func WebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
||||
WebauthnRegistrationGET(ctx)
|
||||
}
|
||||
|
||||
// WebauthnRegistrationGET returns the attestation challenge from the server.
|
||||
func WebauthnRegistrationGET(ctx *middlewares.AutheliaCtx) {
|
||||
var (
|
||||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
|
@ -92,13 +97,8 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
|
|||
}
|
||||
}
|
||||
|
||||
// WebauthnAttestationPOST processes the attestation challenge response from the client.
|
||||
func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||
type requestPostData struct {
|
||||
Credential json.RawMessage `json:"credential"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// WebauthnRegistrationPOST processes the attestation challenge response from the client.
|
||||
func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
|
||||
var (
|
||||
err error
|
||||
w *webauthn.WebAuthn
|
||||
|
@ -106,9 +106,10 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
|
||||
userSession session.UserSession
|
||||
|
||||
attestationResponse *protocol.ParsedCredentialCreationData
|
||||
response *protocol.ParsedCredentialCreationData
|
||||
|
||||
credential *webauthn.Credential
|
||||
postData *requestPostData
|
||||
bodyJSON bodyRegisterWebauthnRequest
|
||||
)
|
||||
|
||||
if userSession, err = ctx.GetSession(); err != nil {
|
||||
|
@ -135,8 +136,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(ctx.PostBody(), &postData)
|
||||
if err != nil {
|
||||
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
|
||||
ctx.Logger.Errorf("Unable to parse %s assertion request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -144,7 +144,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -152,6 +152,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Logger.WithField("att_format", response.Response.AttestationObject.Format).Debug("Response Data")
|
||||
|
||||
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)
|
||||
|
||||
|
@ -160,7 +162,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
@ -168,6 +170,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Logger.WithField("att_type", credential.AttestationType).Debug("Credential Data")
|
||||
|
||||
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username)
|
||||
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)
|
||||
|
@ -178,8 +182,8 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
}
|
||||
|
||||
for _, existingDevice := range devices {
|
||||
if existingDevice.Description == postData.Description {
|
||||
ctx.Logger.Errorf("%s device for for user '%s' with name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, 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, bodyJSON.Description)
|
||||
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
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 {
|
||||
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.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
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
|
@ -35,6 +36,14 @@ type bodySignWebauthnRequest struct {
|
|||
TargetURL string `json:"targetURL"`
|
||||
Workflow string `json:"workflow"`
|
||||
WorkflowID string `json:"workflowID"`
|
||||
|
||||
Response json.RawMessage `json:"response"`
|
||||
}
|
||||
|
||||
type bodyRegisterWebauthnRequest struct {
|
||||
Description string `json:"description"`
|
||||
|
||||
Response json.RawMessage `json:"response"`
|
||||
}
|
||||
|
||||
type bodyEditWebauthnDeviceRequest struct {
|
||||
|
|
|
@ -96,23 +96,21 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
}
|
||||
|
||||
func identityVerificationValidateToken(ctx *AutheliaCtx) (*jwt.Token, error) {
|
||||
var finishBody IdentityVerificationFinishBody
|
||||
var bodyJSON IdentityVerificationFinishBody
|
||||
|
||||
b := ctx.PostBody()
|
||||
|
||||
err := json.Unmarshal(b, &finishBody)
|
||||
err := json.Unmarshal(ctx.PostBody(), &bodyJSON)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if finishBody.Token == "" {
|
||||
if bodyJSON.Token == "" {
|
||||
ctx.Error(fmt.Errorf("No token provided"), messageOperationFailed)
|
||||
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) {
|
||||
return []byte(ctx.Configuration.JWTSecret), nil
|
||||
})
|
||||
|
@ -185,8 +183,7 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c
|
|||
return
|
||||
}
|
||||
|
||||
err = ctx.Providers.StorageProvider.ConsumeIdentityVerification(ctx, claims.ID, model.NewNullIP(ctx.RemoteIP()))
|
||||
if err != nil {
|
||||
if err = ctx.Providers.StorageProvider.ConsumeIdentityVerification(ctx, claims.ID, model.NewNullIP(ctx.RemoteIP())); err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -238,7 +238,11 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
|||
// Webauthn Endpoints.
|
||||
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/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.POST("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionPOST))
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
"@mui/icons-material": "5.11.0",
|
||||
"@mui/material": "5.11.6",
|
||||
"@mui/styles": "5.11.2",
|
||||
"@simplewebauthn/browser": "^7.0.1",
|
||||
"@simplewebauthn/typescript-types": "^7.0.0",
|
||||
"axios": "1.2.6",
|
||||
"broadcast-channel": "4.20.2",
|
||||
"classnames": "2.3.2",
|
||||
|
|
|
@ -14,6 +14,8 @@ specifiers:
|
|||
'@mui/icons-material': 5.11.0
|
||||
'@mui/material': 5.11.6
|
||||
'@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/react': 13.4.0
|
||||
'@types/jest': 29.4.0
|
||||
|
@ -76,6 +78,8 @@ dependencies:
|
|||
'@mui/icons-material': 5.11.0_j5wvuqirnhynb4halegp2mqooy
|
||||
'@mui/material': 5.11.6_rqh7qj4464ntrqrt6banhaqg4q
|
||||
'@mui/styles': 5.11.2_3stiutgnnbnfnf3uowm5cip22i
|
||||
'@simplewebauthn/browser': 7.0.1
|
||||
'@simplewebauthn/typescript-types': 7.0.0
|
||||
axios: 1.2.6
|
||||
broadcast-channel: 4.20.2
|
||||
classnames: 2.3.2
|
||||
|
@ -3395,6 +3399,14 @@ packages:
|
|||
resolution: {integrity: sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==}
|
||||
dev: true
|
||||
|
|
|
@ -11,7 +11,6 @@ import { WebauthnTouchState } from "@models/Webauthn";
|
|||
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
|
||||
|
||||
interface Props {
|
||||
timer: number;
|
||||
onRetryClick: () => void;
|
||||
webauthnTouchState: WebauthnTouchState;
|
||||
}
|
||||
|
@ -61,7 +60,7 @@ export default function WebauthnTryIcon(props: Props) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Box className={styles.icon} sx={{ minHeight: 101 }} align="middle">
|
||||
<Box className={styles.icon} sx={{ minHeight: 101 }}>
|
||||
{touch}
|
||||
{failure}
|
||||
</Box>
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import {
|
||||
AuthenticationResponseJSON,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from "@simplewebauthn/typescript-types";
|
||||
|
||||
export interface PublicKeyCredentialCreationOptionsStatus {
|
||||
options?: PublicKeyCredentialCreationOptions;
|
||||
options?: PublicKeyCredentialCreationOptionsJSON;
|
||||
status: number;
|
||||
}
|
||||
|
||||
|
@ -7,15 +14,8 @@ export interface CredentialCreation {
|
|||
publicKey: PublicKeyCredentialCreationOptionsJSON;
|
||||
}
|
||||
|
||||
export interface PublicKeyCredentialCreationOptionsJSON
|
||||
extends Omit<PublicKeyCredentialCreationOptions, "challenge" | "excludeCredentials" | "user"> {
|
||||
challenge: string;
|
||||
excludeCredentials?: PublicKeyCredentialDescriptorJSON[];
|
||||
user: PublicKeyCredentialUserEntityJSON;
|
||||
}
|
||||
|
||||
export interface PublicKeyCredentialRequestOptionsStatus {
|
||||
options?: PublicKeyCredentialRequestOptions;
|
||||
options?: PublicKeyCredentialRequestOptionsJSON;
|
||||
status: number;
|
||||
}
|
||||
|
||||
|
@ -23,63 +23,6 @@ export interface CredentialRequest {
|
|||
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 {
|
||||
Success = 1,
|
||||
Failure,
|
||||
|
@ -93,21 +36,11 @@ export enum AttestationResult {
|
|||
FailureToken,
|
||||
}
|
||||
|
||||
export interface AttestationPublicKeyCredentialResult {
|
||||
credential?: AttestationPublicKeyCredential;
|
||||
export interface RegistrationResult {
|
||||
response?: RegistrationResponseJSON;
|
||||
result: AttestationResult;
|
||||
}
|
||||
|
||||
export interface AttestationPublicKeyCredentialResultJSON {
|
||||
credential?: AttestationPublicKeyCredentialJSON;
|
||||
result: AttestationResult;
|
||||
}
|
||||
|
||||
export interface AttestationFinishResult {
|
||||
result: AttestationResult;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export enum AssertionResult {
|
||||
Success = 1,
|
||||
Failure,
|
||||
|
@ -121,18 +54,54 @@ export enum AssertionResult {
|
|||
FailureUnrecognized,
|
||||
}
|
||||
|
||||
export interface DiscoverableAssertionResult {
|
||||
result: AssertionResult;
|
||||
username: string;
|
||||
export function AssertionResultFailureString(result: AssertionResult) {
|
||||
switch (result) {
|
||||
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 {
|
||||
credential?: PublicKeyCredential;
|
||||
result: AssertionResult;
|
||||
export function AttestationResultFailureString(result: AttestationResult) {
|
||||
switch (result) {
|
||||
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.";
|
||||
}
|
||||
|
||||
export interface AssertionPublicKeyCredentialResultJSON {
|
||||
credential?: PublicKeyCredentialJSON;
|
||||
return "";
|
||||
}
|
||||
|
||||
export interface AuthenticationResult {
|
||||
response?: AuthenticationResponseJSON;
|
||||
result: AssertionResult;
|
||||
}
|
||||
|
||||
|
@ -156,7 +125,3 @@ export enum WebauthnTouchState {
|
|||
InProgress = 2,
|
||||
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 WebauthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start";
|
||||
export const WebauthnIdentityFinishPath = basePath + "/api/secondfactor/webauthn/identity/finish";
|
||||
export const WebauthnAttestationPath = basePath + "/api/secondfactor/webauthn/attestation";
|
||||
export const WebauthnRegistrationPath = basePath + "/api/secondfactor/register/webauthn";
|
||||
|
||||
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 {
|
||||
AssertionPublicKeyCredentialResult,
|
||||
AssertionResult,
|
||||
AttestationFinishResult,
|
||||
AttestationPublicKeyCredential,
|
||||
AttestationPublicKeyCredentialJSON,
|
||||
AttestationPublicKeyCredentialResult,
|
||||
AttestationResult,
|
||||
AuthenticatorAttestationResponseFuture,
|
||||
AuthenticationResult,
|
||||
CredentialCreation,
|
||||
CredentialRequest,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialCreationOptionsStatus,
|
||||
PublicKeyCredentialDescriptorJSON,
|
||||
PublicKeyCredentialJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsStatus,
|
||||
RegistrationResult,
|
||||
} from "@models/Webauthn";
|
||||
import {
|
||||
AuthenticationOKResponse,
|
||||
OptionalDataServiceResponse,
|
||||
ServiceResponse,
|
||||
WebauthnAssertionPath,
|
||||
WebauthnAttestationPath,
|
||||
WebauthnDevicePath,
|
||||
WebauthnIdentityFinishPath,
|
||||
WebauthnRegistrationPath,
|
||||
validateStatusAuthentication,
|
||||
} from "@services/Api";
|
||||
import { SignInResponse } from "@services/SignIn";
|
||||
import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from "@utils/Base64";
|
||||
|
||||
export function isWebauthnSecure(): boolean {
|
||||
if (window.isSecureContext) {
|
||||
|
@ -51,120 +48,6 @@ export async function isWebauthnPlatformAuthenticatorAvailable(): Promise<boolea
|
|||
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 {
|
||||
// Docs for this section:
|
||||
// https://w3c.github.io/webauthn/#sctn-op-make-cred
|
||||
|
@ -193,7 +76,7 @@ function getAttestationResultFromDOMException(exception: DOMException): Attestat
|
|||
|
||||
function getAssertionResultFromDOMException(
|
||||
exception: DOMException,
|
||||
requestOptions: PublicKeyCredentialRequestOptions,
|
||||
options: PublicKeyCredentialRequestOptionsJSON,
|
||||
): AssertionResult {
|
||||
// Docs for this section:
|
||||
// https://w3c.github.io/webauthn/#sctn-op-get-assertion
|
||||
|
@ -209,7 +92,7 @@ function getAssertionResultFromDOMException(
|
|||
// § 6.3.3 Step 6 and Step 7.
|
||||
return AssertionResult.FailureUserConsent;
|
||||
case "SecurityError":
|
||||
if (requestOptions.extensions?.appid !== undefined) {
|
||||
if (options.extensions?.appid !== undefined) {
|
||||
// § 10.1 and 10.2 Step 3.
|
||||
return AssertionResult.FailureU2FFacetID;
|
||||
} else {
|
||||
|
@ -221,14 +104,10 @@ function getAssertionResultFromDOMException(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getAttestationCreationOptions(
|
||||
token: null | string,
|
||||
): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
||||
export async function getAttestationCreationOptions(): Promise<PublicKeyCredentialCreationOptionsStatus> {
|
||||
let response: AxiosResponse<ServiceResponse<CredentialCreation>>;
|
||||
|
||||
response = await axios.post<ServiceResponse<CredentialCreation>>(WebauthnIdentityFinishPath, {
|
||||
token: token,
|
||||
});
|
||||
response = await axios.get<ServiceResponse<CredentialCreation>>(WebauthnRegistrationPath);
|
||||
|
||||
if (response.data.status !== "OK" || response.data.data == null) {
|
||||
return {
|
||||
|
@ -237,12 +116,12 @@ export async function getAttestationCreationOptions(
|
|||
}
|
||||
|
||||
return {
|
||||
options: decodePublicKeyCredentialCreationOptions(response.data.data.publicKey),
|
||||
options: response.data.data.publicKey,
|
||||
status: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> {
|
||||
export async function getAuthenticationOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> {
|
||||
let response: AxiosResponse<ServiceResponse<CredentialRequest>>;
|
||||
|
||||
response = await axios.get<ServiceResponse<CredentialRequest>>(WebauthnAssertionPath);
|
||||
|
@ -254,65 +133,55 @@ export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialR
|
|||
}
|
||||
|
||||
return {
|
||||
options: decodePublicKeyCredentialRequestOptions(response.data.data.publicKey),
|
||||
options: response.data.data.publicKey,
|
||||
status: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAttestationPublicKeyCredentialResult(
|
||||
creationOptions: PublicKeyCredentialCreationOptions,
|
||||
): Promise<AttestationPublicKeyCredentialResult> {
|
||||
const result: AttestationPublicKeyCredentialResult = {
|
||||
result: AttestationResult.Success,
|
||||
export async function startWebauthnRegistration(options: PublicKeyCredentialCreationOptionsJSON) {
|
||||
const result: RegistrationResult = {
|
||||
result: AttestationResult.Failure,
|
||||
};
|
||||
|
||||
try {
|
||||
result.credential = (await navigator.credentials.create({
|
||||
publicKey: creationOptions,
|
||||
})) as AttestationPublicKeyCredential;
|
||||
result.response = await startRegistration(options);
|
||||
} catch (e) {
|
||||
result.result = AttestationResult.Failure;
|
||||
|
||||
const exception = e as DOMException;
|
||||
if (exception !== undefined) {
|
||||
result.result = getAttestationResultFromDOMException(exception);
|
||||
|
||||
return result;
|
||||
} 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;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getAssertionPublicKeyCredentialResult(
|
||||
requestOptions: PublicKeyCredentialRequestOptions,
|
||||
): Promise<AssertionPublicKeyCredentialResult> {
|
||||
const result: AssertionPublicKeyCredentialResult = {
|
||||
export async function getAuthenticationResult(options: PublicKeyCredentialRequestOptionsJSON) {
|
||||
const result: AuthenticationResult = {
|
||||
result: AssertionResult.Success,
|
||||
};
|
||||
|
||||
try {
|
||||
result.credential = (await navigator.credentials.get({ publicKey: requestOptions })) as PublicKeyCredential;
|
||||
result.response = await startAuthentication(options);
|
||||
} catch (e) {
|
||||
result.result = AssertionResult.Failure;
|
||||
|
||||
const exception = e as DOMException;
|
||||
if (exception !== undefined) {
|
||||
result.result = getAssertionResultFromDOMException(exception, requestOptions);
|
||||
result.result = getAssertionResultFromDOMException(exception, options);
|
||||
|
||||
return result;
|
||||
} 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;
|
||||
} else {
|
||||
result.result = AssertionResult.Success;
|
||||
|
@ -321,84 +190,53 @@ export async function getAssertionPublicKeyCredentialResult(
|
|||
return result;
|
||||
}
|
||||
|
||||
async function postAttestationPublicKeyCredentialResult(
|
||||
credential: AttestationPublicKeyCredential,
|
||||
async function postRegistrationResponse(
|
||||
response: RegistrationResponseJSON,
|
||||
description: string,
|
||||
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
|
||||
const credentialJSON = encodeAttestationPublicKeyCredential(credential);
|
||||
const postBody = {
|
||||
credential: credentialJSON,
|
||||
return axios.post<OptionalDataServiceResponse<any>>(WebauthnRegistrationPath, {
|
||||
response: response,
|
||||
description: description,
|
||||
};
|
||||
return axios.post<OptionalDataServiceResponse<any>>(WebauthnAttestationPath, postBody);
|
||||
});
|
||||
}
|
||||
|
||||
export async function postAssertionPublicKeyCredentialResult(
|
||||
credential: PublicKeyCredential,
|
||||
export async function postAuthenticationResponse(
|
||||
response: AuthenticationResponseJSON,
|
||||
targetURL: string | undefined,
|
||||
workflow?: string,
|
||||
workflowID?: string,
|
||||
): Promise<AxiosResponse<ServiceResponse<SignInResponse>>> {
|
||||
const credentialJSON = encodeAssertionPublicKeyCredential(credential, targetURL, workflow, workflowID);
|
||||
return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, credentialJSON);
|
||||
) {
|
||||
return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, {
|
||||
response: response,
|
||||
targetURL: targetURL,
|
||||
workflow: workflow,
|
||||
workflowID: workflowID,
|
||||
});
|
||||
}
|
||||
|
||||
export async function finishAttestationCeremony(
|
||||
credential: AttestationPublicKeyCredential,
|
||||
description: string,
|
||||
): Promise<AttestationResult> {
|
||||
export async function finishRegistration(response: RegistrationResponseJSON, description: string) {
|
||||
let result = {
|
||||
status: AttestationResult.Failure,
|
||||
message: "Device registration failed.",
|
||||
} as AttestationResult;
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await postAttestationPublicKeyCredentialResult(credential, description);
|
||||
if (response.data.status === "OK" && (response.status === 200 || response.status === 201)) {
|
||||
const resp = await postRegistrationResponse(response, description);
|
||||
if (resp.data.status === "OK" && (resp.status === 200 || resp.status === 201)) {
|
||||
return {
|
||||
status: AttestationResult.Success,
|
||||
} as AttestationFinishResult;
|
||||
message: "",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error instanceof AxiosError && error.response !== undefined) {
|
||||
result.message = error.response.data.message;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return await axios<AuthenticationOKResponse>({
|
||||
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 makeStyles from "@mui/styles/makeStyles";
|
||||
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
@ -10,16 +11,15 @@ import InformationIcon from "@components/InformationIcon";
|
|||
import SuccessIcon from "@components/SuccessIcon";
|
||||
import WebauthnTryIcon from "@components/WebauthnTryIcon";
|
||||
import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
||||
import { IdentityToken } from "@constants/SearchParams";
|
||||
import { useNotifications } from "@hooks/NotificationsContext";
|
||||
import { useQueryParam } from "@hooks/QueryParam";
|
||||
import LoginLayout from "@layouts/LoginLayout";
|
||||
import { AttestationPublicKeyCredential, AttestationResult, WebauthnTouchState } from "@models/Webauthn";
|
||||
import {
|
||||
finishAttestationCeremony,
|
||||
getAttestationCreationOptions,
|
||||
getAttestationPublicKeyCredentialResult,
|
||||
} from "@services/Webauthn";
|
||||
AttestationResult,
|
||||
AttestationResultFailureString,
|
||||
RegistrationResult,
|
||||
WebauthnTouchState,
|
||||
} from "@models/Webauthn";
|
||||
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn";
|
||||
|
||||
const steps = ["Confirm device", "Choose name"];
|
||||
|
||||
|
@ -33,83 +33,60 @@ const RegisterWebauthn = function (props: Props) {
|
|||
const { createErrorNotification } = useNotifications();
|
||||
|
||||
const [activeStep, setActiveStep] = React.useState(0);
|
||||
const [credential, setCredential] = React.useState(null as null | AttestationPublicKeyCredential);
|
||||
const [creationOptions, setCreationOptions] = useState(null as null | PublicKeyCredentialCreationOptions);
|
||||
const [result, setResult] = React.useState(null as null | RegistrationResult);
|
||||
const [options, setOptions] = useState(null as null | PublicKeyCredentialCreationOptionsJSON);
|
||||
const [deviceName, setName] = useState("");
|
||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||
const [nameError, setNameError] = useState(false);
|
||||
|
||||
const processToken = useQueryParam(IdentityToken);
|
||||
|
||||
const handleBackClick = () => {
|
||||
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
||||
};
|
||||
|
||||
const finishAttestation = async () => {
|
||||
if (!credential) {
|
||||
if (!result || !result.response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deviceName.length) {
|
||||
setNameError(true);
|
||||
return;
|
||||
}
|
||||
const result = await finishAttestationCeremony(credential, deviceName);
|
||||
switch (result.status) {
|
||||
|
||||
const res = await finishRegistration(result.response, deviceName);
|
||||
switch (res.status) {
|
||||
case AttestationResult.Success:
|
||||
setActiveStep(2);
|
||||
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
||||
break;
|
||||
case AttestationResult.Failure:
|
||||
createErrorNotification(result.message);
|
||||
createErrorNotification(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
const startAttestation = useCallback(async () => {
|
||||
const startRegistration = useCallback(async () => {
|
||||
if (options === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(WebauthnTouchState.WaitTouch);
|
||||
setActiveStep(0);
|
||||
|
||||
const startResult = await getAttestationPublicKeyCredentialResult(creationOptions);
|
||||
const res = await startWebauthnRegistration(options);
|
||||
|
||||
switch (startResult.result) {
|
||||
case AttestationResult.Success:
|
||||
if (startResult.credential == null) {
|
||||
if (res.result === AttestationResult.Success) {
|
||||
if (res.response == null) {
|
||||
throw new Error("Attestation request succeeded but credential is empty");
|
||||
}
|
||||
setCredential(startResult.credential);
|
||||
|
||||
setResult(res);
|
||||
setActiveStep(1);
|
||||
|
||||
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);
|
||||
} catch (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.",
|
||||
);
|
||||
}
|
||||
}, [creationOptions, createErrorNotification]);
|
||||
}, [options, createErrorNotification]);
|
||||
|
||||
useEffect(() => {
|
||||
if (creationOptions !== null) {
|
||||
startAttestation();
|
||||
if (options !== null) {
|
||||
startRegistration();
|
||||
}
|
||||
}, [creationOptions, startAttestation]);
|
||||
}, [options, startRegistration]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const result = await getAttestationCreationOptions(processToken);
|
||||
if (result.status !== 200 || !result.options) {
|
||||
const res = await getAttestationCreationOptions();
|
||||
if (res.status !== 200 || !res.options) {
|
||||
createErrorNotification(
|
||||
"You must open the link from the same device and browser that initiated the registration process.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
setCreationOptions(result.options);
|
||||
setOptions(res.options);
|
||||
})();
|
||||
}, [processToken, setCreationOptions, createErrorNotification]);
|
||||
}, [setOptions, createErrorNotification]);
|
||||
|
||||
function renderStep(step: number) {
|
||||
switch (step) {
|
||||
|
@ -144,10 +121,10 @@ const RegisterWebauthn = function (props: Props) {
|
|||
return (
|
||||
<Fragment>
|
||||
<div className={styles.icon}>
|
||||
<WebauthnTryIcon onRetryClick={startAttestation} webauthnTouchState={state} />
|
||||
<WebauthnTryIcon onRetryClick={startRegistration} webauthnTouchState={state} />
|
||||
</div>
|
||||
<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}>
|
||||
<Stack direction="row" spacing={1} justifyContent="center">
|
||||
<Button color="primary" onClick={handleBackClick}>
|
||||
|
@ -196,7 +173,7 @@ const RegisterWebauthn = function (props: Props) {
|
|||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" spacing={1} justifyContent="center">
|
||||
<Button color="primary" variant="outlined" onClick={startAttestation}>
|
||||
<Button color="primary" variant="outlined" onClick={startRegistration}>
|
||||
Back
|
||||
</Button>
|
||||
<Button color="primary" variant="contained" onClick={finishAttestation}>
|
||||
|
|
|
@ -5,13 +5,9 @@ import { RedirectionURL } from "@constants/SearchParams";
|
|||
import { useIsMountedRef } from "@hooks/Mounted";
|
||||
import { useQueryParam } from "@hooks/QueryParam";
|
||||
import { useWorkflow } from "@hooks/Workflow";
|
||||
import { AssertionResult, WebauthnTouchState } from "@models/Webauthn";
|
||||
import { AssertionResult, AssertionResultFailureString, WebauthnTouchState } from "@models/Webauthn";
|
||||
import { AuthenticationLevel } from "@services/State";
|
||||
import {
|
||||
getAssertionPublicKeyCredentialResult,
|
||||
getAssertionRequestOptions,
|
||||
postAssertionPublicKeyCredentialResult,
|
||||
} from "@services/Webauthn";
|
||||
import { getAuthenticationOptions, getAuthenticationResult, postAuthenticationResponse } from "@services/Webauthn";
|
||||
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
|
||||
|
||||
export interface Props {
|
||||
|
@ -42,55 +38,28 @@ const WebauthnMethod = function (props: Props) {
|
|||
|
||||
try {
|
||||
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);
|
||||
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await getAssertionPublicKeyCredentialResult(assertionRequestResponse.options);
|
||||
const result = await getAuthenticationResult(optionsStatus.options);
|
||||
|
||||
if (result.result !== AssertionResult.Success) {
|
||||
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);
|
||||
|
||||
onSignInErrorCallback(new Error(AssertionResultFailureString(result.result)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.credential == null) {
|
||||
if (result.response == null) {
|
||||
onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data."));
|
||||
setState(WebauthnTouchState.Failure);
|
||||
|
||||
|
@ -101,12 +70,7 @@ const WebauthnMethod = function (props: Props) {
|
|||
|
||||
setState(WebauthnTouchState.InProgress);
|
||||
|
||||
const response = await postAssertionPublicKeyCredentialResult(
|
||||
result.credential,
|
||||
redirectionURL,
|
||||
workflow,
|
||||
workflowID,
|
||||
);
|
||||
const response = await postAuthenticationResponse(result.response, redirectionURL, workflow, workflowID);
|
||||
|
||||
if (response.data.status === "OK" && response.status === 200) {
|
||||
onSignInSuccessCallback(response.data.data ? response.data.data.redirect : undefined);
|
||||
|
|
Loading…
Reference in New Issue