core: Move SFE package. Prep for prettier config.
This commit is contained in:
@ -1,6 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/web-sfe",
|
"name": "@goauthentik/web-sfe",
|
||||||
"version": "0.0.0",
|
"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": {
|
"dependencies": {
|
||||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
@ -15,15 +23,12 @@
|
|||||||
"@rollup/plugin-swc": "^0.4.0",
|
"@rollup/plugin-swc": "^0.4.0",
|
||||||
"@swc/cli": "^0.4.0",
|
"@swc/cli": "^0.4.0",
|
||||||
"@swc/core": "^1.7.28",
|
"@swc/core": "^1.7.28",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
|
||||||
"@types/jquery": "^3.5.31",
|
"@types/jquery": "^3.5.31",
|
||||||
"lockfile-lint": "^4.14.0",
|
"lockfile-lint": "^4.14.0",
|
||||||
"prettier": "^3.3.2",
|
|
||||||
"rollup": "^4.23.0",
|
"rollup": "^4.23.0",
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"wireit": "^0.14.9"
|
"wireit": "^0.14.9"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core": "^1.7.28",
|
"@swc/core": "^1.7.28",
|
||||||
"@swc/core-darwin-arm64": "^1.6.13",
|
"@swc/core-darwin-arm64": "^1.6.13",
|
||||||
@ -37,13 +42,6 @@
|
|||||||
"@swc/core-win32-ia32-msvc": "^1.6.13",
|
"@swc/core-win32-ia32-msvc": "^1.6.13",
|
||||||
"@swc/core-win32-x64-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": {
|
"wireit": {
|
||||||
"build:sfe": {
|
"build:sfe": {
|
||||||
"command": "rollup -c rollup.config.js --bundleConfigAsCjs",
|
"command": "rollup -c rollup.config.js --bundleConfigAsCjs",
|
@ -102,7 +102,8 @@ class SimpleFlowExecutor {
|
|||||||
new AuthenticatorValidateStage(this, this.challenge).render();
|
new AuthenticatorValidateStage(this, this.challenge).render();
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
this.container.innerText = "Unsupported stage: " + this.challenge?.component;
|
this.container.innerText =
|
||||||
|
"Unsupported stage: " + this.challenge?.component;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,7 +161,7 @@ class Stage<T extends FlowInfoChallenge> {
|
|||||||
const IS_INVALID = "is-invalid";
|
const IS_INVALID = "is-invalid";
|
||||||
|
|
||||||
class IdentificationStage extends Stage<IdentificationChallenge> {
|
class IdentificationStage extends Stage<IdentificationChallenge> {
|
||||||
render() {
|
override render() {
|
||||||
this.html(`
|
this.html(`
|
||||||
<form id="ident-form">
|
<form id="ident-form">
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
@ -273,7 +274,10 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
deviceChallenge?: DeviceChallenge;
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
b64enc(buf: Uint8Array): string {
|
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 {
|
b64RawEnc(buf: Uint8Array): string {
|
||||||
@ -281,8 +285,9 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
}
|
}
|
||||||
|
|
||||||
u8arr(input: string): Uint8Array {
|
u8arr(input: string): Uint8Array {
|
||||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
return Uint8Array.from(
|
||||||
c.charCodeAt(0),
|
atob(input.replace(/_/g, "/").replace(/-/g, "+")),
|
||||||
|
(c) => c.charCodeAt(0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,8 +295,13 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
if ("credentials" in navigator) {
|
if ("credentials" in navigator) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
if (
|
||||||
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
|
window.location.protocol === "http:" &&
|
||||||
|
window.location.hostname !== "localhost"
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
"WebAuthn requires this page to be accessed via HTTPS.",
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
console.warn("WebAuthn not supported by browser.");
|
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.
|
// string, then a byte array, re-encode it and wrap that in an array.
|
||||||
const stringId = decodeURIComponent(window.atob(userId));
|
const stringId = decodeURIComponent(window.atob(userId));
|
||||||
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
|
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, {
|
return Object.assign({}, credentialCreateOptions, {
|
||||||
challenge,
|
challenge,
|
||||||
@ -325,19 +337,28 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
* for posting to the server.
|
* for posting to the server.
|
||||||
* @param {PublicKeyCredential} newAssertion
|
* @param {PublicKeyCredential} newAssertion
|
||||||
*/
|
*/
|
||||||
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
transformNewAssertionForServer(
|
||||||
|
newAssertion: PublicKeyCredential,
|
||||||
|
): Assertion {
|
||||||
const attObj = new Uint8Array(
|
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 rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
|
||||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
const registrationClientExtensions =
|
||||||
|
newAssertion.getClientExtensionResults();
|
||||||
return {
|
return {
|
||||||
id: newAssertion.id,
|
id: newAssertion.id,
|
||||||
rawId: this.b64enc(rawId),
|
rawId: this.b64enc(rawId),
|
||||||
type: newAssertion.type,
|
type: newAssertion.type,
|
||||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
registrationClientExtensions: JSON.stringify(
|
||||||
|
registrationClientExtensions,
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
clientDataJSON: this.b64enc(clientDataJSON),
|
clientDataJSON: this.b64enc(clientDataJSON),
|
||||||
attestationObject: this.b64enc(attObj),
|
attestationObject: this.b64enc(attObj),
|
||||||
@ -348,15 +369,17 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
transformCredentialRequestOptions(
|
transformCredentialRequestOptions(
|
||||||
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
): PublicKeyCredentialRequestOptions {
|
): PublicKeyCredentialRequestOptions {
|
||||||
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
|
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 allowCredentials = (
|
||||||
|
credentialRequestOptions.allowCredentials || []
|
||||||
|
).map((credentialDescriptor) => {
|
||||||
|
const id = this.u8arr(credentialDescriptor.id.toString());
|
||||||
|
return Object.assign({}, credentialDescriptor, { id });
|
||||||
|
});
|
||||||
|
|
||||||
return Object.assign({}, credentialRequestOptions, {
|
return Object.assign({}, credentialRequestOptions, {
|
||||||
challenge,
|
challenge,
|
||||||
allowCredentials,
|
allowCredentials,
|
||||||
@ -367,19 +390,25 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||||
* @param {PublicKeyCredential} newAssertion
|
* @param {PublicKeyCredential} newAssertion
|
||||||
*/
|
*/
|
||||||
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
transformAssertionForServer(
|
||||||
const response = newAssertion.response as AuthenticatorAssertionResponse;
|
newAssertion: PublicKeyCredential,
|
||||||
|
): AuthAssertion {
|
||||||
|
const response =
|
||||||
|
newAssertion.response as AuthenticatorAssertionResponse;
|
||||||
const authData = new Uint8Array(response.authenticatorData);
|
const authData = new Uint8Array(response.authenticatorData);
|
||||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||||
const rawId = new Uint8Array(newAssertion.rawId);
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
const sig = new Uint8Array(response.signature);
|
const sig = new Uint8Array(response.signature);
|
||||||
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
const assertionClientExtensions =
|
||||||
|
newAssertion.getClientExtensionResults();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: newAssertion.id,
|
id: newAssertion.id,
|
||||||
rawId: this.b64enc(rawId),
|
rawId: this.b64enc(rawId),
|
||||||
type: newAssertion.type,
|
type: newAssertion.type,
|
||||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
assertionClientExtensions: JSON.stringify(
|
||||||
|
assertionClientExtensions,
|
||||||
|
),
|
||||||
|
|
||||||
response: {
|
response: {
|
||||||
clientDataJSON: this.b64RawEnc(clientDataJSON),
|
clientDataJSON: this.b64RawEnc(clientDataJSON),
|
||||||
@ -408,10 +437,12 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderChallengePicker() {
|
renderChallengePicker() {
|
||||||
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
|
const challenges = this.challenge.deviceChallenges.filter(
|
||||||
challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
|
(challenge) =>
|
||||||
? undefined
|
challenge.deviceClass === "webauthn" &&
|
||||||
: challenge,
|
!this.checkWebAuthnSupport()
|
||||||
|
? undefined
|
||||||
|
: challenge,
|
||||||
);
|
);
|
||||||
this.html(`<form id="picker-form">
|
this.html(`<form id="picker-form">
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
@ -449,13 +480,12 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
.join("")}
|
.join("")}
|
||||||
</form>`);
|
</form>`);
|
||||||
this.challenge.deviceChallenges.forEach((challenge) => {
|
this.challenge.deviceChallenges.forEach((challenge) => {
|
||||||
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
|
$(
|
||||||
"click",
|
`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`,
|
||||||
() => {
|
).on("click", () => {
|
||||||
this.deviceChallenge = challenge;
|
this.deviceChallenge = challenge;
|
||||||
this.render();
|
this.render();
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,7 +523,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
navigator.credentials
|
navigator.credentials
|
||||||
.get({
|
.get({
|
||||||
publicKey: this.transformCredentialRequestOptions(
|
publicKey: this.transformCredentialRequestOptions(
|
||||||
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
|
this.deviceChallenge
|
||||||
|
?.challenge as PublicKeyCredentialRequestOptions,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.then((assertion) => {
|
.then((assertion) => {
|
||||||
@ -503,16 +534,19 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
|||||||
try {
|
try {
|
||||||
// we now have an authentication assertion! encode the byte arrays contained
|
// we now have an authentication assertion! encode the byte arrays contained
|
||||||
// in the assertion data as strings for posting to the server
|
// in the assertion data as strings for posting to the server
|
||||||
const transformedAssertionForServer = this.transformAssertionForServer(
|
const transformedAssertionForServer =
|
||||||
assertion as PublicKeyCredential,
|
this.transformAssertionForServer(
|
||||||
);
|
assertion as PublicKeyCredential,
|
||||||
|
);
|
||||||
|
|
||||||
// post the assertion to the server for verification.
|
// post the assertion to the server for verification.
|
||||||
this.executor.submit({
|
this.executor.submit({
|
||||||
webauthn: transformedAssertionForServer,
|
webauthn: transformedAssertionForServer,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Error when validating assertion on server: ${err}`);
|
throw new Error(
|
||||||
|
`Error when validating assertion on server: ${err}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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();
|
sfe.start();
|
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
|
"extends": "@goauthentik/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["jquery"],
|
"types": ["jquery"],
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
@ -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"]
|
|
||||||
}
|
|
Reference in New Issue
Block a user