Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ protected String stripOauthParametersFromRedirect() {
KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(facade.getRequest().getURI())
.replaceQueryParam(OAuth2Constants.CODE, null)
.replaceQueryParam(OAuth2Constants.STATE, null)
.replaceQueryParam(OAuth2Constants.SESSION_STATE, null);
.replaceQueryParam(OAuth2Constants.SESSION_STATE, null)
.replaceQueryParam(OAuth2Constants.ISSUER, null);
return builder.buildAsString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ public static void error(int status, HttpEntity entity) throws HttpFailure, IOEx
protected static String stripOauthParametersFromRedirect(String uri) {
KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(uri)
.replaceQueryParam(OAuth2Constants.CODE, null)
.replaceQueryParam(OAuth2Constants.STATE, null);
.replaceQueryParam(OAuth2Constants.STATE, null)
.replaceQueryParam(OAuth2Constants.ISSUER, null);
return builder.buildAsString();
}

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/keycloak/OAuth2Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ public interface OAuth2Constants {

// https://openid.net/specs/openid-financial-api-jarm-ID1.html
String RESPONSE = "response";

// https://www.rfc-editor.org/rfc/rfc9207.html
String ISSUER = "iss";
}


Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("mtls_endpoint_aliases")
private MTLSEndpointAliases mtlsEndpointAliases;

@JsonProperty("authorization_response_iss_parameter_supported")
private Boolean authorizationResponseIssParameterSupported;

protected Map<String, Object> otherClaims = new HashMap<String, Object>();

public String getIssuer() {
Expand Down Expand Up @@ -624,4 +627,13 @@ public Boolean getFrontChannelLogoutSessionSupported() {
public Boolean getFrontChannelLogoutSupported() {
return frontChannelLogoutSupported;
}

public Boolean getAuthorizationResponseIssParameterSupported() {
return authorizationResponseIssParameterSupported;
}

public void setAuthorizationResponseIssParameterSupported(Boolean authorizationResponseIssParameterSupported) {
this.authorizationResponseIssParameterSupported = authorizationResponseIssParameterSupported;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
= Added iss parameter to OAuth 2.0/OpenID Connect Authentication Response

RFC 9207 OAuth 2.0 Authorization Server Issuer Identification specification adds the parameter `iss` in the OAuth 2.0/OpenID Connect Authentication Response for realizing secure authorization responses.

In past releases, we did not have this parameter, but now {project_name} adds this parameter by default, as required by the specification.

However, some OpenID Connect / OAuth2 adapters, and especially older {project_name} adapters, may have issues with this new parameter.

For example, the parameter will be always present in the browser URL after successful authentication to the client application.
In these cases, it may be useful to disable adding the `iss` parameter to the authentication response. This can be done
for the particular client in the {project_name} Admin console, in client details in the section with `OpenID Connect Compatibility Modes`,
described in <<_compatibility_with_older_adapters>>. Dedicated `Exclude Issuer From Authentication Response` switch exists,
which can be turned on to prevent adding the `iss` parameter to the authentication response.
4 changes: 4 additions & 0 deletions docs/documentation/upgrading/topics/keycloak/changes.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
== Migration Changes

=== Migrating to 23.0.0

include::changes-23_0_0.adoc[leveloffset=3]

=== Migrating to 22.0.0

include::changes-22_0_0.adoc[leveloffset=3]
Expand Down
1 change: 1 addition & 0 deletions js/apps/admin-ui/public/locales/en/clients-help.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"authorizationEncryptedResponseEnc": "JWA Algorithm used for content encryption in encrypting the authorization response when the response mode is jwt. This option is needed if you want encrypted authorization response. If left empty, the authorization response is just signed, but not encrypted.",
"openIdConnectCompatibilityModes": "This section is used to configure settings for backward compatibility with older OpenID Connect / OAuth 2 adaptors. It's useful especially if your client uses older version of Keycloak / RH-SSO adapter.",
"excludeSessionStateFromAuthenticationResponse": "If this is on, the parameter 'session_state' will not be included in OpenID Connect Authentication Response. It is useful if your client uses older OIDC / OAuth2 adapter, which does not support 'session_state' parameter.",
"excludeIssuerFromAuthenticationResponse": "If this is on, the parameter 'iss' will not be included in OpenID Connect Authentication Response. It is useful if your client uses older OIDC / OAuth2 adapter, which does not support 'session_state' parameter.",
"useRefreshTokens": "If this is on, a refresh_token will be created and added to the token response. If this is off then no refresh_token will be generated.",
"useRefreshTokenForClientCredentialsGrant": "If this is on, a refresh_token will be created and added to the token response if the client_credentials grant is used. The OAuth 2.0 RFC6749 Section 4.4.3 states that a refresh_token should not be generated when client_credentials grant is used. If this is off then no refresh_token will be generated and the associated user session will be removed.",
"useLowerCaseBearerType": "If this is on, token responses will be set the with the type \"bearer\" in lower-case. By default, the server sets the type as \"Bearer\" as defined by RFC6750.",
Expand Down
1 change: 1 addition & 0 deletions js/apps/admin-ui/public/locales/en/clients.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@
"authorizationEncryptedResponseEnc": "Authorization response encryption content encryption algorithm",
"openIdConnectCompatibilityModes": "Open ID Connect Compatibility Modes",
"excludeSessionStateFromAuthenticationResponse": "Exclude Session State From Authentication Response",
"excludeIssuerFromAuthenticationResponse": "Exclude Issuer From Authentication Response",
"useRefreshTokens": "Use refresh tokens",
"useRefreshTokenForClientCredentialsGrant": "Use refresh tokens for client credentials grant",
"useLowerCaseBearerType": "Use lower-case bearer type in token responses",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,35 @@ export const OpenIdConnectCompatibilityModes = ({
)}
/>
</FormGroup>
<FormGroup
label={t("excludeIssuerFromAuthenticationResponse")}
fieldId="excludeIssuerFromAuthenticationResponse"
hasNoPaddingTop
labelIcon={
<HelpItem
helpText={t("clients-help:excludeIssuerFromAuthenticationResponse")}
fieldLabelId="clients:excludeIssuerFromAuthenticationResponse"
/>
}
>
<Controller
name={convertAttributeNameToForm<FormFields>(
"attributes.exclude.issuer.from.auth.response",
)}
defaultValue=""
control={control}
render={({ field }) => (
<Switch
id="excludeIssuerFromAuthenticationResponse-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("excludeIssuerFromAuthenticationResponse")}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("useRefreshTokens")}
fieldId="useRefreshTokens"
Expand Down
6 changes: 3 additions & 3 deletions js/libs/keycloak-js/src/keycloak.js
Original file line number Diff line number Diff line change
Expand Up @@ -1077,13 +1077,13 @@ function Keycloak (config) {
var supportedParams;
switch (kc.flow) {
case 'standard':
supportedParams = ['code', 'state', 'session_state', 'kc_action_status'];
supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'iss'];
break;
case 'implicit':
supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status'];
supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'iss'];
break;
case 'hybrid':
supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status'];
supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'iss'];
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public final class OIDCConfigAttributes {
public static final String USE_JWKS_STRING = "use.jwks.string";

public static final String EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE = "exclude.session.state.from.auth.response";
public static final String EXCLUDE_ISSUER_FROM_AUTH_RESPONSE = "exclude.issuer.from.auth.response";

public static final String USE_MTLS_HOK_TOKEN = "tls.client.certificate.bound.access.tokens";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ public void setExcludeSessionStateFromAuthResponse(boolean excludeSessionStateFr
setAttribute(OIDCConfigAttributes.EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE, val);
}

public boolean isExcludeIssuerFromAuthResponse() {
String excludeIssuerFromAuthResponse = getAttribute(OIDCConfigAttributes.EXCLUDE_ISSUER_FROM_AUTH_RESPONSE);
return Boolean.parseBoolean(excludeIssuerFromAuthResponse);
}

public void setExcludeIssuerFromAuthResponse(boolean excludeIssuerFromAuthResponse) {
String val = String.valueOf(excludeIssuerFromAuthResponse);
setAttribute(OIDCConfigAttributes.EXCLUDE_ISSUER_FROM_AUTH_RESPONSE, val);
}

// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
public boolean isUseMtlsHokToken() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ public Response authenticated(AuthenticationSessionModel authSession, UserSessio
if (!clientConfig.isExcludeSessionStateFromAuthResponse()) {
redirectUri.addParam(OAuth2Constants.SESSION_STATE, userSession.getId());
}
if (!clientConfig.isExcludeIssuerFromAuthResponse()) {
redirectUri.addParam(OAuth2Constants.ISSUER, clientSession.getNote(OIDCLoginProtocol.ISSUER));
}

String nonce = authSession.getClientNote(OIDCLoginProtocol.NONCE_PARAM);
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, nonce);
Expand Down Expand Up @@ -278,6 +281,9 @@ public Response authenticated(AuthenticationSessionModel authSession, UserSessio
event.error(cpe.getError());
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, cpe.getError());
if (!clientConfig.isExcludeIssuerFromAuthResponse()) {
redirectUri.addParam(OAuth2Constants.ISSUER, clientSession.getNote(OIDCLoginProtocol.ISSUER));
}
return redirectUri.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ public Object getConfig() {
MTLSEndpointAliases mtlsEndpointAliases = getMtlsEndpointAliases(config);
config.setMtlsEndpointAliases(mtlsEndpointAliases);

config.setAuthorizationResponseIssParameterSupported(true);

config = checkConfigOverride(config);
return config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
Expand Down Expand Up @@ -300,6 +301,11 @@ private Response redirectErrorToClient(OIDCResponseMode responseMode, String err
errorResponseBuilder.addParam(OAuth2Constants.STATE, request.getState());
}

OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientModel(client);
if (!clientConfig.isExcludeIssuerFromAuthResponse()) {
errorResponseBuilder.addParam(OAuth2Constants.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
}

return errorResponseBuilder.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
"redirectUris" : [ "http://localhost:8080/hello-world-authz-service/*" ],
"baseUrl": "http://localhost:8080/hello-world-authz-service",
"adminUrl": "http://localhost:8080/hello-world-authz-service",
"directAccessGrantsEnabled" : true
"directAccessGrantsEnabled" : true,
"attributes" : {
"exclude.issuer.from.auth.response": "true"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@
"redirectUris": [
"/servlet-authz-app/*"
],
"secret": "secret"
"secret": "secret",
"attributes" : {
"exclude.issuer.from.auth.response": "true"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@
}
],
"scopes": []
},
"attributes" : {
"exclude.issuer.from.auth.response": "true"
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,8 @@ public static class AuthorizationEndpointResponse {
// Just during FAPI JARM response mode JWT
private String response;

private String issuer;

public AuthorizationEndpointResponse(OAuthClient client) {
boolean fragment;
if (client.responseMode == null || "jwt".equals(client.responseMode)) {
Expand Down Expand Up @@ -1830,6 +1832,7 @@ private void init(OAuthClient client, boolean fragment) {
tokenType = params.get(OAuth2Constants.TOKEN_TYPE);
expiresIn = params.get(OAuth2Constants.EXPIRES_IN);
response = params.get(OAuth2Constants.RESPONSE);
issuer = params.get(OAuth2Constants.ISSUER);
}

public boolean isRedirected() {
Expand Down Expand Up @@ -1875,6 +1878,9 @@ public String getExpiresIn() {
public String getResponse() {
return response;
}
public String getIssuer() {
return issuer;
}
}

public static class AuthenticationRequestAcknowledgement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken;
Expand Down Expand Up @@ -77,8 +78,11 @@
import jakarta.ws.rs.core.UriBuilder;
import java.io.File;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;

Expand Down Expand Up @@ -159,6 +163,11 @@ public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
servlet.getRedirectUris().add(uri + "/*");
servlet.setSecret("password");
servlet.setFullScopeAllowed(true);

Map<String, String> attributes = Optional.ofNullable(servlet.getAttributes()).orElse(new HashMap<>());
attributes.put(OIDCConfigAttributes.EXCLUDE_ISSUER_FROM_AUTH_RESPONSE, Boolean.TRUE.toString());
servlet.setAttributes(attributes);

realm.setClients(new LinkedList<>());
realm.getClients().add(servlet);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.ClientRepresentation;
Expand Down Expand Up @@ -61,6 +62,7 @@
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -138,6 +140,11 @@ public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
servlet.getRedirectUris().add(uri + "/*");
servlet.setSecret("password");
servlet.setFullScopeAllowed(true);

Map<String, String> attributes = Optional.ofNullable(servlet.getAttributes()).orElse(new HashMap<>());
attributes.put(OIDCConfigAttributes.EXCLUDE_ISSUER_FROM_AUTH_RESPONSE, Boolean.TRUE.toString());
servlet.setAttributes(attributes);

realm.setClients(new LinkedList<>());
realm.getClients().add(servlet);
testRealms.add(realm);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public void authorizationRequest() throws IOException {
Assert.assertNotNull(response.getCode());
assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", response.getState());
Assert.assertNull(response.getError());
assertEquals(oauth.AUTH_SERVER_ROOT + "/realms/test", response.getIssuer());

String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
}
Expand Down Expand Up @@ -160,6 +161,7 @@ public void authorizationRequestNoState() throws IOException {
Assert.assertNotNull(response.getCode());
Assert.assertNull(response.getState());
Assert.assertNull(response.getError());
assertEquals(oauth.AUTH_SERVER_ROOT + "/realms/test", response.getIssuer());

String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
}
Expand All @@ -173,6 +175,7 @@ public void authorizationRequestInvalidResponseType() throws IOException {
OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
assertTrue(errorResponse.isRedirected());
Assert.assertEquals(errorResponse.getError(), OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE);
Assert.assertEquals(oauth.AUTH_SERVER_ROOT + "/realms/test", errorResponse.getIssuer());

events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "tokenn").assertEvent();
}
Expand Down Expand Up @@ -284,6 +287,7 @@ public void authorizationRequestFragmentResponseModeNotKept() throws Exception {

Assert.assertNotNull(response.getCode());
Assert.assertNotNull(response.getState());
Assert.assertEquals(oauth.AUTH_SERVER_ROOT + "/realms/test", response.getIssuer());

currentUri = new URI(driver.getCurrentUrl());
Assert.assertNotNull(currentUri.getRawQuery());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ private void checkRedirectUri(String redirectUri, boolean expectValid, boolean c
.replaceQueryParam(OAuth2Constants.CODE, null)
.replaceQueryParam(OAuth2Constants.STATE, null)
.replaceQueryParam(OAuth2Constants.SESSION_STATE, null)
.replaceQueryParam(OAuth2Constants.ISSUER, null)
.build().toString();
if (browserUrlAfterRedirectFromKeycloak.endsWith("/")) browserUrlAfterRedirectFromKeycloak = browserUrlAfterRedirectFromKeycloak.substring(0, browserUrlAfterRedirectFromKeycloak.length() - 1);
if (Constants.INSTALLED_APP_URN.equals(redirectUri)) {
Expand Down
Loading