Thanks to visit codestin.com
Credit goes to github.com

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
16 changes: 14 additions & 2 deletions docs/documentation/release_notes/topics/26_2_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ For information on how to upgrade from the legacy token exchange used in previou

= Fine-grained admin permissions supported

This release introduces support for a new version of fine-grained admin permissions. Version 2 (V2) provides enhanced flexibility and control over administrative access within realms.
This release introduces support for a new version of fine-grained admin permissions. Version 2 (V2) provides enhanced flexibility and control over administrative access within realms.
With this feature, administrators can define permissions for administering users, groups, clients, and roles without relying on broad administrative roles. V2 offers the same level of access control over realm resources as the previous version, with plans to extend its capabilities in future versions. Some key points follow:

* *Centralized Admin Console Management* - New *Permissions* section was introduced to allow management from a single place without having to navigate to different places in the Admin Console.
* *Centralized Admin Console Management* - New *Permissions* section was introduced to allow management from a single place without having to navigate to different places in the Admin Console.
* *Improved manageability* - Administrators can more easily search and evaluate permissions when building a permission model for realm resources.
* *Resource-Specific and Global Permissions* – Permissions can be defined for individual resources (such as specific users or groups), or entire resource types (such as all users or all groups).
* *Explicit Operation Scoping* – Permissions are now independent, removing hidden dependencies between operations. Administrators must assign each scope explicitly, making it easier to see what is granted without needing prior knowledge of implicit relationships.
Expand Down Expand Up @@ -96,6 +96,18 @@ For more information, check the link:https://www.keycloak.org/server/management-
Introduced the ability to dynamically select authentication flows based on conditions such as requested scopes, ACR (Authentication Context Class Reference) and others.
This can be achieved using link:{adminguide_link}#_client_policies[Client Policies] by combining the new `AuthenticationFlowSelectorExecutor` with conditions like the new `ACRCondition`. For more details, see the link:{adminguide_link}#_client-policy-auth-flow[{adminguide_name}].

= JWT Client authentication aligned with the latest OIDC specification

The latest version of the link:https://openid.net/specs/openid-connect-core-1_0-36.html#rfc.section.9[OpenID Connect Core Specification] tightened the rules for
audience validation in JWT client assertions for the Client Authentication methods `private_key_jwt` and `client_secret_jwt` . {project_name} now enforces by default that there is single audience
in the JWT token used for client authentication.

For information on the changed audience validation in JWT Client authentication {project_name} versions, see the link:{upgradingguide_link}[{upgradingguide_name}].

ifeval::[{project_community}==true]
Many thanks to https://github.com/thomasdarimont[Thomas Darimont] for the contribution.
endif::[]

= Federated credentials are available now when fetching user credentials

Until now, querying user credentials using the User API will not return credentials managed by user storage providers and, as a consequence,
Expand Down
20 changes: 20 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_2_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ the `X-Forwarded-Port` header with the desired port.
The required JAR for the Oracle JDBC driver that needs to be explicitly added to the distribution has changed.
Instead of providing `ojdbc11` JAR, use `ojdbc17` JAR as stated in the https://www.keycloak.org/server/db#_installing_the_oracle_database_driver[Installing the Oracle Database driver] guide.

=== JWT Client authentication aligned with the latest OIDC specification

The latest draft version of the link:https://openid.net/specs/openid-connect-core-1_0-36.html#rfc.section.9[OpenID Connect core specification] changed the rules for
audience validation in JWT client assertions for the Client Authentication methods `private_key_jwt` and `client_secret_jwt`.

Previously, the `aud` claim of a JWT client assertion was loosely defined as `The Audience SHOULD be the URL of the Authorization Server's Token Endpoint`, which did not exclude the usage of other URLs.

The revised OIDC Core specification uses a stricter audience check: `The Audience value MUST be the OP's Issuer Identifier passed as a string, and not a single-element array.`.

We adapted the JWT client authentication authenticators of both `private_key_jwt` and `client_secret_jwt` to allow only a single audience in the token by default. For now, the audience can be
issuer, token endpoint, introspection endpoint or some other OAuth/OIDC endpoint, which is used by client JWT authentication. However since there is single audience allowed now, it means that it is not possible
to use other unrelated audience values, which is to make sure that JWT token is really only useful by the {project_name} for client authentication.

This strict audience check can be reverted to the previous more lenient check with a new option of OIDC login protocol SPI. It will be still allowed to use multiple audiences in JWT if server is started with the option:

`--spi-login-protocol-openid-connect-allow-multiple-audiences-for-jwt-client-authentication=true`

Note that this option might be removed in the future. Possibly in {project_name} 27. So it is highly recommended to update your clients to use single audience instead of using this option. It is also
recommended that your clients use the issuer URL for the audience when sending JWT for client authentication as that is going to be compatible with the future version of OIDC specification.

== Notable changes

Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,7 @@
package org.keycloak.authentication.authenticators.client;


import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.ws.rs.core.Response;

import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
Expand All @@ -42,13 +30,18 @@
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.protocol.oidc.grants.ciba.CibaGrantType;
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;

import java.security.PublicKey;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.keycloak.models.TokenManager.DEFAULT_VALIDATOR;

Expand Down Expand Up @@ -113,13 +106,7 @@ public void authenticateClient(ClientAuthenticationFlowContext context) {
throw new RuntimeException("Signature on JWT token failed validation");
}

// Allow both "issuer" or "token-endpoint" as audience
List<String> expectedAudiences = getExpectedAudiences(context, realm);

if (!token.hasAnyAudience(expectedAudiences)) {
throw new RuntimeException("Token audience doesn't match domain. Expected audiences are any of " + expectedAudiences
+ " but audience from token is '" + Arrays.asList(token.getAudience()) + "'");
}
validator.validateTokenAudience(context, realm, token);

validator.validateToken();
validator.validateTokenReuse();
Expand Down Expand Up @@ -208,16 +195,4 @@ public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
return Collections.emptySet();
}
}

private List<String> getExpectedAudiences(ClientAuthenticationFlowContext context, RealmModel realm) {
String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
String tokenIntrospectUrl = OIDCLoginProtocolService.tokenIntrospectionUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
String parEndpointUrl = ParEndpoint.parUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
List<String> expectedAudiences = new ArrayList<>(Arrays.asList(issuerUrl, tokenUrl, tokenIntrospectUrl, parEndpointUrl));
String backchannelAuthenticationUrl = CibaGrantType.authorizationUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
expectedAudiences.add(backchannelAuthenticationUrl);

return expectedAudiences;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.keycloak.authentication.authenticators.client;

import jakarta.ws.rs.core.Response;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.crypto.ClientSignatureVerifierProvider;
Expand All @@ -26,14 +27,10 @@
import org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;

import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -114,13 +111,7 @@ public void authenticateClient(ClientAuthenticationFlowContext context) {
}
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
// JWT contents and verification in client_secret_jwt is the same as in private_key_jwt

// Allow both "issuer" or "token-endpoint" as audience
String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
if (!token.hasAudience(issuerUrl) && !token.hasAudience(tokenUrl)) {
throw new RuntimeException("Token audience doesn't match domain. Realm issuer is '" + issuerUrl + "' but audience from token is '" + Arrays.asList(token.getAudience()).toString() + "'");
}
validator.validateTokenAudience(context, realm, token);

validator.validateToken();
validator.validateTokenReuse();
Expand Down Expand Up @@ -199,7 +190,6 @@ public Requirement[] getRequirementChoices() {
@Override
public String getHelpText() {
return "Validates client based on signed JWT issued by client and signed with the Client Secret";

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

package org.keycloak.authentication.authenticators.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
Expand All @@ -33,8 +37,15 @@
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.OIDCProviderConfig;
import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType;
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.Urls;

/**
* Common validation for JWT client authentication with private_key_jwt or with client_secret
Expand Down Expand Up @@ -247,6 +258,37 @@ public void validateTokenReuse() {
}
}

public void validateTokenAudience(ClientAuthenticationFlowContext context, RealmModel realm, JsonWebToken token) {
List<String> expectedAudiences = getExpectedAudiences(context, realm);
if (!token.hasAnyAudience(expectedAudiences)) {
throw new RuntimeException("Token audience doesn't match domain. Expected audiences are any of " + expectedAudiences
+ " but audience from token is '" + Arrays.asList(token.getAudience()) + "'");
}

if (!isAllowMultipleAudiencesForJwtClientAuthentication(context) && token.getAudience().length > 1) {
throw new RuntimeException("Multiple audiences not allowed in the JWT token for client authentication");
}
}

private boolean isAllowMultipleAudiencesForJwtClientAuthentication(ClientAuthenticationFlowContext context) {
OIDCLoginProtocol loginProtocol = (OIDCLoginProtocol) context.getSession().getProvider(LoginProtocol.class, OIDCLoginProtocol.LOGIN_PROTOCOL);
OIDCProviderConfig config = loginProtocol.getConfig();
return config.isAllowMultipleAudiencesForJwtClientAuthentication();
}

private List<String> getExpectedAudiences(ClientAuthenticationFlowContext context, RealmModel realm) {

String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
String tokenIntrospectUrl = OIDCLoginProtocolService.tokenIntrospectionUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
String parEndpointUrl = ParEndpoint.parUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
List<String> expectedAudiences = new ArrayList<>(Arrays.asList(issuerUrl, tokenUrl, tokenIntrospectUrl, parEndpointUrl));
String backchannelAuthenticationUrl = CibaGrantType.authorizationUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
expectedAudiences.add(backchannelAuthenticationUrl);

return expectedAudiences;
}

public ClientAuthenticationFlowContext getContext() {
return context;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,21 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
public static final String CONFIG_OIDC_REQ_PARAMS_MAX_OVERALL_SIZE = "add-req-params-max-overall-size";
public static final String CONFIG_OIDC_REQ_PARAMS_FAIL_FAST = "add-req-params-fail-fast";

/**
* @deprecated To be removed in Keycloak 27
*/
public static final String CONFIG_OIDC_ALLOW_MULTIPLE_AUDIENCES_FOR_JWT_CLIENT_AUTHENTICATION = "allow-multiple-audiences-for-jwt-client-authentication";

private OIDCProviderConfig providerConfig;

@Override
public void init(Config.Scope config) {
this.providerConfig = new OIDCProviderConfig(config);
if (this.providerConfig.isAllowMultipleAudiencesForJwtClientAuthentication()) {
logger.warnf("It is allowed to have multiple audiences for the JWT client authentication. This option is not recommended and will be removed in one of the future releases."
+ " It is recommended to update your OAuth/OIDC clients to rather use single audience in the JWT token used for the client authentication.");
}

initBuiltIns();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,26 @@ public class OIDCProviderConfig {
*/
private final int additionalReqParamsMaxOverallSize;

/**
* @deprecated to be removed in Keycloak 27
*/
public static final boolean DEFAULT_ALLOW_MULTIPLE_AUDIENCES_FOR_JWT_CLIENT_AUTHENTICATION = false;

/**
* Whether to allow multiple audiences for JWT client authentication
* @deprecated To be removed in Keycloak 27
*/
private final boolean allowMultipleAudiencesForJwtClientAuthentication;



public OIDCProviderConfig(Config.Scope config) {
this.additionalReqParamsMaxNumber = config.getInt(OIDCLoginProtocolFactory.CONFIG_OIDC_REQ_PARAMS_MAX_NUMBER, DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_NUMBER);
this.additionalReqParamsMaxSize = config.getInt(OIDCLoginProtocolFactory.CONFIG_OIDC_REQ_PARAMS_MAX_SIZE, DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_SIZE);
this.additionalReqParamsMaxOverallSize = config.getInt(OIDCLoginProtocolFactory.CONFIG_OIDC_REQ_PARAMS_MAX_OVERALL_SIZE, DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_OVERALL_SIZE);
this.additionalReqParamsFailFast = config.getBoolean(OIDCLoginProtocolFactory.CONFIG_OIDC_REQ_PARAMS_FAIL_FAST, DEFAULT_ADDITIONAL_REQ_PARAMS_FAIL_FAST);

this.allowMultipleAudiencesForJwtClientAuthentication = config.getBoolean(OIDCLoginProtocolFactory.CONFIG_OIDC_ALLOW_MULTIPLE_AUDIENCES_FOR_JWT_CLIENT_AUTHENTICATION, DEFAULT_ALLOW_MULTIPLE_AUDIENCES_FOR_JWT_CLIENT_AUTHENTICATION);
}

public int getAdditionalReqParamsMaxNumber() {
Expand All @@ -71,4 +85,8 @@ public boolean isAdditionalReqParamsFailFast() {
public int getAdditionalReqParamsMaxOverallSize() {
return additionalReqParamsMaxOverallSize;
}

public boolean isAllowMultipleAudiencesForJwtClientAuthentication() {
return allowMultipleAudiencesForJwtClientAuthentication;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,6 @@ public String createSignedRequestToken(String clientId, String realmInfoUrl) {
.rsa256(keyPair.getPrivate());
}
}

@Override
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl);
String tokenEndpointUrl = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(getAuthServerRoot())).build(REALM_NAME).toString();
jwt.audience(tokenEndpointUrl);
return jwt;
}

};
jwtProvider.setupKeyPair(keyPair);
jwtProvider.setTokenTimeout(10);
Expand Down
Loading
Loading