From acc3f8df91729fbeb8e9213944e565a501dc66c4 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 21 Jan 2025 09:50:14 +0100 Subject: [PATCH] Support for prompt=create in keycloak.js closes #36085 Signed-off-by: mposolda --- .../securing-apps/javascript-adapter.adoc | 13 +++ js/libs/keycloak-js/lib/keycloak.d.ts | 21 ++++ js/libs/keycloak-js/lib/keycloak.js | 109 +++++++++++++----- .../javascript/JavascriptAdapterTest.java | 4 +- 4 files changed, 115 insertions(+), 32 deletions(-) diff --git a/docs/guides/securing-apps/javascript-adapter.adoc b/docs/guides/securing-apps/javascript-adapter.adoc index 3ef17e640a8d..2ba4790d1109 100644 --- a/docs/guides/securing-apps/javascript-adapter.adoc +++ b/docs/guides/securing-apps/javascript-adapter.adoc @@ -372,6 +372,19 @@ adapter:: responseType:: Response type sent to {project_name} with login requests. This is determined based on the flow value used during initialization, but can be overridden by setting this value. +omitWellKnownConfigRequest:: + When the option is false, which is by default, adapter will send initial request to OIDC well-known endpoint. The response from this request can help to + retrieve some available capabilities of the server, which can allow adapter to optimize it's behaviour. If the option is true, adapter + will omit the request and the behaviour will stick to the default options. + +useDeprecatedRegisterEndpoint:: + When false and the `keycloak.register` is invoked, adapter will use the official OIDC way to send request to {project_name} registration. This means + the request to OIDC authentication endpoint with the parameter `prompt=create`. When true, the adapter will use deprecated register endpoint, which is not compatible + with OIDC specification and might be removed in the future versions of the {project_name} server. + The default value of this option is retrieved from the OIDC well-known endpoint request based on the fact if new OIDC way is supported by the server, + which is as long as OIDC well-known response contains "create" in the supported values of the prompt parameters. From the {project_name} 26.1, this will be false by default, + but for the older {project_name} server versions, this will be true by default as those support only deprecated registration endpoint. + === Methods *init(options)* diff --git a/js/libs/keycloak-js/lib/keycloak.d.ts b/js/libs/keycloak-js/lib/keycloak.d.ts index 507d31ecaf7d..e336e4ad4791 100644 --- a/js/libs/keycloak-js/lib/keycloak.d.ts +++ b/js/libs/keycloak-js/lib/keycloak.d.ts @@ -208,6 +208,27 @@ export interface KeycloakInitOptions { * HTTP method for calling the end_session endpoint. Defaults to 'GET'. */ logoutMethod?: 'GET' | 'POST'; + + /** + * By default, adapter will send initial request to OIDC well-known endpoint. The response from this request can help to + * retrieve some available capabilities of the server, which can allow adapter to optimize it's behaviour. If the option is true, adapter + * will omit the request and the behaviour will stick to the default options. + * @default false + */ + omitWellKnownConfigRequest?: boolean; + + /** + * When false and the keycloak.register is invoked, adapter will use the official OIDC way to send request to Keycloak registration. This means + * the request to OIDC authentication endpoint with the parameter prompt=create. When true, the adapter will use deprecated register endpoint, which is not compatible + * with OIDC specification and might be removed in the future versions of the Keycloak server. + * + * The default value of this option is retrieved from the OIDC well-known endpoint request based on the fact if new OIDC way is supported by the server, + * which is as long as OIDC well-known response contains "create" in the supported values of the prompt parameters. From the Keycloak 26.1, this will be false by default, but for the older + * Keycloak server versions, this will be true by default as those support only deprecated registration endpoint. + * + * @default false + */ + useDeprecatedRegisterEndpoint?: boolean; } export interface KeycloakLoginOptions { diff --git a/js/libs/keycloak-js/lib/keycloak.js b/js/libs/keycloak-js/lib/keycloak.js index 47225f4dd433..ce001f501e72 100755 --- a/js/libs/keycloak-js/lib/keycloak.js +++ b/js/libs/keycloak-js/lib/keycloak.js @@ -159,6 +159,18 @@ function Keycloak (config) { kc.enableLogging = false; } + if (typeof initOptions.omitWellKnownConfigRequest === 'boolean') { + kc.omitWellKnownConfigRequest = initOptions.omitWellKnownConfigRequest; + } else { + kc.omitWellKnownConfigRequest = false; + } + + if (typeof initOptions.useDeprecatedRegisterEndpoint === 'boolean') { + kc.useDeprecatedRegisterEndpoint = initOptions.useDeprecatedRegisterEndpoint; + } else { + kc.useDeprecatedRegisterEndpoint = false; + } + if (initOptions.logoutMethod === 'POST') { kc.logoutMethod = 'POST'; } else { @@ -394,17 +406,22 @@ function Keycloak (config) { loginOptions: options }; - if (options && options.prompt) { - callbackState.prompt = options.prompt; - } + var prompt = (options && options.prompt) ? options.prompt : null; var baseUrl; if (options && options.action == 'register') { baseUrl = kc.endpoints.register(); + if (!kc.useDeprecatedRegisterEndpoint) { + prompt = prompt ? prompt + " create" : "create"; + } } else { baseUrl = kc.endpoints.authorize(); } + if (prompt) { + callbackState.prompt = prompt; + } + var scope = options && options.scope || kc.scope; if (!scope) { // if scope is not set, default to "openid" @@ -425,8 +442,8 @@ function Keycloak (config) { url = url + '&nonce=' + encodeURIComponent(nonce); } - if (options && options.prompt) { - url += '&prompt=' + encodeURIComponent(options.prompt); + if (prompt) { + url += '&prompt=' + encodeURIComponent(prompt); } if (options && typeof options.maxAge === 'number') { @@ -817,7 +834,20 @@ function Keycloak (config) { configUrl = config; } - function setupOidcEndoints(oidcConfiguration) { + function processOidcConfiguration(oidcConfiguration) { + + function getRegistrationUrl(authzEndpointUrl) { + if (kc.useDeprecatedRegisterEndpoint) { + var realmUrl = getRealmUrl(); + if (!realmUrl) { + throw 'Redirection to "Register user" page not supported in standard OIDC mode'; + } + return realmUrl + '/protocol/openid-connect/registrations'; + } else { + return authzEndpointUrl; + } + } + if (! oidcConfiguration) { kc.endpoints = { authorize: function() { @@ -836,13 +866,18 @@ function Keycloak (config) { return getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html'; }, register: function() { - return getRealmUrl() + '/protocol/openid-connect/registrations'; + return getRegistrationUrl(getRealmUrl() + '/protocol/openid-connect/auth'); }, userinfo: function() { return getRealmUrl() + '/protocol/openid-connect/userinfo'; } }; } else { + if (!kc.useDeprecatedRegisterEndpoint) { + // Get from the OIDC configuration if it was not enforced by configuration to useDeprecatedRegisterEndpoint + kc.useDeprecatedRegisterEndpoint = !oidcConfiguration.prompt_values_supported || !oidcConfiguration.prompt_values_supported.includes("create"); + } + kc.endpoints = { authorize: function() { return oidcConfiguration.authorization_endpoint; @@ -863,7 +898,7 @@ function Keycloak (config) { return oidcConfiguration.check_session_iframe; }, register: function() { - throw 'Redirection to "Register user" page not supported in standard OIDC mode'; + return getRegistrationUrl(oidcConfiguration.authorization_endpoint); }, userinfo: function() { if (!oidcConfiguration.userinfo_endpoint) { @@ -873,6 +908,38 @@ function Keycloak (config) { } } } + + logInfo('[KEYCLOAK] Will use deprecated register endpoint: ' + kc.useDeprecatedRegisterEndpoint); + } + + function checkSendingOidcWellKnownRequest(oidcWellKnownUrl) { + if (kc.omitWellKnownConfigRequest) { + logInfo('[KEYCLOAK] Will omit request to OIDC well-known endpoint'); + if (!kc.authServerUrl || !kc.realm || !kc.clientId) { + throw "Requested to omit sending request to OIDC well-known endpoint, but some required parameters missing from OIDC configuration"; + } + processOidcConfiguration(null); + promise.setSuccess(); + } else { + logInfo('[KEYCLOAK] Sending request to OIDC well-known endpoint on URL ' + oidcWellKnownUrl); + var req = new XMLHttpRequest(); + req.open('GET', oidcWellKnownUrl, true); + req.setRequestHeader('Accept', 'application/json'); + + req.onreadystatechange = function () { + if (req.readyState == 4) { + if (req.status == 200 || fileLoaded(req)) { + var oidcProviderConfig = JSON.parse(req.responseText); + processOidcConfiguration(oidcProviderConfig); + promise.setSuccess(); + } else { + promise.setError({ error: "Incorrect response from the OIDC well-known endpoint."}); + } + } + }; + + req.send(); + } } if (configUrl) { @@ -888,8 +955,7 @@ function Keycloak (config) { kc.authServerUrl = config['auth-server-url']; kc.realm = config['realm']; kc.clientId = config['resource']; - setupOidcEndoints(null); - promise.setSuccess(); + checkSendingOidcWellKnownRequest(getRealmUrl() + '/.well-known/openid-configuration'); } else { promise.setError(); } @@ -904,8 +970,7 @@ function Keycloak (config) { if (!oidcProvider) { kc.authServerUrl = config.url; kc.realm = config.realm; - setupOidcEndoints(null); - promise.setSuccess(); + checkSendingOidcWellKnownRequest(getRealmUrl() + '/.well-known/openid-configuration'); } else { if (typeof oidcProvider === 'string') { var oidcProviderConfigUrl; @@ -914,25 +979,9 @@ function Keycloak (config) { } else { oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration'; } - var req = new XMLHttpRequest(); - req.open('GET', oidcProviderConfigUrl, true); - req.setRequestHeader('Accept', 'application/json'); - - req.onreadystatechange = function () { - if (req.readyState == 4) { - if (req.status == 200 || fileLoaded(req)) { - var oidcProviderConfig = JSON.parse(req.responseText); - setupOidcEndoints(oidcProviderConfig); - promise.setSuccess(); - } else { - promise.setError(); - } - } - }; - - req.send(); + checkSendingOidcWellKnownRequest(oidcProviderConfigUrl); } else { - setupOidcEndoints(oidcProvider); + processOidcConfiguration(oidcProvider); promise.setSuccess(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java index 17fef5fe6e99..15fcf8f59b43 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java @@ -935,7 +935,7 @@ public void checkInitWithInvalidRealm() { testExecutor .configure(keycloakConfig) - .init(initOptions, assertErrorResponse("Timeout when waiting for 3rd party check iframe message.")); + .init(initOptions, assertErrorResponse("Incorrect response from the OIDC well-known endpoint.")); } @@ -953,7 +953,7 @@ public void checkInitWithUnavailableAuthServer() { testExecutor .configure(keycloakConfig) - .init(initOptions, assertErrorResponse("Timeout when waiting for 3rd party check iframe message.")); + .init(initOptions, assertErrorResponse("Incorrect response from the OIDC well-known endpoint.")); }