Compare commits

..

13 Commits

Author SHA1 Message Date
20c6d2ea86 Bump version 2024-12-19 19:07:24 +01:00
f43544e134 ensure no diff if config hasn't changed 2024-12-19 19:06:54 +01:00
474a863708 Correctly patch the security-admin-console client so that it can be run with HMR 2024-12-18 20:57:42 +01:00
0bacdca8fe Bump version 2024-12-17 18:04:22 +01:00
f023d6bca7 Fixes windows issues #747 2024-12-17 18:04:06 +01:00
150b01f1f3 Bump version 2024-12-17 10:44:38 +01:00
2b2bb20658 #746 2024-12-17 10:44:24 +01:00
70570faed6 Bump version 2024-12-16 18:04:01 +01:00
5d3b7c9a82 Add necessary token claim to access admin in dev mode 2024-12-16 18:04:01 +01:00
95b9b12a3b Try to fix error on windows 2024-12-16 18:04:01 +01:00
0e027055cb Bump version 2024-12-15 19:48:42 +01:00
e47b002535 #744 2024-12-15 19:48:23 +01:00
8dd6dcd1fc Merge pull request #745 from keycloakify/keycloak_config_persistance
Keycloak config persistance
2024-12-15 19:45:52 +01:00
25 changed files with 859 additions and 290 deletions

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "11.4.5",
"version": "11.5.4",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",

View File

@ -67,7 +67,9 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
)
);
run(`npx webpack --config ${webpackConfigJsFilePath}`);
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
cwd: pathDirname(webpackConfigJsFilePath)
});
fs.readdirSync(webpackOutputDirPath)
.filter(fileBasename => !fileBasename.endsWith(".txt"))

View File

@ -3,10 +3,9 @@ import child_process from "child_process";
import { SemVer } from "../src/bin/tools/SemVer";
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
import { cacheDirPath } from "./shared/cacheDirPath";
import { runPrettier } from "../src/bin/tools/runPrettier";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
import { join as pathJoin } from "path";
import * as fs from "fs";
import chalk from "chalk";
(async () => {
@ -26,9 +25,7 @@ import chalk from "chalk";
realmName: "myrealm"
});
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
const filePath = pathJoin(
const realmJsonFilePath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
@ -38,12 +35,11 @@ import chalk from "chalk";
`realm-kc-${keycloakMajorVersionNumber}.json`
);
sourceCode = await runPrettier({
sourceCode,
filePath
await writeRealmJsonFile({
parsedRealmJson,
realmJsonFilePath,
keycloakMajorVersionNumber
});
fs.writeFileSync(filePath, Buffer.from(sourceCode, "utf8"));
console.log(chalk.green(`Realm config dumped to ${filePath}`));
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
})();

View File

@ -5,9 +5,6 @@ import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process";
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
import { getBuildContext } from "./shared/buildContext";
import { SemVer } from "./tools/SemVer";
import { assert, is } from "tsafe/assert";
import chalk from "chalk";
type CliCommandOptions = {
projectDirPath: string | undefined;
@ -137,47 +134,11 @@ program
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
const { command } = await import("./start-keycloak");
validate_keycloak_version: {
if (keycloakVersion === undefined) {
break validate_keycloak_version;
}
const isValidVersion = (() => {
if (typeof keycloakVersion === "number") {
return false;
}
try {
SemVer.parse(keycloakVersion);
} catch {
return false;
}
return;
})();
if (isValidVersion) {
break validate_keycloak_version;
}
console.log(
chalk.red(
[
`Invalid Keycloak version: ${keycloakVersion}`,
"It should be a valid semver version example: 26.0.4"
].join(" ")
)
);
process.exit(1);
}
assert(is<string | undefined>(keycloakVersion));
await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: {
keycloakVersion,
keycloakVersion:
keycloakVersion === undefined ? undefined : `${keycloakVersion}`,
port,
realmJsonFilePath
}

View File

@ -10,6 +10,7 @@ import { join as pathJoin, dirname as pathDirname } from "path";
import * as fs from "fs/promises";
import { existsAsync } from "../tools/fs.existsAsync";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import type { ReturnType } from "tsafe";
export type BuildContextLike = {
fetchOptions: BuildContext["fetchOptions"];
@ -20,7 +21,10 @@ assert<BuildContext extends BuildContextLike ? true : false>;
export async function getSupportedDockerImageTags(params: {
buildContext: BuildContextLike;
}) {
}): Promise<{
allSupportedTags: string[];
latestMajorTags: string[];
}> {
const { buildContext } = params;
{
@ -31,14 +35,14 @@ export async function getSupportedDockerImageTags(params: {
}
}
const tags: string[] = [];
const tags_queryResponse: string[] = [];
await (async function callee(url: string) {
const r = await fetch(url, buildContext.fetchOptions);
await Promise.all([
(async () => {
tags.push(
tags_queryResponse.push(
...z
.object({
tags: z.array(z.string())
@ -70,7 +74,9 @@ export async function getSupportedDockerImageTags(params: {
]);
})("https://quay.io/v2/keycloak/keycloak/tags/list");
const arr = tags
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
const allSupportedTags_withVersion = tags_queryResponse
.map(tag => ({
tag,
version: (() => {
@ -86,28 +92,35 @@ export async function getSupportedDockerImageTags(params: {
return undefined;
}
if (tag.split(".").length !== 3) {
return undefined;
}
if (!supportedKeycloakMajorVersions.includes(version.major)) {
return undefined;
}
return version;
})()
}))
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
.filter(exclude(undefined));
.filter(exclude(undefined))
.sort(({ version: a }, { version: b }) => SemVer.compare(b, a));
const versionByMajor: Record<number, SemVer | undefined> = {};
const latestTagByMajor: Record<number, SemVer | undefined> = {};
for (const { version } of arr) {
const version_current = versionByMajor[version.major];
for (const { version } of allSupportedTags_withVersion) {
const version_current = latestTagByMajor[version.major];
if (
version_current === undefined ||
SemVer.compare(version_current, version) === -1
) {
versionByMajor[version.major] = version;
latestTagByMajor[version.major] = version;
}
}
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
const result = Object.entries(versionByMajor)
const latestMajorTags = Object.entries(latestTagByMajor)
.sort(([a], [b]) => parseInt(b) - parseInt(a))
.map(([, version]) => version)
.map(version => {
@ -121,16 +134,40 @@ export async function getSupportedDockerImageTags(params: {
})
.filter(exclude(undefined));
const allSupportedTags = allSupportedTags_withVersion.map(({ tag }) => tag);
const result = {
latestMajorTags,
allSupportedTags
};
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
return result;
}
const { getCachedValue, setCachedValue } = (() => {
type Result = ReturnType<typeof getSupportedDockerImageTags>;
const zResult = (() => {
type TargetType = Result;
const zTargetType = z.object({
allSupportedTags: z.array(z.string()),
latestMajorTags: z.array(z.string())
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<TargetType, InferredType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
type Cache = {
keycloakifyVersion: string;
time: number;
result: string[];
result: Result;
};
const zCache = (() => {
@ -139,7 +176,7 @@ const { getCachedValue, setCachedValue } = (() => {
const zTargetType = z.object({
keycloakifyVersion: z.string(),
time: z.number(),
result: z.array(z.string())
result: zResult
});
type InferredType = z.infer<typeof zTargetType>;

View File

@ -1,8 +1,6 @@
import { z } from "zod";
import { assert, type Equals } from "tsafe/assert";
import { is } from "tsafe/is";
import { id } from "tsafe/id";
import * as fs from "fs";
export type ParsedRealmJson = {
realm: string;
@ -39,11 +37,18 @@ export type ParsedRealmJson = {
"post.logout.redirect.uris"?: string;
};
protocol?: string;
protocolMappers?: unknown[];
protocolMappers?: {
id: string;
name: string;
protocol: string; // "openid-connect" or something else
protocolMapper: string; // "oidc-hardcoded-claim-mapper" or something else
consentRequired: boolean;
config?: Record<string, string>;
}[];
}[];
};
const zParsedRealmJson = (() => {
export const zParsedRealmJson = (() => {
type TargetType = ParsedRealmJson;
const zTargetType = z.object({
@ -89,7 +94,18 @@ const zParsedRealmJson = (() => {
})
.optional(),
protocol: z.string().optional(),
protocolMappers: z.array(z.unknown()).optional()
protocolMappers: z
.array(
z.object({
id: z.string(),
name: z.string(),
protocol: z.string(),
protocolMapper: z.string(),
consentRequired: z.boolean(),
config: z.record(z.string()).optional()
})
)
.optional()
})
)
});
@ -100,19 +116,3 @@ const zParsedRealmJson = (() => {
return id<z.ZodType<TargetType>>(zTargetType);
})();
export function readRealmJsonFile(params: {
realmJsonFilePath: string;
}): ParsedRealmJson {
const { realmJsonFilePath } = params;
const parsedRealmJson = JSON.parse(
fs.readFileSync(realmJsonFilePath).toString("utf8")
) as unknown;
zParsedRealmJson.parse(parsedRealmJson);
assert(is<ParsedRealmJson>(parsedRealmJson));
return parsedRealmJson;
}

View File

@ -0,0 +1,3 @@
export type { ParsedRealmJson } from "./ParsedRealmJson";
export { readRealmJsonFile } from "./readRealmJsonFile";
export { writeRealmJsonFile } from "./writeRealmJsonFile";

View File

@ -0,0 +1,20 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import * as fs from "fs";
import { type ParsedRealmJson, zParsedRealmJson } from "./ParsedRealmJson";
export function readRealmJsonFile(params: {
realmJsonFilePath: string;
}): ParsedRealmJson {
const { realmJsonFilePath } = params;
const parsedRealmJson = JSON.parse(
fs.readFileSync(realmJsonFilePath).toString("utf8")
) as unknown;
zParsedRealmJson.parse(parsedRealmJson);
assert(is<ParsedRealmJson>(parsedRealmJson));
return parsedRealmJson;
}

View File

@ -0,0 +1,29 @@
import * as fsPr from "fs/promises";
import { getIsPrettierAvailable, runPrettier } from "../../../tools/runPrettier";
import { canonicalStringify } from "../../../tools/canonicalStringify";
import type { ParsedRealmJson } from "./ParsedRealmJson";
import { getDefaultConfig } from "../defaultConfig";
export async function writeRealmJsonFile(params: {
realmJsonFilePath: string;
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;
}): Promise<void> {
const { realmJsonFilePath, parsedRealmJson, keycloakMajorVersionNumber } = params;
let sourceCode = canonicalStringify({
data: parsedRealmJson,
referenceData: getDefaultConfig({
keycloakMajorVersionNumber
})
});
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode: sourceCode,
filePath: realmJsonFilePath
});
}
await fsPr.writeFile(realmJsonFilePath, Buffer.from(sourceCode, "utf8"));
}

View File

@ -3,11 +3,10 @@ import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDi
import * as fs from "fs";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import { type ParsedRealmJson, readRealmJsonFile } from "../ParsedRealmJson";
import { readRealmJsonFile } from "../ParsedRealmJson/readRealmJsonFile";
import type { ParsedRealmJson } from "../ParsedRealmJson/ParsedRealmJson";
export function getDefaultRealmJsonFilePath(params: {
keycloakMajorVersionNumber: number;
}) {
function getDefaultRealmJsonFilePath(params: { keycloakMajorVersionNumber: number }) {
const { keycloakMajorVersionNumber } = params;
return pathJoin(

View File

@ -756,6 +756,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1336,13 +1354,13 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper"
]
}
@ -1393,13 +1411,13 @@
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper"
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper"
]
}
},
@ -1517,7 +1535,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "223ce532-2038-4f24-a606-2a5c73f7bd65",
"id": "f664efe4-102d-4ec1-bf11-11af67e3f178",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1543,7 +1561,7 @@
]
},
{
"id": "57e47732-79cc-4d60-bee7-4f0b8fd44540",
"id": "8a5630c5-eca1-4b6a-8e59-459cb6c84535",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1577,7 +1595,7 @@
]
},
{
"id": "c2735d89-60c0-45a4-9b3c-ae5df17df395",
"id": "c1a3eed3-25ce-44ae-93d1-f0b8148a0f8c",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1603,7 +1621,7 @@
]
},
{
"id": "11a5a507-2b9a-443f-961b-dffd66f4318d",
"id": "6eb188ad-1041-44dd-bf8f-37cae0d98bf1",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1629,7 +1647,7 @@
]
},
{
"id": "963bd753-6ea7-4d93-ab56-30f9ab59d597",
"id": "4ee215ac-f4e5-4edb-bf76-65dc9e211543",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1655,7 +1673,7 @@
]
},
{
"id": "1db6a489-a3b4-44c4-b480-1d1e8c123d20",
"id": "5a1eac7e-06a0-46d8-b9ae-1f2c934331f9",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1681,7 +1699,7 @@
]
},
{
"id": "7a38f32d-4f34-450f-8f03-64802d7cb8f1",
"id": "ed165166-4521-4a62-b185-c4b51643cbb1",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1707,7 +1725,7 @@
]
},
{
"id": "0df88739-3739-4d70-8893-47c546f19003",
"id": "4788fb1f-fd81-4f5d-9abe-4199dd641c1e",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1734,7 +1752,7 @@
]
},
{
"id": "35025424-e291-4c54-8a29-70aadba549ce",
"id": "d778a70f-f472-4dd3-ac40-cb5612ddc171",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1760,7 +1778,7 @@
]
},
{
"id": "1813b7f2-c3c2-4b92-8ffc-9ff2d12186c6",
"id": "9c1ea8ea-7c23-4e60-b02d-1900d9dc4109",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1802,7 +1820,7 @@
]
},
{
"id": "954283ac-f1c2-40b6-a39f-bf23ff9f3ce8",
"id": "0ebdf418-d57d-4318-9359-7bd0cb2381f2",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1844,7 +1862,7 @@
]
},
{
"id": "52a789ce-2cad-4f0f-93b2-295b7fd519f0",
"id": "5cc89293-c72e-4c5e-b31c-15558588a60d",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1878,7 +1896,7 @@
]
},
{
"id": "5a6a71e1-9105-45b6-b5f0-52538461357b",
"id": "5ae5a321-ccac-449e-9c19-d6dc22ab8085",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1896,7 +1914,7 @@
]
},
{
"id": "8392b6e7-bdbf-4d7f-97b6-885761c200db",
"id": "7737fdd1-0875-47e6-977b-12561cddfdc3",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1923,7 +1941,7 @@
]
},
{
"id": "52136d70-8d08-42ea-b04b-cf40ea2807aa",
"id": "90f975c3-9826-461f-88ca-27c697aff86b",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1949,7 +1967,7 @@
]
},
{
"id": "26bbc7e6-ef01-4cdb-9dba-520e2f3f8993",
"id": "ce2722d5-9f4f-41a2-8f81-e01f7b6cee57",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1975,7 +1993,7 @@
]
},
{
"id": "f0887979-04eb-4033-8f19-0ffd8c8b7f6a",
"id": "31b5bfa7-98ad-47a2-b8e6-0669022cd8cb",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -1994,7 +2012,7 @@
]
},
{
"id": "a3b7b94b-bfbf-4760-a8c9-7d9cd98d262e",
"id": "bf8a950b-be3b-4e44-8602-64e0bba492eb",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2036,7 +2054,7 @@
]
},
{
"id": "dc68a665-2e51-4a22-aaad-bd693ddc77cc",
"id": "e3519800-971b-4b1d-b64e-3983ccd02dea",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2078,7 +2096,7 @@
]
},
{
"id": "ae6b73aa-1318-4ae8-a3d9-d01b5e7d957e",
"id": "9d5a33a2-e777-4beb-95de-b84812f69c56",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2098,14 +2116,14 @@
],
"authenticatorConfig": [
{
"id": "0c18de7f-0714-41f4-9a3f-ed4edd53ae9c",
"id": "4901c91d-59bd-4727-b585-8e4e44828d0a",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "65b3c8bb-34a4-4d19-b578-245dc8ff53ea",
"id": "5062a078-83a7-4933-b0d5-3f75cc2a5003",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -764,6 +764,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1344,14 +1362,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper"
"oidc-usermodel-property-mapper"
]
}
},
@ -1400,14 +1418,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper"
"saml-role-list-mapper"
]
}
},
@ -1525,7 +1543,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "1f4d4e13-1591-4751-8985-17886a8c98a9",
"id": "8ccfe057-5ce6-499b-9fae-3cd89b62bf01",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1551,7 +1569,7 @@
]
},
{
"id": "126f07c3-1bcb-4a02-bf16-bb44674bf55d",
"id": "f3b9ab2e-41c2-4e73-876b-e2c275d6d14e",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1585,7 +1603,7 @@
]
},
{
"id": "eb3a08c8-5f99-49b6-b02b-16b62571f273",
"id": "df1329cc-777c-42d8-aa2f-c5d5ddaaf5a4",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1611,7 +1629,7 @@
]
},
{
"id": "3dc19838-5025-4bbb-b569-b574bd5a8d90",
"id": "f78a4cbc-66ff-4caa-8066-67aff94946f4",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1637,7 +1655,7 @@
]
},
{
"id": "70d6fd40-d740-4dae-b0e6-350f8e9d4a1c",
"id": "4b20995b-5553-45db-86b0-05c3fe14edb1",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1663,7 +1681,7 @@
]
},
{
"id": "6e24dcb3-5818-483c-8e44-883858171901",
"id": "0a7cc6b7-e427-4f72-b44e-a02133241bad",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1689,7 +1707,7 @@
]
},
{
"id": "ac6254cd-403b-457b-b308-22a2a0e4f99d",
"id": "e24e73c0-dd51-4fdc-a916-284f11f38487",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1715,7 +1733,7 @@
]
},
{
"id": "485e74e6-9b3e-4b2c-a9b9-927802dc4f06",
"id": "37ee5a12-01c2-41b0-aafa-e9c6661ff544",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1742,7 +1760,7 @@
]
},
{
"id": "ff9bb879-1d6a-4d1c-9836-1e4fab6f8997",
"id": "8902a1a7-c2ee-4648-869f-dd5ef89184fc",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1768,7 +1786,7 @@
]
},
{
"id": "af8b2470-d581-401c-9984-762b966ebcc2",
"id": "77c78eed-4bcd-4779-b39f-10135be84946",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1810,7 +1828,7 @@
]
},
{
"id": "414dbda4-eb3f-4baa-b23a-d3423af1eae6",
"id": "c6398883-01e6-47a1-bb97-c09f2983155d",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1852,7 +1870,7 @@
]
},
{
"id": "1cae0c4b-8dfb-4f5d-a781-e74d0a13c940",
"id": "78ab5fb8-f35b-4053-b264-94b208000b13",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1886,7 +1904,7 @@
]
},
{
"id": "e798b655-7d85-4b6b-aee7-1448a3e1e0ea",
"id": "959e154b-034e-413d-9b19-211e7d9ba33d",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1904,7 +1922,7 @@
]
},
{
"id": "eb94b723-1041-426a-87bf-f7b4bd2f485d",
"id": "001e253d-bdbd-41e2-81c7-1c7b239feeb1",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1931,7 +1949,7 @@
]
},
{
"id": "452d1d5f-7632-44d7-bc89-77ff2b209b3e",
"id": "45481bb0-18fe-4a26-a77c-35a5afe58436",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1957,7 +1975,7 @@
]
},
{
"id": "7c1b9e8f-6b57-49d1-a9a7-494862f93c0f",
"id": "bb47b847-5a55-4c08-909e-9f6f8d8a0636",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1983,7 +2001,7 @@
]
},
{
"id": "2b38f34a-1739-499e-bb24-1dff96f32009",
"id": "77e6e169-05b7-4b89-af00-09cfe1604eed",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -2002,7 +2020,7 @@
]
},
{
"id": "d26ae72b-a933-44dc-9927-1c82757004b2",
"id": "aef03fe8-1a70-40c3-879f-25588f75c119",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2044,7 +2062,7 @@
]
},
{
"id": "222ee8d6-1892-4768-9ada-720274b6bf9a",
"id": "990abff7-e2ba-4217-984e-8890cbc2b3a9",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2086,7 +2104,7 @@
]
},
{
"id": "e8b4d92c-27c1-4a9b-9b16-7ceb810fa230",
"id": "d9894cf6-2f99-493e-ac47-853f54bfc9c6",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2106,14 +2124,14 @@
],
"authenticatorConfig": [
{
"id": "e5847a0b-855d-4d93-85fd-94714be3ed92",
"id": "101ed8ff-4383-4539-aa52-2d1e69698b78",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "a2a18aa4-bd4c-4c2a-9286-e9d6c64f4812",
"id": "049042a5-3551-4c16-81a1-64d86f5aa1e5",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -775,6 +775,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1355,14 +1373,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper"
"saml-user-attribute-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper"
]
}
},
@ -1411,14 +1429,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper"
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper"
]
}
},
@ -1536,7 +1554,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "c40791b4-4d59-4df2-bebd-2b71e793704f",
"id": "30a878f0-57aa-4d20-bab0-6cf1d7317a5c",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1562,7 +1580,7 @@
]
},
{
"id": "8813b6d1-8b88-4672-b29b-8420ce3f3975",
"id": "d386affe-d1fe-472a-bee6-54105d0101f5",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1596,7 +1614,7 @@
]
},
{
"id": "a9937c40-a1ee-4c57-adf7-ede0a9983953",
"id": "77b95bc0-bd0c-46b7-8240-3182023e9d50",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1622,7 +1640,7 @@
]
},
{
"id": "2d494b5a-eb73-40d0-94d3-a8d8024a7db4",
"id": "bc96d3d6-29a1-42af-a63e-bb67a8c6d78f",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1648,7 +1666,7 @@
]
},
{
"id": "2e977f5a-8110-412b-b704-3e15164dbb1b",
"id": "7697ca74-5c2b-45ab-9335-e0f6dec59b5c",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1674,7 +1692,7 @@
]
},
{
"id": "6f171b4b-8723-4e6d-bb1e-6b4293a7bb3f",
"id": "534cb120-f600-4f40-9707-7b781bdbce48",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1700,7 +1718,7 @@
]
},
{
"id": "2dbb7f27-757d-4178-8217-4a24fdb0163c",
"id": "f884b048-b223-4ed6-ae16-e49a4255131e",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1726,7 +1744,7 @@
]
},
{
"id": "7295aaf7-acf4-4b78-8186-d2415ea4ede0",
"id": "61c7966c-ad72-49f5-84dd-376152348092",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1753,7 +1771,7 @@
]
},
{
"id": "e0d34d7c-7bbb-4847-8864-fbd97a1f3e89",
"id": "72412d0f-dd1b-49fe-bb0b-9dad99eb0491",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1779,7 +1797,7 @@
]
},
{
"id": "5f3d0fb0-d95e-4841-89d3-a27d0cdbbcb4",
"id": "6b76613e-0d39-440d-aab4-98eaffb1e96a",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1821,7 +1839,7 @@
]
},
{
"id": "c246380d-af25-4151-ab19-1f1e5b553008",
"id": "0ff60395-fa89-41be-ad22-fab339e67c49",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1863,7 +1881,7 @@
]
},
{
"id": "abacf398-0f1f-4f28-a310-8d306d588048",
"id": "bbb3ece7-7dbf-4aba-80c3-dde4b9cdd0b6",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1897,7 +1915,7 @@
]
},
{
"id": "a0f87683-619a-44d4-8b4f-4b053bba2346",
"id": "f5f2c0f6-7dbf-4978-845e-6cacac23aa13",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1915,7 +1933,7 @@
]
},
{
"id": "e8820c7c-22a7-4618-beb7-3e09be72c00c",
"id": "cf463104-19e2-41a8-8a53-d3dd30b75344",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1942,7 +1960,7 @@
]
},
{
"id": "cac00c38-ee44-44c9-b95e-cc755bab36ef",
"id": "b99b60dc-41ad-487d-be69-a2eefa954a9d",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1968,7 +1986,7 @@
]
},
{
"id": "688cde36-507e-4a68-afdf-18ec4ad626a7",
"id": "18731296-2c96-4f98-a884-027e629e4f9d",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1994,7 +2012,7 @@
]
},
{
"id": "e058697c-f450-4f14-ae64-04e9299fa24f",
"id": "9a9dce17-5425-4fd5-b3b8-81410e1dbce4",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -2013,7 +2031,7 @@
]
},
{
"id": "ad768088-32c9-4979-90dd-61bf111fd72e",
"id": "d0a24e08-cb69-4949-9518-50ae7a96ee49",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2055,7 +2073,7 @@
]
},
{
"id": "47d4b090-f965-4588-b5bc-029ccb59876f",
"id": "6a9aa554-afba-487f-9c82-e94c81c15b3b",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2097,7 +2115,7 @@
]
},
{
"id": "1f68feec-7f99-4c49-afe6-45d46684ca21",
"id": "e0361d46-eab4-41a6-bb2e-1dc6a5a6b073",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2117,14 +2135,14 @@
],
"authenticatorConfig": [
{
"id": "bd7365c7-842b-4bc6-a4ca-498cf025c210",
"id": "053d6017-e54c-418a-abe7-44dd4752eacb",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "b929192d-f650-4a09-9701-3d3216547552",
"id": "8b545cf4-ab9e-4226-b3c0-d7ac773eae2f",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -408,9 +408,9 @@
"otpPolicyPeriod": 30,
"otpPolicyCodeReusable": false,
"otpSupportedApplications": [
"totpAppGoogleName",
"totpAppFreeOTPName",
"totpAppMicrosoftAuthenticatorName"
"totpAppMicrosoftAuthenticatorName",
"totpAppGoogleName"
],
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
@ -779,6 +779,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1359,13 +1377,13 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper"
]
}
@ -1415,14 +1433,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-address-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper"
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper"
]
}
},

View File

@ -789,6 +789,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
},
{
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
"name": "locale",
@ -1401,14 +1419,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper"
"saml-role-list-mapper"
]
}
},
@ -1477,14 +1495,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper"
"oidc-sha256-pairwise-sub-mapper"
]
}
}

View File

@ -919,6 +919,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
}
],
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
@ -1545,14 +1563,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper"
"oidc-sha256-pairwise-sub-mapper"
]
}
},
@ -1584,14 +1602,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper"
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper"
]
}
},

View File

@ -964,6 +964,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
}
],
"defaultClientScopes": [
@ -1618,14 +1636,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper"
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper"
]
}
},
@ -1657,12 +1675,12 @@
"allowed-protocol-mapper-types": [
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper"
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper"
]
}
},

View File

@ -985,6 +985,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"claim.value": "[\"*\"]",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"lightweight.claim": "true",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false"
}
}
],
"defaultClientScopes": [
@ -1610,7 +1628,7 @@
"smtpServer": {},
"loginTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"adminTheme": "keycloakify-starter",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
@ -1639,14 +1657,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper"
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper"
]
}
},
@ -1676,14 +1694,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper"
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper"
]
}
},

View File

@ -1,6 +1,6 @@
import { CONTAINER_NAME } from "../../shared/constants";
import child_process from "child_process";
import { join as pathJoin } from "path";
import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert, is } from "tsafe/assert";
@ -20,16 +20,37 @@ export async function dumpContainerConfig(params: {
}): Promise<ParsedRealmJson> {
const { realmName, keycloakMajorVersionNumber, buildContext } = params;
{
// https://github.com/keycloak/keycloak/issues/33800
const doesUseLockedH2Database = keycloakMajorVersionNumber >= 25;
// https://github.com/keycloak/keycloak/issues/33800
const doesUseLockedH2Database = keycloakMajorVersionNumber >= 25;
if (doesUseLockedH2Database) {
child_process.execSync(
`docker exec ${CONTAINER_NAME} sh -c "cp -rp /opt/keycloak/data/h2 /tmp"`
);
if (doesUseLockedH2Database) {
const dCompleted = new Deferred<void>();
const cmd = `docker exec ${CONTAINER_NAME} sh -c "cp -rp /opt/keycloak/data/h2 /tmp"`;
child_process.exec(cmd, error => {
if (error !== null) {
dCompleted.reject(error);
return;
}
dCompleted.resolve();
});
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(`Docker command failed: ${cmd}`));
console.log(chalk.red(error.message));
throw error;
}
}
{
const dCompleted = new Deferred<void>();
const child = child_process.spawn(
@ -46,7 +67,7 @@ export async function dumpContainerConfig(params: {
...["--db", "dev-file"],
...[
"--db-url",
"'jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE'"
'"jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE"'
]
])
],
@ -56,7 +77,9 @@ export async function dumpContainerConfig(params: {
let output = "";
const onExit = (code: number | null) => {
dCompleted.reject(new Error(`Exited with code ${code}`));
dCompleted.reject(
new Error(`docker exec kc.sh export command failed with code ${code}`)
);
};
child.once("exit", onExit);
@ -96,25 +119,34 @@ export async function dumpContainerConfig(params: {
console.log(output);
process.exit(1);
throw error;
}
}
if (doesUseLockedH2Database) {
const dCompleted = new Deferred<void>();
if (doesUseLockedH2Database) {
const dCompleted = new Deferred<void>();
child_process.exec(
`docker exec ${CONTAINER_NAME} sh -c "rm -rf /tmp/h2"`,
error => {
if (error !== null) {
dCompleted.reject(error);
return;
}
const cmd = `docker exec ${CONTAINER_NAME} sh -c "rm -rf /tmp/h2"`;
dCompleted.resolve();
}
);
child_process.exec(cmd, error => {
if (error !== null) {
dCompleted.reject(error);
return;
}
dCompleted.resolve();
});
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(`Docker command failed: ${cmd}`));
console.log(chalk.red(error.message));
throw error;
}
}
@ -126,8 +158,13 @@ export async function dumpContainerConfig(params: {
{
const dCompleted = new Deferred<void>();
const cmd = `docker cp ${CONTAINER_NAME}:/tmp/${realmName}-realm.json ${pathBasename(targetRealmConfigJsonFilePath_tmp)}`;
child_process.exec(
`docker cp ${CONTAINER_NAME}:/tmp/${realmName}-realm.json ${targetRealmConfigJsonFilePath_tmp}`,
cmd,
{
cwd: pathDirname(targetRealmConfigJsonFilePath_tmp)
},
error => {
if (error !== null) {
dCompleted.reject(error);
@ -138,7 +175,17 @@ export async function dumpContainerConfig(params: {
}
);
await dCompleted.pr;
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(`Docker command failed: ${cmd}`));
console.log(chalk.red(error.message));
throw error;
}
}
return readRealmJsonFile({

View File

@ -276,7 +276,7 @@ function editAccountConsoleAndSecurityAdminConsole(params: {
}) {
const { parsedRealmJson } = params;
for (const clientId of ["account-console", "security-admin-console"]) {
for (const clientId of ["account-console", "security-admin-console"] as const) {
const client = parsedRealmJson.clients.find(
client => client.clientId === clientId
);
@ -298,5 +298,68 @@ function editAccountConsoleAndSecurityAdminConsole(params: {
(client.attributes ??= {})["post.logout.redirect.uris"] = "+";
client.webOrigins = ["*"];
admin_specific: {
if (clientId !== "security-admin-console") {
break admin_specific;
}
const protocolMapper_preexisting = client.protocolMappers?.find(
protocolMapper => {
if (protocolMapper.protocolMapper !== "oidc-hardcoded-claim-mapper") {
return false;
}
if (protocolMapper.protocol !== "openid-connect") {
return false;
}
if (protocolMapper.config === undefined) {
return false;
}
if (protocolMapper.config["claim.name"] !== "allowed-origins") {
return false;
}
return true;
}
);
let protocolMapper: NonNullable<typeof protocolMapper_preexisting>;
const config = {
"introspection.token.claim": "true",
"claim.value": '["*"]',
"userinfo.token.claim": "true",
"id.token.claim": "false",
"lightweight.claim": "true",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false"
};
if (protocolMapper_preexisting !== undefined) {
protocolMapper = protocolMapper_preexisting;
} else {
protocolMapper = {
id: "8fd0d584-7052-4d04-a615-d18a71050873",
name: "allowed-origins",
protocol: "openid-connect",
protocolMapper: "oidc-hardcoded-claim-mapper",
consentRequired: false,
config
};
(client.protocolMappers ??= []).push(protocolMapper);
}
assert(protocolMapper.config !== undefined);
if (config !== protocolMapper.config) {
Object.assign(protocolMapper.config, config);
}
}
}
}

View File

@ -1,6 +1,5 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
import { getDefaultConfig } from "./defaultConfig";
import {
prepareRealmConfig,
@ -14,7 +13,11 @@ import {
sep as pathSep
} from "path";
import { existsAsync } from "../../tools/fs.existsAsync";
import { readRealmJsonFile, type ParsedRealmJson } from "./ParsedRealmJson";
import {
readRealmJsonFile,
writeRealmJsonFile,
type ParsedRealmJson
} from "./ParsedRealmJson";
import {
dumpContainerConfig,
type BuildContextLike as BuildContextLike_dumpContainerConfig
@ -80,22 +83,11 @@ export async function getRealmConfig(params: {
}
}
const writeRealmJsonFile = async (params: { parsedRealmJson: ParsedRealmJson }) => {
const { parsedRealmJson } = params;
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: realmJsonFilePath
});
}
fs.writeFileSync(realmJsonFilePath, sourceCode);
};
await writeRealmJsonFile({ parsedRealmJson });
await writeRealmJsonFile({
realmJsonFilePath,
parsedRealmJson,
keycloakMajorVersionNumber
});
const { onRealmConfigChange } = (() => {
const run = runExclusive.build(async () => {
@ -105,14 +97,26 @@ export async function getRealmConfig(params: {
chalk.grey(`Changes detected to the '${realmName}' config, backing up...`)
);
const parsedRealmJson = await dumpContainerConfig({
buildContext,
realmName,
let parsedRealmJson: ParsedRealmJson;
try {
parsedRealmJson = await dumpContainerConfig({
buildContext,
realmName,
keycloakMajorVersionNumber
});
} catch (error) {
console.log(chalk.red(`Failed to backup '${realmName}' config:`));
return;
}
await writeRealmJsonFile({
realmJsonFilePath,
parsedRealmJson,
keycloakMajorVersionNumber
});
await writeRealmJsonFile({ parsedRealmJson });
console.log(
[
chalk.grey(

View File

@ -97,7 +97,7 @@ export async function command(params: {
const { cliCommandOptions, buildContext } = params;
const availableTags = await getSupportedDockerImageTags({
const { allSupportedTags, latestMajorTags } = await getSupportedDockerImageTags({
buildContext
});
@ -105,7 +105,7 @@ export async function command(params: {
if (cliCommandOptions.keycloakVersion !== undefined) {
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
const tag = availableTags.find(tag =>
const tag = allSupportedTags.find(tag =>
tag.startsWith(cliCommandOptions_keycloakVersion)
);
@ -143,7 +143,7 @@ export async function command(params: {
);
const { value: tag } = await cliSelect<string>({
values: availableTags
values: latestMajorTags
}).catch(() => {
process.exit(-1);
});

View File

@ -44,6 +44,7 @@ export function startViteDevServer(params: {
//Local: http://localhost:8083/
const match = data
.toString("utf8")
.replace(/\x1b[[0-9;]*m/g, "")
.match(/Local:\s*http:\/\/(?:localhost|127\.0\.0\.1):(\d+)\//);
if (match === null) {

View File

@ -0,0 +1,99 @@
import { z } from "zod";
import { same } from "evt/tools/inDepth/same";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
export type Stringifyable =
| StringifyableAtomic
| StringifyableObject
| StringifyableArray;
export type StringifyableAtomic = string | number | boolean | null;
// NOTE: Use Record<string, Stringifyable>
interface StringifyableObject {
[key: string]: Stringifyable;
}
// NOTE: Use Stringifyable[]
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface StringifyableArray extends Array<Stringifyable> {}
export const zStringifyableAtomic = (() => {
type TargetType = StringifyableAtomic;
const zTargetType = z.union([z.string(), z.number(), z.boolean(), z.null()]);
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
export const zStringifyable: z.ZodType<Stringifyable> = z
.any()
.superRefine((val, ctx) => {
const isStringifyable = same(JSON.parse(JSON.stringify(val)), val);
if (!isStringifyable) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Not stringifyable"
});
}
});
export function getIsAtomic(
stringifyable: Stringifyable
): stringifyable is StringifyableAtomic {
return (
["string", "number", "boolean"].includes(typeof stringifyable) ||
stringifyable === null
);
}
export const { getValueAtPath } = (() => {
function getValueAtPath_rec(
stringifyable: Stringifyable,
path: (string | number)[]
): Stringifyable | undefined {
if (path.length === 0) {
return stringifyable;
}
if (getIsAtomic(stringifyable)) {
return undefined;
}
const [first, ...rest] = path;
let dereferenced: Stringifyable | undefined;
if (stringifyable instanceof Array) {
if (typeof first !== "number") {
return undefined;
}
dereferenced = stringifyable[first];
} else {
if (typeof first !== "string") {
return undefined;
}
dereferenced = stringifyable[first];
}
if (dereferenced === undefined) {
return undefined;
}
return getValueAtPath_rec(dereferenced, rest);
}
function getValueAtPath(
stringifyableObjectOrArray: Record<string, Stringifyable> | Stringifyable[],
path: (string | number)[]
): Stringifyable | undefined {
return getValueAtPath_rec(stringifyableObjectOrArray, path);
}
return { getValueAtPath };
})();

View File

@ -0,0 +1,164 @@
import { getIsAtomic, getValueAtPath, type Stringifyable } from "./Stringifyable";
export function canonicalStringify(params: {
data: Record<string, Stringifyable> | Stringifyable[];
referenceData: Record<string, Stringifyable> | Stringifyable[];
}): string {
const { data, referenceData } = params;
return JSON.stringify(
makeDeterministicCopy({
path: [],
data,
getCanonicalKeys: path => {
const referenceValue = (() => {
const path_patched: (string | number)[] = [];
for (let i = 0; i < path.length; i++) {
let value_i = getValueAtPath(referenceData, [
...path_patched,
path[i]
]);
if (value_i !== undefined) {
path_patched.push(path[i]);
continue;
}
if (typeof path[i] !== "number") {
return undefined;
}
value_i = getValueAtPath(referenceData, [...path_patched, 0]);
if (value_i !== undefined) {
path_patched.push(0);
continue;
}
return undefined;
}
return getValueAtPath(referenceData, path_patched);
})();
if (referenceValue === undefined) {
return undefined;
}
if (getIsAtomic(referenceValue)) {
return undefined;
}
if (referenceValue instanceof Array) {
return undefined;
}
return Object.keys(referenceValue);
}
}),
null,
2
);
}
function makeDeterministicCopy(params: {
path: (string | number)[];
data: Stringifyable;
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Stringifyable {
const { path, data, getCanonicalKeys } = params;
if (getIsAtomic(data)) {
return data;
}
if (data instanceof Array) {
return makeDeterministicCopy_array({
path,
data,
getCanonicalKeys
});
}
return makeDeterministicCopy_record({
path,
data,
getCanonicalKeys
});
}
function makeDeterministicCopy_record(params: {
path: (string | number)[];
data: Record<string, Stringifyable>;
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Record<string, Stringifyable> {
const { path, data, getCanonicalKeys } = params;
const keysOfAtomicValues: string[] = [];
const keysOfNonAtomicValues: string[] = [];
for (const [key, value] of Object.entries(data)) {
if (getIsAtomic(value)) {
keysOfAtomicValues.push(key);
} else {
keysOfNonAtomicValues.push(key);
}
}
keysOfAtomicValues.sort();
keysOfNonAtomicValues.sort();
const keys = [...keysOfAtomicValues, ...keysOfNonAtomicValues];
reorder_according_to_canonical: {
const canonicalKeys = getCanonicalKeys(path);
if (canonicalKeys === undefined) {
break reorder_according_to_canonical;
}
const keys_toPrepend: string[] = [];
for (const key of canonicalKeys) {
const indexOfKey = keys.indexOf(key);
if (indexOfKey === -1) {
continue;
}
keys.splice(indexOfKey, 1);
keys_toPrepend.push(key);
}
keys.unshift(...keys_toPrepend);
}
const result: Record<string, Stringifyable> = {};
for (const key of keys) {
result[key] = makeDeterministicCopy({
path: [...path, key],
data: data[key],
getCanonicalKeys
});
}
return result;
}
function makeDeterministicCopy_array(params: {
path: (string | number)[];
data: Stringifyable[];
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Stringifyable[] {
const { path, data, getCanonicalKeys } = params;
return [...data].map((entry, i) =>
makeDeterministicCopy({
path: [...path, i],
data: entry,
getCanonicalKeys
})
);
}