diff --git a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index 8ccc2ed6fff5..ea4e4bad2b5d 100755 --- a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -73,6 +73,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("subject_types_supported") private List subjectTypesSupported; + @JsonProperty("prompt_values_supported") + private List promptValuesSupported; + @JsonProperty("id_token_signing_alg_values_supported") private List idTokenSigningAlgValuesSupported; @@ -655,4 +658,11 @@ public void setAuthorizationResponseIssParameterSupported(Boolean authorizationR this.authorizationResponseIssParameterSupported = authorizationResponseIssParameterSupported; } + public List getPromptValuesSupported() { + return promptValuesSupported; + } + + public void setPromptValuesSupported(List promptValuesSupported) { + this.promptValuesSupported = promptValuesSupported; + } } diff --git a/docs/documentation/server_admin/topics/authentication/flows.adoc b/docs/documentation/server_admin/topics/authentication/flows.adoc index b06cf0ca4a77..e336a646e646 100644 --- a/docs/documentation/server_admin/topics/authentication/flows.adoc +++ b/docs/documentation/server_admin/topics/authentication/flows.adoc @@ -419,7 +419,8 @@ Sometimes it can be useful for the client application to directly redirect the u user clicks *Register* or *Forget password* on the normal login screen. Automatic redirect to the registration or reset-credentials screen can be done as follows: * When the client wants the user to be redirected directly to the registration, the OIDC client should replace the very last snippet from the OIDC login URL path (`/auth`) with `/registrations` . So the full URL -might be similar to the following: `https://keycloak.example.com/realms/your_realm/protocol/openid-connect/registrations`. +might be similar to the following: `https://keycloak.example.com/realms/your_realm/protocol/openid-connect/registrations`. As an alternative to using the `/registrations` endpoint, clients can use the OIDC +`prompt` parameter with `prompt=create` to redirect the user to the registration. * When the client wants a user to be redirected directly to the `Reset credentials` flow, the OIDC client should replace the very last snippet from the OIDC login URL path (`/auth`) with `/forgot-credentials` . diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index ffdeccfd9198..b74df93990f0 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -110,6 +110,7 @@ public class OIDCLoginProtocol implements LoginProtocol { public static final String PROMPT_VALUE_NONE = "none"; public static final String PROMPT_VALUE_LOGIN = "login"; public static final String PROMPT_VALUE_CONSENT = "consent"; + public static final String PROMPT_VALUE_CREATE = "create"; public static final String PROMPT_VALUE_SELECT_ACCOUNT = "select_account"; // Client authentication methods diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index 82ddcc02bddb..623f8a214da3 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -91,6 +91,9 @@ public class OIDCWellKnownProvider implements WellKnownProvider { // KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange public static final List DEFAULT_CODE_CHALLENGE_METHODS_SUPPORTED = list(OAuth2Constants.PKCE_METHOD_PLAIN, OAuth2Constants.PKCE_METHOD_S256); + // See: GH-10701, note that the supported prompt value "create" is only added if the realm supports registrations. + public static final List DEFAULT_PROMPT_VALUES_SUPPORTED = list(OIDCLoginProtocol.PROMPT_VALUE_NONE /*, OIDCLoginProtocol.PROMPT_VALUE_CREATE*/, OIDCLoginProtocol.PROMPT_VALUE_LOGIN, OIDCLoginProtocol.PROMPT_VALUE_CONSENT); + private final KeycloakSession session; private final Map openidConfigOverride; private final boolean includeClientScopes; @@ -167,6 +170,8 @@ public Object getConfig() { config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_SUPPORTED); config.setAcrValuesSupported(getAcrValuesSupported(realm)); + config.setPromptValuesSupported(getPromptValuesSupported(realm)); + config.setTokenEndpointAuthMethodsSupported(getClientAuthMethodsSupported()); config.setTokenEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false)); config.setIntrospectionEndpointAuthMethodsSupported(getClientAuthMethodsSupported()); @@ -235,6 +240,14 @@ public Object getConfig() { return config; } + protected List getPromptValuesSupported(RealmModel realm) { + List prompts = new ArrayList<>(DEFAULT_PROMPT_VALUES_SUPPORTED); + if (realm.isRegistrationAllowed()) { + prompts.add(OIDCLoginProtocol.PROMPT_VALUE_CREATE); + } + return prompts; + } + @Override public void close() { } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 5d0ac2ef051e..4252682533a5 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -204,6 +204,16 @@ private Response process(final MultivaluedMap params) { // So back button doesn't work CacheControlUtil.noBackButtonCacheControlHeader(session); + + // Add support for Initiating User Registration via OpenID Connect 1.0 via prompt=create + // see: https://openid.net/specs/openid-connect-prompt-create-1_0.html#section-4.1 + if (OIDCLoginProtocol.PROMPT_VALUE_CREATE.equals(params.getFirst(OAuth2Constants.PROMPT))) { + if (!Organizations.isRegistrationAllowed(session, realm)) { + throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED); + } + return buildRegister(); + } + switch (action) { case REGISTER: return buildRegister(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 91d424dfb1ff..948877803259 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -80,7 +80,9 @@ import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; +import org.keycloak.testsuite.util.RealmManager; import org.keycloak.util.JWKSUtils; import org.keycloak.util.JsonSerialization; import org.keycloak.testsuite.util.OAuthClient; @@ -124,6 +126,9 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest @Page protected AppPage appPage; + @Page + protected RegisterPage registerPage; + @Page protected LoginPage loginPage; @@ -406,6 +411,35 @@ public void promptLogin() { Assert.assertEquals(oldIdToken.getSessionState(), newIdToken.getSessionState()); } + // prompt=create + @Test + public void promptCreate() { + + // Assert registration page with prompt=login + driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=create"); + registerPage.assertCurrent(); + } + + // prompt=create + @Test + public void promptCreateShouldFailWhenRegistrationsAreDisabled() { + + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + Boolean registrationAllowed = realmRep.isRegistrationAllowed(); + realmRep.setRegistrationAllowed(false); + adminClient.realm("test").update(realmRep); + + // Assert registration page with prompt=login + try { + driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=create"); + errorPage.assertCurrent(); + assertTrue(errorPage.getError().contains("Registration not allowed")); + } finally { + realmRep.setRegistrationAllowed(registrationAllowed); + adminClient.realm("test").update(realmRep); + } + } + // prompt=consent @Test public void promptConsent() {