Revert "web/flow: cleanup WebAuthn helper functions (#14460)"
This reverts commit e86c40a00c.
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
			
			
This commit is contained in:
		
							
								
								
									
										59
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -37,6 +37,7 @@ | ||||
|                 "@sentry/browser": "^9.30.0", | ||||
|                 "@spotlightjs/spotlight": "^3.0.1", | ||||
|                 "@webcomponents/webcomponentsjs": "^2.8.0", | ||||
|                 "base64-js": "^1.5.1", | ||||
|                 "change-case": "^5.4.4", | ||||
|                 "chart.js": "^4.4.9", | ||||
|                 "chartjs-adapter-date-fns": "^3.0.0", | ||||
| @ -68,7 +69,6 @@ | ||||
|                 "trusted-types": "^2.0.0", | ||||
|                 "ts-pattern": "^5.7.1", | ||||
|                 "unist-util-visit": "^5.0.0", | ||||
|                 "webauthn-polyfills": "^0.1.7", | ||||
|                 "webcomponent-qr-code": "^1.2.0", | ||||
|                 "yaml": "^2.8.0" | ||||
|             }, | ||||
| @ -4768,12 +4768,6 @@ | ||||
|             "dev": true, | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@simplewebauthn/types": { | ||||
|             "version": "11.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-11.0.0.tgz", | ||||
|             "integrity": "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@sinclair/typebox": { | ||||
|             "version": "0.27.8", | ||||
|             "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", | ||||
| @ -7361,12 +7355,6 @@ | ||||
|             "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", | ||||
|             "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" | ||||
|         }, | ||||
|         "node_modules/@types/ua-parser-js": { | ||||
|             "version": "0.7.39", | ||||
|             "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", | ||||
|             "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/unist": { | ||||
|             "version": "3.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", | ||||
| @ -11989,7 +11977,8 @@ | ||||
|         "node_modules/compare-versions": { | ||||
|             "version": "6.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", | ||||
|             "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==" | ||||
|             "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/compatx": { | ||||
|             "version": "0.1.8", | ||||
| @ -27224,32 +27213,6 @@ | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/ua-parser-js": { | ||||
|             "version": "1.0.40", | ||||
|             "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", | ||||
|             "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", | ||||
|             "funding": [ | ||||
|                 { | ||||
|                     "type": "opencollective", | ||||
|                     "url": "https://opencollective.com/ua-parser-js" | ||||
|                 }, | ||||
|                 { | ||||
|                     "type": "paypal", | ||||
|                     "url": "https://paypal.me/faisalman" | ||||
|                 }, | ||||
|                 { | ||||
|                     "type": "github", | ||||
|                     "url": "https://github.com/sponsors/faisalman" | ||||
|                 } | ||||
|             ], | ||||
|             "license": "MIT", | ||||
|             "bin": { | ||||
|                 "ua-parser-js": "script/cli.js" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/uc.micro": { | ||||
|             "version": "2.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", | ||||
| @ -28599,18 +28562,6 @@ | ||||
|             "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", | ||||
|             "license": "Apache-2.0" | ||||
|         }, | ||||
|         "node_modules/webauthn-polyfills": { | ||||
|             "version": "0.1.7", | ||||
|             "resolved": "https://registry.npmjs.org/webauthn-polyfills/-/webauthn-polyfills-0.1.7.tgz", | ||||
|             "integrity": "sha512-tOA5KPHhN8j8EBA9I90bYmsEc6CAKd1SbWJzmVn0hmTfvfiNJLGGzRPlSW4fKiQPm8BC6doPQC0CnaQdhxsL3Q==", | ||||
|             "license": "Apache-2.0", | ||||
|             "dependencies": { | ||||
|                 "@simplewebauthn/types": "^11.0.0", | ||||
|                 "@types/ua-parser-js": "^0.7.39", | ||||
|                 "compare-versions": "^6.1.1", | ||||
|                 "ua-parser-js": "^1.0.39" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/webcomponent-qr-code": { | ||||
|             "version": "1.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/webcomponent-qr-code/-/webcomponent-qr-code-1.2.0.tgz", | ||||
| @ -29535,11 +29486,11 @@ | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@goauthentik/api": "^2024.6.0-1719577139", | ||||
|                 "base64-js": "^1.5.1", | ||||
|                 "bootstrap": "^4.6.1", | ||||
|                 "formdata-polyfill": "^4.0.10", | ||||
|                 "jquery": "^3.7.1", | ||||
|                 "weakmap-polyfill": "^2.0.4", | ||||
|                 "webauthn-polyfills": "^0.1.7" | ||||
|                 "weakmap-polyfill": "^2.0.4" | ||||
|             }, | ||||
|             "devDependencies": { | ||||
|                 "@goauthentik/core": "^1.0.0", | ||||
|  | ||||
| @ -108,6 +108,7 @@ | ||||
|         "@sentry/browser": "^9.30.0", | ||||
|         "@spotlightjs/spotlight": "^3.0.1", | ||||
|         "@webcomponents/webcomponentsjs": "^2.8.0", | ||||
|         "base64-js": "^1.5.1", | ||||
|         "change-case": "^5.4.4", | ||||
|         "chart.js": "^4.4.9", | ||||
|         "chartjs-adapter-date-fns": "^3.0.0", | ||||
| @ -139,7 +140,6 @@ | ||||
|         "trusted-types": "^2.0.0", | ||||
|         "ts-pattern": "^5.7.1", | ||||
|         "unist-util-visit": "^5.0.0", | ||||
|         "webauthn-polyfills": "^0.1.7", | ||||
|         "webcomponent-qr-code": "^1.2.0", | ||||
|         "yaml": "^2.8.0" | ||||
|     }, | ||||
|  | ||||
| @ -11,11 +11,11 @@ | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@goauthentik/api": "^2024.6.0-1719577139", | ||||
|         "base64-js": "^1.5.1", | ||||
|         "bootstrap": "^4.6.1", | ||||
|         "formdata-polyfill": "^4.0.10", | ||||
|         "jquery": "^3.7.1", | ||||
|         "weakmap-polyfill": "^2.0.4", | ||||
|         "webauthn-polyfills": "^0.1.7" | ||||
|         "weakmap-polyfill": "^2.0.4" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@goauthentik/core": "^1.0.0", | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { fromByteArray } from "base64-js"; | ||||
| import "formdata-polyfill"; | ||||
| import $ from "jquery"; | ||||
| import "weakmap-polyfill"; | ||||
| import "webauthn-polyfills"; | ||||
|  | ||||
| import { | ||||
|     type AuthenticatorValidationChallenge, | ||||
| @ -257,9 +257,47 @@ class AutosubmitStage extends Stage<AutosubmitChallenge> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| export interface Assertion { | ||||
|     id: string; | ||||
|     rawId: string; | ||||
|     type: string; | ||||
|     registrationClientExtensions: string; | ||||
|     response: { | ||||
|         clientDataJSON: string; | ||||
|         attestationObject: string; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export interface AuthAssertion { | ||||
|     id: string; | ||||
|     rawId: string; | ||||
|     type: string; | ||||
|     assertionClientExtensions: string; | ||||
|     response: { | ||||
|         clientDataJSON: string; | ||||
|         authenticatorData: string; | ||||
|         signature: string; | ||||
|         userHandle: string | null; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> { | ||||
|     deviceChallenge?: DeviceChallenge; | ||||
|  | ||||
|     b64enc(buf: Uint8Array): string { | ||||
|         return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); | ||||
|     } | ||||
|  | ||||
|     b64RawEnc(buf: Uint8Array): string { | ||||
|         return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_"); | ||||
|     } | ||||
|  | ||||
|     u8arr(input: string): Uint8Array { | ||||
|         return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) => | ||||
|             c.charCodeAt(0), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     checkWebAuthnSupport(): boolean { | ||||
|         if ("credentials" in navigator) { | ||||
|             return true; | ||||
| @ -272,6 +310,98 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Transforms items in the credentialCreateOptions generated on the server | ||||
|      * into byte arrays expected by the navigator.credentials.create() call | ||||
|      */ | ||||
|     transformCredentialCreateOptions( | ||||
|         credentialCreateOptions: PublicKeyCredentialCreationOptions, | ||||
|         userId: string, | ||||
|     ): PublicKeyCredentialCreationOptions { | ||||
|         const user = credentialCreateOptions.user; | ||||
|         // Because json can't contain raw bytes, the server base64-encodes the User ID | ||||
|         // So to get the base64 encoded byte array, we first need to convert it to a regular | ||||
|         // string, then a byte array, re-encode it and wrap that in an array. | ||||
|         const stringId = decodeURIComponent(window.atob(userId)); | ||||
|         user.id = this.u8arr(this.b64enc(this.u8arr(stringId))); | ||||
|         const challenge = this.u8arr(credentialCreateOptions.challenge.toString()); | ||||
|  | ||||
|         return Object.assign({}, credentialCreateOptions, { | ||||
|             challenge, | ||||
|             user, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Transforms the binary data in the credential into base64 strings | ||||
|      * for posting to the server. | ||||
|      * @param {PublicKeyCredential} newAssertion | ||||
|      */ | ||||
|     transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion { | ||||
|         const attObj = new Uint8Array( | ||||
|             (newAssertion.response as AuthenticatorAttestationResponse).attestationObject, | ||||
|         ); | ||||
|         const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON); | ||||
|         const rawId = new Uint8Array(newAssertion.rawId); | ||||
|  | ||||
|         const registrationClientExtensions = newAssertion.getClientExtensionResults(); | ||||
|         return { | ||||
|             id: newAssertion.id, | ||||
|             rawId: this.b64enc(rawId), | ||||
|             type: newAssertion.type, | ||||
|             registrationClientExtensions: JSON.stringify(registrationClientExtensions), | ||||
|             response: { | ||||
|                 clientDataJSON: this.b64enc(clientDataJSON), | ||||
|                 attestationObject: this.b64enc(attObj), | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     transformCredentialRequestOptions( | ||||
|         credentialRequestOptions: PublicKeyCredentialRequestOptions, | ||||
|     ): PublicKeyCredentialRequestOptions { | ||||
|         const challenge = this.u8arr(credentialRequestOptions.challenge.toString()); | ||||
|  | ||||
|         const allowCredentials = (credentialRequestOptions.allowCredentials || []).map( | ||||
|             (credentialDescriptor) => { | ||||
|                 const id = this.u8arr(credentialDescriptor.id.toString()); | ||||
|                 return Object.assign({}, credentialDescriptor, { id }); | ||||
|             }, | ||||
|         ); | ||||
|  | ||||
|         return Object.assign({}, credentialRequestOptions, { | ||||
|             challenge, | ||||
|             allowCredentials, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Encodes the binary data in the assertion into strings for posting to the server. | ||||
|      * @param {PublicKeyCredential} newAssertion | ||||
|      */ | ||||
|     transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion { | ||||
|         const response = newAssertion.response as AuthenticatorAssertionResponse; | ||||
|         const authData = new Uint8Array(response.authenticatorData); | ||||
|         const clientDataJSON = new Uint8Array(response.clientDataJSON); | ||||
|         const rawId = new Uint8Array(newAssertion.rawId); | ||||
|         const sig = new Uint8Array(response.signature); | ||||
|         const assertionClientExtensions = newAssertion.getClientExtensionResults(); | ||||
|  | ||||
|         return { | ||||
|             id: newAssertion.id, | ||||
|             rawId: this.b64enc(rawId), | ||||
|             type: newAssertion.type, | ||||
|             assertionClientExtensions: JSON.stringify(assertionClientExtensions), | ||||
|  | ||||
|             response: { | ||||
|                 clientDataJSON: this.b64RawEnc(clientDataJSON), | ||||
|                 signature: this.b64RawEnc(sig), | ||||
|                 authenticatorData: this.b64RawEnc(authData), | ||||
|                 userHandle: null, | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         if (this.challenge.deviceChallenges.length === 1) { | ||||
|             this.deviceChallenge = this.challenge.deviceChallenges[0]; | ||||
| @ -375,8 +505,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> | ||||
|             `); | ||||
|         navigator.credentials | ||||
|             .get({ | ||||
|                 publicKey: PublicKeyCredential.parseRequestOptionsFromJSON( | ||||
|                     this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptionsJSON, | ||||
|                 publicKey: this.transformCredentialRequestOptions( | ||||
|                     this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions, | ||||
|                 ), | ||||
|             }) | ||||
|             .then((assertion) => { | ||||
| @ -384,9 +514,15 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> | ||||
|                     throw new Error("No assertion"); | ||||
|                 } | ||||
|                 try { | ||||
|                     // we now have an authentication assertion! encode the byte arrays contained | ||||
|                     // in the assertion data as strings for posting to the server | ||||
|                     const transformedAssertionForServer = this.transformAssertionForServer( | ||||
|                         assertion as PublicKeyCredential, | ||||
|                     ); | ||||
|  | ||||
|                     // post the assertion to the server for verification. | ||||
|                     this.executor.submit({ | ||||
|                         webauthn: (assertion as PublicKeyCredential).toJSON(), | ||||
|                         webauthn: transformedAssertionForServer, | ||||
|                     }); | ||||
|                 } catch (err) { | ||||
|                     throw new Error(`Error when validating assertion on server: ${err}`); | ||||
|  | ||||
| @ -1,5 +1,21 @@ | ||||
| import * as base64js from "base64-js"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
|  | ||||
| export function b64enc(buf: Uint8Array): string { | ||||
|     return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); | ||||
| } | ||||
|  | ||||
| export function b64RawEnc(buf: Uint8Array): string { | ||||
|     return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_"); | ||||
| } | ||||
|  | ||||
| export function u8arr(input: string): Uint8Array { | ||||
|     return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) => | ||||
|         c.charCodeAt(0), | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export function checkWebAuthnSupport() { | ||||
|     if ("credentials" in navigator) { | ||||
|         return; | ||||
| @ -9,3 +25,121 @@ export function checkWebAuthnSupport() { | ||||
|     } | ||||
|     throw new Error(msg("WebAuthn not supported by browser.")); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Transforms items in the credentialCreateOptions generated on the server | ||||
|  * into byte arrays expected by the navigator.credentials.create() call | ||||
|  */ | ||||
| export function transformCredentialCreateOptions( | ||||
|     credentialCreateOptions: PublicKeyCredentialCreationOptions, | ||||
|     userId: string, | ||||
| ): PublicKeyCredentialCreationOptions { | ||||
|     const user = credentialCreateOptions.user; | ||||
|     // Because json can't contain raw bytes, the server base64-encodes the User ID | ||||
|     // So to get the base64 encoded byte array, we first need to convert it to a regular | ||||
|     // string, then a byte array, re-encode it and wrap that in an array. | ||||
|     const stringId = decodeURIComponent(window.atob(userId)); | ||||
|     user.id = u8arr(b64enc(u8arr(stringId))); | ||||
|     const challenge = u8arr(credentialCreateOptions.challenge.toString()); | ||||
|  | ||||
|     return { | ||||
|         ...credentialCreateOptions, | ||||
|         challenge, | ||||
|         user, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export interface Assertion { | ||||
|     id: string; | ||||
|     rawId: string; | ||||
|     type: string; | ||||
|     registrationClientExtensions: string; | ||||
|     response: { | ||||
|         clientDataJSON: string; | ||||
|         attestationObject: string; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Transforms the binary data in the credential into base64 strings | ||||
|  * for posting to the server. | ||||
|  * @param {PublicKeyCredential} newAssertion | ||||
|  */ | ||||
| export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion { | ||||
|     const attObj = new Uint8Array( | ||||
|         (newAssertion.response as AuthenticatorAttestationResponse).attestationObject, | ||||
|     ); | ||||
|     const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON); | ||||
|     const rawId = new Uint8Array(newAssertion.rawId); | ||||
|  | ||||
|     const registrationClientExtensions = newAssertion.getClientExtensionResults(); | ||||
|     return { | ||||
|         id: newAssertion.id, | ||||
|         rawId: b64enc(rawId), | ||||
|         type: newAssertion.type, | ||||
|         registrationClientExtensions: JSON.stringify(registrationClientExtensions), | ||||
|         response: { | ||||
|             clientDataJSON: b64enc(clientDataJSON), | ||||
|             attestationObject: b64enc(attObj), | ||||
|         }, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export function transformCredentialRequestOptions( | ||||
|     credentialRequestOptions: PublicKeyCredentialRequestOptions, | ||||
| ): PublicKeyCredentialRequestOptions { | ||||
|     const challenge = u8arr(credentialRequestOptions.challenge.toString()); | ||||
|  | ||||
|     const allowCredentials = (credentialRequestOptions.allowCredentials || []).map( | ||||
|         (credentialDescriptor) => { | ||||
|             const id = u8arr(credentialDescriptor.id.toString()); | ||||
|             return Object.assign({}, credentialDescriptor, { id }); | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     return { | ||||
|         ...credentialRequestOptions, | ||||
|         challenge, | ||||
|         allowCredentials, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export interface AuthAssertion { | ||||
|     id: string; | ||||
|     rawId: string; | ||||
|     type: string; | ||||
|     assertionClientExtensions: string; | ||||
|     response: { | ||||
|         clientDataJSON: string; | ||||
|         authenticatorData: string; | ||||
|         signature: string; | ||||
|         userHandle: string | null; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Encodes the binary data in the assertion into strings for posting to the server. | ||||
|  * @param {PublicKeyCredential} newAssertion | ||||
|  */ | ||||
| export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion { | ||||
|     const response = newAssertion.response as AuthenticatorAssertionResponse; | ||||
|     const authData = new Uint8Array(response.authenticatorData); | ||||
|     const clientDataJSON = new Uint8Array(response.clientDataJSON); | ||||
|     const rawId = new Uint8Array(newAssertion.rawId); | ||||
|     const sig = new Uint8Array(response.signature); | ||||
|     const assertionClientExtensions = newAssertion.getClientExtensionResults(); | ||||
|  | ||||
|     return { | ||||
|         id: newAssertion.id, | ||||
|         rawId: b64enc(rawId), | ||||
|         type: newAssertion.type, | ||||
|         assertionClientExtensions: JSON.stringify(assertionClientExtensions), | ||||
|  | ||||
|         response: { | ||||
|             clientDataJSON: b64RawEnc(clientDataJSON), | ||||
|             signature: b64RawEnc(sig), | ||||
|             authenticatorData: b64RawEnc(authData), | ||||
|             userHandle: null, | ||||
|         }, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,8 @@ | ||||
| import { checkWebAuthnSupport } from "@goauthentik/common/helpers/webauthn"; | ||||
| import { | ||||
|     checkWebAuthnSupport, | ||||
|     transformAssertionForServer, | ||||
|     transformCredentialRequestOptions, | ||||
| } from "@goauthentik/common/helpers/webauthn"; | ||||
| import "@goauthentik/elements/EmptyState"; | ||||
| import { BaseDeviceStage } from "@goauthentik/flow/stages/authenticator_validate/base"; | ||||
|  | ||||
| @ -34,12 +38,12 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage< | ||||
|     async authenticate(): Promise<void> { | ||||
|         // request the authenticator to create an assertion signature using the | ||||
|         // credential private key | ||||
|         let assertion: PublicKeyCredential; | ||||
|         let assertion; | ||||
|         checkWebAuthnSupport(); | ||||
|         try { | ||||
|             assertion = (await navigator.credentials.get({ | ||||
|             assertion = await navigator.credentials.get({ | ||||
|                 publicKey: this.transformedCredentialRequestOptions, | ||||
|             })) as PublicKeyCredential; | ||||
|             }); | ||||
|             if (!assertion) { | ||||
|                 throw new Error("Assertions is empty"); | ||||
|             } | ||||
| @ -47,11 +51,17 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage< | ||||
|             throw new Error(`Error when creating credential: ${err}`); | ||||
|         } | ||||
|  | ||||
|         // we now have an authentication assertion! encode the byte arrays contained | ||||
|         // in the assertion data as strings for posting to the server | ||||
|         const transformedAssertionForServer = transformAssertionForServer( | ||||
|             assertion as PublicKeyCredential, | ||||
|         ); | ||||
|  | ||||
|         // post the assertion to the server for verification. | ||||
|         try { | ||||
|             await this.host?.submit( | ||||
|                 { | ||||
|                     webauthn: assertion.toJSON(), | ||||
|                     webauthn: transformedAssertionForServer, | ||||
|                 }, | ||||
|                 { | ||||
|                     invisible: true, | ||||
| @ -64,10 +74,12 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage< | ||||
|  | ||||
|     updated(changedProperties: PropertyValues<this>) { | ||||
|         if (changedProperties.has("challenge") && this.challenge !== undefined) { | ||||
|             // convert certain members of the PublicKeyCredentialRequestOptions into | ||||
|             // byte arrays as expected by the spec. | ||||
|             const credentialRequestOptions = this.deviceChallenge | ||||
|                 ?.challenge as unknown as PublicKeyCredentialRequestOptionsJSON; | ||||
|                 ?.challenge as PublicKeyCredentialRequestOptions; | ||||
|             this.transformedCredentialRequestOptions = | ||||
|                 PublicKeyCredential.parseRequestOptionsFromJSON(credentialRequestOptions); | ||||
|                 transformCredentialRequestOptions(credentialRequestOptions); | ||||
|             this.authenticateWrapper(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1,4 +1,9 @@ | ||||
| import { checkWebAuthnSupport } from "@goauthentik/common/helpers/webauthn"; | ||||
| import { | ||||
|     Assertion, | ||||
|     checkWebAuthnSupport, | ||||
|     transformCredentialCreateOptions, | ||||
|     transformNewAssertionForServer, | ||||
| } from "@goauthentik/common/helpers/webauthn"; | ||||
| import "@goauthentik/elements/EmptyState"; | ||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
|  | ||||
| @ -19,6 +24,10 @@ import { | ||||
|     AuthenticatorWebAuthnChallengeResponseRequest, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| export interface WebAuthnAuthenticatorRegisterChallengeResponse { | ||||
|     response: Assertion; | ||||
| } | ||||
|  | ||||
| @customElement("ak-stage-authenticator-webauthn") | ||||
| export class WebAuthnAuthenticatorRegisterStage extends BaseStage< | ||||
|     AuthenticatorWebAuthnChallenge, | ||||
| @ -59,7 +68,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage< | ||||
|         } | ||||
|         checkWebAuthnSupport(); | ||||
|         // request the authenticator(s) to create a new credential keypair. | ||||
|         let credential: PublicKeyCredential; | ||||
|         let credential; | ||||
|         try { | ||||
|             credential = (await navigator.credentials.create({ | ||||
|                 publicKey: this.publicKeyCredentialCreateOptions, | ||||
| @ -71,12 +80,16 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage< | ||||
|             throw new Error(msg(str`Error creating credential: ${err}`)); | ||||
|         } | ||||
|  | ||||
|         // we now have a new credential! We now need to encode the byte arrays | ||||
|         // in the credential into strings, for posting to our server. | ||||
|         const newAssertionForServer = transformNewAssertionForServer(credential); | ||||
|  | ||||
|         // post the transformed credential data to the server for validation | ||||
|         // and storing the public key | ||||
|         try { | ||||
|             await this.host?.submit( | ||||
|                 { | ||||
|                     response: credential.toJSON(), | ||||
|                     response: newAssertionForServer, | ||||
|                 }, | ||||
|                 { | ||||
|                     invisible: true, | ||||
| @ -105,10 +118,12 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage< | ||||
|  | ||||
|     updated(changedProperties: PropertyValues<this>) { | ||||
|         if (changedProperties.has("challenge") && this.challenge !== undefined) { | ||||
|             this.publicKeyCredentialCreateOptions = | ||||
|                 PublicKeyCredential.parseCreationOptionsFromJSON( | ||||
|                     this.challenge?.registration as PublicKeyCredentialCreationOptionsJSON, | ||||
|                 ); | ||||
|             // convert certain members of the PublicKeyCredentialCreateOptions into | ||||
|             // byte arrays as expected by the spec. | ||||
|             this.publicKeyCredentialCreateOptions = transformCredentialCreateOptions( | ||||
|                 this.challenge?.registration as PublicKeyCredentialCreationOptions, | ||||
|                 this.challenge?.registration.user.id, | ||||
|             ); | ||||
|             this.registerWrapper(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -3,7 +3,6 @@ import "construct-style-sheets-polyfill"; | ||||
| import "@webcomponents/webcomponentsjs"; | ||||
| import "lit/polyfill-support.js"; | ||||
| import "core-js/actual"; | ||||
| import "webauthn-polyfills"; | ||||
|  | ||||
| import "@formatjs/intl-listformat/polyfill"; | ||||
| import "@formatjs/intl-listformat/locale-data/en"; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L.
					Jens L.