core: Move SFE package. Prep for prettier config.

This commit is contained in:
Teffen Ellis
2025-03-26 17:13:25 +01:00
parent 6383550914
commit d421b25760
7 changed files with 87 additions and 74 deletions

View File

@ -1,6 +1,14 @@
{
"name": "@goauthentik/web-sfe",
"version": "0.0.0",
"private": true,
"license": "MIT",
"scripts": {
"build": "wireit",
"lint:lockfile": "wireit",
"prettier": "prettier --write ./src ./tsconfig.json ./rollup.config.js ./package.json",
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
},
"dependencies": {
"@goauthentik/api": "^2024.6.0-1719577139",
"base64-js": "^1.5.1",
@ -15,15 +23,12 @@
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.28",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/jquery": "^3.5.31",
"lockfile-lint": "^4.14.0",
"prettier": "^3.3.2",
"rollup": "^4.23.0",
"rollup-plugin-copy": "^3.5.0",
"wireit": "^0.14.9"
},
"license": "MIT",
"optionalDependencies": {
"@swc/core": "^1.7.28",
"@swc/core-darwin-arm64": "^1.6.13",
@ -37,13 +42,6 @@
"@swc/core-win32-ia32-msvc": "^1.6.13",
"@swc/core-win32-x64-msvc": "^1.6.13"
},
"private": true,
"scripts": {
"build": "wireit",
"lint:lockfile": "wireit",
"prettier": "prettier --write ./src ./tsconfig.json ./rollup.config.js ./package.json",
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
},
"wireit": {
"build:sfe": {
"command": "rollup -c rollup.config.js --bundleConfigAsCjs",

View File

@ -102,7 +102,8 @@ class SimpleFlowExecutor {
new AuthenticatorValidateStage(this, this.challenge).render();
return;
default:
this.container.innerText = "Unsupported stage: " + this.challenge?.component;
this.container.innerText =
"Unsupported stage: " + this.challenge?.component;
return;
}
}
@ -160,7 +161,7 @@ class Stage<T extends FlowInfoChallenge> {
const IS_INVALID = "is-invalid";
class IdentificationStage extends Stage<IdentificationChallenge> {
render() {
override render() {
this.html(`
<form id="ident-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
@ -273,7 +274,10 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
deviceChallenge?: DeviceChallenge;
b64enc(buf: Uint8Array): string {
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
return fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
b64RawEnc(buf: Uint8Array): string {
@ -281,8 +285,9 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
return Uint8Array.from(
atob(input.replace(/_/g, "/").replace(/-/g, "+")),
(c) => c.charCodeAt(0),
);
}
@ -290,8 +295,13 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
if ("credentials" in navigator) {
return true;
}
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
if (
window.location.protocol === "http:" &&
window.location.hostname !== "localhost"
) {
console.warn(
"WebAuthn requires this page to be accessed via HTTPS.",
);
return false;
}
console.warn("WebAuthn not supported by browser.");
@ -312,7 +322,9 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
// 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());
const challenge = this.u8arr(
credentialCreateOptions.challenge.toString(),
);
return Object.assign({}, credentialCreateOptions, {
challenge,
@ -325,19 +337,28 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
* for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
transformNewAssertionForServer(
newAssertion: PublicKeyCredential,
): Assertion {
const attObj = new Uint8Array(
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
(
newAssertion.response as AuthenticatorAttestationResponse
).attestationObject,
);
const clientDataJSON = new Uint8Array(
newAssertion.response.clientDataJSON,
);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions = newAssertion.getClientExtensionResults();
const registrationClientExtensions =
newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
registrationClientExtensions: JSON.stringify(
registrationClientExtensions,
),
response: {
clientDataJSON: this.b64enc(clientDataJSON),
attestationObject: this.b64enc(attObj),
@ -348,15 +369,17 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
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 });
},
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,
@ -367,19 +390,25 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
* 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;
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();
const assertionClientExtensions =
newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
assertionClientExtensions: JSON.stringify(
assertionClientExtensions,
),
response: {
clientDataJSON: this.b64RawEnc(clientDataJSON),
@ -408,10 +437,12 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
renderChallengePicker() {
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
? undefined
: challenge,
const challenges = this.challenge.deviceChallenges.filter(
(challenge) =>
challenge.deviceClass === "webauthn" &&
!this.checkWebAuthnSupport()
? undefined
: challenge,
);
this.html(`<form id="picker-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
@ -449,13 +480,12 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
.join("")}
</form>`);
this.challenge.deviceChallenges.forEach((challenge) => {
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
"click",
() => {
this.deviceChallenge = challenge;
this.render();
},
);
$(
`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`,
).on("click", () => {
this.deviceChallenge = challenge;
this.render();
});
});
}
@ -493,7 +523,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
navigator.credentials
.get({
publicKey: this.transformCredentialRequestOptions(
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
this.deviceChallenge
?.challenge as PublicKeyCredentialRequestOptions,
),
})
.then((assertion) => {
@ -503,16 +534,19 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
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,
);
const transformedAssertionForServer =
this.transformAssertionForServer(
assertion as PublicKeyCredential,
);
// post the assertion to the server for verification.
this.executor.submit({
webauthn: transformedAssertionForServer,
});
} catch (err) {
throw new Error(`Error when validating assertion on server: ${err}`);
throw new Error(
`Error when validating assertion on server: ${err}`,
);
}
})
.catch((error) => {
@ -523,5 +557,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
}
const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement);
const sfe = new SimpleFlowExecutor(
$("#flow-sfe-container")[0] as HTMLDivElement,
);
sfe.start();

View File

@ -1,4 +1,6 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"types": ["jquery"],
"esModuleInterop": true,

View File

@ -1,23 +0,0 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "consistent",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
}