Skip to content

Commit 4c8dbbc

Browse files
BeryJucavefire
authored andcommitted
revert: web/flow: cleanup WebAuthn helper functions (goauthentik#14460)" (goauthentik#15172)
Revert "web/flow: cleanup WebAuthn helper functions (goauthentik#14460)" This reverts commit e86c40a. Signed-off-by: Jens Langhammer <jens@goauthentik.io> # Conflicts: # web/package-lock.json
1 parent dd0bc07 commit 4c8dbbc

File tree

8 files changed

+323
-76
lines changed

8 files changed

+323
-76
lines changed

web/package-lock.json

Lines changed: 5 additions & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"@sentry/browser": "^9.30.0",
109109
"@spotlightjs/spotlight": "^3.0.1",
110110
"@webcomponents/webcomponentsjs": "^2.8.0",
111+
"base64-js": "^1.5.1",
111112
"change-case": "^5.4.4",
112113
"chart.js": "^4.4.9",
113114
"chartjs-adapter-date-fns": "^3.0.0",
@@ -139,7 +140,6 @@
139140
"trusted-types": "^2.0.0",
140141
"ts-pattern": "^5.7.1",
141142
"unist-util-visit": "^5.0.0",
142-
"webauthn-polyfills": "^0.1.7",
143143
"webcomponent-qr-code": "^1.2.0",
144144
"yaml": "^2.8.0"
145145
},

web/packages/sfe/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
},
1212
"dependencies": {
1313
"@goauthentik/api": "^2024.6.0-1719577139",
14+
"base64-js": "^1.5.1",
1415
"bootstrap": "^4.6.1",
1516
"formdata-polyfill": "^4.0.10",
1617
"jquery": "^3.7.1",
17-
"weakmap-polyfill": "^2.0.4",
18-
"webauthn-polyfills": "^0.1.7"
18+
"weakmap-polyfill": "^2.0.4"
1919
},
2020
"devDependencies": {
2121
"@goauthentik/core": "^1.0.0",

web/packages/sfe/src/index.ts

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { fromByteArray } from "base64-js";
12
import "formdata-polyfill";
23
import $ from "jquery";
34
import "weakmap-polyfill";
4-
import "webauthn-polyfills";
55

66
import {
77
type AuthenticatorValidationChallenge,
@@ -257,9 +257,47 @@ class AutosubmitStage extends Stage<AutosubmitChallenge> {
257257
}
258258
}
259259

260+
export interface Assertion {
261+
id: string;
262+
rawId: string;
263+
type: string;
264+
registrationClientExtensions: string;
265+
response: {
266+
clientDataJSON: string;
267+
attestationObject: string;
268+
};
269+
}
270+
271+
export interface AuthAssertion {
272+
id: string;
273+
rawId: string;
274+
type: string;
275+
assertionClientExtensions: string;
276+
response: {
277+
clientDataJSON: string;
278+
authenticatorData: string;
279+
signature: string;
280+
userHandle: string | null;
281+
};
282+
}
283+
260284
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
261285
deviceChallenge?: DeviceChallenge;
262286

287+
b64enc(buf: Uint8Array): string {
288+
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
289+
}
290+
291+
b64RawEnc(buf: Uint8Array): string {
292+
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
293+
}
294+
295+
u8arr(input: string): Uint8Array {
296+
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
297+
c.charCodeAt(0),
298+
);
299+
}
300+
263301
checkWebAuthnSupport(): boolean {
264302
if ("credentials" in navigator) {
265303
return true;
@@ -272,6 +310,98 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
272310
return false;
273311
}
274312

313+
/**
314+
* Transforms items in the credentialCreateOptions generated on the server
315+
* into byte arrays expected by the navigator.credentials.create() call
316+
*/
317+
transformCredentialCreateOptions(
318+
credentialCreateOptions: PublicKeyCredentialCreationOptions,
319+
userId: string,
320+
): PublicKeyCredentialCreationOptions {
321+
const user = credentialCreateOptions.user;
322+
// Because json can't contain raw bytes, the server base64-encodes the User ID
323+
// So to get the base64 encoded byte array, we first need to convert it to a regular
324+
// string, then a byte array, re-encode it and wrap that in an array.
325+
const stringId = decodeURIComponent(window.atob(userId));
326+
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
327+
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
328+
329+
return Object.assign({}, credentialCreateOptions, {
330+
challenge,
331+
user,
332+
});
333+
}
334+
335+
/**
336+
* Transforms the binary data in the credential into base64 strings
337+
* for posting to the server.
338+
* @param {PublicKeyCredential} newAssertion
339+
*/
340+
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
341+
const attObj = new Uint8Array(
342+
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
343+
);
344+
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
345+
const rawId = new Uint8Array(newAssertion.rawId);
346+
347+
const registrationClientExtensions = newAssertion.getClientExtensionResults();
348+
return {
349+
id: newAssertion.id,
350+
rawId: this.b64enc(rawId),
351+
type: newAssertion.type,
352+
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
353+
response: {
354+
clientDataJSON: this.b64enc(clientDataJSON),
355+
attestationObject: this.b64enc(attObj),
356+
},
357+
};
358+
}
359+
360+
transformCredentialRequestOptions(
361+
credentialRequestOptions: PublicKeyCredentialRequestOptions,
362+
): PublicKeyCredentialRequestOptions {
363+
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
364+
365+
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
366+
(credentialDescriptor) => {
367+
const id = this.u8arr(credentialDescriptor.id.toString());
368+
return Object.assign({}, credentialDescriptor, { id });
369+
},
370+
);
371+
372+
return Object.assign({}, credentialRequestOptions, {
373+
challenge,
374+
allowCredentials,
375+
});
376+
}
377+
378+
/**
379+
* Encodes the binary data in the assertion into strings for posting to the server.
380+
* @param {PublicKeyCredential} newAssertion
381+
*/
382+
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
383+
const response = newAssertion.response as AuthenticatorAssertionResponse;
384+
const authData = new Uint8Array(response.authenticatorData);
385+
const clientDataJSON = new Uint8Array(response.clientDataJSON);
386+
const rawId = new Uint8Array(newAssertion.rawId);
387+
const sig = new Uint8Array(response.signature);
388+
const assertionClientExtensions = newAssertion.getClientExtensionResults();
389+
390+
return {
391+
id: newAssertion.id,
392+
rawId: this.b64enc(rawId),
393+
type: newAssertion.type,
394+
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
395+
396+
response: {
397+
clientDataJSON: this.b64RawEnc(clientDataJSON),
398+
signature: this.b64RawEnc(sig),
399+
authenticatorData: this.b64RawEnc(authData),
400+
userHandle: null,
401+
},
402+
};
403+
}
404+
275405
render() {
276406
if (this.challenge.deviceChallenges.length === 1) {
277407
this.deviceChallenge = this.challenge.deviceChallenges[0];
@@ -375,18 +505,24 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
375505
`);
376506
navigator.credentials
377507
.get({
378-
publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(
379-
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptionsJSON,
508+
publicKey: this.transformCredentialRequestOptions(
509+
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
380510
),
381511
})
382512
.then((assertion) => {
383513
if (!assertion) {
384514
throw new Error("No assertion");
385515
}
386516
try {
517+
// we now have an authentication assertion! encode the byte arrays contained
518+
// in the assertion data as strings for posting to the server
519+
const transformedAssertionForServer = this.transformAssertionForServer(
520+
assertion as PublicKeyCredential,
521+
);
522+
387523
// post the assertion to the server for verification.
388524
this.executor.submit({
389-
webauthn: (assertion as PublicKeyCredential).toJSON(),
525+
webauthn: transformedAssertionForServer,
390526
});
391527
} catch (err) {
392528
throw new Error(`Error when validating assertion on server: ${err}`);

0 commit comments

Comments
 (0)