This commit is contained in:
Joseph Garrone
2024-09-02 03:26:39 +02:00
parent 7260589136
commit c6b52acf2f
15 changed files with 748 additions and 89 deletions

View File

@ -15,7 +15,7 @@ import * as child_process from "child_process";
import {
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT
} from "./constants";
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe";
@ -547,7 +547,7 @@ export function getBuildContext(params: {
`${themeNames[0]}-keycloak-theme`,
loginThemeResourcesFromKeycloakVersion:
buildOptions.loginThemeResourcesFromKeycloakVersion ??
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT,
projectDirPath,
projectBuildDirPath,
keycloakifyBuildDirPath: (() => {

View File

@ -50,7 +50,9 @@ export const LOGIN_THEME_PAGE_IDS = [
"login-recovery-authn-code-input.ftl",
"login-reset-otp.ftl",
"login-x509-info.ftl",
"webauthn-error.ftl"
"webauthn-error.ftl",
"login-passkeys-conditional-authenticate.ftl",
"login-idp-link-confirm-override.ftl"
] as const;
export const ACCOUNT_THEME_PAGE_IDS = [
@ -70,4 +72,4 @@ export const CONTAINER_NAME = "keycloak-keycloakify";
export const FALLBACK_LANGUAGE_TAG = "en";
export const LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT = "24.0.4";
export const LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT = "24.0.4";

View File

@ -1,8 +1,10 @@
import { join as pathJoin, relative as pathRelative } from "path";
import { type BuildContext } from "./buildContext";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import { type BuildContext } from "../buildContext";
import { assert } from "tsafe/assert";
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "./constants";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "../constants";
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import * as fsPr from "fs/promises";
export type BuildContextLike = {
cacheDirPath: string;
@ -20,6 +22,8 @@ export async function downloadKeycloakDefaultTheme(params: {
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
let areExtraAssetsFor24Copied = false;
const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath,
@ -32,8 +36,6 @@ export async function downloadKeycloakDefaultTheme(params: {
return;
}
const { readFile, writeFile } = params;
skip_keycloak_v2: {
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
break skip_keycloak_v2;
@ -42,6 +44,8 @@ export async function downloadKeycloakDefaultTheme(params: {
return;
}
const { readFile, writeFile } = params;
last_account_v1_transformations: {
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
break last_account_v1_transformations;
@ -168,6 +172,42 @@ export async function downloadKeycloakDefaultTheme(params: {
}
}
copy_extra_assets: {
if (keycloakVersion !== "24.0.4") {
break copy_extra_assets;
}
if (areExtraAssetsFor24Copied) {
break copy_extra_assets;
}
const extraAssetsDirPath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
__dirname.split(`${pathSep}bin${pathSep}`)[1],
"extra-assets"
);
await Promise.all(
["webauthnAuthenticate.js", "passkeysConditionalAuth.js"].map(
async fileBasename =>
writeFile({
fileRelativePath: pathJoin(
"base",
"login",
"resources",
"js",
fileBasename
),
modifiedData: await fsPr.readFile(
pathJoin(extraAssetsDirPath, fileBasename)
)
})
)
);
}
skip_unused_resources: {
if (keycloakVersion !== "24.0.4") {
break skip_unused_resources;

View File

@ -0,0 +1,79 @@
import { base64url } from "rfc4648";
import { returnSuccess, returnFailure } from "./webauthnAuthenticate.js";
export function initAuthenticate(input) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
returnFailure(input.errmsg);
return;
}
if (input.isUserIdentified || typeof PublicKeyCredential.isConditionalMediationAvailable === "undefined") {
document.getElementById("kc-form-passkey-button").style.display = 'block';
} else {
tryAutoFillUI(input);
}
}
function doAuthenticate(input) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
returnFailure(input.errmsg);
return;
}
const publicKey = {
rpId : input.rpId,
challenge: base64url.parse(input.challenge, { loose: true })
};
publicKey.allowCredentials = !input.isUserIdentified ? [] : getAllowCredentials();
if (input.createTimeout !== 0) {
publicKey.timeout = input.createTimeout * 1000;
}
if (input.userVerification !== 'not specified') {
publicKey.userVerification = input.userVerification;
}
return navigator.credentials.get({
publicKey: publicKey,
...input.additionalOptions
});
}
async function tryAutoFillUI(input) {
const isConditionalMediationAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
if (isConditionalMediationAvailable) {
document.getElementById("kc-form-login").style.display = "block";
input.additionalOptions = { mediation: 'conditional'};
try {
const result = await doAuthenticate(input);
returnSuccess(result);
} catch (error) {
returnFailure(error);
}
} else {
document.getElementById("kc-form-passkey-button").style.display = 'block';
}
}
function getAllowCredentials() {
const allowCredentials = [];
const authnUse = document.forms['authn_select'].authn_use_chk;
if (authnUse !== undefined) {
if (authnUse.length === undefined) {
allowCredentials.push({
id: base64url.parse(authnUse.value, {loose: true}),
type: 'public-key',
});
} else {
authnUse.forEach((entry) =>
allowCredentials.push({
id: base64url.parse(entry.value, {loose: true}),
type: 'public-key',
}));
}
}
return allowCredentials;
}

View File

@ -0,0 +1,82 @@
import { base64url } from "rfc4648";
export async function authenticateByWebAuthn(input) {
if (!input.isUserIdentified) {
try {
const result = await doAuthenticate([], input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
returnSuccess(result);
} catch (error) {
returnFailure(error);
}
return;
}
checkAllowCredentials(input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
}
async function checkAllowCredentials(challenge, userVerification, rpId, createTimeout, errmsg) {
const allowCredentials = [];
const authnUse = document.forms['authn_select'].authn_use_chk;
if (authnUse !== undefined) {
if (authnUse.length === undefined) {
allowCredentials.push({
id: base64url.parse(authnUse.value, {loose: true}),
type: 'public-key',
});
} else {
authnUse.forEach((entry) =>
allowCredentials.push({
id: base64url.parse(entry.value, {loose: true}),
type: 'public-key',
}));
}
}
try {
const result = await doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg);
returnSuccess(result);
} catch (error) {
returnFailure(error);
}
}
function doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
returnFailure(errmsg);
return;
}
const publicKey = {
rpId : rpId,
challenge: base64url.parse(challenge, { loose: true })
};
if (createTimeout !== 0) {
publicKey.timeout = createTimeout * 1000;
}
if (allowCredentials.length) {
publicKey.allowCredentials = allowCredentials;
}
if (userVerification !== 'not specified') {
publicKey.userVerification = userVerification;
}
return navigator.credentials.get({publicKey});
}
export function returnSuccess(result) {
document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), { pad: false });
document.getElementById("authenticatorData").value = base64url.stringify(new Uint8Array(result.response.authenticatorData), { pad: false });
document.getElementById("signature").value = base64url.stringify(new Uint8Array(result.response.signature), { pad: false });
document.getElementById("credentialId").value = result.id;
if (result.response.userHandle) {
document.getElementById("userHandle").value = base64url.stringify(new Uint8Array(result.response.userHandle), { pad: false });
}
document.getElementById("webauth").submit();
}
export function returnFailure(err) {
document.getElementById("error").value = err;
document.getElementById("webauth").submit();
}

View File

@ -0,0 +1 @@
export * from "./downloadKeycloakDefaultTheme";