From 3f3066cae5ff3272af16222a2fb0999229170f07 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 27 Feb 2024 18:59:35 +0100 Subject: [PATCH 01/25] First unit test for OIDCConfig --- test/server/lib/OIDCConfig.ts | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/server/lib/OIDCConfig.ts diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts new file mode 100644 index 00000000..92c9b3af --- /dev/null +++ b/test/server/lib/OIDCConfig.ts @@ -0,0 +1,46 @@ +import {OIDCConfig} from "app/server/lib/OIDCConfig"; +import {assert} from "chai"; +import {EnvironmentSnapshot} from "../testUtils"; + + +describe('OIDCConfig', () => { + let oldEnv: EnvironmentSnapshot; + + before(() => { + oldEnv = new EnvironmentSnapshot(); + }); + + afterEach(() => { + oldEnv.restore(); + }); + + function setEnvVars() { + process.env.GRIST_OIDC_SP_HOST = 'http://localhost:8484'; + process.env.GRIST_OIDC_IDP_CLIENT_ID = 'client id'; + process.env.GRIST_OIDC_IDP_CLIENT_SECRET = 'secret'; + process.env.GRIST_OIDC_IDP_ISSUER = 'http://localhost:8000'; + process.env.GRIST_OIDC_IDP_SCOPES = 'openid email profile'; + process.env.GRIST_OIDC_SP_PROFILE_NAME_ATTR = ''; // use the default behavior + process.env.GRIST_OIDC_SP_PROFILE_EMAIL_ATTR = ''; // use the default behavior + process.env.GRIST_OIDC_IDP_END_SESSION_ENDPOINT = 'http://localhost:8484/logout'; + process.env.GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT = 'false'; + process.env.GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED = 'false'; + } + + it('should throw when required env variables are not passed', async () => { + for (const envVar of [ + 'GRIST_OIDC_SP_HOST', + 'GRIST_OIDC_IDP_ISSUER', + 'GRIST_OIDC_IDP_CLIENT_ID', + 'GRIST_OIDC_IDP_CLIENT_SECRET', + ]) { + setEnvVars(); + delete process.env[envVar]; + const promise = OIDCConfig.build(); + await assert.isRejected(promise, `missing environment variable: ${envVar}`); + } + }); + + it('should throw when required env variables are empty', async () => { +}); + From 0eba178f05feb5106fd9227c0cd3741bb5dd58b4 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 27 Feb 2024 19:00:09 +0100 Subject: [PATCH 02/25] WIP --- app/server/lib/OIDCConfig.ts | 66 +++++++++++++++++++++++++++++------ test/server/lib/OIDCConfig.ts | 1 - 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 86f78bce..025b5487 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -55,13 +55,31 @@ import { GristLoginSystem, GristServer } from './GristServer'; import { Client, generators, Issuer, UserinfoResponse } from 'openid-client'; import { Sessions } from './Sessions'; import log from 'app/server/lib/log'; -import { appSettings } from './AppSettings'; +import { AppSettings, appSettings } from './AppSettings'; import { RequestWithLogin } from './Authorizer'; import { UserProfile } from 'app/common/LoginSessionAPI'; +import _ from 'lodash'; + +enum ENABLED_PROTECTIONS { + NONCE, + PKCE, + STATE, +} + +type EnabledProtectionsString = keyof typeof ENABLED_PROTECTIONS; const CALLBACK_URL = '/oauth2/callback'; export class OIDCConfig { + /** + * Handy alias to create an OIDCConfig instance and initialize it. + */ + public static async build(): Promise { + const config = new OIDCConfig(); + await config.initOIDC(); + return config; + } + private _client: Client; private _redirectUrl: string; private _namePropertyKey?: string; @@ -69,8 +87,9 @@ export class OIDCConfig { private _endSessionEndpoint: string; private _skipEndSessionEndpoint: boolean; private _ignoreEmailVerified: boolean; + private _enabledProtections: EnabledProtectionsString[] = []; - public constructor() { + private constructor() { } public async initOIDC(): Promise { @@ -113,6 +132,8 @@ export class OIDCConfig { defaultValue: false, })!; + this._enabledProtections = this._buildEnabledProtections(section); + const issuer = await Issuer.discover(issuerUrl); this._redirectUrl = new URL(CALLBACK_URL, spHost).href; this._client = new issuer.Client({ @@ -139,7 +160,7 @@ export class OIDCConfig { try { const params = this._client.callbackParams(req); const { state, targetUrl } = mreq.session?.oidc ?? {}; - if (!state) { + if (!state && this._supportsProtection('STATE')) { throw new Error('Login or logout failed to complete'); } @@ -210,15 +231,39 @@ export class OIDCConfig { private async _generateAndStoreConnectionInfo(req: express.Request, targetUrl: string) { const mreq = req as RequestWithLogin; if (!mreq.session) { throw new Error('no session available'); } - const codeVerifier = generators.codeVerifier(); - const state = generators.state(); - mreq.session.oidc = { - codeVerifier, - state, + const oidcInfo: {[key: string]: string} = { targetUrl }; + if (this._supportsProtection('PKCE')) { + oidcInfo.codeVerifier = generators.codeVerifier(); + } + if (this._supportsProtection('STATE')) { + oidcInfo.state = generators.state(); + } + if (this._supportsProtection('NONCE')) { + oidcInfo.nonce = generators.nonce(); + } + + mreq.session.oidc = oidcInfo; + + return _.pick(oidcInfo, ['codeVerifier', 'state', 'nonce']); + } - return { codeVerifier, state }; + private _supportsProtection(protection: EnabledProtectionsString) { + return this._enabledProtections.includes(protection); + } + + private _buildEnabledProtections(section: AppSettings): EnabledProtectionsString[] { + const enabledProtections = section.flag('enabledProtections').readString({ + envVar: 'GRIST_OIDC_ENABLED_PROTECTIONS', + defaultValue: 'PKCE,STATE', + })!.split(','); + for (const protection of enabledProtections) { + if (!ENABLED_PROTECTIONS[protection as EnabledProtectionsString]) { + throw new Error(`OIDC: Invalid protection in GRIST_OIDC_ENABLED_PROTECTIONS: ${protection}`); + } + } + return enabledProtections as EnabledProtectionsString[]; } private async _retrieveCodeVerifierFromSession(req: express.Request) { @@ -251,8 +296,7 @@ export async function getOIDCLoginSystem(): Promise if (!process.env.GRIST_OIDC_IDP_ISSUER) { return undefined; } return { async getMiddleware(gristServer: GristServer) { - const config = new OIDCConfig(); - await config.initOIDC(); + const config = await OIDCConfig.build(); return { getLoginRedirectUrl: config.getLoginRedirectUrl.bind(config), getSignUpRedirectUrl: config.getLoginRedirectUrl.bind(config), diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index 92c9b3af..d843211f 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -41,6 +41,5 @@ describe('OIDCConfig', () => { } }); - it('should throw when required env variables are empty', async () => { }); From 6c95af32b839d0ba494e030dc5d1998dba250c94 Mon Sep 17 00:00:00 2001 From: fflorent Date: Thu, 29 Feb 2024 12:54:16 +0100 Subject: [PATCH 03/25] WIP --- app/server/lib/OIDCConfig.ts | 48 +++++++----- test/server/lib/OIDCConfig.ts | 144 ++++++++++++++++++++++++++++++---- 2 files changed, 156 insertions(+), 36 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 025b5487..e466e254 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -80,7 +80,7 @@ export class OIDCConfig { return config; } - private _client: Client; + protected _client: Client; private _redirectUrl: string; private _namePropertyKey?: string; private _emailPropertyKey: string; @@ -89,7 +89,7 @@ export class OIDCConfig { private _ignoreEmailVerified: boolean; private _enabledProtections: EnabledProtectionsString[] = []; - private constructor() { + protected constructor() { } public async initOIDC(): Promise { @@ -133,15 +133,9 @@ export class OIDCConfig { })!; this._enabledProtections = this._buildEnabledProtections(section); - - const issuer = await Issuer.discover(issuerUrl); this._redirectUrl = new URL(CALLBACK_URL, spHost).href; - this._client = new issuer.Client({ - client_id: clientId, - client_secret: clientSecret, - redirect_uris: [ this._redirectUrl ], - response_types: [ 'code' ], - }); + await this._initClient({issuerUrl, clientId, clientSecret}); + if (this._client.issuer.metadata.end_session_endpoint === undefined && !this._endSessionEndpoint && !this._skipEndSessionEndpoint) { throw new Error('The Identity provider does not propose end_session_endpoint. ' + @@ -160,7 +154,7 @@ export class OIDCConfig { try { const params = this._client.callbackParams(req); const { state, targetUrl } = mreq.session?.oidc ?? {}; - if (!state && this._supportsProtection('STATE')) { + if (!state && this.supportsProtection('STATE')) { throw new Error('Login or logout failed to complete'); } @@ -228,19 +222,35 @@ export class OIDCConfig { }); } + public supportsProtection(protection: EnabledProtectionsString) { + return this._enabledProtections.includes(protection); + } + + protected async _initClient({issuerUrl, clientId, clientSecret}: + {issuerUrl: string, clientId: string, clientSecret: string} + ): Promise { + const issuer = await Issuer.discover(issuerUrl); + this._client = new issuer.Client({ + client_id: clientId, + client_secret: clientSecret, + redirect_uris: [ this._redirectUrl ], + response_types: [ 'code' ], + }); + } + private async _generateAndStoreConnectionInfo(req: express.Request, targetUrl: string) { const mreq = req as RequestWithLogin; if (!mreq.session) { throw new Error('no session available'); } const oidcInfo: {[key: string]: string} = { targetUrl }; - if (this._supportsProtection('PKCE')) { + if (this.supportsProtection('PKCE')) { oidcInfo.codeVerifier = generators.codeVerifier(); } - if (this._supportsProtection('STATE')) { + if (this.supportsProtection('STATE')) { oidcInfo.state = generators.state(); } - if (this._supportsProtection('NONCE')) { + if (this.supportsProtection('NONCE')) { oidcInfo.nonce = generators.nonce(); } @@ -249,18 +259,14 @@ export class OIDCConfig { return _.pick(oidcInfo, ['codeVerifier', 'state', 'nonce']); } - private _supportsProtection(protection: EnabledProtectionsString) { - return this._enabledProtections.includes(protection); - } - private _buildEnabledProtections(section: AppSettings): EnabledProtectionsString[] { const enabledProtections = section.flag('enabledProtections').readString({ - envVar: 'GRIST_OIDC_ENABLED_PROTECTIONS', + envVar: 'GRIST_OIDC_IDP_ENABLED_PROTECTIONS', defaultValue: 'PKCE,STATE', })!.split(','); for (const protection of enabledProtections) { - if (!ENABLED_PROTECTIONS[protection as EnabledProtectionsString]) { - throw new Error(`OIDC: Invalid protection in GRIST_OIDC_ENABLED_PROTECTIONS: ${protection}`); + if (!ENABLED_PROTECTIONS.hasOwnProperty(protection as EnabledProtectionsString)) { + throw new Error(`OIDC: Invalid protection in GRIST_OIDC_IDP_ENABLED_PROTECTIONS: ${protection}`); } } return enabledProtections as EnabledProtectionsString[]; diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index d843211f..a46e7141 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -1,17 +1,51 @@ import {OIDCConfig} from "app/server/lib/OIDCConfig"; import {assert} from "chai"; import {EnvironmentSnapshot} from "../testUtils"; +import Sinon from "sinon"; +import {Client} from "openid-client"; +class OIDCConfigStubbed extends OIDCConfig { + public static async build(clientStub?: Client): Promise { + const result = new OIDCConfigStubbed(); + if (clientStub) { + result._initClient = Sinon.spy(() => { + result._client = clientStub!; + }); + } + await result.initOIDC(); + return result; + } + + public _initClient: Sinon.SinonSpy; +} + +class ClientStub { + public issuer: { + metadata: { + end_session_endpoint: string | undefined; + } + } = { + metadata: { + end_session_endpoint: 'http://localhost:8484/logout', + } + }; +} describe('OIDCConfig', () => { let oldEnv: EnvironmentSnapshot; + let sandbox: Sinon.SinonSandbox; before(() => { oldEnv = new EnvironmentSnapshot(); }); + beforeEach(() => { + sandbox = Sinon.createSandbox(); + }); + afterEach(() => { oldEnv.restore(); + sandbox.restore(); }); function setEnvVars() { @@ -22,24 +56,104 @@ describe('OIDCConfig', () => { process.env.GRIST_OIDC_IDP_SCOPES = 'openid email profile'; process.env.GRIST_OIDC_SP_PROFILE_NAME_ATTR = ''; // use the default behavior process.env.GRIST_OIDC_SP_PROFILE_EMAIL_ATTR = ''; // use the default behavior - process.env.GRIST_OIDC_IDP_END_SESSION_ENDPOINT = 'http://localhost:8484/logout'; - process.env.GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT = 'false'; - process.env.GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED = 'false'; } - it('should throw when required env variables are not passed', async () => { - for (const envVar of [ - 'GRIST_OIDC_SP_HOST', - 'GRIST_OIDC_IDP_ISSUER', - 'GRIST_OIDC_IDP_CLIENT_ID', - 'GRIST_OIDC_IDP_CLIENT_SECRET', - ]) { + describe('build', () => { + it('should reject when required env variables are not passed', async () => { + for (const envVar of [ + 'GRIST_OIDC_SP_HOST', + 'GRIST_OIDC_IDP_ISSUER', + 'GRIST_OIDC_IDP_CLIENT_ID', + 'GRIST_OIDC_IDP_CLIENT_SECRET', + ]) { + setEnvVars(); + delete process.env[envVar]; + const promise = OIDCConfig.build(); + await assert.isRejected(promise, `missing environment variable: ${envVar}`); + } + }); + + it('should reject when the client initialization fails', async () => { setEnvVars(); - delete process.env[envVar]; - const promise = OIDCConfig.build(); - await assert.isRejected(promise, `missing environment variable: ${envVar}`); - } + sandbox.stub(OIDCConfigStubbed.prototype, '_initClient').rejects(new Error('client init failed')); + const promise = OIDCConfigStubbed.build(); + await assert.isRejected(promise, 'client init failed'); + }); + + describe('End Session Endpoint', () => { + [ + { + itMsg: 'should fulfill when the end_session_endpoint is not known ' + + 'and GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true', + end_session_endpoint: undefined, + env: { + GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT: 'true' + } + }, + { + itMsg: 'should fulfill when the end_session_endpoint is not known ' + + 'and GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true', + end_session_endpoint: undefined, + env: { + GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT: 'true' + } + }, + { + itMsg: 'should fulfill when the end_session_endpoint is provided with GRIST_OIDC_IDP_END_SESSION_ENDPOINT', + end_session_endpoint: undefined, + env: { + GRIST_OIDC_IDP_END_SESSION_ENDPOINT: 'http://localhost:8484/logout' + } + }, + { + itMsg: 'should fulfill when the end_session_endpoint is provided with the issuer', + end_session_endpoint: 'http://localhost:8484/logout', + }, + { + itMsg: 'should reject when the end_session_endpoint is not known', + errorMsg: /If that is expected, please set GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT/, + end_session_endpoint: undefined, + } + ].forEach((ctx) => { + it(ctx.itMsg, async () => { + setEnvVars(); + Object.assign(process.env, ctx.env); + const client = new ClientStub(); + client.issuer.metadata.end_session_endpoint = ctx.end_session_endpoint; + const promise = OIDCConfigStubbed.build(client as Client); + if (ctx.errorMsg) { + await assert.isRejected(promise, ctx.errorMsg); + } else { + await assert.isFulfilled(promise); + } + }); + }); + }); }); -}); + describe('GRIST_OIDC_IDP_ENABLED_PROTECTIONS', () => { + it('should throw when GRIST_OIDC_IDP_ENABLED_PROTECTIONS contains unsupported values', async () => { + setEnvVars(); + process.env.GRIST_OIDC_IDP_ENABLED_PROTECTIONS = 'STATE,NONCE,PKCE,invalid'; + const promise = OIDCConfig.build(); + await assert.isRejected(promise, 'OIDC: Invalid protection in GRIST_OIDC_IDP_ENABLED_PROTECTIONS: invalid'); + }); + it('should successfully change the supported protections', async function () { + setEnvVars(); + process.env.GRIST_OIDC_IDP_ENABLED_PROTECTIONS = 'NONCE'; + const config = await OIDCConfigStubbed.build(new ClientStub() as Client); + assert.isTrue(config.supportsProtection("NONCE")); + assert.isFalse(config.supportsProtection("PKCE")); + assert.isFalse(config.supportsProtection("STATE")); + }); + + it('if omitted, should defaults to "STATE,PKCE"', async function () { + setEnvVars(); + const config = await OIDCConfigStubbed.build(new ClientStub() as Client); + assert.isFalse(config.supportsProtection("NONCE")); + assert.isTrue(config.supportsProtection("PKCE")); + assert.isTrue(config.supportsProtection("STATE")); + }); + }); +}); From 75b06d622e547cf27394e6d80de59ef072a44d3d Mon Sep 17 00:00:00 2001 From: fflorent Date: Thu, 29 Feb 2024 18:26:58 +0100 Subject: [PATCH 04/25] WIP --- app/server/lib/OIDCConfig.ts | 29 ++++- test/server/lib/OIDCConfig.ts | 196 ++++++++++++++++++++++++++++++++-- 2 files changed, 214 insertions(+), 11 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index e466e254..e45ed320 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -35,6 +35,11 @@ * env GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED * If set to "true", the user will be allowed to login even if the email is not verified by the IDP. * Defaults to false. + * env GRIST_OIDC_IDP_ENABLED_PROTECTIONS + * A comma-separated list of protections to enable. Supported values are "PKCE", "STATE", "NONCE". + * Defaults to "PKCE,STATE". + * env GRIST_OIDC_IDP_ACR_VALUES + * A space-separated list of ACR values to request from the IdP. Optional. * * This version of OIDCConfig has been tested with Keycloak OIDC IdP following the instructions * at: @@ -88,6 +93,7 @@ export class OIDCConfig { private _skipEndSessionEndpoint: boolean; private _ignoreEmailVerified: boolean; private _enabledProtections: EnabledProtectionsString[] = []; + private _acrValues?: string; protected constructor() { } @@ -127,6 +133,10 @@ export class OIDCConfig { defaultValue: false, })!; + this._acrValues = section.flag('acrValues').readString({ + envVar: 'GRIST_OIDC_IDP_ACR_VALUES', + })!; + this._ignoreEmailVerified = section.flag('ignoreEmailVerified').readBool({ envVar: 'GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED', defaultValue: false, @@ -196,14 +206,12 @@ export class OIDCConfig { } public async getLoginRedirectUrl(req: express.Request, targetUrl: URL): Promise { - const { codeVerifier, state } = await this._generateAndStoreConnectionInfo(req, targetUrl.href); - const codeChallenge = generators.codeChallenge(codeVerifier); + const protections = await this._generateAndStoreConnectionInfo(req, targetUrl.href); const authUrl = this._client.authorizationUrl({ scope: process.env.GRIST_OIDC_IDP_SCOPES || 'openid email profile', - code_challenge: codeChallenge, - code_challenge_method: 'S256', - state, + acr_values: this._acrValues ?? undefined, + ...this._forgeProtectionParamsForAuthUrl(protections), }); return authUrl; } @@ -238,6 +246,17 @@ export class OIDCConfig { }); } + private _forgeProtectionParamsForAuthUrl(protections: {codeVerifier?: string, state?: string, nonce?: string}) { + return _.omitBy({ + state: protections.state, + nonce: protections.nonce, + code_challenge: protections.codeVerifier ? + generators.codeChallenge(protections.codeVerifier) : + undefined, + code_challenge_method: protections.codeVerifier ? 'S256' : undefined, + }, _.isUndefined); + } + private async _generateAndStoreConnectionInfo(req: express.Request, targetUrl: string) { const mreq = req as RequestWithLogin; if (!mreq.session) { throw new Error('no session available'); } diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index a46e7141..01343d57 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -2,10 +2,13 @@ import {OIDCConfig} from "app/server/lib/OIDCConfig"; import {assert} from "chai"; import {EnvironmentSnapshot} from "../testUtils"; import Sinon from "sinon"; -import {Client} from "openid-client"; +import {Client, generators} from "openid-client"; +import express from "express"; +import log from "app/server/lib/log"; +import _ from "lodash"; class OIDCConfigStubbed extends OIDCConfig { - public static async build(clientStub?: Client): Promise { + public static async build(clientStub?: Client): Promise { const result = new OIDCConfigStubbed(); if (clientStub) { result._initClient = Sinon.spy(() => { @@ -20,6 +23,8 @@ class OIDCConfigStubbed extends OIDCConfig { } class ClientStub { + public static FAKE_REDIRECT_URL = 'FAKE_REDIRECT_URL'; + public authorizationUrl = Sinon.stub().returns(ClientStub.FAKE_REDIRECT_URL); public issuer: { metadata: { end_session_endpoint: string | undefined; @@ -29,11 +34,18 @@ class ClientStub { end_session_endpoint: 'http://localhost:8484/logout', } }; + public asClient() { + return this as unknown as Client; + } + public getAuthorizationUrlStub() { + return this.authorizationUrl; + } } describe('OIDCConfig', () => { let oldEnv: EnvironmentSnapshot; let sandbox: Sinon.SinonSandbox; + let logInfoStub: Sinon.SinonStub; before(() => { oldEnv = new EnvironmentSnapshot(); @@ -41,6 +53,7 @@ describe('OIDCConfig', () => { beforeEach(() => { sandbox = Sinon.createSandbox(); + logInfoStub = sandbox.stub(log, 'info'); }); afterEach(() => { @@ -53,7 +66,6 @@ describe('OIDCConfig', () => { process.env.GRIST_OIDC_IDP_CLIENT_ID = 'client id'; process.env.GRIST_OIDC_IDP_CLIENT_SECRET = 'secret'; process.env.GRIST_OIDC_IDP_ISSUER = 'http://localhost:8000'; - process.env.GRIST_OIDC_IDP_SCOPES = 'openid email profile'; process.env.GRIST_OIDC_SP_PROFILE_NAME_ATTR = ''; // use the default behavior process.env.GRIST_OIDC_SP_PROFILE_EMAIL_ATTR = ''; // use the default behavior } @@ -80,6 +92,23 @@ describe('OIDCConfig', () => { await assert.isRejected(promise, 'client init failed'); }); + it('should create a client with passed information', async () => { + setEnvVars(); + const client = new ClientStub(); + const config = await OIDCConfigStubbed.build(client.asClient()); + assert.isTrue(config._initClient.calledOnce); + assert.deepEqual(config._initClient.firstCall.args, [{ + clientId: process.env.GRIST_OIDC_IDP_CLIENT_ID, + clientSecret: process.env.GRIST_OIDC_IDP_CLIENT_SECRET, + issuerUrl: process.env.GRIST_OIDC_IDP_ISSUER, + }]); + assert.isTrue(logInfoStub.calledOnce); + assert.deepEqual( + logInfoStub.firstCall.args, + [`OIDCConfig: initialized with issuer ${process.env.GRIST_OIDC_IDP_ISSUER}`] + ); + }); + describe('End Session Endpoint', () => { [ { @@ -120,11 +149,13 @@ describe('OIDCConfig', () => { Object.assign(process.env, ctx.env); const client = new ClientStub(); client.issuer.metadata.end_session_endpoint = ctx.end_session_endpoint; - const promise = OIDCConfigStubbed.build(client as Client); + const promise = OIDCConfigStubbed.build(client.asClient()); if (ctx.errorMsg) { await assert.isRejected(promise, ctx.errorMsg); + assert.isFalse(logInfoStub.calledOnce); } else { await assert.isFulfilled(promise); + assert.isTrue(logInfoStub.calledOnce); } }); }); @@ -142,7 +173,7 @@ describe('OIDCConfig', () => { it('should successfully change the supported protections', async function () { setEnvVars(); process.env.GRIST_OIDC_IDP_ENABLED_PROTECTIONS = 'NONCE'; - const config = await OIDCConfigStubbed.build(new ClientStub() as Client); + const config = await OIDCConfigStubbed.build((new ClientStub()).asClient()); assert.isTrue(config.supportsProtection("NONCE")); assert.isFalse(config.supportsProtection("PKCE")); assert.isFalse(config.supportsProtection("STATE")); @@ -150,10 +181,163 @@ describe('OIDCConfig', () => { it('if omitted, should defaults to "STATE,PKCE"', async function () { setEnvVars(); - const config = await OIDCConfigStubbed.build(new ClientStub() as Client); + const config = await OIDCConfigStubbed.build((new ClientStub()).asClient()); assert.isFalse(config.supportsProtection("NONCE")); assert.isTrue(config.supportsProtection("PKCE")); assert.isTrue(config.supportsProtection("STATE")); }); }); + + describe('getLoginRedirectUrl', () => { + const FAKE_NONCE = 'fake-nonce'; + const FAKE_STATE = 'fake-state'; + const FAKE_CODE_VERIFIER = 'fake-code-verifier'; + const FAKE_CODE_CHALLENGE = 'fake-code-challenge'; + const TARGET_URL = 'http://localhost:8484/'; + + beforeEach(() => { + sandbox.stub(generators, 'nonce').returns(FAKE_NONCE); + sandbox.stub(generators, 'state').returns(FAKE_STATE); + sandbox.stub(generators, 'codeVerifier').returns(FAKE_CODE_VERIFIER); + sandbox.stub(generators, 'codeChallenge').returns(FAKE_CODE_CHALLENGE); + }); + + [ + { + itMsg: 'should forge the url with default values', + expectedCalledWith: [{ + scope: 'openid email profile', + acr_values: undefined, + code_challenge: FAKE_CODE_CHALLENGE, + code_challenge_method: 'S256', + state: FAKE_STATE, + }], + expectedSession: { + oidc: { + codeVerifier: FAKE_CODE_VERIFIER, + state: FAKE_STATE, + targetUrl: TARGET_URL, + } + } + }, + { + itMsg: 'should forge the URL with passed GRIST_OIDC_IDP_SCOPES', + env: { + GRIST_OIDC_IDP_SCOPES: 'my scopes', + }, + expectedCalledWith: [{ + scope: 'my scopes', + acr_values: undefined, + code_challenge: FAKE_CODE_CHALLENGE, + code_challenge_method: 'S256', + state: FAKE_STATE, + }], + expectedSession: { + oidc: { + codeVerifier: FAKE_CODE_VERIFIER, + state: FAKE_STATE, + targetUrl: TARGET_URL, + } + } + }, + { + itMsg: 'should pass the nonce when GRIST_OIDC_IDP_ENABLED_PROTECTIONS includes NONCE', + env: { + GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE,PKCE', + }, + expectedCalledWith: [{ + scope: 'openid email profile', + acr_values: undefined, + code_challenge: FAKE_CODE_CHALLENGE, + code_challenge_method: 'S256', + state: FAKE_STATE, + nonce: FAKE_NONCE, + }], + expectedSession: { + oidc: { + codeVerifier: FAKE_CODE_VERIFIER, + nonce: FAKE_NONCE, + state: FAKE_STATE, + targetUrl: TARGET_URL, + } + } + }, + { + itMsg: 'should not pass the code_challenge when PKCE is omitted in GRIST_OIDC_IDP_ENABLED_PROTECTIONS', + env: { + GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE', + }, + expectedCalledWith: [{ + scope: 'openid email profile', + acr_values: undefined, + state: FAKE_STATE, + nonce: FAKE_NONCE, + }], + expectedSession: { + oidc: { + nonce: FAKE_NONCE, + state: FAKE_STATE, + targetUrl: TARGET_URL, + } + } + }, + ].forEach(ctx => { + it(ctx.itMsg, async () => { + setEnvVars(); + Object.assign(process.env, ctx.env); + const clientStub = new ClientStub(); + const config = await OIDCConfigStubbed.build(clientStub.asClient()); + const session = {}; + const req = { + session + } as unknown as express.Request; + const url = await config.getLoginRedirectUrl(req, new URL(TARGET_URL)); + assert.equal(url, ClientStub.FAKE_REDIRECT_URL); + assert.isTrue(clientStub.authorizationUrl.calledOnce); + assert.deepEqual(clientStub.authorizationUrl.firstCall.args, ctx.expectedCalledWith); + assert.deepEqual(session, ctx.expectedSession); + }); + }); + }); + + // describe('handleCallback', () => { + // const FAKE_STATE = 'fake-state'; + // const FAKE_CODE_VERIFIER = 'fake-code-verifier'; + // const FAKE_CODE_CHALLENGE = 'fake-code-challenge'; + // const FAKE_NONCE = 'fake-nonce'; + // const FAKE_CODE = 'fake-code'; + // let fakeRes: express.Response; + + // beforeEach(() => { + // fakeRes = { + // redirect: sandbox.stub(), + // status: sandbox.stub().returnsThis(), + // send: sandbox.stub().returnsThis(), + // } as unknown as express.Response; + // }); + + // [ + // { + // itMsg: 'should reject when the state is not found in the session', + // session: {}, + // expectedErrorMsg: /Login or logout failed to complete/, + // } + // ].forEach(ctx => { + // it(ctx.itMsg, async () => { + // setEnvVars(); + // const clientStub = new ClientStub(); + // const config = await OIDCConfigStubbed.build(clientStub.asClient()); + // const req = { + // session: ctx.session, + // query: { + // state: FAKE_STATE, + // code: FAKE_CODE, + // } + // } as unknown as express.Request; + // const promise = config.handleCallback(req, fakeRes); + // await assert.isRejected(promise, ctx.expectedErrorMsg); + // }); + // }); + // }); + }); From d3bf14fe28046020ee5feffff78d0e9e384d239e Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 5 Mar 2024 14:08:59 +0100 Subject: [PATCH 05/25] More tests --- app/server/lib/OIDCConfig.ts | 7 +- test/server/lib/OIDCConfig.ts | 146 +++++++++++++++++++++++++--------- 2 files changed, 113 insertions(+), 40 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index e45ed320..95584083 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -201,7 +201,7 @@ export class OIDCConfig { // // Also session deletion must be done before sending the response. delete mreq.session.oidc; - res.status(500).send(`OIDC callback failed.`); + res.status(500).send('OIDC callback failed.'); } } @@ -283,6 +283,9 @@ export class OIDCConfig { envVar: 'GRIST_OIDC_IDP_ENABLED_PROTECTIONS', defaultValue: 'PKCE,STATE', })!.split(','); + if (enabledProtections.length === 1 && enabledProtections[0] === '') { + return []; + } for (const protection of enabledProtections) { if (!ENABLED_PROTECTIONS.hasOwnProperty(protection as EnabledProtectionsString)) { throw new Error(`OIDC: Invalid protection in GRIST_OIDC_IDP_ENABLED_PROTECTIONS: ${protection}`); @@ -295,7 +298,7 @@ export class OIDCConfig { const mreq = req as RequestWithLogin; if (!mreq.session) { throw new Error('no session available'); } const codeVerifier = mreq.session.oidc?.codeVerifier; - if (!codeVerifier) { throw new Error('Login is stale'); } + if (!codeVerifier && this.supportsProtection('PKCE') ) { throw new Error('Login is stale'); } return codeVerifier; } diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index 01343d57..20508330 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -5,7 +5,7 @@ import Sinon from "sinon"; import {Client, generators} from "openid-client"; import express from "express"; import log from "app/server/lib/log"; -import _ from "lodash"; +import {Sessions} from "app/server/lib/Sessions"; class OIDCConfigStubbed extends OIDCConfig { public static async build(clientStub?: Client): Promise { @@ -25,6 +25,9 @@ class OIDCConfigStubbed extends OIDCConfig { class ClientStub { public static FAKE_REDIRECT_URL = 'FAKE_REDIRECT_URL'; public authorizationUrl = Sinon.stub().returns(ClientStub.FAKE_REDIRECT_URL); + public callbackParams = Sinon.stub().returns(undefined); + public callback = Sinon.stub().returns(undefined); + public userinfo = Sinon.stub().returns(undefined); public issuer: { metadata: { end_session_endpoint: string | undefined; @@ -46,6 +49,7 @@ describe('OIDCConfig', () => { let oldEnv: EnvironmentSnapshot; let sandbox: Sinon.SinonSandbox; let logInfoStub: Sinon.SinonStub; + let logErrorStub: Sinon.SinonStub; before(() => { oldEnv = new EnvironmentSnapshot(); @@ -54,6 +58,7 @@ describe('OIDCConfig', () => { beforeEach(() => { sandbox = Sinon.createSandbox(); logInfoStub = sandbox.stub(log, 'info'); + logErrorStub = sandbox.stub(log, 'error'); }); afterEach(() => { @@ -179,6 +184,15 @@ describe('OIDCConfig', () => { assert.isFalse(config.supportsProtection("STATE")); }); + it('should successfully accept an empty string', async function () { + setEnvVars(); + process.env.GRIST_OIDC_IDP_ENABLED_PROTECTIONS = ''; + const config = await OIDCConfigStubbed.build((new ClientStub()).asClient()); + assert.isFalse(config.supportsProtection("NONCE")); + assert.isFalse(config.supportsProtection("PKCE")); + assert.isFalse(config.supportsProtection("STATE")); + }); + it('if omitted, should defaults to "STATE,PKCE"', async function () { setEnvVars(); const config = await OIDCConfigStubbed.build((new ClientStub()).asClient()); @@ -300,44 +314,100 @@ describe('OIDCConfig', () => { }); }); - // describe('handleCallback', () => { - // const FAKE_STATE = 'fake-state'; - // const FAKE_CODE_VERIFIER = 'fake-code-verifier'; - // const FAKE_CODE_CHALLENGE = 'fake-code-challenge'; - // const FAKE_NONCE = 'fake-nonce'; - // const FAKE_CODE = 'fake-code'; - // let fakeRes: express.Response; + describe('handleCallback', () => { + const FAKE_STATE = 'fake-state'; + const FAKE_CODE_VERIFIER = 'fake-code-verifier'; + const FAKE_USER_INFO = { + email: 'fake-email', + name: 'fake-name', + email_verified: true, + }; + const DEFAULT_SESSION = { + oidc: { + codeVerifier: FAKE_CODE_VERIFIER, + state: FAKE_STATE + } + }; + let fakeRes: { + status: Sinon.SinonStub; + send: Sinon.SinonStub; + redirect: Sinon.SinonStub; + }; + let fakeSessions: { + getOrCreateSessionFromRequest: Sinon.SinonStub + }; + let fakeScopedSession; - // beforeEach(() => { - // fakeRes = { - // redirect: sandbox.stub(), - // status: sandbox.stub().returnsThis(), - // send: sandbox.stub().returnsThis(), - // } as unknown as express.Response; - // }); + beforeEach(() => { + fakeRes = { + redirect: sandbox.stub(), + status: sandbox.stub().returnsThis(), + send: sandbox.stub().returnsThis(), + }; + fakeScopedSession = { + operateOnScopedSession: sandbox.stub().resolves(), + }; + fakeSessions = { + getOrCreateSessionFromRequest: sandbox.stub().returns(fakeScopedSession), + }; + }); - // [ - // { - // itMsg: 'should reject when the state is not found in the session', - // session: {}, - // expectedErrorMsg: /Login or logout failed to complete/, - // } - // ].forEach(ctx => { - // it(ctx.itMsg, async () => { - // setEnvVars(); - // const clientStub = new ClientStub(); - // const config = await OIDCConfigStubbed.build(clientStub.asClient()); - // const req = { - // session: ctx.session, - // query: { - // state: FAKE_STATE, - // code: FAKE_CODE, - // } - // } as unknown as express.Request; - // const promise = config.handleCallback(req, fakeRes); - // await assert.isRejected(promise, ctx.expectedErrorMsg); - // }); - // }); - // }); + [ + { + itMsg: 'should reject when the state is not found in the session', + session: {}, + expectedErrorMsg: /Login or logout failed to complete/, + }, + { + itMsg: 'should resolve when the state is not found in the session but ' + + 'GRIST_OIDC_IDP_ENABLED_PROTECTIONS omits STATE', + session: DEFAULT_SESSION, + env: { + GRIST_OIDC_IDP_ENABLED_PROTECTIONS: '', + }, + } + ].forEach(ctx => { + it(ctx.itMsg, async () => { + setEnvVars(); + Object.assign(process.env, ctx.env); + const clientStub = new ClientStub(); + const fakeParams = { + state: FAKE_STATE, + }; + clientStub.callbackParams.returns(fakeParams); + clientStub.userinfo.returns(FAKE_USER_INFO); + const config = await OIDCConfigStubbed.build(clientStub.asClient()); + const req = { + session: ctx.session, + query: { + state: FAKE_STATE, + codeVerifier: FAKE_CODE_VERIFIER, + } + } as unknown as express.Request; + + await config.handleCallback( + fakeSessions as unknown as Sessions, + req, + fakeRes as unknown as express.Response + ); + + if (ctx.expectedErrorMsg) { + assert.isTrue(logErrorStub.calledOnce); + assert.match(logErrorStub.firstCall.args[0], ctx.expectedErrorMsg); + assert.isTrue(fakeRes.status.calledOnceWith(500)); + assert.isTrue(fakeRes.send.calledOnceWith('OIDC callback failed.')); + } else { + assert.isFalse(logErrorStub.called, 'no error should be logged'); + assert.isTrue(fakeRes.redirect.calledOnce, 'should redirect'); + assert.isTrue(clientStub.callback.calledOnce); + assert.deepEqual(clientStub.callback.firstCall.args, [ + 'http://localhost:8484/oauth2/callback', + fakeParams, + { state: FAKE_STATE, code_verifier: FAKE_CODE_VERIFIER } + ]); + } + }); + }); + }); }); From c64032960cd6860f25647d0d4991bc32b391c16f Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 5 Mar 2024 18:05:32 +0100 Subject: [PATCH 06/25] More tests, finish implementation --- app/server/lib/BrowserSession.ts | 1 + app/server/lib/OIDCConfig.ts | 33 +++--- test/server/lib/OIDCConfig.ts | 178 +++++++++++++++++++++++++++++-- 3 files changed, 186 insertions(+), 26 deletions(-) diff --git a/app/server/lib/BrowserSession.ts b/app/server/lib/BrowserSession.ts index 07136e3e..be3140ff 100644 --- a/app/server/lib/BrowserSession.ts +++ b/app/server/lib/BrowserSession.ts @@ -74,6 +74,7 @@ export interface SessionObj { codeVerifier?: string; state?: string; targetUrl?: string; + nonce?: string; } } diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 95584083..f54cd7fe 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -64,6 +64,7 @@ import { AppSettings, appSettings } from './AppSettings'; import { RequestWithLogin } from './Authorizer'; import { UserProfile } from 'app/common/LoginSessionAPI'; import _ from 'lodash'; +import {SessionObj} from './BrowserSession'; enum ENABLED_PROTECTIONS { NONCE, @@ -163,20 +164,12 @@ export class OIDCConfig { const mreq = req as RequestWithLogin; try { const params = this._client.callbackParams(req); - const { state, targetUrl } = mreq.session?.oidc ?? {}; - if (!state && this.supportsProtection('STATE')) { - throw new Error('Login or logout failed to complete'); - } - - const codeVerifier = await this._retrieveCodeVerifierFromSession(req); + const { targetUrl } = mreq.session?.oidc ?? {}; + const checks = await this._retrieveChecksFromSession(mreq); // The callback function will compare the state present in the params and the one we retrieved from the session. // If they don't match, it will throw an error. - const tokenSet = await this._client.callback( - this._redirectUrl, - params, - { state, code_verifier: codeVerifier } - ); + const tokenSet = await this._client.callback(this._redirectUrl, params, checks); const userInfo = await this._client.userinfo(tokenSet); @@ -260,7 +253,7 @@ export class OIDCConfig { private async _generateAndStoreConnectionInfo(req: express.Request, targetUrl: string) { const mreq = req as RequestWithLogin; if (!mreq.session) { throw new Error('no session available'); } - const oidcInfo: {[key: string]: string} = { + const oidcInfo: SessionObj['oidc'] = { targetUrl }; if (this.supportsProtection('PKCE')) { @@ -294,12 +287,22 @@ export class OIDCConfig { return enabledProtections as EnabledProtectionsString[]; } - private async _retrieveCodeVerifierFromSession(req: express.Request) { - const mreq = req as RequestWithLogin; + private async _retrieveChecksFromSession(mreq: RequestWithLogin): + Promise<{code_verifier?: string, state?: string, nonce?: string}> { if (!mreq.session) { throw new Error('no session available'); } + + const state = mreq.session.oidc?.state; + if (!state && this.supportsProtection('STATE')) { + throw new Error('Login or logout failed to complete'); + } + const codeVerifier = mreq.session.oidc?.codeVerifier; if (!codeVerifier && this.supportsProtection('PKCE') ) { throw new Error('Login is stale'); } - return codeVerifier; + + const nonce = mreq.session.oidc?.nonce; + if (!nonce && this.supportsProtection('NONCE')) { throw new Error('Login is stale'); } + + return _.omitBy({ code_verifier: codeVerifier, state, nonce }, _.isUndefined); } private _makeUserProfileFromUserInfo(userInfo: UserinfoResponse): Partial { diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index 20508330..4842e426 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -6,6 +6,7 @@ import {Client, generators} from "openid-client"; import express from "express"; import log from "app/server/lib/log"; import {Sessions} from "app/server/lib/Sessions"; +import _ from "lodash"; class OIDCConfigStubbed extends OIDCConfig { public static async build(clientStub?: Client): Promise { @@ -71,8 +72,6 @@ describe('OIDCConfig', () => { process.env.GRIST_OIDC_IDP_CLIENT_ID = 'client id'; process.env.GRIST_OIDC_IDP_CLIENT_SECRET = 'secret'; process.env.GRIST_OIDC_IDP_ISSUER = 'http://localhost:8000'; - process.env.GRIST_OIDC_SP_PROFILE_NAME_ATTR = ''; // use the default behavior - process.env.GRIST_OIDC_SP_PROFILE_EMAIL_ATTR = ''; // use the default behavior } describe('build', () => { @@ -316,6 +315,7 @@ describe('OIDCConfig', () => { describe('handleCallback', () => { const FAKE_STATE = 'fake-state'; + const FAKE_NONCE = 'fake-nonce'; const FAKE_CODE_VERIFIER = 'fake-code-verifier'; const FAKE_USER_INFO = { email: 'fake-email', @@ -328,6 +328,10 @@ describe('OIDCConfig', () => { state: FAKE_STATE } }; + const DEFAULT_EXPECTED_CHECKS = { + state: FAKE_STATE, + code_verifier: FAKE_CODE_VERIFIER + }; let fakeRes: { status: Sinon.SinonStub; send: Sinon.SinonStub; @@ -336,7 +340,9 @@ describe('OIDCConfig', () => { let fakeSessions: { getOrCreateSessionFromRequest: Sinon.SinonStub }; - let fakeScopedSession; + let fakeScopedSession: { + operateOnScopedSession: Sinon.SinonStub + }; beforeEach(() => { fakeRes = { @@ -353,19 +359,157 @@ describe('OIDCConfig', () => { }); [ + { + itMsg: 'should resolve when the state and the code challenge are found in the session', + session: DEFAULT_SESSION, + }, { itMsg: 'should reject when the state is not found in the session', session: {}, expectedErrorMsg: /Login or logout failed to complete/, }, { - itMsg: 'should resolve when the state is not found in the session but ' + - 'GRIST_OIDC_IDP_ENABLED_PROTECTIONS omits STATE', + itMsg: 'should resolve when the state is missing and its check has been disabled', session: DEFAULT_SESSION, env: { GRIST_OIDC_IDP_ENABLED_PROTECTIONS: '', }, - } + }, + { + itMsg: 'should reject when the codeVerifier is missing from the session', + session: { + oidc: { + state: FAKE_STATE + } + }, + expectedErrorMsg: /Login is stale/, + }, + { + itMsg: 'should resolve when the codeVerifier is missing and its check has been disabled', + session: { + oidc: { + state: FAKE_STATE, + nonce: FAKE_NONCE + } + }, + env: { + GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE', + }, + expectedChecks: { + state: FAKE_STATE, + nonce: FAKE_NONCE + }, + }, + { + itMsg: 'should reject when nonce is missing from the session despite its check being enabled', + session: DEFAULT_SESSION, + env: { + GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE,PKCE', + }, + expectedErrorMsg: /Login is stale/, + }, { + itMsg: 'should resolve when nonce is present in the session and its check is enabled', + session: { + oidc: { + state: FAKE_STATE, + nonce: FAKE_NONCE, + }, + }, + env: { + GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE', + }, + expectedChecks: { + state: FAKE_STATE, + nonce: FAKE_NONCE, + }, + }, + { + itMsg: 'should reject when the userinfo mail is not verified', + session: DEFAULT_SESSION, + userInfo: { + ...FAKE_USER_INFO, + email_verified: false, + }, + expectedErrorMsg: /email not verified for/, + }, + { + itMsg: 'should resolve when the userinfo mail is not verified but its check disabled', + session: DEFAULT_SESSION, + userInfo: { + ...FAKE_USER_INFO, + email_verified: false, + }, + env: { + GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED: 'true', + } + }, + { + itMsg: 'should resolve when the userinfo mail is not verified but its check disabled', + session: DEFAULT_SESSION, + userInfo: { + ...FAKE_USER_INFO, + email_verified: false, + }, + env: { + GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED: 'true', + }, + }, + { + itMsg: 'should fill user profile with email and name', + session: DEFAULT_SESSION, + userInfo: FAKE_USER_INFO, + expectedUserProfile: { + email: FAKE_USER_INFO.email, + name: FAKE_USER_INFO.name, + } + }, + { + itMsg: 'should fill user profile with name constructed using ' + + 'given_name and family_name when GRIST_OIDC_SP_PROFILE_NAME_ATTR is not set', + session: DEFAULT_SESSION, + userInfo: { + ...FAKE_USER_INFO, + given_name: 'given_name', + family_name: 'family_name', + }, + expectedUserProfile: { + email: 'fake-email', + name: 'given_name family_name', + } + }, + { + itMsg: 'should fill user profile with email and name when ' + + 'GRIST_OIDC_SP_PROFILE_NAME_ATTR and GRIST_OIDC_SP_PROFILE_EMAIL_ATTR are set', + session: DEFAULT_SESSION, + userInfo: { + ...FAKE_USER_INFO, + fooMail: 'fake-email2', + fooName: 'fake-name2', + }, + env: { + GRIST_OIDC_SP_PROFILE_NAME_ATTR: 'fooName', + GRIST_OIDC_SP_PROFILE_EMAIL_ATTR: 'fooMail', + }, + expectedUserProfile: { + email: 'fake-email2', + name: 'fake-name2', + } + }, + { + itMsg: 'should redirect by default to the root page', + session: DEFAULT_SESSION, + expectedRedirection: '/', + }, + { + itMsg: 'should redirect to the targetUrl when it is present in the session', + session: { + oidc: { + ...DEFAULT_SESSION.oidc, + targetUrl: 'http://localhost:8484/some/path' + } + }, + expectedRedirection: 'http://localhost:8484/some/path', + }, ].forEach(ctx => { it(ctx.itMsg, async () => { setEnvVars(); @@ -374,16 +518,19 @@ describe('OIDCConfig', () => { const fakeParams = { state: FAKE_STATE, }; - clientStub.callbackParams.returns(fakeParams); - clientStub.userinfo.returns(FAKE_USER_INFO); const config = await OIDCConfigStubbed.build(clientStub.asClient()); + const session = _.clone(ctx.session); // session is modified, so clone it const req = { - session: ctx.session, + session, query: { state: FAKE_STATE, codeVerifier: FAKE_CODE_VERIFIER, } } as unknown as express.Request; + clientStub.callbackParams.returns(fakeParams); + clientStub.userinfo.returns(_.clone(ctx.userInfo ?? FAKE_USER_INFO)); + const user: { profile?: object } = {}; + fakeScopedSession.operateOnScopedSession.yields(user); await config.handleCallback( fakeSessions as unknown as Sessions, @@ -397,14 +544,23 @@ describe('OIDCConfig', () => { assert.isTrue(fakeRes.status.calledOnceWith(500)); assert.isTrue(fakeRes.send.calledOnceWith('OIDC callback failed.')); } else { - assert.isFalse(logErrorStub.called, 'no error should be logged'); + assert.isFalse(logErrorStub.called, 'no error should be logged. Got: ' + logErrorStub.firstCall?.args[0]); assert.isTrue(fakeRes.redirect.calledOnce, 'should redirect'); + if (ctx.expectedRedirection) { + assert.deepEqual(fakeRes.redirect.firstCall.args, [ctx.expectedRedirection], + `should have redirected to ${ctx.expectedRedirection}`); + } assert.isTrue(clientStub.callback.calledOnce); assert.deepEqual(clientStub.callback.firstCall.args, [ 'http://localhost:8484/oauth2/callback', fakeParams, - { state: FAKE_STATE, code_verifier: FAKE_CODE_VERIFIER } + ctx.expectedChecks ?? DEFAULT_EXPECTED_CHECKS ]); + assert.isEmpty(session, 'oidc info should have been removed from the session'); + if (ctx.expectedUserProfile) { + assert.deepEqual(user.profile, ctx.expectedUserProfile, + `user profile should have been populated with ${JSON.stringify(ctx.expectedUserProfile)}`); + } } }); }); From 92c0a0994c0115039f52fdca262bcf282785becd Mon Sep 17 00:00:00 2001 From: fflorent Date: Wed, 6 Mar 2024 09:46:51 +0100 Subject: [PATCH 07/25] Test getLogoutRedirectUrl --- test/server/lib/OIDCConfig.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index 4842e426..ac4afd22 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -29,6 +29,7 @@ class ClientStub { public callbackParams = Sinon.stub().returns(undefined); public callback = Sinon.stub().returns(undefined); public userinfo = Sinon.stub().returns(undefined); + public endSessionUrl = Sinon.stub().returns(undefined); public issuer: { metadata: { end_session_endpoint: string | undefined; @@ -566,4 +567,37 @@ describe('OIDCConfig', () => { }); }); + describe('getLogoutRedirectUrl', () => { + const REDIRECT_URL = new URL('http://localhost:8484/docs/signed-out'); + const URL_RETURNED_BY_CLIENT = 'http://localhost:8484/logout_url_from_issuer'; + const ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT = 'http://localhost:8484/logout'; + + [{ + itMsg: 'should skip the end session endpoint when GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true', + env: { + GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT: 'true', + }, + expectedUrl: REDIRECT_URL.href, + }, { + itMsg: 'should use the GRIST_OIDC_IDP_END_SESSION_ENDPOINT when it is set', + env: { + GRIST_OIDC_IDP_END_SESSION_ENDPOINT: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT + }, + expectedUrl: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT + }, { + itMsg: 'should call the end session endpoint from the issuer metadata', + expectedUrl: URL_RETURNED_BY_CLIENT + }].forEach(ctx => { + it(ctx.itMsg, async () => { + setEnvVars(); + Object.assign(process.env, ctx.env); + const clientStub = new ClientStub(); + clientStub.endSessionUrl.returns(URL_RETURNED_BY_CLIENT); + const config = await OIDCConfigStubbed.build(clientStub.asClient()); + const req = {} as unknown as express.Request; // not used + const url = await config.getLogoutRedirectUrl(req, REDIRECT_URL); + assert.equal(url, ctx.expectedUrl); + }); + }); + }); }); From 58b3d3352a3301de08d759cfb8750596c65628e1 Mon Sep 17 00:00:00 2001 From: fflorent Date: Wed, 6 Mar 2024 11:48:43 +0100 Subject: [PATCH 08/25] Minor changes --- app/server/lib/OIDCConfig.ts | 1 + test/server/lib/OIDCConfig.ts | 44 ++++++++++++++++++----------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index f54cd7fe..bad54ec9 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -37,6 +37,7 @@ * Defaults to false. * env GRIST_OIDC_IDP_ENABLED_PROTECTIONS * A comma-separated list of protections to enable. Supported values are "PKCE", "STATE", "NONCE". + * It's highly recommended that you enable STATE, and at least either PKCE or NONCE. * Defaults to "PKCE,STATE". * env GRIST_OIDC_IDP_ACR_VALUES * A space-separated list of ACR values to request from the IdP. Optional. diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index ac4afd22..a6b0d112 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -347,15 +347,15 @@ describe('OIDCConfig', () => { beforeEach(() => { fakeRes = { - redirect: sandbox.stub(), - status: sandbox.stub().returnsThis(), - send: sandbox.stub().returnsThis(), + redirect: Sinon.stub(), + status: Sinon.stub().returnsThis(), + send: Sinon.stub().returnsThis(), }; fakeScopedSession = { - operateOnScopedSession: sandbox.stub().resolves(), + operateOnScopedSession: Sinon.stub().resolves(), }; fakeSessions = { - getOrCreateSessionFromRequest: sandbox.stub().returns(fakeScopedSession), + getOrCreateSessionFromRequest: Sinon.stub().returns(fakeScopedSession), }; }); @@ -572,22 +572,24 @@ describe('OIDCConfig', () => { const URL_RETURNED_BY_CLIENT = 'http://localhost:8484/logout_url_from_issuer'; const ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT = 'http://localhost:8484/logout'; - [{ - itMsg: 'should skip the end session endpoint when GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true', - env: { - GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT: 'true', - }, - expectedUrl: REDIRECT_URL.href, - }, { - itMsg: 'should use the GRIST_OIDC_IDP_END_SESSION_ENDPOINT when it is set', - env: { - GRIST_OIDC_IDP_END_SESSION_ENDPOINT: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT - }, - expectedUrl: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT - }, { - itMsg: 'should call the end session endpoint from the issuer metadata', - expectedUrl: URL_RETURNED_BY_CLIENT - }].forEach(ctx => { + [ + { + itMsg: 'should skip the end session endpoint when GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true', + env: { + GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT: 'true', + }, + expectedUrl: REDIRECT_URL.href, + }, { + itMsg: 'should use the GRIST_OIDC_IDP_END_SESSION_ENDPOINT when it is set', + env: { + GRIST_OIDC_IDP_END_SESSION_ENDPOINT: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT + }, + expectedUrl: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT + }, { + itMsg: 'should call the end session endpoint from the issuer metadata', + expectedUrl: URL_RETURNED_BY_CLIENT + } + ].forEach(ctx => { it(ctx.itMsg, async () => { setEnvVars(); Object.assign(process.env, ctx.env); From 81d2736ccbdcd1e91135399d57663fcbceb9b61e Mon Sep 17 00:00:00 2001 From: fflorent Date: Wed, 6 Mar 2024 14:28:29 +0100 Subject: [PATCH 09/25] WIP: LoginWithOIDC --- test/nbrowser/LoginWithOIDC.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 test/nbrowser/LoginWithOIDC.ts diff --git a/test/nbrowser/LoginWithOIDC.ts b/test/nbrowser/LoginWithOIDC.ts new file mode 100644 index 00000000..bf45e853 --- /dev/null +++ b/test/nbrowser/LoginWithOIDC.ts @@ -0,0 +1,21 @@ +// import {assert, driver} from 'mocha-webdriver'; +// import * as gu from 'test/nbrowser/gristUtils'; +// import {setupTestSuite} from 'test/nbrowser/testUtils'; +// import express from 'express'; + +// describe('LoginWithOIDC', function () { +// this.timeout(60000); +// setupTestSuite(); +// before(async () => { +// const app = express(); +// app.get('/.well-known/openid-configuration', (req, res) => { +// res.json({ +// }); +// }); +// app.listen(gu. +// }); +// gu.withEnvironmentSnapshot({ +// 'GRIST_OIDC_IDP_ISSUER': 'https://accounts.google.com', +// }) +// }); + From 2d2906c2e79ec1221befd699a7c8d362ff7eae5e Mon Sep 17 00:00:00 2001 From: fflorent Date: Thu, 7 Mar 2024 11:31:21 +0100 Subject: [PATCH 10/25] Log err.response if present --- app/server/lib/OIDCConfig.ts | 3 +++ test/nbrowser/LoginWithOIDC.ts | 2 ++ test/server/lib/OIDCConfig.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index bad54ec9..c4aea0b5 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -190,6 +190,9 @@ export class OIDCConfig { res.redirect(targetUrl ?? '/'); } catch (err) { log.error(`OIDC callback failed: ${err.stack}`); + if (Object.prototype.hasOwnProperty.call(err, 'response')) { + log.error(`Response received: ${err.response?.body ?? err.response}`); + } // Delete the session data even if the login failed. // This way, we prevent several login attempts. // diff --git a/test/nbrowser/LoginWithOIDC.ts b/test/nbrowser/LoginWithOIDC.ts index bf45e853..d3cf9b88 100644 --- a/test/nbrowser/LoginWithOIDC.ts +++ b/test/nbrowser/LoginWithOIDC.ts @@ -3,6 +3,8 @@ // import {setupTestSuite} from 'test/nbrowser/testUtils'; // import express from 'express'; +export {}; + // describe('LoginWithOIDC', function () { // this.timeout(60000); // setupTestSuite(); diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index a6b0d112..73d4fba0 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -565,6 +565,36 @@ describe('OIDCConfig', () => { } }); }); + + it('should log err.response when userinfo fails to parse response body', async () => { + // See https://github.com/panva/node-openid-client/blob/47a549cb4e36ffe2ebfe2dc9d6b69a02643cc0a9/lib/client.js#L1293 + setEnvVars(); + const clientStub = new ClientStub(); + const config = await OIDCConfigStubbed.build(clientStub.asClient()); + const req = { + session: DEFAULT_SESSION, + query: { + state: FAKE_STATE, + codeVerifier: FAKE_CODE_VERIFIER, + } + } as unknown as express.Request; + clientStub.callbackParams.returns({state: FAKE_STATE}); + + const err: Error & {response?: {body: string }} = new Error('userinfo failed'); + err.response = { body: 'response here' }; + clientStub.userinfo.rejects(err); + + await config.handleCallback( + fakeSessions as unknown as Sessions, + req, + fakeRes as unknown as express.Response + ); + + assert.isTrue(logErrorStub.calledTwice); + assert.include(logErrorStub.firstCall.args[0], err.message); + assert.include(logErrorStub.secondCall.args[0], err.response.body); + assert.isTrue(fakeRes.status.calledOnceWith(500)); + }); }); describe('getLogoutRedirectUrl', () => { From 454e97f1a7f7542a1786f5d7abd357e1f8eec057 Mon Sep 17 00:00:00 2001 From: fflorent Date: Thu, 7 Mar 2024 14:18:56 +0100 Subject: [PATCH 11/25] Add support for extra OIDC client metadata --- app/server/lib/OIDCConfig.ts | 18 ++++++++++++++---- test/server/lib/OIDCConfig.ts | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index c4aea0b5..4e99d5e0 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -41,6 +41,10 @@ * Defaults to "PKCE,STATE". * env GRIST_OIDC_IDP_ACR_VALUES * A space-separated list of ACR values to request from the IdP. Optional. + * env GRIST_OIDC_IDP_EXTRA_CLIENT_METADATA + * A JSON object with extra client metadata to pass to openid-client. Optional. + * More info: https://github.com/panva/node-openid-client/tree/main/docs#new-clientmetadata-jwks-options + * * * This version of OIDCConfig has been tested with Keycloak OIDC IdP following the instructions * at: @@ -58,7 +62,7 @@ import * as express from 'express'; import { GristLoginSystem, GristServer } from './GristServer'; -import { Client, generators, Issuer, UserinfoResponse } from 'openid-client'; +import { Client, ClientMetadata, generators, Issuer, UserinfoResponse } from 'openid-client'; import { Sessions } from './Sessions'; import log from 'app/server/lib/log'; import { AppSettings, appSettings } from './AppSettings'; @@ -144,9 +148,14 @@ export class OIDCConfig { defaultValue: false, })!; + const extraMetadata: Partial = JSON.parse(section.flag('extraClientMetadata').readString({ + envVar: 'GRIST_OIDC_IDP_EXTRA_CLIENT_METADATA', + defaultValue: '{}' + })!); + this._enabledProtections = this._buildEnabledProtections(section); this._redirectUrl = new URL(CALLBACK_URL, spHost).href; - await this._initClient({issuerUrl, clientId, clientSecret}); + await this._initClient({issuerUrl, clientId, clientSecret, extraMetadata}); if (this._client.issuer.metadata.end_session_endpoint === undefined && !this._endSessionEndpoint && !this._skipEndSessionEndpoint) { @@ -231,8 +240,8 @@ export class OIDCConfig { return this._enabledProtections.includes(protection); } - protected async _initClient({issuerUrl, clientId, clientSecret}: - {issuerUrl: string, clientId: string, clientSecret: string} + protected async _initClient({issuerUrl, clientId, clientSecret, extraMetadata}: + {issuerUrl: string, clientId: string, clientSecret: string, extraMetadata: Partial } ): Promise { const issuer = await Issuer.discover(issuerUrl); this._client = new issuer.Client({ @@ -240,6 +249,7 @@ export class OIDCConfig { client_secret: clientSecret, redirect_uris: [ this._redirectUrl ], response_types: [ 'code' ], + ...extraMetadata, }); } diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index 73d4fba0..c6b32dcb 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -106,6 +106,7 @@ describe('OIDCConfig', () => { clientId: process.env.GRIST_OIDC_IDP_CLIENT_ID, clientSecret: process.env.GRIST_OIDC_IDP_CLIENT_SECRET, issuerUrl: process.env.GRIST_OIDC_IDP_ISSUER, + extraMetadata: {}, }]); assert.isTrue(logInfoStub.calledOnce); assert.deepEqual( @@ -114,6 +115,23 @@ describe('OIDCConfig', () => { ); }); + it('should create a client with passed information with extra configuration', async () => { + setEnvVars(); + const extraMetadata = { + userinfo_signed_response_alg: 'RS256', + }; + process.env.GRIST_OIDC_IDP_EXTRA_CLIENT_METADATA = JSON.stringify(extraMetadata); + const client = new ClientStub(); + const config = await OIDCConfigStubbed.build(client.asClient()); + assert.isTrue(config._initClient.calledOnce); + assert.deepEqual(config._initClient.firstCall.args, [{ + clientId: process.env.GRIST_OIDC_IDP_CLIENT_ID, + clientSecret: process.env.GRIST_OIDC_IDP_CLIENT_SECRET, + issuerUrl: process.env.GRIST_OIDC_IDP_ISSUER, + extraMetadata, + }]); + }); + describe('End Session Endpoint', () => { [ { From 8cc71ae22a253068a5ce40bdfb2fa8f035980cd0 Mon Sep 17 00:00:00 2001 From: fflorent Date: Thu, 7 Mar 2024 15:23:42 +0100 Subject: [PATCH 12/25] Log userinfo in debug and check passed info in endSessionUrl --- app/server/lib/OIDCConfig.ts | 1 + test/server/lib/OIDCConfig.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 4e99d5e0..5ac76e7b 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -182,6 +182,7 @@ export class OIDCConfig { const tokenSet = await this._client.callback(this._redirectUrl, params, checks); const userInfo = await this._client.userinfo(tokenSet); + log.debug("Got userinfo: %o", userInfo); if (!this._ignoreEmailVerified && userInfo.email_verified !== true) { throw new Error(`OIDCConfig: email not verified for ${userInfo.email}`); diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index c6b32dcb..fe33350b 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -61,6 +61,7 @@ describe('OIDCConfig', () => { sandbox = Sinon.createSandbox(); logInfoStub = sandbox.stub(log, 'info'); logErrorStub = sandbox.stub(log, 'error'); + sandbox.stub(log, 'debug'); }); afterEach(() => { @@ -69,6 +70,12 @@ describe('OIDCConfig', () => { }); function setEnvVars() { + // Prevent any environment variable from leaking into the test: + for (const envVar in process.env) { + if (envVar.startsWith('GRIST_OIDC_')) { + delete process.env[envVar]; + } + } process.env.GRIST_OIDC_SP_HOST = 'http://localhost:8484'; process.env.GRIST_OIDC_IDP_CLIENT_ID = 'client id'; process.env.GRIST_OIDC_IDP_CLIENT_SECRET = 'secret'; @@ -635,7 +642,10 @@ describe('OIDCConfig', () => { expectedUrl: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT }, { itMsg: 'should call the end session endpoint from the issuer metadata', - expectedUrl: URL_RETURNED_BY_CLIENT + expectedUrl: URL_RETURNED_BY_CLIENT, + expectedLogoutParams: { + post_logout_redirect_uri: REDIRECT_URL.href + } } ].forEach(ctx => { it(ctx.itMsg, async () => { @@ -647,6 +657,10 @@ describe('OIDCConfig', () => { const req = {} as unknown as express.Request; // not used const url = await config.getLogoutRedirectUrl(req, REDIRECT_URL); assert.equal(url, ctx.expectedUrl); + if (ctx.expectedLogoutParams) { + assert.isTrue(clientStub.endSessionUrl.calledOnce); + assert.deepEqual(clientStub.endSessionUrl.firstCall.args, [ctx.expectedLogoutParams]); + } }); }); }); From c8f7e2a451d4d27b53f998499e3537b886bd18ab Mon Sep 17 00:00:00 2001 From: fflorent Date: Thu, 7 Mar 2024 17:40:47 +0100 Subject: [PATCH 13/25] Many other stuffs + cleanup + working version with Agent Connect --- app/server/lib/BrowserSession.ts | 1 + app/server/lib/OIDCConfig.ts | 24 ++++++- test/server/lib/OIDCConfig.ts | 112 +++++++++++++++++++++++-------- 3 files changed, 105 insertions(+), 32 deletions(-) diff --git a/app/server/lib/BrowserSession.ts b/app/server/lib/BrowserSession.ts index be3140ff..6f051727 100644 --- a/app/server/lib/BrowserSession.ts +++ b/app/server/lib/BrowserSession.ts @@ -75,6 +75,7 @@ export interface SessionObj { state?: string; targetUrl?: string; nonce?: string; + idToken?: string; } } diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 5ac76e7b..70a8051f 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -62,7 +62,7 @@ import * as express from 'express'; import { GristLoginSystem, GristServer } from './GristServer'; -import { Client, ClientMetadata, generators, Issuer, UserinfoResponse } from 'openid-client'; +import { Client, ClientMetadata, generators, Issuer, TokenSet, UserinfoResponse } from 'openid-client'; import { Sessions } from './Sessions'; import log from 'app/server/lib/log'; import { AppSettings, appSettings } from './AppSettings'; @@ -81,6 +81,17 @@ type EnabledProtectionsString = keyof typeof ENABLED_PROTECTIONS; const CALLBACK_URL = '/oauth2/callback'; +function formatTokenForLogs(token: TokenSet) { + return _.chain(token) + .omitBy(_.isFunction) + .mapValues((v, k) => { + if (!['token_type', 'expires_in', 'expires_at', 'scope'].includes(k)) { + return 'REDACTED'; + } + return v; + }).value(); +} + export class OIDCConfig { /** * Handy alias to create an OIDCConfig instance and initialize it. @@ -180,6 +191,7 @@ export class OIDCConfig { // The callback function will compare the state present in the params and the one we retrieved from the session. // If they don't match, it will throw an error. const tokenSet = await this._client.callback(this._redirectUrl, params, checks); + log.debug("Got tokenSet: %o", formatTokenForLogs(tokenSet)); const userInfo = await this._client.userinfo(tokenSet); log.debug("Got userinfo: %o", userInfo); @@ -196,7 +208,10 @@ export class OIDCConfig { profile, })); - delete mreq.session.oidc; + mreq.session.oidc = { + idToken: tokenSet.id_token, // keep idToken for logout + state: mreq.session.oidc?.state, // also keep state for logout + }; res.redirect(targetUrl ?? '/'); } catch (err) { log.error(`OIDC callback failed: ${err.stack}`); @@ -224,6 +239,7 @@ export class OIDCConfig { } public async getLogoutRedirectUrl(req: express.Request, redirectUrl: URL): Promise { + const mreq = req as RequestWithLogin; // For IdPs that don't have end_session_endpoint, we just redirect to the logout page. if (this._skipEndSessionEndpoint) { return redirectUrl.href; @@ -233,7 +249,9 @@ export class OIDCConfig { return this._endSessionEndpoint; } return this._client.endSessionUrl({ - post_logout_redirect_uri: redirectUrl.href + post_logout_redirect_uri: redirectUrl.href, + state: mreq.session.oidc?.state, + id_token_hint: mreq.session.oidc?.idToken, }); } diff --git a/test/server/lib/OIDCConfig.ts b/test/server/lib/OIDCConfig.ts index fe33350b..e5a94e8a 100644 --- a/test/server/lib/OIDCConfig.ts +++ b/test/server/lib/OIDCConfig.ts @@ -1,12 +1,14 @@ +import {EnvironmentSnapshot} from "../testUtils"; import {OIDCConfig} from "app/server/lib/OIDCConfig"; +import {SessionObj} from "app/server/lib/BrowserSession"; +import {Sessions} from "app/server/lib/Sessions"; +import log from "app/server/lib/log"; import {assert} from "chai"; -import {EnvironmentSnapshot} from "../testUtils"; import Sinon from "sinon"; import {Client, generators} from "openid-client"; import express from "express"; -import log from "app/server/lib/log"; -import {Sessions} from "app/server/lib/Sessions"; import _ from "lodash"; +import {RequestWithLogin} from "app/server/lib/Authorizer"; class OIDCConfigStubbed extends OIDCConfig { public static async build(clientStub?: Client): Promise { @@ -52,6 +54,7 @@ describe('OIDCConfig', () => { let sandbox: Sinon.SinonSandbox; let logInfoStub: Sinon.SinonStub; let logErrorStub: Sinon.SinonStub; + let logDebugStub: Sinon.SinonStub; before(() => { oldEnv = new EnvironmentSnapshot(); @@ -61,7 +64,7 @@ describe('OIDCConfig', () => { sandbox = Sinon.createSandbox(); logInfoStub = sandbox.stub(log, 'info'); logErrorStub = sandbox.stub(log, 'error'); - sandbox.stub(log, 'debug'); + logDebugStub = sandbox.stub(log, 'debug'); }); afterEach(() => { @@ -353,8 +356,8 @@ describe('OIDCConfig', () => { codeVerifier: FAKE_CODE_VERIFIER, state: FAKE_STATE } - }; - const DEFAULT_EXPECTED_CHECKS = { + } as SessionObj; + const DEFAULT_EXPECTED_CALLBACK_CHECKS = { state: FAKE_STATE, code_verifier: FAKE_CODE_VERIFIER }; @@ -384,6 +387,20 @@ describe('OIDCConfig', () => { }; }); + function checkUserProfile(expectedUserProfile: object) { + return function ({user}: {user: any}) { + assert.deepEqual(user.profile, expectedUserProfile, + `user profile should have been populated with ${JSON.stringify(expectedUserProfile)}`); + }; + } + + function checkRedirect(expectedRedirection: string) { + return function ({fakeRes}: {fakeRes: any}) { + assert.deepEqual(fakeRes.redirect.firstCall.args, [expectedRedirection], + `should have redirected to ${expectedRedirection}`); + }; + } + [ { itMsg: 'should resolve when the state and the code challenge are found in the session', @@ -421,7 +438,7 @@ describe('OIDCConfig', () => { env: { GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE', }, - expectedChecks: { + expectedCbChecks: { state: FAKE_STATE, nonce: FAKE_NONCE }, @@ -444,7 +461,7 @@ describe('OIDCConfig', () => { env: { GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE', }, - expectedChecks: { + expectedCbChecks: { state: FAKE_STATE, nonce: FAKE_NONCE, }, @@ -484,10 +501,10 @@ describe('OIDCConfig', () => { itMsg: 'should fill user profile with email and name', session: DEFAULT_SESSION, userInfo: FAKE_USER_INFO, - expectedUserProfile: { + extraChecks: checkUserProfile({ email: FAKE_USER_INFO.email, name: FAKE_USER_INFO.name, - } + }) }, { itMsg: 'should fill user profile with name constructed using ' + @@ -498,10 +515,10 @@ describe('OIDCConfig', () => { given_name: 'given_name', family_name: 'family_name', }, - expectedUserProfile: { + extrachecks: checkUserProfile({ email: 'fake-email', name: 'given_name family_name', - } + }) }, { itMsg: 'should fill user profile with email and name when ' + @@ -516,15 +533,15 @@ describe('OIDCConfig', () => { GRIST_OIDC_SP_PROFILE_NAME_ATTR: 'fooName', GRIST_OIDC_SP_PROFILE_EMAIL_ATTR: 'fooMail', }, - expectedUserProfile: { + extraChecks: checkUserProfile({ email: 'fake-email2', name: 'fake-name2', - } + }), }, { itMsg: 'should redirect by default to the root page', session: DEFAULT_SESSION, - expectedRedirection: '/', + extraChecks: checkRedirect('/'), }, { itMsg: 'should redirect to the targetUrl when it is present in the session', @@ -534,7 +551,34 @@ describe('OIDCConfig', () => { targetUrl: 'http://localhost:8484/some/path' } }, - expectedRedirection: 'http://localhost:8484/some/path', + extraChecks: checkRedirect('http://localhost:8484/some/path'), + }, + { + itMsg: "should redact confidential information in the tokenSet in the logs", + session: DEFAULT_SESSION, + tokenSet: { + id_token: 'fake-id-token', + access_token: 'fake-access', + whatever: 'fake-whatever', + token_type: 'fake-token-type', + expires_at: 1234567890, + expires_in: 987654321, + scope: 'fake-scope', + }, + extraChecks: function () { + assert.isTrue(logDebugStub.called); + assert.deepEqual(logDebugStub.firstCall.args, [ + 'Got tokenSet: %o', { + id_token: 'REDACTED', + access_token: 'REDACTED', + whatever: 'REDACTED', + token_type: this.tokenSet.token_type, + expires_at: this.tokenSet.expires_at, + expires_in: this.tokenSet.expires_in, + scope: this.tokenSet.scope, + } + ]); + } }, ].forEach(ctx => { it(ctx.itMsg, async () => { @@ -554,6 +598,8 @@ describe('OIDCConfig', () => { } } as unknown as express.Request; clientStub.callbackParams.returns(fakeParams); + const tokenSet = { id_token: 'id_token', ...ctx.tokenSet }; + clientStub.callback.resolves(tokenSet); clientStub.userinfo.returns(_.clone(ctx.userInfo ?? FAKE_USER_INFO)); const user: { profile?: object } = {}; fakeScopedSession.operateOnScopedSession.yields(user); @@ -572,21 +618,19 @@ describe('OIDCConfig', () => { } else { assert.isFalse(logErrorStub.called, 'no error should be logged. Got: ' + logErrorStub.firstCall?.args[0]); assert.isTrue(fakeRes.redirect.calledOnce, 'should redirect'); - if (ctx.expectedRedirection) { - assert.deepEqual(fakeRes.redirect.firstCall.args, [ctx.expectedRedirection], - `should have redirected to ${ctx.expectedRedirection}`); - } assert.isTrue(clientStub.callback.calledOnce); assert.deepEqual(clientStub.callback.firstCall.args, [ 'http://localhost:8484/oauth2/callback', fakeParams, - ctx.expectedChecks ?? DEFAULT_EXPECTED_CHECKS + ctx.expectedCbChecks ?? DEFAULT_EXPECTED_CALLBACK_CHECKS ]); - assert.isEmpty(session, 'oidc info should have been removed from the session'); - if (ctx.expectedUserProfile) { - assert.deepEqual(user.profile, ctx.expectedUserProfile, - `user profile should have been populated with ${JSON.stringify(ctx.expectedUserProfile)}`); - } + assert.deepEqual(session, { + oidc: { + state: FAKE_STATE, + idToken: tokenSet.id_token, + } + }, 'oidc info should only keep state and id_token in the session and for the logout'); + ctx.extraChecks?.({fakeRes, user}); } }); }); @@ -626,6 +670,12 @@ describe('OIDCConfig', () => { const REDIRECT_URL = new URL('http://localhost:8484/docs/signed-out'); const URL_RETURNED_BY_CLIENT = 'http://localhost:8484/logout_url_from_issuer'; const ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT = 'http://localhost:8484/logout'; + const FAKE_SESSION = { + oidc: { + idToken: 'id_token', + state: 'state', + } + } as SessionObj; [ { @@ -641,10 +691,12 @@ describe('OIDCConfig', () => { }, expectedUrl: ENV_VALUE_GRIST_OIDC_IDP_END_SESSION_ENDPOINT }, { - itMsg: 'should call the end session endpoint from the issuer metadata', + itMsg: 'should call the end session endpoint with the expected parameters', expectedUrl: URL_RETURNED_BY_CLIENT, expectedLogoutParams: { - post_logout_redirect_uri: REDIRECT_URL.href + post_logout_redirect_uri: REDIRECT_URL.href, + id_token_hint: FAKE_SESSION.oidc!.idToken, + state: FAKE_SESSION.oidc!.state, } } ].forEach(ctx => { @@ -654,7 +706,9 @@ describe('OIDCConfig', () => { const clientStub = new ClientStub(); clientStub.endSessionUrl.returns(URL_RETURNED_BY_CLIENT); const config = await OIDCConfigStubbed.build(clientStub.asClient()); - const req = {} as unknown as express.Request; // not used + const req = { + session: FAKE_SESSION + } as unknown as RequestWithLogin; const url = await config.getLogoutRedirectUrl(req, REDIRECT_URL); assert.equal(url, ctx.expectedUrl); if (ctx.expectedLogoutParams) { From b01a8a8e77815a1099f7838c89f63b812a0d5f67 Mon Sep 17 00:00:00 2001 From: fflorent Date: Wed, 13 Mar 2024 10:46:21 +0100 Subject: [PATCH 14/25] WIP: integration tests --- .../import_keycloak/grist-realm.json | 1787 +++++++++++++++++ .../import_keycloak/grist-users-0.json | 27 + .github/workflows/main.yml | 19 +- app/server/lib/FlexServer.ts | 2 +- test/nbrowser/LoginWithOIDC.ts | 64 +- 5 files changed, 1877 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/import_keycloak/grist-realm.json create mode 100644 .github/workflows/import_keycloak/grist-users-0.json diff --git a/.github/workflows/import_keycloak/grist-realm.json b/.github/workflows/import_keycloak/grist-realm.json new file mode 100644 index 00000000..4159d05f --- /dev/null +++ b/.github/workflows/import_keycloak/grist-realm.json @@ -0,0 +1,1787 @@ +{ + "id" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", + "realm" : "grist", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "815d5e80-c709-40f7-a927-f90e3dad51a2", + "name" : "default-roles-grist", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", + "attributes" : { } + }, { + "id" : "9023045c-64fd-4e62-baa7-2875e0d87019", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", + "attributes" : { } + }, { + "id" : "98761135-6b8d-4a52-a95d-d1e13257d156", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "1867f490-b610-45a4-9a75-b9cf29f3c48f", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "86bd9709-78bb-4af1-a77f-b33c30dc422a", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "8dfcb717-6d35-4746-a717-c8655ba03427", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "51998cd6-041c-473f-90a7-e6d247ef6292", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "6d6eca69-e7f4-48c9-be72-0a571f03552c", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "4e2e78ef-5175-4e2a-b213-6d520fc712a7", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "562ee9cf-a814-4dd3-a5cd-96b6cd12109f", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "c99029ca-60a9-4dc4-905d-d223758e77e7", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "31acf0b1-3085-40d0-a513-5e889a47d93d", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-realm", "manage-clients", "view-identity-providers", "view-authorization", "query-clients", "manage-identity-providers", "manage-authorization", "view-users", "manage-events", "view-events", "manage-users", "query-users", "impersonation", "query-groups", "query-realms", "view-clients", "manage-realm", "create-client" ] + } + }, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "8d2b0514-e8df-4eb3-b525-c524d0740d6b", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "b48961c1-ea7a-4b9c-a601-7905aae69e34", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "03c79004-9a25-4ba6-8010-2bdb9d80af13", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "60d3f4a2-799b-435a-970d-07ac20900d74", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "88fe03e9-8415-4a40-8e6c-1d75426f28be", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "a02b201e-c426-462b-a6db-13b1e88fd695", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "b1d340f3-48a8-4e52-823e-b3e14dc89c7b", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "5075a68e-8771-4421-bdb9-bd1f7d57d1e5", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "254b1a1e-1e97-4a88-ba65-18a10917352a", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + }, { + "id" : "99b0a077-fdf0-48a5-8aa8-f22aada4b80c", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "broker" : [ { + "id" : "6448f386-787b-4750-943e-50727b3c079b", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "7bc4b7df-32f1-41bc-9afa-5cc9487a2e42", + "attributes" : { } + } ], + "keycloak_clientid" : [ ], + "account" : [ { + "id" : "5cf3a372-6e9d-4136-b152-c3908033edf9", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + }, { + "id" : "52224265-1dcf-4761-87b5-0da97238c42a", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + }, { + "id" : "1f5f1039-b1c5-4a1b-9648-dd1352fafc72", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + }, { + "id" : "11521553-cd9c-4730-8fe5-5af7198dbb19", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + }, { + "id" : "563e5aef-01a5-48d7-95a5-3b58723146d0", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + }, { + "id" : "17b76778-7811-41f3-b2ec-77fe99ea9518", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + }, { + "id" : "9ccfd5de-60f9-4132-85a3-67de26a8e3e2", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + }, { + "id" : "067cefec-b9cd-42a6-bfe7-9468487cd0c2", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "815d5e80-c709-40f7-a927-f90e3dad51a2", + "name" : "default-roles-grist", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clients" : [ { + "id" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/grist/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/grist/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "4b1169ff-343e-4c34-bcf3-cd3f5dfc505f", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "7bc4b7df-32f1-41bc-9afa-5cc9487a2e42", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "clientId": "keycloak_clientid", + "name": "keycloak_clientid", + "description": "", + "rootUrl": "http://localhost:8484", + "adminUrl": "http://localhost:8484", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "xLn3SBOgrnsUvGn95AEOMWW7fNROuqMi", + "redirectUris": [ + "/oauth2/callback" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": false, + "client.secret.creation.time": "1710262687", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "/*", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "login_theme": "", + "frontchannel.logout.url": "", + "backchannel.logout.url": "" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + }, + "secret": "keycloak_secret", + "authorizationServicesEnabled": false +}, { + "id" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "df7ce056-467d-4306-96b1-e1b66cf1245d", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/grist/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/grist/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "27a32ca6-d3d0-4033-921e-a1ac8e958d7e", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "eefa2fd5-889a-4a5e-8121-6f8262c9651b", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "71d5597d-c0a6-400a-ba65-d35ee79225bc", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "6f873430-2e38-4122-9602-d42028fd87c8", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "12bbd05f-96e6-4bb2-9d09-03561ef52996", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "1642de6b-4021-40ee-b1bd-eb9e4844779a", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "b5ff5770-9d03-4252-b7f3-62abe65e8154", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "7633a138-4ac1-4fcb-8ec4-43adbcf191d6", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "3ed856d5-05e3-4d9a-a754-090faf2ff56f", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "5d6398a6-e2f1-48d9-b58f-abc9f1c138e7", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "46276353-8787-4b54-bb74-6c2b62e6601f", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "bc0529b1-facc-48af-afba-6b58183c18cc", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "f5cda86d-cf80-460c-a807-2af7ef1f1f4c", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "8b278ba2-b6c9-4382-a793-54d68cbca348", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "5306b57f-8307-4c16-b68e-2105f5dbf04b", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "a8500c2c-ad1e-4000-a83c-f9ad7905d702", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "5eea6909-2d59-455c-9838-ff2d6f226c00", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "08a95f5a-1f6d-41c2-a1e9-4ae00a61f69a", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "9255aa99-a703-41f6-bff8-1c509bd9981f", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "62e7a0fa-e394-4954-8301-d6d79bf6aa9f", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "00d8c6b5-2a94-44e8-a88a-b0cf8684a06b", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "eaf45331-ce30-4f84-a627-15c88a15e090", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "4475fc2e-1282-40e7-a130-b4c8c6166e9f", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "8c23603e-eeeb-43f5-9467-a356ec475826", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "91ffe8ea-d943-4986-a0d7-8d4a06ac08f1", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "4fa72a9e-2e85-42ca-9e60-c0ddd40c2f9c", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "d782d3bf-9c97-47db-92f5-d3c5b24d6c23", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "21d306d5-df03-482e-b831-21017d424fb4", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "bdf8c994-c660-4b9e-933c-4d504f459c50", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "9d328e54-8aae-4181-ae15-655d5ba46144", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "fcac977e-795d-49ba-a441-12c418e741bb", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "190c193b-7822-427c-883b-e1f108ad23bb", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "307ef86f-db58-4a7a-82b1-8c4389af1cd2", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "ece9fac8-9d8a-4070-93f3-0bfc6281bc43", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "623dfc8c-3579-4dcd-bf24-e8208cf8778e", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "026bd8e3-c7ca-40d7-a9f2-bfc17e1b5040", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "8b98b389-1934-4653-b1f6-fff7c945992f", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "1c86a1f8-3f7b-41cb-b5e4-a9b68339923e", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "9d03a2d6-f3a5-4124-a483-aaf720df4453", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "b4fc16b0-465c-4b5e-97bb-20bbf42b8bb4", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "df16ebbb-ffb8-48ab-9640-a687b1a49f27", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "1f49d690-0011-430e-9f63-59d94fb6c126", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper" ] + } + }, { + "id" : "c2c2bdfc-d47a-4468-819b-0b0cb587cd04", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "0ca14b74-68f5-49bf-9b8a-47ee10d0f6ec", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "b5d5ca65-e26d-422a-a404-debb2d72375d", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "8405f54d-1b84-4448-9659-46bb1bd82932", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "1ed7555f-b58d-4b57-ba77-634d7b915a40", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "fff7bfdb-bbb4-433c-893b-5c7b7b30dbc1" ], + "secret" : [ "bfxeMUAb1ZAd1sZC4LmxlQ" ], + "priority" : [ "100" ] + } + }, { + "id" : "2c8b618d-037f-4057-9697-677d89c5381a", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEogIBAAKCAQEAyD8dRdrxIwVqCWw7xsagXXu99EBDRPsYQUT/tj3NWenagZbQ0e6V2bD5ZfLYU4dlZ9bEfXUHcnBtQd3s4ebcBpBFPIKqOlBBFVsn98csZzLgzVp4Mr6vbcVSKZmm5R+pVoYC6GR3TqzXGDe8G9qIBKSGvzkWqkOpeomTRe/NHeSKv6rsJLZnOnsXB5qwGtKYRHl97EuhEbo07AQ5myKmMXNRPPfLLVT5jBEblqtLIUk996tl8CN22u/QcxeNUh59LKGXZ3xtmFwJMMDoKz15wPy8uB6xpfGcPenUQiI76+Sn3lYavkh0CzchTKF9DYHEcKSbx/CY6AntYOppQPTYGwIDAQABAoIBAARz6nitx+h4xfC4hLuWDk5kaEFAxXtCywrfbFKeV0pVVAyYp/1yWg3gI/xSx/jHh2RraH6vsErACJ6oM3VshV52W9GZDgzPdCPFS+v/0ZkWyQrX9mUaHThzEz0OZSFcjdSnq/OzEU8ysnKrjSdsIl9Z3/eOU2Iy3xGAih7KJC2nFmZAc/9nrz5LPW66w2Y4boUmJcOrizUNRUn/Oghn6SRzvK7UHAmGtLFOo2W07zuFpbXIWXkDodHm2a4zfjHa6LJziRmRb/hcqTJxaeaqA3IQIYZ2dw8pzUsxx3tDDdjfBftbWjpM6yqBho7kZsbQsAKZi0bbq/sEvpJQB8UZ8ikCgYEA8qOiSUgwBHsK/HWiZRCDV5tBfA2zJ0Bv+lYs72y6OUR6npIkYy4gZFUdVx+rlFuu71LpBaanqi5YwD1Ldm7GlfWIZpovVP6zRhA8ViE3rfgURWwbWhwHypGBVuHWtyGZl9XROtDjLyOdCsobPBOTWn11KOqaAe+BysJW/wERWVkCgYEA00XkU1N1Sch+3iecNLvRI36RC3mtgV7M0ut5l1S0cKmmJslQ81biLGd3K0GUDMSBiZ2SDByK9bXCNu9tyJN6NGF29n6vOOjy8khoBTS3X80aRH1Bw1nB8U9PDSB7fPJBfaEjJGLLRcnNDS6elfG2mVhTtpnHstH7Ts4fjq9XmpMCgYAtBiI6GPQYEMD0Idv1hv/oRL39CAnDcdiVimIiN3nC4KskO5gW81s9YvHj1dOf3vdyH19wFgGsuZbsbTNQkbO15e7eoyO/UNfxW1fm35kWZh9U1n+o0+S6OQ/YEGYoa0q1+w4tLM/LUn90nhY5qqRAOWGBKy9Sxp++ARvli8wtWQKBgA0GGfUpB+nseiWnu3Fkwpe1jatvbMq01VuLOIujpRvs2Vk6v8rAaGDkX+xCtqWy12lsVTx55fcPpVFNoS7kKHxiJbs8RAD2G0PkQsVPYp59PklKj2tDdTky8mSUxAgHxxG/hTMRBAbhUcqmPRBxPhhl4YM4J59WYm+RNVDOblARAoGACpStAKUodYYEATFQJ8dpYY50Ql8DzdpORrUxlj8COSsyT+MIrm+7kLiTYoswuv6P+mi0OfYgZfOAco9Ukh5VEF/ccVCvw0N3jmcQjNy8qy31k57XCrJC9+Fc1bxhtTqIZ6rJvWN3GKVwcMNZJxTXW5oxx9H60b8w6lfV4ao/b90=" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICmTCCAYECBgGOMyFJbzANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVncmlzdDAeFw0yNDAzMTIxNDQ1MDNaFw0zNDAzMTIxNDQ2NDNaMBAxDjAMBgNVBAMMBWdyaXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyD8dRdrxIwVqCWw7xsagXXu99EBDRPsYQUT/tj3NWenagZbQ0e6V2bD5ZfLYU4dlZ9bEfXUHcnBtQd3s4ebcBpBFPIKqOlBBFVsn98csZzLgzVp4Mr6vbcVSKZmm5R+pVoYC6GR3TqzXGDe8G9qIBKSGvzkWqkOpeomTRe/NHeSKv6rsJLZnOnsXB5qwGtKYRHl97EuhEbo07AQ5myKmMXNRPPfLLVT5jBEblqtLIUk996tl8CN22u/QcxeNUh59LKGXZ3xtmFwJMMDoKz15wPy8uB6xpfGcPenUQiI76+Sn3lYavkh0CzchTKF9DYHEcKSbx/CY6AntYOppQPTYGwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCyyzkuukYOMjeEgBStMBcqyFZaH1tLfZOhWR9pi1hxKaNO67e2noNT9PykkGo9BpvbqzSjG3ep81Es6s1yTuCTyXCiRkLXJBoUiHlVlHh5Pv9UuGBSZu40r0E1EdO0GMxE18rOAZnEgGNyBOQsyhAWJ0mpHiyeF7gTDDiE/6Sc7n4OWUgdeLF3mIJpL+C5MXjsMrzW3tGXWEg1eDo6xBsZmmoF/pl26z3+rUJWBLgvbxnrR6huQ87xhyUcS0sFxAwEwp9J1L0dkdSlmq6vKdn5ru8LsdxXwZ8S5VaX/PWUetkql6UtoLFbCyiGfb41M4xaD5V9ZD8RkeErOBFrVqdC" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "4f59d164-b073-4fe4-af43-0603c8aa15a6", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "d3dbdb87-2b9a-4738-a877-38d17f01947c" ], + "secret" : [ "WWHAzayR3k7CNc1_asOnTDiJl9AGLQ1_tc0o3iUGlsDAQpf-qLVVcRTbvHmC_uOB9d6NBxHp_IMAZ3Zi2cxZ9w" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "bbdfa46d-09ae-4131-a33a-e6f99db1c1f3", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEogIBAAKCAQEAzO9iV5YvEj1kpdG11iURKvQB1JN746q6aB05t5wwFFcKvOzp4n0iDbLG/2qxM2U8Aw5v5cC72uwWh6cOhICxSjG+Y+4xsA3zWbJ6xDysag0xn0Cs/mDPsOLb9cU+TN64s7cVl54gY9YpcSeBIJLkHhvgnwPoiEslHEPC1zfPWKGKDGbaThL4PSsvEcvcJYwhYgAHX/eO0/5F/VnBbTTVqHOjaDYMjYfCGDdHs37wS10muIEsrqoTC+MJfj+CNNgcKHfQXd/+0ae/Q+slw1A1EkEBs1oyU/zcbie3u5cA8JWsnvJgyjZh/0mbItTn1n5J8abwvgfffRqD0xg3KN+d7QIDAQABAoIBADDZpzGFpTbN154HPTcMoukAPSd0+IUufzyuKsHvwy42CWM7fgz1Exb81J6xygecTA/WcynrJVxsBnrTgYxoONqcvOuJLeLvkGCDQOxiIh8tgfSaMCJ65Uce7JvLJqygMpr0O3tmwAXMWRiV+BvRp/rdXk/JWLaUYwY3yMwQi6Zufbi7jIpR/v5lkA5jKDrvBwUngNZlLwVad2hU2ulwETlAlZvpoGTNTtC6Q3pmLwFPBzKvP4TewI93i0H4TqJgsSWo1NIJJta1hcLjfGi/PtrmSNbM8EBmh2nKqacY3ijGX9GrT83CZ0Bq/1pZyu8VkbqmbnthrMGILmlqQkDgfikCgYEA94mYGGFnsQGacbn8Ct4/MqcagdOAbai/3s70yWIa1EEXI7P9sjyLO9iF/Ha2JEeE7GslFomZIBsgLXON5eCyhjr5so2+/n0KUA2b2KgDDXvMnJcqUXXjM7KPYOkCIVm2Es4TZV3bqVkvPM0ZS7OGcOoW/UXuUFWna4Z8TsyECTMCgYEA0/Dw8/kU68k07nCRu5z8FEDvUkIyzgMBXWbDCyVe2nqiwLhUVkcxGcPrTQ/bOdE1MzpHG4z0ziGgPO44VfvdKYmQbe6ToaXbRZUBzS9MowNpF5TEbVD2GiBoVXuXDHaU/y+iwGl3g2jxLMp0w3vxLmcpi60Pfseq8Xfkkt3m/F8CgYBdIY5wtcz+Yp0J5rB2IlHiq84kRD/QginWGUUts1RmwSqEi0aK1Y6I8JjQeJVkpufSzykABrruwmXj09LyRwzDxdKGJCBUvRSxM72L0QJ9AzPjQlhwl4rou2iITII5q/f9sTzI6Xwohd5o4L2ApsWRG/GUTsgvv1oi8VE5kGao0wKBgFKBxLulpuBXlvSP/BvGdFfKI6CpRq/ueZSL0bhAFxoEjeFqoOJpmpLGM47vck+iwwwrTs1J5W9tpbyynFnUz/dAp2o0a2KNd7wx0t624CXBySK19nX8A6KOJS/KCjZ+32gsejZfmHge3WyrcCM919lRrdnDSHn5bvHL077dBfQPAoGAEhnuRHkMfR2FM8d9WnMkwifFgkc1bEiwJIVWeS2jtxbONvj37EvLJiG14UY81vUb8QjJVOFhj68fstU4nMGrnMBnYATbMMgW8R41gF6NHXgjvuTWbl+at41kKFDYJmZq0tgiFU3xQPEhn5C3ux+UIqzaO62WMAAX0FRivcVJnfs=" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICmTCCAYECBgGOMyFIszANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVncmlzdDAeFw0yNDAzMTIxNDQ1MDNaFw0zNDAzMTIxNDQ2NDNaMBAxDjAMBgNVBAMMBWdyaXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzO9iV5YvEj1kpdG11iURKvQB1JN746q6aB05t5wwFFcKvOzp4n0iDbLG/2qxM2U8Aw5v5cC72uwWh6cOhICxSjG+Y+4xsA3zWbJ6xDysag0xn0Cs/mDPsOLb9cU+TN64s7cVl54gY9YpcSeBIJLkHhvgnwPoiEslHEPC1zfPWKGKDGbaThL4PSsvEcvcJYwhYgAHX/eO0/5F/VnBbTTVqHOjaDYMjYfCGDdHs37wS10muIEsrqoTC+MJfj+CNNgcKHfQXd/+0ae/Q+slw1A1EkEBs1oyU/zcbie3u5cA8JWsnvJgyjZh/0mbItTn1n5J8abwvgfffRqD0xg3KN+d7QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBi3gzML02PkgsHF5fncFAO34wZ8+1Z4H/5Sa5em/Az1TFKolsCSP7DspS+tZ3+RyKUCQ8WvKtVjWOmanHM/5ybkSy3Aum6SVkCXxV3PAJ0Yc9hQfsG7CZ1QE+Qw1WclIx2pZ6tBppqeKJ4tvwTmnMInkY0kLSFynzHB7qSotZg+K6s3j8+gk4jAxdLzDJ3HLJ9iAXGMG7dnjJ6r/93HKt3QDB0vpM8tt91sxFZ6FzfUDez2FApKGxhQ+89C2/fx4s9C0WhyVHJSIzd4/k7+Bjw3L4HV2jU3TaYG3Fp4cn+KmTfRjYcJr+BkioV+/DYipfO5sV8YZ6SOZL8lPiQOerf" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "7a21c238-64a0-46a0-b730-b45ef72ab542", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "73e817b3-14e2-4e28-8d34-7acb27437639", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "ad0538ac-76ce-44ad-b050-3a138c4574f7", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "087eac7a-6dd9-4af3-9d93-6005ef335fcc", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "06228470-26c1-4090-bd57-159b474de7c0", + "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", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "3b997775-3fc3-4cc2-b38b-59e9f3ddfc18", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "2594a601-978d-4d10-b515-06af70e05799", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "e9ea5830-502b-448d-bdfd-5f161c79be6f", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "3498ba3d-f218-44bf-866c-04430cd4aff1", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "3b2f9a6b-74be-48e2-bab5-8d3ac7319f25", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "d03c6c2a-bb0b-4276-8cc4-48293549cbea", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e1a34850-be66-4c5f-b9c4-cdda7a854bdb", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cf675a31-f86d-4c44-b47a-66aeafdbc623", + "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", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "f7fae163-25bf-4856-a627-833c4780165f", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "a15f14af-f7f8-4581-9ef1-3474b7468a33", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "d631be42-1abe-4030-b5d6-9ad5db146463", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "f4c6f99b-3741-4ec2-8186-a9338f99c780", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "1c642cec-e0d0-47de-a2d4-b70ad8ff75ec", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "dda99951-aa1b-4821-a4fa-a09ff62dd10d", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "5ddc4880-954f-4232-ae29-675d3ffd77a9", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "oauth2DevicePollingInterval" : "5", + "parRequestUriLifespan" : "60", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "22.0.5", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} diff --git a/.github/workflows/import_keycloak/grist-users-0.json b/.github/workflows/import_keycloak/grist-users-0.json new file mode 100644 index 00000000..bc6ba075 --- /dev/null +++ b/.github/workflows/import_keycloak/grist-users-0.json @@ -0,0 +1,27 @@ +{ + "realm" : "grist", + "users" : [ { + "id" : "1564f73d-c385-4269-84da-34b40f494dea", + "createdTimestamp" : 1710254868534, + "username" : "keycloakuser", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "Keycloak", + "lastName" : "User", + "email" : "keycloakuser@example.com", + "credentials" : [ { + "id" : "3ceee294-209a-4187-aede-1dcfa2dac006", + "type" : "password", + "userLabel" : "Password: keycloakpassword", + "createdDate" : 1710254893700, + "secretData" : "{\"value\":\"kZZMgT2g89C+LFfigQt/qu5H9vs188wWgVK1KqnO12Q=\",\"salt\":\"ffAeQSmuJ7cGFE8rzN+f/g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-grist" ], + "notBefore" : 0, + "groups" : [ ] + } ] +} \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 646e0461..2a24e756 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,16 @@ jobs: if: contains(matrix.tests, ':common:') run: yarn run test:common - - name: Run server tests with minio and redis + - name: Setup Keycloak realm and client + if: contains(matrix.tests, ':server-') + server: http://localhost:8080/auth + username: admin + password: admin + kcadm: | + create realms -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-realms.json + create users -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-users-0.json + + - name: Run server tests with minio, keycloak and redis if: contains(matrix.tests, ':server-') run: | export TEST_SPLITS=$(echo $TESTS | sed "s/.*:server-\([^:]*\).*/\1/") @@ -161,6 +170,14 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + keycloak: + # image: fflorent/keycloak-with-import-realm:latest # Image with overridden entrypoint. + image: quay.io/keycloak/keycloak + port: 8080:8080 + env: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + volume: ${{ github.workspace }}/.github/workflows/import_keycloak:/opt/keycloak/data/import candidate: needs: build_and_test diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 86ff54ad..2ba1c5c4 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -2370,7 +2370,7 @@ function configServer(server: T): T { // Returns true if environment is configured to allow unauthenticated test logins. function allowTestLogin() { - return Boolean(process.env.GRIST_TEST_LOGIN); + return isAffirmative(process.env.GRIST_TEST_LOGIN); } // Check OPTIONS requests for allowed origins, and return heads to allow the browser to proceed diff --git a/test/nbrowser/LoginWithOIDC.ts b/test/nbrowser/LoginWithOIDC.ts index d3cf9b88..eb528924 100644 --- a/test/nbrowser/LoginWithOIDC.ts +++ b/test/nbrowser/LoginWithOIDC.ts @@ -1,23 +1,47 @@ -// import {assert, driver} from 'mocha-webdriver'; -// import * as gu from 'test/nbrowser/gristUtils'; -// import {setupTestSuite} from 'test/nbrowser/testUtils'; -// import express from 'express'; +import {driver} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {server, setupTestSuite} from 'test/nbrowser/testUtils'; -export {}; +describe('LoginWithOIDC', function () { + this.timeout(60000); + setupTestSuite(); + gu.withEnvironmentSnapshot({ + get 'GRIST_OIDC_SP_HOST' () { return server.getHost(); }, + 'GRIST_OIDC_IDP_ISSUER': 'http://localhost:8081/realms/grist', + 'GRIST_OIDC_IDP_CLIENT_ID': 'keycloak_clientid', + 'GRIST_OIDC_IDP_CLIENT_SECRET': 'keycloak_secret', + 'GRIST_OIDC_IDP_SCOPES': 'openid email profile', + 'GRIST_TEST_LOGIN': 0, + }); -// describe('LoginWithOIDC', function () { -// this.timeout(60000); -// setupTestSuite(); -// before(async () => { -// const app = express(); -// app.get('/.well-known/openid-configuration', (req, res) => { -// res.json({ -// }); -// }); -// app.listen(gu. -// }); -// gu.withEnvironmentSnapshot({ -// 'GRIST_OIDC_IDP_ISSUER': 'https://accounts.google.com', -// }) -// }); + it('should login using OIDC', async () => { + console.log("HERE"); + await driver.get(`${server.getHost()}/o/docs/login`); + await driver.findWait('#kc-form-login', 10_000); + await driver.find('#username').sendKeys('keycloackuser'); + await driver.find('#password').sendKeys('keycloakpassword'); + await driver.find('#kc-login').click(); + + // await driver.wait( + // async () => { + // const url = await driver.getCurrentUrl(); + // return url.startsWith(server.getHost()); + // }, + // 20_000 + // ); + // await driver.find('.weasel-popup-open').click(); + // await gu.openAccountMenu(); + // assert.equal(await driver.find('.test-usermenu-name').getText(), 'keycloackuser'); + // assert.equal(await driver.find('.test-usermenu-email').getText(), 'keycloakuser@example.com'); + // await driver.find('.test-dm-log-out').click(); + // await driver.wait( + // async () => { + // const url = await driver.getCurrentUrl(); + // return url.startsWith(`${server.getHost()}/o/docs/signed-out`); + // }, + // 20_000 + // ); + // assert.equal(await driver.find('.test-error-header').getText(), 'Signed out'); + }); +}); From 2a2dad389f7d8802a14847d316403074377a43ca Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 19 Mar 2024 09:15:15 +0100 Subject: [PATCH 15/25] Improve formatTokenForLogs + format file --- app/server/lib/OIDCConfig.ts | 38 +++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 70a8051f..eb155123 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -69,7 +69,7 @@ import { AppSettings, appSettings } from './AppSettings'; import { RequestWithLogin } from './Authorizer'; import { UserProfile } from 'app/common/LoginSessionAPI'; import _ from 'lodash'; -import {SessionObj} from './BrowserSession'; +import { SessionObj } from './BrowserSession'; enum ENABLED_PROTECTIONS { NONCE, @@ -84,11 +84,9 @@ const CALLBACK_URL = '/oauth2/callback'; function formatTokenForLogs(token: TokenSet) { return _.chain(token) .omitBy(_.isFunction) - .mapValues((v, k) => { - if (!['token_type', 'expires_in', 'expires_at', 'scope'].includes(k)) { - return 'REDACTED'; - } - return v; + .mapValues((value, key) => { + const showValueInClear = ['token_type', 'expires_in', 'expires_at', 'scope'].includes(key); + return showValueInClear ? value : 'REDACTED'; }).value(); } @@ -166,10 +164,10 @@ export class OIDCConfig { this._enabledProtections = this._buildEnabledProtections(section); this._redirectUrl = new URL(CALLBACK_URL, spHost).href; - await this._initClient({issuerUrl, clientId, clientSecret, extraMetadata}); + await this._initClient({ issuerUrl, clientId, clientSecret, extraMetadata }); if (this._client.issuer.metadata.end_session_endpoint === undefined && - !this._endSessionEndpoint && !this._skipEndSessionEndpoint) { + !this._endSessionEndpoint && !this._skipEndSessionEndpoint) { throw new Error('The Identity provider does not propose end_session_endpoint. ' + 'If that is expected, please set GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true ' + 'or provide an alternative logout URL in GRIST_OIDC_IDP_END_SESSION_ENDPOINT'); @@ -259,20 +257,20 @@ export class OIDCConfig { return this._enabledProtections.includes(protection); } - protected async _initClient({issuerUrl, clientId, clientSecret, extraMetadata}: - {issuerUrl: string, clientId: string, clientSecret: string, extraMetadata: Partial } + protected async _initClient({ issuerUrl, clientId, clientSecret, extraMetadata }: + { issuerUrl: string, clientId: string, clientSecret: string, extraMetadata: Partial } ): Promise { const issuer = await Issuer.discover(issuerUrl); this._client = new issuer.Client({ client_id: clientId, client_secret: clientSecret, - redirect_uris: [ this._redirectUrl ], - response_types: [ 'code' ], + redirect_uris: [this._redirectUrl], + response_types: ['code'], ...extraMetadata, }); } - private _forgeProtectionParamsForAuthUrl(protections: {codeVerifier?: string, state?: string, nonce?: string}) { + private _forgeProtectionParamsForAuthUrl(protections: { codeVerifier?: string, state?: string, nonce?: string }) { return _.omitBy({ state: protections.state, nonce: protections.nonce, @@ -321,7 +319,7 @@ export class OIDCConfig { } private async _retrieveChecksFromSession(mreq: RequestWithLogin): - Promise<{code_verifier?: string, state?: string, nonce?: string}> { + Promise<{ code_verifier?: string, state?: string, nonce?: string }> { if (!mreq.session) { throw new Error('no session available'); } const state = mreq.session.oidc?.state; @@ -330,7 +328,7 @@ export class OIDCConfig { } const codeVerifier = mreq.session.oidc?.codeVerifier; - if (!codeVerifier && this.supportsProtection('PKCE') ) { throw new Error('Login is stale'); } + if (!codeVerifier && this.supportsProtection('PKCE')) { throw new Error('Login is stale'); } const nonce = mreq.session.oidc?.nonce; if (!nonce && this.supportsProtection('NONCE')) { throw new Error('Login is stale'); } @@ -340,14 +338,14 @@ export class OIDCConfig { private _makeUserProfileFromUserInfo(userInfo: UserinfoResponse): Partial { return { - email: String(userInfo[ this._emailPropertyKey ]), + email: String(userInfo[this._emailPropertyKey]), name: this._extractName(userInfo) }; } - private _extractName(userInfo: UserinfoResponse): string|undefined { + private _extractName(userInfo: UserinfoResponse): string | undefined { if (this._namePropertyKey) { - return (userInfo[ this._namePropertyKey ] as any)?.toString(); + return (userInfo[this._namePropertyKey] as any)?.toString(); } const fname = userInfo.given_name ?? ''; const lname = userInfo.family_name ?? ''; @@ -356,7 +354,7 @@ export class OIDCConfig { } } -export async function getOIDCLoginSystem(): Promise { +export async function getOIDCLoginSystem(): Promise { if (!process.env.GRIST_OIDC_IDP_ISSUER) { return undefined; } return { async getMiddleware(gristServer: GristServer) { @@ -371,6 +369,6 @@ export async function getOIDCLoginSystem(): Promise }, }; }, - async deleteUser() {}, + async deleteUser() { }, }; } From 8fc0962d7f5608617d4dfcd23c0ed069bc791671 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 19 Mar 2024 12:20:43 +0100 Subject: [PATCH 16/25] Selenium tests working --- .../import_keycloak/grist-realm.json | 6 +-- app/server/lib/FlexServer.ts | 2 +- test/nbrowser/LoginWithOIDC.ts | 44 ++++++++----------- test/nbrowser/homeUtil.ts | 2 +- test/nbrowser/testServer.ts | 5 ++- 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/.github/workflows/import_keycloak/grist-realm.json b/.github/workflows/import_keycloak/grist-realm.json index 4159d05f..e751cf20 100644 --- a/.github/workflows/import_keycloak/grist-realm.json +++ b/.github/workflows/import_keycloak/grist-realm.json @@ -461,8 +461,6 @@ "clientId": "keycloak_clientid", "name": "keycloak_clientid", "description": "", - "rootUrl": "http://localhost:8484", - "adminUrl": "http://localhost:8484", "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, @@ -470,7 +468,7 @@ "clientAuthenticatorType": "client-secret", "secret": "xLn3SBOgrnsUvGn95AEOMWW7fNROuqMi", "redirectUris": [ - "/oauth2/callback" + "*" ], "webOrigins": [], "notBefore": 0, @@ -487,7 +485,7 @@ "oidc.ciba.grant.enabled": false, "client.secret.creation.time": "1710262687", "backchannel.logout.session.required": "true", - "post.logout.redirect.uris": "/*", + "post.logout.redirect.uris": "*", "oauth2.device.authorization.grant.enabled": "false", "display.on.consent.screen": "false", "backchannel.logout.revoke.offline.tokens": "false", diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 2ba1c5c4..2f8ebe9a 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1849,7 +1849,7 @@ export class FlexServer implements GristServer { } public resolveLoginSystem() { - return process.env.GRIST_TEST_LOGIN ? getTestLoginSystem() : (this._getLoginSystem?.() || getLoginSystem()); + return isAffirmative(process.env.GRIST_TEST_LOGIN) ? getTestLoginSystem() : (this._getLoginSystem?.() || getLoginSystem()); } // Adds endpoints that support imports and exports. diff --git a/test/nbrowser/LoginWithOIDC.ts b/test/nbrowser/LoginWithOIDC.ts index eb528924..2c632c7c 100644 --- a/test/nbrowser/LoginWithOIDC.ts +++ b/test/nbrowser/LoginWithOIDC.ts @@ -1,12 +1,13 @@ -import {driver} from 'mocha-webdriver'; +import { assert } from 'chai'; +import { driver } from 'mocha-webdriver'; import * as gu from 'test/nbrowser/gristUtils'; -import {server, setupTestSuite} from 'test/nbrowser/testUtils'; +import { server, setupTestSuite } from 'test/nbrowser/testUtils'; -describe('LoginWithOIDC', function () { +describe('LoginWithOIDC', function() { this.timeout(60000); setupTestSuite(); gu.withEnvironmentSnapshot({ - get 'GRIST_OIDC_SP_HOST' () { return server.getHost(); }, + get 'GRIST_OIDC_SP_HOST'() { return server.getHost(); }, 'GRIST_OIDC_IDP_ISSUER': 'http://localhost:8081/realms/grist', 'GRIST_OIDC_IDP_CLIENT_ID': 'keycloak_clientid', 'GRIST_OIDC_IDP_CLIENT_SECRET': 'keycloak_secret', @@ -15,33 +16,24 @@ describe('LoginWithOIDC', function () { }); it('should login using OIDC', async () => { - console.log("HERE"); await driver.get(`${server.getHost()}/o/docs/login`); await driver.findWait('#kc-form-login', 10_000); - await driver.find('#username').sendKeys('keycloackuser'); + await driver.find('#username').sendKeys('keycloakuser'); await driver.find('#password').sendKeys('keycloakpassword'); await driver.find('#kc-login').click(); - // await driver.wait( - // async () => { - // const url = await driver.getCurrentUrl(); - // return url.startsWith(server.getHost()); - // }, - // 20_000 - // ); - // await driver.find('.weasel-popup-open').click(); - // await gu.openAccountMenu(); - // assert.equal(await driver.find('.test-usermenu-name').getText(), 'keycloackuser'); - // assert.equal(await driver.find('.test-usermenu-email').getText(), 'keycloakuser@example.com'); - // await driver.find('.test-dm-log-out').click(); - // await driver.wait( - // async () => { - // const url = await driver.getCurrentUrl(); - // return url.startsWith(`${server.getHost()}/o/docs/signed-out`); - // }, - // 20_000 - // ); - // assert.equal(await driver.find('.test-error-header').getText(), 'Signed out'); + await driver.wait( + async () => { + const url = await driver.getCurrentUrl(); + return url.startsWith(server.getHost()); + }, + 20_000 + ); + await gu.openAccountMenu(); + assert.equal(await driver.find('.test-usermenu-name').getText(), 'Keycloak User'); + assert.equal(await driver.find('.test-usermenu-email').getText(), 'keycloakuser@example.com'); + await driver.find('.test-dm-log-out').click(); + await driver.findContentWait('.test-error-header', /Signed out/, 20_000, 'Should be signed out'); }); }); diff --git a/test/nbrowser/homeUtil.ts b/test/nbrowser/homeUtil.ts index 31690a8c..33861dc7 100644 --- a/test/nbrowser/homeUtil.ts +++ b/test/nbrowser/homeUtil.ts @@ -376,7 +376,7 @@ export class HomeUtil { /** * Waits for browser to navigate to a Grist login page. */ - public async checkGristLoginPage(waitMs: number = 2000) { + public async checkGristLoginPage(waitMs: number = 2000) { await this.driver.wait(this.isOnGristLoginPage.bind(this), waitMs); } diff --git a/test/nbrowser/testServer.ts b/test/nbrowser/testServer.ts index 16e357e2..beae73a3 100644 --- a/test/nbrowser/testServer.ts +++ b/test/nbrowser/testServer.ts @@ -149,7 +149,10 @@ export class TestServerMerged extends EventEmitter implements IMochaServer { delete env.DOC_WORKER_COUNT; } this._server = spawn('node', [cmd], { - env, + env: { + ...env, + ...(process.env.SERVER_NODE_OPTIONS ? {NODE_OPTIONS: process.env.SERVER_NODE_OPTIONS} : {}) + }, stdio: quiet ? 'ignore' : ['inherit', serverLog, serverLog], }); this._exitPromise = exitPromise(this._server); From 6b1247f7aaf0b446de5b72adb7885f9c0c07e509 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 19 Mar 2024 12:28:28 +0100 Subject: [PATCH 17/25] Fix CI? --- .github/workflows/main.yml | 22 ++++++++++++++-------- app/server/lib/FlexServer.ts | 4 +++- test/nbrowser/LoginWithOIDC.ts | 9 +++++---- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2a24e756..c92afcc0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,13 +93,15 @@ jobs: run: yarn run test:common - name: Setup Keycloak realm and client + uses: carlosthe19916/keycloak-action@0.4 if: contains(matrix.tests, ':server-') - server: http://localhost:8080/auth - username: admin - password: admin - kcadm: | - create realms -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-realms.json - create users -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-users-0.json + with: + server: http://localhost:8080/auth + username: admin + password: admin + kcadm: | + create realms -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-realms.json + create users -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-users-0.json - name: Run server tests with minio, keycloak and redis if: contains(matrix.tests, ':server-') @@ -115,6 +117,10 @@ jobs: GRIST_DOCS_MINIO_ENDPOINT: localhost GRIST_DOCS_MINIO_PORT: 9000 GRIST_DOCS_MINIO_BUCKET: grist-docs-test + GRIST_OIDC_IDP_ISSUER: "http://127.0.0.1:8080/realms/grist" + GRIST_OIDC_IDP_CLIENT_ID: "keycloak_clientid" + GRIST_OIDC_IDP_CLIENT_SECRET: "keycloak_secret" + GRIST_OIDC_IDP_SCOPES: "openid email profile" - name: Run main tests without minio and redis if: contains(matrix.tests, ':nbrowser-') @@ -173,11 +179,11 @@ jobs: keycloak: # image: fflorent/keycloak-with-import-realm:latest # Image with overridden entrypoint. image: quay.io/keycloak/keycloak - port: 8080:8080 + ports: + - 8080:8080 env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin - volume: ${{ github.workspace }}/.github/workflows/import_keycloak:/opt/keycloak/data/import candidate: needs: build_and_test diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 2f8ebe9a..33e4869f 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1849,7 +1849,9 @@ export class FlexServer implements GristServer { } public resolveLoginSystem() { - return isAffirmative(process.env.GRIST_TEST_LOGIN) ? getTestLoginSystem() : (this._getLoginSystem?.() || getLoginSystem()); + return isAffirmative(process.env.GRIST_TEST_LOGIN) ? + getTestLoginSystem() : + (this._getLoginSystem?.() || getLoginSystem()); } // Adds endpoints that support imports and exports. diff --git a/test/nbrowser/LoginWithOIDC.ts b/test/nbrowser/LoginWithOIDC.ts index 2c632c7c..5ba5ae5c 100644 --- a/test/nbrowser/LoginWithOIDC.ts +++ b/test/nbrowser/LoginWithOIDC.ts @@ -4,14 +4,15 @@ import * as gu from 'test/nbrowser/gristUtils'; import { server, setupTestSuite } from 'test/nbrowser/testUtils'; describe('LoginWithOIDC', function() { + before(function() { + if (!process.env.GRIST_OIDC_SP_HOST) { + return this.skip(); + } + }); this.timeout(60000); setupTestSuite(); gu.withEnvironmentSnapshot({ get 'GRIST_OIDC_SP_HOST'() { return server.getHost(); }, - 'GRIST_OIDC_IDP_ISSUER': 'http://localhost:8081/realms/grist', - 'GRIST_OIDC_IDP_CLIENT_ID': 'keycloak_clientid', - 'GRIST_OIDC_IDP_CLIENT_SECRET': 'keycloak_secret', - 'GRIST_OIDC_IDP_SCOPES': 'openid email profile', 'GRIST_TEST_LOGIN': 0, }); From 63f05e5fd0fac358404e34d13ad4b60d6eb643ff Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 19 Mar 2024 14:00:43 +0100 Subject: [PATCH 18/25] Tests with keycloak are not in test:server --- .github/workflows/main.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c92afcc0..a3d8a9ab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,7 @@ jobs: - ':nbrowser-^[M-O]:' - ':nbrowser-^[P-S]:' - ':nbrowser-^[^A-S]:' + - ':nbrowser:keycloak:' include: - tests: ':lint:python:client:common:smoke:' node-version: 18.x @@ -73,7 +74,7 @@ jobs: run: yarn run build:prod - name: Install chromedriver - if: contains(matrix.tests, ':nbrowser-') || contains(matrix.tests, ':smoke:') + if: contains(matrix.tests, ':nbrowser') || contains(matrix.tests, ':smoke:') run: ./node_modules/selenium-webdriver/bin/linux/selenium-manager --driver chromedriver - name: Run smoke test @@ -94,7 +95,7 @@ jobs: - name: Setup Keycloak realm and client uses: carlosthe19916/keycloak-action@0.4 - if: contains(matrix.tests, ':server-') + if: contains(matrix.tests, ':keycloak:') with: server: http://localhost:8080/auth username: admin @@ -103,7 +104,7 @@ jobs: create realms -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-realms.json create users -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-users-0.json - - name: Run server tests with minio, keycloak and redis + - name: Run server tests with minio and redis if: contains(matrix.tests, ':server-') run: | export TEST_SPLITS=$(echo $TESTS | sed "s/.*:server-\([^:]*\).*/\1/") @@ -117,10 +118,6 @@ jobs: GRIST_DOCS_MINIO_ENDPOINT: localhost GRIST_DOCS_MINIO_PORT: 9000 GRIST_DOCS_MINIO_BUCKET: grist-docs-test - GRIST_OIDC_IDP_ISSUER: "http://127.0.0.1:8080/realms/grist" - GRIST_OIDC_IDP_CLIENT_ID: "keycloak_clientid" - GRIST_OIDC_IDP_CLIENT_SECRET: "keycloak_secret" - GRIST_OIDC_IDP_SCOPES: "openid email profile" - name: Run main tests without minio and redis if: contains(matrix.tests, ':nbrowser-') @@ -133,6 +130,18 @@ jobs: MOCHA_WEBDRIVER_LOGDIR: ${{ runner.temp }}/test-logs/webdriver TESTDIR: ${{ runner.temp }}/test-logs + - name: Run integration with Keycloak + if: contains(matrix.tests, ':nbrowser:keycloak:') + env: + GRIST_OIDC_IDP_ISSUER: "http://127.0.0.1:8080/realms/grist" + GRIST_OIDC_IDP_CLIENT_ID: "keycloak_clientid" + GRIST_OIDC_IDP_CLIENT_SECRET: "keycloak_secret" + GRIST_OIDC_IDP_SCOPES: "openid email profile" + run: | + mkdir -p $MOCHA_WEBDRIVER_LOGDIR + export GREP_TESTS="LoginWithOIDC" + MOCHA_WEBDRIVER_SKIP_CLEANUP=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:nbrowser + - name: Prepare for saving artifact if: failure() run: | From 1dedcc16fde39ba500d67f04da786931c980cbb3 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 19 Mar 2024 14:17:22 +0100 Subject: [PATCH 19/25] Fix CI ? + Skip in LoginWithOIDC --- .github/workflows/main.yml | 13 +++++---- test/nbrowser/LoginWithOIDC.ts | 52 ++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a3d8a9ab..4afc563f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,12 +97,12 @@ jobs: uses: carlosthe19916/keycloak-action@0.4 if: contains(matrix.tests, ':keycloak:') with: - server: http://localhost:8080/auth + server: http://keycloak:8080/auth username: admin password: admin kcadm: | - create realms -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-realms.json - create users -f ${{ github.workspace }}/.github/workflows/import_keycloak/grist-users-0.json + create realms -f .github/workflows/import_keycloak/grist-realm.json + create users -f .github/workflows/import_keycloak/grist-users-0.json - name: Run server tests with minio and redis if: contains(matrix.tests, ':server-') @@ -186,14 +186,17 @@ jobs: --health-timeout 5s --health-retries 5 keycloak: - # image: fflorent/keycloak-with-import-realm:latest # Image with overridden entrypoint. image: quay.io/keycloak/keycloak ports: - 8080:8080 env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin - + options: >- + --health-cmd "curl --fail http://localhost:8080/auth || exit 1" + --health-interval 10s + --health-timeout 5s + --health-retries 5 candidate: needs: build_and_test if: ${{ success() && github.event_name == 'push' }} diff --git a/test/nbrowser/LoginWithOIDC.ts b/test/nbrowser/LoginWithOIDC.ts index 5ba5ae5c..b8ba0a8b 100644 --- a/test/nbrowser/LoginWithOIDC.ts +++ b/test/nbrowser/LoginWithOIDC.ts @@ -3,38 +3,40 @@ import { driver } from 'mocha-webdriver'; import * as gu from 'test/nbrowser/gristUtils'; import { server, setupTestSuite } from 'test/nbrowser/testUtils'; -describe('LoginWithOIDC', function() { +describe('IntegrationWithKeycloak', function() { before(function() { if (!process.env.GRIST_OIDC_SP_HOST) { return this.skip(); } }); - this.timeout(60000); - setupTestSuite(); - gu.withEnvironmentSnapshot({ - get 'GRIST_OIDC_SP_HOST'() { return server.getHost(); }, - 'GRIST_TEST_LOGIN': 0, - }); + describe('LoginWithOIDC', function() { + this.timeout(60000); + setupTestSuite(); + gu.withEnvironmentSnapshot({ + get 'GRIST_OIDC_SP_HOST'() { return server.getHost(); }, + 'GRIST_TEST_LOGIN': 0, + }); - it('should login using OIDC', async () => { - await driver.get(`${server.getHost()}/o/docs/login`); - await driver.findWait('#kc-form-login', 10_000); - await driver.find('#username').sendKeys('keycloakuser'); - await driver.find('#password').sendKeys('keycloakpassword'); - await driver.find('#kc-login').click(); + it('should login using OIDC', async () => { + await driver.get(`${server.getHost()}/o/docs/login`); + await driver.findWait('#kc-form-login', 10_000); + await driver.find('#username').sendKeys('keycloakuser'); + await driver.find('#password').sendKeys('keycloakpassword'); + await driver.find('#kc-login').click(); - await driver.wait( - async () => { - const url = await driver.getCurrentUrl(); - return url.startsWith(server.getHost()); - }, - 20_000 - ); - await gu.openAccountMenu(); - assert.equal(await driver.find('.test-usermenu-name').getText(), 'Keycloak User'); - assert.equal(await driver.find('.test-usermenu-email').getText(), 'keycloakuser@example.com'); - await driver.find('.test-dm-log-out').click(); - await driver.findContentWait('.test-error-header', /Signed out/, 20_000, 'Should be signed out'); + await driver.wait( + async () => { + const url = await driver.getCurrentUrl(); + return url.startsWith(server.getHost()); + }, + 20_000 + ); + await gu.openAccountMenu(); + assert.equal(await driver.find('.test-usermenu-name').getText(), 'Keycloak User'); + assert.equal(await driver.find('.test-usermenu-email').getText(), 'keycloakuser@example.com'); + await driver.find('.test-dm-log-out').click(); + await driver.findContentWait('.test-error-header', /Signed out/, 20_000, 'Should be signed out'); + }); }); }); From 4cef07191399eaa0983347678d660987f11f0a37 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 19 Mar 2024 16:40:22 +0100 Subject: [PATCH 20/25] Remove health-checks for keycloak, curl being not avaible in container --- .github/workflows/main.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4afc563f..9ac3f501 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -192,11 +192,6 @@ jobs: env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin - options: >- - --health-cmd "curl --fail http://localhost:8080/auth || exit 1" - --health-interval 10s - --health-timeout 5s - --health-retries 5 candidate: needs: build_and_test if: ${{ success() && github.event_name == 'push' }} From a87578c2966ebc6dfbb43c7760610550f2aac4d2 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 19 Mar 2024 17:56:02 +0100 Subject: [PATCH 21/25] Attempt fix using docker-run-action --- .../import_keycloak/grist-users-0.json | 49 +++++++++---------- .github/workflows/main.yml | 25 +++++++--- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/.github/workflows/import_keycloak/grist-users-0.json b/.github/workflows/import_keycloak/grist-users-0.json index bc6ba075..c4b362e9 100644 --- a/.github/workflows/import_keycloak/grist-users-0.json +++ b/.github/workflows/import_keycloak/grist-users-0.json @@ -1,27 +1,24 @@ { - "realm" : "grist", - "users" : [ { - "id" : "1564f73d-c385-4269-84da-34b40f494dea", - "createdTimestamp" : 1710254868534, - "username" : "keycloakuser", - "enabled" : true, - "totp" : false, - "emailVerified" : true, - "firstName" : "Keycloak", - "lastName" : "User", - "email" : "keycloakuser@example.com", - "credentials" : [ { - "id" : "3ceee294-209a-4187-aede-1dcfa2dac006", - "type" : "password", - "userLabel" : "Password: keycloakpassword", - "createdDate" : 1710254893700, - "secretData" : "{\"value\":\"kZZMgT2g89C+LFfigQt/qu5H9vs188wWgVK1KqnO12Q=\",\"salt\":\"ffAeQSmuJ7cGFE8rzN+f/g==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-grist" ], - "notBefore" : 0, - "groups" : [ ] - } ] -} \ No newline at end of file + "id" : "1564f73d-c385-4269-84da-34b40f494dea", + "createdTimestamp" : 1710254868534, + "username" : "keycloakuser", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "Keycloak", + "lastName" : "User", + "email" : "keycloakuser@example.com", + "credentials" : [ { + "id" : "3ceee294-209a-4187-aede-1dcfa2dac006", + "type" : "password", + "userLabel" : "Password: keycloakpassword", + "createdDate" : 1710254893700, + "secretData" : "{\"value\":\"kZZMgT2g89C+LFfigQt/qu5H9vs188wWgVK1KqnO12Q=\",\"salt\":\"ffAeQSmuJ7cGFE8rzN+f/g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-grist" ], + "notBefore" : 0, + "groups" : [ ] +} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9ac3f501..6ca1e46c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -94,15 +94,17 @@ jobs: run: yarn run test:common - name: Setup Keycloak realm and client - uses: carlosthe19916/keycloak-action@0.4 + uses: addnab/docker-run-action@v3 if: contains(matrix.tests, ':keycloak:') with: - server: http://keycloak:8080/auth - username: admin - password: admin - kcadm: | - create realms -f .github/workflows/import_keycloak/grist-realm.json - create users -f .github/workflows/import_keycloak/grist-users-0.json + image: quay.io/keycloak/keycloak + options: -v ${{ github.workspace }}:/workspace + run: | + set -eu -o pipefail + cd /opt/keycloak/bin + ./kcadm.sh config credentials --server http://keycloak:8080/ --realm=master --user=admin --password=admin + ./kcadm.sh create realms -f /workspace/.github/workflows/import_keycloak/grist-realm.json + ./kcadm.sh create users -r grist -f /workspace/.github/workflows/import_keycloak/grist-users-0.json - name: Run server tests with minio and redis if: contains(matrix.tests, ':server-') @@ -192,6 +194,15 @@ jobs: env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin + + # curl not being present in the keycloak container, use this trick: + # https://github.com/keycloak/keycloak/discussions/17319#discussioncomment-5192267 + options: >- + --health-cmd "echo > /dev/tcp/localhost/8080" + --health-interval 20s + --health-timeout 5s + --health-retries 20 + candidate: needs: build_and_test if: ${{ success() && github.event_name == 'push' }} From 1bf114a38bf9a1d5925ce1d65fa5938c964dac07 Mon Sep 17 00:00:00 2001 From: fflorent Date: Wed, 20 Mar 2024 09:49:32 +0100 Subject: [PATCH 22/25] Remove integration tests (for now) --- .../import_keycloak/grist-realm.json | 1785 ----------------- .../import_keycloak/grist-users-0.json | 24 - .github/workflows/main.yml | 43 +- test/nbrowser/LoginWithOIDC.ts | 42 - 4 files changed, 1 insertion(+), 1893 deletions(-) delete mode 100644 .github/workflows/import_keycloak/grist-realm.json delete mode 100644 .github/workflows/import_keycloak/grist-users-0.json delete mode 100644 test/nbrowser/LoginWithOIDC.ts diff --git a/.github/workflows/import_keycloak/grist-realm.json b/.github/workflows/import_keycloak/grist-realm.json deleted file mode 100644 index e751cf20..00000000 --- a/.github/workflows/import_keycloak/grist-realm.json +++ /dev/null @@ -1,1785 +0,0 @@ -{ - "id" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", - "realm" : "grist", - "notBefore" : 0, - "defaultSignatureAlgorithm" : "RS256", - "revokeRefreshToken" : false, - "refreshTokenMaxReuse" : 0, - "accessTokenLifespan" : 300, - "accessTokenLifespanForImplicitFlow" : 900, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "ssoSessionIdleTimeoutRememberMe" : 0, - "ssoSessionMaxLifespanRememberMe" : 0, - "offlineSessionIdleTimeout" : 2592000, - "offlineSessionMaxLifespanEnabled" : false, - "offlineSessionMaxLifespan" : 5184000, - "clientSessionIdleTimeout" : 0, - "clientSessionMaxLifespan" : 0, - "clientOfflineSessionIdleTimeout" : 0, - "clientOfflineSessionMaxLifespan" : 0, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, - "accessCodeLifespanLogin" : 1800, - "actionTokenGeneratedByAdminLifespan" : 43200, - "actionTokenGeneratedByUserLifespan" : 300, - "oauth2DeviceCodeLifespan" : 600, - "oauth2DevicePollingInterval" : 5, - "enabled" : true, - "sslRequired" : "external", - "registrationAllowed" : false, - "registrationEmailAsUsername" : false, - "rememberMe" : false, - "verifyEmail" : false, - "loginWithEmailAllowed" : true, - "duplicateEmailsAllowed" : false, - "resetPasswordAllowed" : false, - "editUsernameAllowed" : false, - "bruteForceProtected" : false, - "permanentLockout" : false, - "maxFailureWaitSeconds" : 900, - "minimumQuickLoginWaitSeconds" : 60, - "waitIncrementSeconds" : 60, - "quickLoginCheckMilliSeconds" : 1000, - "maxDeltaTimeSeconds" : 43200, - "failureFactor" : 30, - "roles" : { - "realm" : [ { - "id" : "815d5e80-c709-40f7-a927-f90e3dad51a2", - "name" : "default-roles-grist", - "description" : "${role_default-roles}", - "composite" : true, - "composites" : { - "realm" : [ "offline_access", "uma_authorization" ], - "client" : { - "account" : [ "view-profile", "manage-account" ] - } - }, - "clientRole" : false, - "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", - "attributes" : { } - }, { - "id" : "9023045c-64fd-4e62-baa7-2875e0d87019", - "name" : "offline_access", - "description" : "${role_offline-access}", - "composite" : false, - "clientRole" : false, - "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", - "attributes" : { } - }, { - "id" : "98761135-6b8d-4a52-a95d-d1e13257d156", - "name" : "uma_authorization", - "description" : "${role_uma_authorization}", - "composite" : false, - "clientRole" : false, - "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f", - "attributes" : { } - } ], - "client" : { - "realm-management" : [ { - "id" : "1867f490-b610-45a4-9a75-b9cf29f3c48f", - "name" : "view-realm", - "description" : "${role_view-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "86bd9709-78bb-4af1-a77f-b33c30dc422a", - "name" : "manage-clients", - "description" : "${role_manage-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "8dfcb717-6d35-4746-a717-c8655ba03427", - "name" : "view-authorization", - "description" : "${role_view-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "51998cd6-041c-473f-90a7-e6d247ef6292", - "name" : "view-identity-providers", - "description" : "${role_view-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "6d6eca69-e7f4-48c9-be72-0a571f03552c", - "name" : "manage-identity-providers", - "description" : "${role_manage-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "4e2e78ef-5175-4e2a-b213-6d520fc712a7", - "name" : "query-clients", - "description" : "${role_query-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "562ee9cf-a814-4dd3-a5cd-96b6cd12109f", - "name" : "manage-authorization", - "description" : "${role_manage-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "c99029ca-60a9-4dc4-905d-d223758e77e7", - "name" : "view-users", - "description" : "${role_view-users}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-users", "query-groups" ] - } - }, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "31acf0b1-3085-40d0-a513-5e889a47d93d", - "name" : "realm-admin", - "description" : "${role_realm-admin}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "view-realm", "manage-clients", "view-identity-providers", "view-authorization", "query-clients", "manage-identity-providers", "manage-authorization", "view-users", "manage-events", "view-events", "manage-users", "query-users", "impersonation", "query-groups", "query-realms", "view-clients", "manage-realm", "create-client" ] - } - }, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "8d2b0514-e8df-4eb3-b525-c524d0740d6b", - "name" : "manage-events", - "description" : "${role_manage-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "b48961c1-ea7a-4b9c-a601-7905aae69e34", - "name" : "view-events", - "description" : "${role_view-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "03c79004-9a25-4ba6-8010-2bdb9d80af13", - "name" : "manage-users", - "description" : "${role_manage-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "60d3f4a2-799b-435a-970d-07ac20900d74", - "name" : "query-users", - "description" : "${role_query-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "88fe03e9-8415-4a40-8e6c-1d75426f28be", - "name" : "impersonation", - "description" : "${role_impersonation}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "a02b201e-c426-462b-a6db-13b1e88fd695", - "name" : "query-groups", - "description" : "${role_query-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "b1d340f3-48a8-4e52-823e-b3e14dc89c7b", - "name" : "query-realms", - "description" : "${role_query-realms}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "5075a68e-8771-4421-bdb9-bd1f7d57d1e5", - "name" : "view-clients", - "description" : "${role_view-clients}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-clients" ] - } - }, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "254b1a1e-1e97-4a88-ba65-18a10917352a", - "name" : "manage-realm", - "description" : "${role_manage-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - }, { - "id" : "99b0a077-fdf0-48a5-8aa8-f22aada4b80c", - "name" : "create-client", - "description" : "${role_create-client}", - "composite" : false, - "clientRole" : true, - "containerId" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "attributes" : { } - } ], - "security-admin-console" : [ ], - "admin-cli" : [ ], - "broker" : [ { - "id" : "6448f386-787b-4750-943e-50727b3c079b", - "name" : "read-token", - "description" : "${role_read-token}", - "composite" : false, - "clientRole" : true, - "containerId" : "7bc4b7df-32f1-41bc-9afa-5cc9487a2e42", - "attributes" : { } - } ], - "keycloak_clientid" : [ ], - "account" : [ { - "id" : "5cf3a372-6e9d-4136-b152-c3908033edf9", - "name" : "manage-consent", - "description" : "${role_manage-consent}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "view-consent" ] - } - }, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - }, { - "id" : "52224265-1dcf-4761-87b5-0da97238c42a", - "name" : "view-profile", - "description" : "${role_view-profile}", - "composite" : false, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - }, { - "id" : "1f5f1039-b1c5-4a1b-9648-dd1352fafc72", - "name" : "view-applications", - "description" : "${role_view-applications}", - "composite" : false, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - }, { - "id" : "11521553-cd9c-4730-8fe5-5af7198dbb19", - "name" : "view-consent", - "description" : "${role_view-consent}", - "composite" : false, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - }, { - "id" : "563e5aef-01a5-48d7-95a5-3b58723146d0", - "name" : "delete-account", - "description" : "${role_delete-account}", - "composite" : false, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - }, { - "id" : "17b76778-7811-41f3-b2ec-77fe99ea9518", - "name" : "manage-account", - "description" : "${role_manage-account}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "manage-account-links" ] - } - }, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - }, { - "id" : "9ccfd5de-60f9-4132-85a3-67de26a8e3e2", - "name" : "manage-account-links", - "description" : "${role_manage-account-links}", - "composite" : false, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - }, { - "id" : "067cefec-b9cd-42a6-bfe7-9468487cd0c2", - "name" : "view-groups", - "description" : "${role_view-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "attributes" : { } - } ] - } - }, - "groups" : [ ], - "defaultRole" : { - "id" : "815d5e80-c709-40f7-a927-f90e3dad51a2", - "name" : "default-roles-grist", - "description" : "${role_default-roles}", - "composite" : true, - "clientRole" : false, - "containerId" : "f8d5d4da-8201-453a-9667-24bc11a07e2f" - }, - "requiredCredentials" : [ "password" ], - "otpPolicyType" : "totp", - "otpPolicyAlgorithm" : "HmacSHA1", - "otpPolicyInitialCounter" : 0, - "otpPolicyDigits" : 6, - "otpPolicyLookAheadWindow" : 1, - "otpPolicyPeriod" : 30, - "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName" ], - "webAuthnPolicyRpEntityName" : "keycloak", - "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyRpId" : "", - "webAuthnPolicyAttestationConveyancePreference" : "not specified", - "webAuthnPolicyAuthenticatorAttachment" : "not specified", - "webAuthnPolicyRequireResidentKey" : "not specified", - "webAuthnPolicyUserVerificationRequirement" : "not specified", - "webAuthnPolicyCreateTimeout" : 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyAcceptableAaguids" : [ ], - "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyPasswordlessRpId" : "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", - "webAuthnPolicyPasswordlessCreateTimeout" : 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], - "scopeMappings" : [ { - "clientScope" : "offline_access", - "roles" : [ "offline_access" ] - } ], - "clients" : [ { - "id" : "a1d0b1da-0652-4f8d-8e00-328aa68d9ed1", - "clientId" : "account", - "name" : "${client_account}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/grist/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/grist/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "4b1169ff-343e-4c34-bcf3-cd3f5dfc505f", - "clientId" : "admin-cli", - "name" : "${client_admin-cli}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : false, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "7bc4b7df-32f1-41bc-9afa-5cc9487a2e42", - "clientId" : "broker", - "name" : "${client_broker}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "clientId": "keycloak_clientid", - "name": "keycloak_clientid", - "description": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "xLn3SBOgrnsUvGn95AEOMWW7fNROuqMi", - "redirectUris": [ - "*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "oidc.ciba.grant.enabled": false, - "client.secret.creation.time": "1710262687", - "backchannel.logout.session.required": "true", - "post.logout.redirect.uris": "*", - "oauth2.device.authorization.grant.enabled": "false", - "display.on.consent.screen": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "login_theme": "", - "frontchannel.logout.url": "", - "backchannel.logout.url": "" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - }, - { - "name": "Client Host", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientHost", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientHost", - "jsonType.label": "String" - } - }, - { - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "client_id", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "client_id", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "access": { - "view": true, - "configure": true, - "manage": true - }, - "secret": "keycloak_secret", - "authorizationServicesEnabled": false -}, { - "id" : "4ec5e86c-3eb0-4add-8627-4198e3109a0e", - "clientId" : "realm-management", - "name" : "${client_realm-management}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "df7ce056-467d-4306-96b1-e1b66cf1245d", - "clientId" : "security-admin-console", - "name" : "${client_security-admin-console}", - "rootUrl" : "${authAdminUrl}", - "baseUrl" : "/admin/grist/console/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/admin/grist/console/*" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "27a32ca6-d3d0-4033-921e-a1ac8e958d7e", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - } ], - "clientScopes" : [ { - "id" : "eefa2fd5-889a-4a5e-8121-6f8262c9651b", - "name" : "offline_access", - "description" : "OpenID Connect built-in scope: offline_access", - "protocol" : "openid-connect", - "attributes" : { - "consent.screen.text" : "${offlineAccessScopeConsentText}", - "display.on.consent.screen" : "true" - } - }, { - "id" : "71d5597d-c0a6-400a-ba65-d35ee79225bc", - "name" : "role_list", - "description" : "SAML role list", - "protocol" : "saml", - "attributes" : { - "consent.screen.text" : "${samlRoleListScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "6f873430-2e38-4122-9602-d42028fd87c8", - "name" : "role list", - "protocol" : "saml", - "protocolMapper" : "saml-role-list-mapper", - "consentRequired" : false, - "config" : { - "single" : "false", - "attribute.nameformat" : "Basic", - "attribute.name" : "Role" - } - } ] - }, { - "id" : "12bbd05f-96e6-4bb2-9d09-03561ef52996", - "name" : "phone", - "description" : "OpenID Connect built-in scope: phone", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${phoneScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "1642de6b-4021-40ee-b1bd-eb9e4844779a", - "name" : "phone number", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumber", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number", - "jsonType.label" : "String" - } - }, { - "id" : "b5ff5770-9d03-4252-b7f3-62abe65e8154", - "name" : "phone number verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumberVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number_verified", - "jsonType.label" : "boolean" - } - } ] - }, { - "id" : "7633a138-4ac1-4fcb-8ec4-43adbcf191d6", - "name" : "acr", - "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "3ed856d5-05e3-4d9a-a754-090faf2ff56f", - "name" : "acr loa level", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-acr-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "access.token.claim" : "true" - } - } ] - }, { - "id" : "5d6398a6-e2f1-48d9-b58f-abc9f1c138e7", - "name" : "email", - "description" : "OpenID Connect built-in scope: email", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${emailScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "46276353-8787-4b54-bb74-6c2b62e6601f", - "name" : "email", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "email", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email", - "jsonType.label" : "String" - } - }, { - "id" : "bc0529b1-facc-48af-afba-6b58183c18cc", - "name" : "email verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "emailVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email_verified", - "jsonType.label" : "boolean" - } - } ] - }, { - "id" : "f5cda86d-cf80-460c-a807-2af7ef1f1f4c", - "name" : "microprofile-jwt", - "description" : "Microprofile - JWT built-in scope", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "8b278ba2-b6c9-4382-a793-54d68cbca348", - "name" : "upn", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "upn", - "jsonType.label" : "String" - } - }, { - "id" : "5306b57f-8307-4c16-b68e-2105f5dbf04b", - "name" : "groups", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "multivalued" : "true", - "user.attribute" : "foo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "groups", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "a8500c2c-ad1e-4000-a83c-f9ad7905d702", - "name" : "profile", - "description" : "OpenID Connect built-in scope: profile", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${profileScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "5eea6909-2d59-455c-9838-ff2d6f226c00", - "name" : "full name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-full-name-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - }, { - "id" : "08a95f5a-1f6d-41c2-a1e9-4ae00a61f69a", - "name" : "given name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "firstName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "given_name", - "jsonType.label" : "String" - } - }, { - "id" : "9255aa99-a703-41f6-bff8-1c509bd9981f", - "name" : "updated at", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "updatedAt", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "updated_at", - "jsonType.label" : "long" - } - }, { - "id" : "62e7a0fa-e394-4954-8301-d6d79bf6aa9f", - "name" : "profile", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "profile", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "profile", - "jsonType.label" : "String" - } - }, { - "id" : "00d8c6b5-2a94-44e8-a88a-b0cf8684a06b", - "name" : "picture", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "picture", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "picture", - "jsonType.label" : "String" - } - }, { - "id" : "eaf45331-ce30-4f84-a627-15c88a15e090", - "name" : "website", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "website", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "website", - "jsonType.label" : "String" - } - }, { - "id" : "4475fc2e-1282-40e7-a130-b4c8c6166e9f", - "name" : "middle name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "middleName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "middle_name", - "jsonType.label" : "String" - } - }, { - "id" : "8c23603e-eeeb-43f5-9467-a356ec475826", - "name" : "gender", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "gender", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "gender", - "jsonType.label" : "String" - } - }, { - "id" : "91ffe8ea-d943-4986-a0d7-8d4a06ac08f1", - "name" : "family name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "lastName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "family_name", - "jsonType.label" : "String" - } - }, { - "id" : "4fa72a9e-2e85-42ca-9e60-c0ddd40c2f9c", - "name" : "nickname", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "nickname", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "nickname", - "jsonType.label" : "String" - } - }, { - "id" : "d782d3bf-9c97-47db-92f5-d3c5b24d6c23", - "name" : "zoneinfo", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "zoneinfo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "zoneinfo", - "jsonType.label" : "String" - } - }, { - "id" : "21d306d5-df03-482e-b831-21017d424fb4", - "name" : "birthdate", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "birthdate", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "birthdate", - "jsonType.label" : "String" - } - }, { - "id" : "bdf8c994-c660-4b9e-933c-4d504f459c50", - "name" : "username", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "preferred_username", - "jsonType.label" : "String" - } - }, { - "id" : "9d328e54-8aae-4181-ae15-655d5ba46144", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "fcac977e-795d-49ba-a441-12c418e741bb", - "name" : "roles", - "description" : "OpenID Connect scope for add user roles to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${rolesScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "190c193b-7822-427c-883b-e1f108ad23bb", - "name" : "client roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-client-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "access.token.claim" : "true", - "claim.name" : "resource_access.${client_id}.roles", - "jsonType.label" : "String", - "multivalued" : "true" - } - }, { - "id" : "307ef86f-db58-4a7a-82b1-8c4389af1cd2", - "name" : "realm roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "access.token.claim" : "true", - "claim.name" : "realm_access.roles", - "jsonType.label" : "String", - "multivalued" : "true" - } - }, { - "id" : "ece9fac8-9d8a-4070-93f3-0bfc6281bc43", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { } - } ] - }, { - "id" : "623dfc8c-3579-4dcd-bf24-e8208cf8778e", - "name" : "web-origins", - "description" : "OpenID Connect scope for add allowed web origins to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false", - "consent.screen.text" : "" - }, - "protocolMappers" : [ { - "id" : "026bd8e3-c7ca-40d7-a9f2-bfc17e1b5040", - "name" : "allowed web origins", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-allowed-origins-mapper", - "consentRequired" : false, - "config" : { } - } ] - }, { - "id" : "8b98b389-1934-4653-b1f6-fff7c945992f", - "name" : "address", - "description" : "OpenID Connect built-in scope: address", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${addressScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "1c86a1f8-3f7b-41cb-b5e4-a9b68339923e", - "name" : "address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-address-mapper", - "consentRequired" : false, - "config" : { - "user.attribute.formatted" : "formatted", - "user.attribute.country" : "country", - "user.attribute.postal_code" : "postal_code", - "userinfo.token.claim" : "true", - "user.attribute.street" : "street", - "id.token.claim" : "true", - "user.attribute.region" : "region", - "access.token.claim" : "true", - "user.attribute.locality" : "locality" - } - } ] - } ], - "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], - "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], - "browserSecurityHeaders" : { - "contentSecurityPolicyReportOnly" : "", - "xContentTypeOptions" : "nosniff", - "referrerPolicy" : "no-referrer", - "xRobotsTag" : "none", - "xFrameOptions" : "SAMEORIGIN", - "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection" : "1; mode=block", - "strictTransportSecurity" : "max-age=31536000; includeSubDomains" - }, - "smtpServer" : { }, - "eventsEnabled" : false, - "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ ], - "adminEventsEnabled" : false, - "adminEventsDetailsEnabled" : false, - "identityProviders" : [ ], - "identityProviderMappers" : [ ], - "components" : { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { - "id" : "9d03a2d6-f3a5-4124-a483-aaf720df4453", - "name" : "Trusted Hosts", - "providerId" : "trusted-hosts", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "host-sending-registration-request-must-match" : [ "true" ], - "client-uris-must-match" : [ "true" ] - } - }, { - "id" : "b4fc16b0-465c-4b5e-97bb-20bbf42b8bb4", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - }, { - "id" : "df16ebbb-ffb8-48ab-9640-a687b1a49f27", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - }, { - "id" : "1f49d690-0011-430e-9f63-59d94fb6c126", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper" ] - } - }, { - "id" : "c2c2bdfc-d47a-4468-819b-0b0cb587cd04", - "name" : "Max Clients Limit", - "providerId" : "max-clients", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "max-clients" : [ "200" ] - } - }, { - "id" : "0ca14b74-68f5-49bf-9b8a-47ee10d0f6ec", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-address-mapper" ] - } - }, { - "id" : "b5d5ca65-e26d-422a-a404-debb2d72375d", - "name" : "Consent Required", - "providerId" : "consent-required", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - }, { - "id" : "8405f54d-1b84-4448-9659-46bb1bd82932", - "name" : "Full Scope Disabled", - "providerId" : "scope", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - } ], - "org.keycloak.keys.KeyProvider" : [ { - "id" : "1ed7555f-b58d-4b57-ba77-634d7b915a40", - "name" : "aes-generated", - "providerId" : "aes-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "fff7bfdb-bbb4-433c-893b-5c7b7b30dbc1" ], - "secret" : [ "bfxeMUAb1ZAd1sZC4LmxlQ" ], - "priority" : [ "100" ] - } - }, { - "id" : "2c8b618d-037f-4057-9697-677d89c5381a", - "name" : "rsa-enc-generated", - "providerId" : "rsa-enc-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEogIBAAKCAQEAyD8dRdrxIwVqCWw7xsagXXu99EBDRPsYQUT/tj3NWenagZbQ0e6V2bD5ZfLYU4dlZ9bEfXUHcnBtQd3s4ebcBpBFPIKqOlBBFVsn98csZzLgzVp4Mr6vbcVSKZmm5R+pVoYC6GR3TqzXGDe8G9qIBKSGvzkWqkOpeomTRe/NHeSKv6rsJLZnOnsXB5qwGtKYRHl97EuhEbo07AQ5myKmMXNRPPfLLVT5jBEblqtLIUk996tl8CN22u/QcxeNUh59LKGXZ3xtmFwJMMDoKz15wPy8uB6xpfGcPenUQiI76+Sn3lYavkh0CzchTKF9DYHEcKSbx/CY6AntYOppQPTYGwIDAQABAoIBAARz6nitx+h4xfC4hLuWDk5kaEFAxXtCywrfbFKeV0pVVAyYp/1yWg3gI/xSx/jHh2RraH6vsErACJ6oM3VshV52W9GZDgzPdCPFS+v/0ZkWyQrX9mUaHThzEz0OZSFcjdSnq/OzEU8ysnKrjSdsIl9Z3/eOU2Iy3xGAih7KJC2nFmZAc/9nrz5LPW66w2Y4boUmJcOrizUNRUn/Oghn6SRzvK7UHAmGtLFOo2W07zuFpbXIWXkDodHm2a4zfjHa6LJziRmRb/hcqTJxaeaqA3IQIYZ2dw8pzUsxx3tDDdjfBftbWjpM6yqBho7kZsbQsAKZi0bbq/sEvpJQB8UZ8ikCgYEA8qOiSUgwBHsK/HWiZRCDV5tBfA2zJ0Bv+lYs72y6OUR6npIkYy4gZFUdVx+rlFuu71LpBaanqi5YwD1Ldm7GlfWIZpovVP6zRhA8ViE3rfgURWwbWhwHypGBVuHWtyGZl9XROtDjLyOdCsobPBOTWn11KOqaAe+BysJW/wERWVkCgYEA00XkU1N1Sch+3iecNLvRI36RC3mtgV7M0ut5l1S0cKmmJslQ81biLGd3K0GUDMSBiZ2SDByK9bXCNu9tyJN6NGF29n6vOOjy8khoBTS3X80aRH1Bw1nB8U9PDSB7fPJBfaEjJGLLRcnNDS6elfG2mVhTtpnHstH7Ts4fjq9XmpMCgYAtBiI6GPQYEMD0Idv1hv/oRL39CAnDcdiVimIiN3nC4KskO5gW81s9YvHj1dOf3vdyH19wFgGsuZbsbTNQkbO15e7eoyO/UNfxW1fm35kWZh9U1n+o0+S6OQ/YEGYoa0q1+w4tLM/LUn90nhY5qqRAOWGBKy9Sxp++ARvli8wtWQKBgA0GGfUpB+nseiWnu3Fkwpe1jatvbMq01VuLOIujpRvs2Vk6v8rAaGDkX+xCtqWy12lsVTx55fcPpVFNoS7kKHxiJbs8RAD2G0PkQsVPYp59PklKj2tDdTky8mSUxAgHxxG/hTMRBAbhUcqmPRBxPhhl4YM4J59WYm+RNVDOblARAoGACpStAKUodYYEATFQJ8dpYY50Ql8DzdpORrUxlj8COSsyT+MIrm+7kLiTYoswuv6P+mi0OfYgZfOAco9Ukh5VEF/ccVCvw0N3jmcQjNy8qy31k57XCrJC9+Fc1bxhtTqIZ6rJvWN3GKVwcMNZJxTXW5oxx9H60b8w6lfV4ao/b90=" ], - "keyUse" : [ "ENC" ], - "certificate" : [ "MIICmTCCAYECBgGOMyFJbzANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVncmlzdDAeFw0yNDAzMTIxNDQ1MDNaFw0zNDAzMTIxNDQ2NDNaMBAxDjAMBgNVBAMMBWdyaXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyD8dRdrxIwVqCWw7xsagXXu99EBDRPsYQUT/tj3NWenagZbQ0e6V2bD5ZfLYU4dlZ9bEfXUHcnBtQd3s4ebcBpBFPIKqOlBBFVsn98csZzLgzVp4Mr6vbcVSKZmm5R+pVoYC6GR3TqzXGDe8G9qIBKSGvzkWqkOpeomTRe/NHeSKv6rsJLZnOnsXB5qwGtKYRHl97EuhEbo07AQ5myKmMXNRPPfLLVT5jBEblqtLIUk996tl8CN22u/QcxeNUh59LKGXZ3xtmFwJMMDoKz15wPy8uB6xpfGcPenUQiI76+Sn3lYavkh0CzchTKF9DYHEcKSbx/CY6AntYOppQPTYGwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCyyzkuukYOMjeEgBStMBcqyFZaH1tLfZOhWR9pi1hxKaNO67e2noNT9PykkGo9BpvbqzSjG3ep81Es6s1yTuCTyXCiRkLXJBoUiHlVlHh5Pv9UuGBSZu40r0E1EdO0GMxE18rOAZnEgGNyBOQsyhAWJ0mpHiyeF7gTDDiE/6Sc7n4OWUgdeLF3mIJpL+C5MXjsMrzW3tGXWEg1eDo6xBsZmmoF/pl26z3+rUJWBLgvbxnrR6huQ87xhyUcS0sFxAwEwp9J1L0dkdSlmq6vKdn5ru8LsdxXwZ8S5VaX/PWUetkql6UtoLFbCyiGfb41M4xaD5V9ZD8RkeErOBFrVqdC" ], - "priority" : [ "100" ], - "algorithm" : [ "RSA-OAEP" ] - } - }, { - "id" : "4f59d164-b073-4fe4-af43-0603c8aa15a6", - "name" : "hmac-generated", - "providerId" : "hmac-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "d3dbdb87-2b9a-4738-a877-38d17f01947c" ], - "secret" : [ "WWHAzayR3k7CNc1_asOnTDiJl9AGLQ1_tc0o3iUGlsDAQpf-qLVVcRTbvHmC_uOB9d6NBxHp_IMAZ3Zi2cxZ9w" ], - "priority" : [ "100" ], - "algorithm" : [ "HS256" ] - } - }, { - "id" : "bbdfa46d-09ae-4131-a33a-e6f99db1c1f3", - "name" : "rsa-generated", - "providerId" : "rsa-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEogIBAAKCAQEAzO9iV5YvEj1kpdG11iURKvQB1JN746q6aB05t5wwFFcKvOzp4n0iDbLG/2qxM2U8Aw5v5cC72uwWh6cOhICxSjG+Y+4xsA3zWbJ6xDysag0xn0Cs/mDPsOLb9cU+TN64s7cVl54gY9YpcSeBIJLkHhvgnwPoiEslHEPC1zfPWKGKDGbaThL4PSsvEcvcJYwhYgAHX/eO0/5F/VnBbTTVqHOjaDYMjYfCGDdHs37wS10muIEsrqoTC+MJfj+CNNgcKHfQXd/+0ae/Q+slw1A1EkEBs1oyU/zcbie3u5cA8JWsnvJgyjZh/0mbItTn1n5J8abwvgfffRqD0xg3KN+d7QIDAQABAoIBADDZpzGFpTbN154HPTcMoukAPSd0+IUufzyuKsHvwy42CWM7fgz1Exb81J6xygecTA/WcynrJVxsBnrTgYxoONqcvOuJLeLvkGCDQOxiIh8tgfSaMCJ65Uce7JvLJqygMpr0O3tmwAXMWRiV+BvRp/rdXk/JWLaUYwY3yMwQi6Zufbi7jIpR/v5lkA5jKDrvBwUngNZlLwVad2hU2ulwETlAlZvpoGTNTtC6Q3pmLwFPBzKvP4TewI93i0H4TqJgsSWo1NIJJta1hcLjfGi/PtrmSNbM8EBmh2nKqacY3ijGX9GrT83CZ0Bq/1pZyu8VkbqmbnthrMGILmlqQkDgfikCgYEA94mYGGFnsQGacbn8Ct4/MqcagdOAbai/3s70yWIa1EEXI7P9sjyLO9iF/Ha2JEeE7GslFomZIBsgLXON5eCyhjr5so2+/n0KUA2b2KgDDXvMnJcqUXXjM7KPYOkCIVm2Es4TZV3bqVkvPM0ZS7OGcOoW/UXuUFWna4Z8TsyECTMCgYEA0/Dw8/kU68k07nCRu5z8FEDvUkIyzgMBXWbDCyVe2nqiwLhUVkcxGcPrTQ/bOdE1MzpHG4z0ziGgPO44VfvdKYmQbe6ToaXbRZUBzS9MowNpF5TEbVD2GiBoVXuXDHaU/y+iwGl3g2jxLMp0w3vxLmcpi60Pfseq8Xfkkt3m/F8CgYBdIY5wtcz+Yp0J5rB2IlHiq84kRD/QginWGUUts1RmwSqEi0aK1Y6I8JjQeJVkpufSzykABrruwmXj09LyRwzDxdKGJCBUvRSxM72L0QJ9AzPjQlhwl4rou2iITII5q/f9sTzI6Xwohd5o4L2ApsWRG/GUTsgvv1oi8VE5kGao0wKBgFKBxLulpuBXlvSP/BvGdFfKI6CpRq/ueZSL0bhAFxoEjeFqoOJpmpLGM47vck+iwwwrTs1J5W9tpbyynFnUz/dAp2o0a2KNd7wx0t624CXBySK19nX8A6KOJS/KCjZ+32gsejZfmHge3WyrcCM919lRrdnDSHn5bvHL077dBfQPAoGAEhnuRHkMfR2FM8d9WnMkwifFgkc1bEiwJIVWeS2jtxbONvj37EvLJiG14UY81vUb8QjJVOFhj68fstU4nMGrnMBnYATbMMgW8R41gF6NHXgjvuTWbl+at41kKFDYJmZq0tgiFU3xQPEhn5C3ux+UIqzaO62WMAAX0FRivcVJnfs=" ], - "keyUse" : [ "SIG" ], - "certificate" : [ "MIICmTCCAYECBgGOMyFIszANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVncmlzdDAeFw0yNDAzMTIxNDQ1MDNaFw0zNDAzMTIxNDQ2NDNaMBAxDjAMBgNVBAMMBWdyaXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzO9iV5YvEj1kpdG11iURKvQB1JN746q6aB05t5wwFFcKvOzp4n0iDbLG/2qxM2U8Aw5v5cC72uwWh6cOhICxSjG+Y+4xsA3zWbJ6xDysag0xn0Cs/mDPsOLb9cU+TN64s7cVl54gY9YpcSeBIJLkHhvgnwPoiEslHEPC1zfPWKGKDGbaThL4PSsvEcvcJYwhYgAHX/eO0/5F/VnBbTTVqHOjaDYMjYfCGDdHs37wS10muIEsrqoTC+MJfj+CNNgcKHfQXd/+0ae/Q+slw1A1EkEBs1oyU/zcbie3u5cA8JWsnvJgyjZh/0mbItTn1n5J8abwvgfffRqD0xg3KN+d7QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBi3gzML02PkgsHF5fncFAO34wZ8+1Z4H/5Sa5em/Az1TFKolsCSP7DspS+tZ3+RyKUCQ8WvKtVjWOmanHM/5ybkSy3Aum6SVkCXxV3PAJ0Yc9hQfsG7CZ1QE+Qw1WclIx2pZ6tBppqeKJ4tvwTmnMInkY0kLSFynzHB7qSotZg+K6s3j8+gk4jAxdLzDJ3HLJ9iAXGMG7dnjJ6r/93HKt3QDB0vpM8tt91sxFZ6FzfUDez2FApKGxhQ+89C2/fx4s9C0WhyVHJSIzd4/k7+Bjw3L4HV2jU3TaYG3Fp4cn+KmTfRjYcJr+BkioV+/DYipfO5sV8YZ6SOZL8lPiQOerf" ], - "priority" : [ "100" ] - } - } ] - }, - "internationalizationEnabled" : false, - "supportedLocales" : [ ], - "authenticationFlows" : [ { - "id" : "7a21c238-64a0-46a0-b730-b45ef72ab542", - "alias" : "Account verification options", - "description" : "Method with which to verity the existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-email-verification", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Verify Existing Account by Re-authentication", - "userSetupAllowed" : false - } ] - }, { - "id" : "73e817b3-14e2-4e28-8d34-7acb27437639", - "alias" : "Browser - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "ad0538ac-76ce-44ad-b050-3a138c4574f7", - "alias" : "Direct Grant - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "087eac7a-6dd9-4af3-9d93-6005ef335fcc", - "alias" : "First broker login - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "06228470-26c1-4090-bd57-159b474de7c0", - "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", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-confirm-link", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Account verification options", - "userSetupAllowed" : false - } ] - }, { - "id" : "3b997775-3fc3-4cc2-b38b-59e9f3ddfc18", - "alias" : "Reset - Conditional OTP", - "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "2594a601-978d-4d10-b515-06af70e05799", - "alias" : "User creation or linking", - "description" : "Flow for the existing/non-existing user alternatives", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "create unique user config", - "authenticator" : "idp-create-user-if-unique", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Handle Existing Account", - "userSetupAllowed" : false - } ] - }, { - "id" : "e9ea5830-502b-448d-bdfd-5f161c79be6f", - "alias" : "Verify Existing Account by Re-authentication", - "description" : "Reauthentication of existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "First broker login - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "3498ba3d-f218-44bf-866c-04430cd4aff1", - "alias" : "browser", - "description" : "browser based authentication", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-cookie", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-spnego", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "identity-provider-redirector", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 25, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "forms", - "userSetupAllowed" : false - } ] - }, { - "id" : "3b2f9a6b-74be-48e2-bab5-8d3ac7319f25", - "alias" : "clients", - "description" : "Base authentication for clients", - "providerId" : "client-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "client-secret", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-secret-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-x509", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 40, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "d03c6c2a-bb0b-4276-8cc4-48293549cbea", - "alias" : "direct grant", - "description" : "OpenID Connect Resource Owner Grant", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "direct-grant-validate-username", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "Direct Grant - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "e1a34850-be66-4c5f-b9c4-cdda7a854bdb", - "alias" : "docker auth", - "description" : "Used by Docker clients to authenticate against the IDP", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "docker-http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "cf675a31-f86d-4c44-b47a-66aeafdbc623", - "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", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "review profile config", - "authenticator" : "idp-review-profile", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "User creation or linking", - "userSetupAllowed" : false - } ] - }, { - "id" : "f7fae163-25bf-4856-a627-833c4780165f", - "alias" : "forms", - "description" : "Username, password, otp and other auth forms.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Browser - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "a15f14af-f7f8-4581-9ef1-3474b7468a33", - "alias" : "registration", - "description" : "registration flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-page-form", - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : true, - "flowAlias" : "registration form", - "userSetupAllowed" : false - } ] - }, { - "id" : "d631be42-1abe-4030-b5d6-9ad5db146463", - "alias" : "registration form", - "description" : "registration form", - "providerId" : "form-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-user-creation", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-profile-action", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 40, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-password-action", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 50, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-recaptcha-action", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 60, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "f4c6f99b-3741-4ec2-8186-a9338f99c780", - "alias" : "reset credentials", - "description" : "Reset credentials for a user if they forgot their password or something", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "reset-credentials-choose-user", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-credential-email", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 40, - "autheticatorFlow" : true, - "flowAlias" : "Reset - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "1c642cec-e0d0-47de-a2d4-b70ad8ff75ec", - "alias" : "saml ecp", - "description" : "SAML ECP Profile Authentication Flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - } ], - "authenticatorConfig" : [ { - "id" : "dda99951-aa1b-4821-a4fa-a09ff62dd10d", - "alias" : "create unique user config", - "config" : { - "require.password.update.after.registration" : "false" - } - }, { - "id" : "5ddc4880-954f-4232-ae29-675d3ffd77a9", - "alias" : "review profile config", - "config" : { - "update.profile.on.first.login" : "missing" - } - } ], - "requiredActions" : [ { - "alias" : "CONFIGURE_TOTP", - "name" : "Configure OTP", - "providerId" : "CONFIGURE_TOTP", - "enabled" : true, - "defaultAction" : false, - "priority" : 10, - "config" : { } - }, { - "alias" : "TERMS_AND_CONDITIONS", - "name" : "Terms and Conditions", - "providerId" : "TERMS_AND_CONDITIONS", - "enabled" : false, - "defaultAction" : false, - "priority" : 20, - "config" : { } - }, { - "alias" : "UPDATE_PASSWORD", - "name" : "Update Password", - "providerId" : "UPDATE_PASSWORD", - "enabled" : true, - "defaultAction" : false, - "priority" : 30, - "config" : { } - }, { - "alias" : "UPDATE_PROFILE", - "name" : "Update Profile", - "providerId" : "UPDATE_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 40, - "config" : { } - }, { - "alias" : "VERIFY_EMAIL", - "name" : "Verify Email", - "providerId" : "VERIFY_EMAIL", - "enabled" : true, - "defaultAction" : false, - "priority" : 50, - "config" : { } - }, { - "alias" : "delete_account", - "name" : "Delete Account", - "providerId" : "delete_account", - "enabled" : false, - "defaultAction" : false, - "priority" : 60, - "config" : { } - }, { - "alias" : "webauthn-register", - "name" : "Webauthn Register", - "providerId" : "webauthn-register", - "enabled" : true, - "defaultAction" : false, - "priority" : 70, - "config" : { } - }, { - "alias" : "webauthn-register-passwordless", - "name" : "Webauthn Register Passwordless", - "providerId" : "webauthn-register-passwordless", - "enabled" : true, - "defaultAction" : false, - "priority" : 80, - "config" : { } - }, { - "alias" : "update_user_locale", - "name" : "Update User Locale", - "providerId" : "update_user_locale", - "enabled" : true, - "defaultAction" : false, - "priority" : 1000, - "config" : { } - } ], - "browserFlow" : "browser", - "registrationFlow" : "registration", - "directGrantFlow" : "direct grant", - "resetCredentialsFlow" : "reset credentials", - "clientAuthenticationFlow" : "clients", - "dockerAuthenticationFlow" : "docker auth", - "attributes" : { - "cibaBackchannelTokenDeliveryMode" : "poll", - "cibaExpiresIn" : "120", - "cibaAuthRequestedUserHint" : "login_hint", - "oauth2DeviceCodeLifespan" : "600", - "oauth2DevicePollingInterval" : "5", - "parRequestUriLifespan" : "60", - "cibaInterval" : "5", - "realmReusableOtpCode" : "false" - }, - "keycloakVersion" : "22.0.5", - "userManagedAccessAllowed" : false, - "clientProfiles" : { - "profiles" : [ ] - }, - "clientPolicies" : { - "policies" : [ ] - } -} diff --git a/.github/workflows/import_keycloak/grist-users-0.json b/.github/workflows/import_keycloak/grist-users-0.json deleted file mode 100644 index c4b362e9..00000000 --- a/.github/workflows/import_keycloak/grist-users-0.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "id" : "1564f73d-c385-4269-84da-34b40f494dea", - "createdTimestamp" : 1710254868534, - "username" : "keycloakuser", - "enabled" : true, - "totp" : false, - "emailVerified" : true, - "firstName" : "Keycloak", - "lastName" : "User", - "email" : "keycloakuser@example.com", - "credentials" : [ { - "id" : "3ceee294-209a-4187-aede-1dcfa2dac006", - "type" : "password", - "userLabel" : "Password: keycloakpassword", - "createdDate" : 1710254893700, - "secretData" : "{\"value\":\"kZZMgT2g89C+LFfigQt/qu5H9vs188wWgVK1KqnO12Q=\",\"salt\":\"ffAeQSmuJ7cGFE8rzN+f/g==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-grist" ], - "notBefore" : 0, - "groups" : [ ] -} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6ca1e46c..646e0461 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,6 @@ jobs: - ':nbrowser-^[M-O]:' - ':nbrowser-^[P-S]:' - ':nbrowser-^[^A-S]:' - - ':nbrowser:keycloak:' include: - tests: ':lint:python:client:common:smoke:' node-version: 18.x @@ -74,7 +73,7 @@ jobs: run: yarn run build:prod - name: Install chromedriver - if: contains(matrix.tests, ':nbrowser') || contains(matrix.tests, ':smoke:') + if: contains(matrix.tests, ':nbrowser-') || contains(matrix.tests, ':smoke:') run: ./node_modules/selenium-webdriver/bin/linux/selenium-manager --driver chromedriver - name: Run smoke test @@ -93,19 +92,6 @@ jobs: if: contains(matrix.tests, ':common:') run: yarn run test:common - - name: Setup Keycloak realm and client - uses: addnab/docker-run-action@v3 - if: contains(matrix.tests, ':keycloak:') - with: - image: quay.io/keycloak/keycloak - options: -v ${{ github.workspace }}:/workspace - run: | - set -eu -o pipefail - cd /opt/keycloak/bin - ./kcadm.sh config credentials --server http://keycloak:8080/ --realm=master --user=admin --password=admin - ./kcadm.sh create realms -f /workspace/.github/workflows/import_keycloak/grist-realm.json - ./kcadm.sh create users -r grist -f /workspace/.github/workflows/import_keycloak/grist-users-0.json - - name: Run server tests with minio and redis if: contains(matrix.tests, ':server-') run: | @@ -132,18 +118,6 @@ jobs: MOCHA_WEBDRIVER_LOGDIR: ${{ runner.temp }}/test-logs/webdriver TESTDIR: ${{ runner.temp }}/test-logs - - name: Run integration with Keycloak - if: contains(matrix.tests, ':nbrowser:keycloak:') - env: - GRIST_OIDC_IDP_ISSUER: "http://127.0.0.1:8080/realms/grist" - GRIST_OIDC_IDP_CLIENT_ID: "keycloak_clientid" - GRIST_OIDC_IDP_CLIENT_SECRET: "keycloak_secret" - GRIST_OIDC_IDP_SCOPES: "openid email profile" - run: | - mkdir -p $MOCHA_WEBDRIVER_LOGDIR - export GREP_TESTS="LoginWithOIDC" - MOCHA_WEBDRIVER_SKIP_CLEANUP=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:nbrowser - - name: Prepare for saving artifact if: failure() run: | @@ -187,21 +161,6 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - keycloak: - image: quay.io/keycloak/keycloak - ports: - - 8080:8080 - env: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - - # curl not being present in the keycloak container, use this trick: - # https://github.com/keycloak/keycloak/discussions/17319#discussioncomment-5192267 - options: >- - --health-cmd "echo > /dev/tcp/localhost/8080" - --health-interval 20s - --health-timeout 5s - --health-retries 20 candidate: needs: build_and_test diff --git a/test/nbrowser/LoginWithOIDC.ts b/test/nbrowser/LoginWithOIDC.ts deleted file mode 100644 index b8ba0a8b..00000000 --- a/test/nbrowser/LoginWithOIDC.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { assert } from 'chai'; -import { driver } from 'mocha-webdriver'; -import * as gu from 'test/nbrowser/gristUtils'; -import { server, setupTestSuite } from 'test/nbrowser/testUtils'; - -describe('IntegrationWithKeycloak', function() { - before(function() { - if (!process.env.GRIST_OIDC_SP_HOST) { - return this.skip(); - } - }); - describe('LoginWithOIDC', function() { - this.timeout(60000); - setupTestSuite(); - gu.withEnvironmentSnapshot({ - get 'GRIST_OIDC_SP_HOST'() { return server.getHost(); }, - 'GRIST_TEST_LOGIN': 0, - }); - - it('should login using OIDC', async () => { - await driver.get(`${server.getHost()}/o/docs/login`); - await driver.findWait('#kc-form-login', 10_000); - await driver.find('#username').sendKeys('keycloakuser'); - await driver.find('#password').sendKeys('keycloakpassword'); - await driver.find('#kc-login').click(); - - await driver.wait( - async () => { - const url = await driver.getCurrentUrl(); - return url.startsWith(server.getHost()); - }, - 20_000 - ); - await gu.openAccountMenu(); - assert.equal(await driver.find('.test-usermenu-name').getText(), 'Keycloak User'); - assert.equal(await driver.find('.test-usermenu-email').getText(), 'keycloakuser@example.com'); - await driver.find('.test-dm-log-out').click(); - await driver.findContentWait('.test-error-header', /Signed out/, 20_000, 'Should be signed out'); - }); - }); -}); - From 0115474fa69d9a567bf0d9486f85ed5d27abea69 Mon Sep 17 00:00:00 2001 From: fflorent Date: Tue, 26 Mar 2024 14:37:38 +0100 Subject: [PATCH 23/25] Add friendly page for login failure --- app/client/ui/errorPages.ts | 14 ++++++++++ app/server/lib/OIDCConfig.ts | 36 +++++++++++++++++++----- app/server/lib/sendAppPage.ts | 12 ++++---- test/server/lib/OIDCConfig.ts | 52 ++++++++++++++++++++++------------- 4 files changed, 82 insertions(+), 32 deletions(-) diff --git a/app/client/ui/errorPages.ts b/app/client/ui/errorPages.ts index b21ac106..10d4b8df 100644 --- a/app/client/ui/errorPages.ts +++ b/app/client/ui/errorPages.ts @@ -21,6 +21,7 @@ export function createErrPage(appModel: AppModel) { errPage === 'not-found' ? createNotFoundPage(appModel, errMessage) : errPage === 'access-denied' ? createForbiddenPage(appModel, errMessage) : errPage === 'account-deleted' ? createAccountDeletedPage(appModel) : + errPage === 'signin-failed' ? createSigninFailedPage(appModel, errMessage) : createOtherErrorPage(appModel, errMessage); } @@ -98,6 +99,19 @@ export function createNotFoundPage(appModel: AppModel, message?: string) { ]); } +export function createSigninFailedPage(appModel: AppModel, message?: string) { + document.title = t("Signin failed{{suffix}}", {suffix: getPageTitleSuffix(getGristConfig())}); + return pagePanelsError(appModel, t("Signin failed{{suffix}}", {suffix: ''}), [ + cssErrorText(message ?? + t("Failed to login.{{separator}}Please try again or contact the support.", { + separator: dom('br') + })), + cssButtonWrap(bigPrimaryButtonLink(t("Login again"), testId('error-primary-btn'), + urlState().setLinkUrl({login: 'login', params: {}}))), + cssButtonWrap(bigBasicButtonLink(t("Contact support"), {href: commonUrls.contactSupport})), + ]); +} + /** * Creates a generic error page with the given message. */ diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index eb155123..a1c6708c 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -70,6 +70,7 @@ import { RequestWithLogin } from './Authorizer'; import { UserProfile } from 'app/common/LoginSessionAPI'; import _ from 'lodash'; import { SessionObj } from './BrowserSession'; +import { SendAppPage } from './sendAppPage'; enum ENABLED_PROTECTIONS { NONCE, @@ -90,12 +91,21 @@ function formatTokenForLogs(token: TokenSet) { }).value(); } +const DEFAULT_USER_FRIENDLY_MESSAGE = + "Something went wrong while logging, please try again or contact your administrator if the problem persists"; + +class ErrorWithUserFriendlyMessage extends Error { + constructor(errMessage: string, public readonly userFriendlyMessage: string = DEFAULT_USER_FRIENDLY_MESSAGE) { + super(errMessage); + } +} + export class OIDCConfig { /** * Handy alias to create an OIDCConfig instance and initialize it. */ - public static async build(): Promise { - const config = new OIDCConfig(); + public static async build(sendAppPage: SendAppPage): Promise { + const config = new OIDCConfig(sendAppPage); await config.initOIDC(); return config; } @@ -110,8 +120,9 @@ export class OIDCConfig { private _enabledProtections: EnabledProtectionsString[] = []; private _acrValues?: string; - protected constructor() { - } + protected constructor( + private _sendAppPage: SendAppPage + ) {} public async initOIDC(): Promise { const section = appSettings.section('login').section('system').section('oidc'); @@ -195,7 +206,11 @@ export class OIDCConfig { log.debug("Got userinfo: %o", userInfo); if (!this._ignoreEmailVerified && userInfo.email_verified !== true) { - throw new Error(`OIDCConfig: email not verified for ${userInfo.email}`); + throw new ErrorWithUserFriendlyMessage( + `OIDCConfig: email not verified for ${userInfo.email}`, + "Your email is not verified according to the identity provider, please take the neccessary steps for that " + + "and log in again." + ); } const profile = this._makeUserProfileFromUserInfo(userInfo); @@ -221,7 +236,14 @@ export class OIDCConfig { // // Also session deletion must be done before sending the response. delete mreq.session.oidc; - res.status(500).send('OIDC callback failed.'); + await this._sendAppPage(req, res, { + path: 'error.html', + status: 500, + config: { + errPage: 'signin-failed', + errMessage: err.userFriendlyMessage + }, + }); } } @@ -358,7 +380,7 @@ export async function getOIDCLoginSystem(): Promise Promise; + /** * Send a simple template page, read from file at pagePath (relative to static/), with certain * placeholders replaced. */ -export function makeSendAppPage(opts: { - server: GristServer, staticDir: string, tag: string, testLogin?: boolean, - baseDomain?: string -}) { - const {server, staticDir, tag, testLogin} = opts; +export function makeSendAppPage({ server, staticDir, tag, testLogin, baseDomain }: { + server: GristServer, staticDir: string, tag: string, testLogin?: boolean, baseDomain?: string +}): SendAppPage { // If env var GRIST_INCLUDE_CUSTOM_SCRIPT_URL is set, load it in a