mirror of
https://github.com/keycloak/keycloak.git
synced 2026-06-09 00:52:07 -04:00
Allow client_id as an audience in the JWT Authorization Grant and Client Assertions
Closes #45178 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
fccd46f7ba
commit
07b9b9656b
9 changed files with 102 additions and 12 deletions
|
|
@ -91,6 +91,9 @@ to forward are any non OpenID Connect/OAuth standard parameter or standard param
|
|||
|Allows client assertions to be re-used
|
||||
|By default, a client assertion can not be used multiple times. If the client is not able to retrieve a new client assertion for each request this option can be enabled to allow re-use of the same client assertion.
|
||||
|
||||
|Allows Client ID as audience for assertions
|
||||
|If enabled, the Client ID configured in the Identity Provider is the only valid audience for assertions used in Federated client authentication and in JWT Authorization Grants (Client Assertions and JWT Authorization Grant). The client ID is used instead of the token-url/issuer-url defined in the respective specifications. Note this behavior is not covered by any standard.
|
||||
|
||||
|===
|
||||
|
||||
You can import all this configuration data by providing a URL or file that points to OpenID Provider Metadata. If you connect to a {project_name} external IDP, you can import the IDP settings from `<root>{kc_realms_path}/{realm-name}/.well-known/openid-configuration`. This link is a JSON document describing metadata about the IDP.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ The Identity Provider (both types commented in the introduction) needs to also b
|
|||
* **Allowed clock skew**: Clock skew in seconds that is tolerated when validating identity provider tokens. Default value is zero.
|
||||
* **Limit access token expiration**: If enabled the access token lifespan will be limited to the expiration of the JWT assertion but only if the JWT assertion expiration is less than the calculated access token expiration.
|
||||
|
||||
NOTE: The OpenID Connect Identity Provider types have one extra configuration switch **Allows Client ID as audience for assertions**, placed in the **Advance settings** section. This option, when enabled, sets the the Client ID of the provider configuration as the only valid audience for assertions used in Federated client authentication and in JWT Authorization Grants. The client ID is used instead of the token-url/issuer-url defined in the respective specifications. This behavior is not covered by any standard.
|
||||
|
||||
.OIDC Identity Provider configuration for JWT Authorization Grant
|
||||
image::jwt-authorization-grant-oidc-provider-config.png[Identity Provider configuration for JWT Authorization Grant]
|
||||
|
||||
|
|
|
|||
|
|
@ -3287,6 +3287,8 @@ caseSensitiveOriginalUsername=Case-sensitive username
|
|||
caseSensitiveOriginalUsernameHelp=If enabled, the original username from the identity provider is kept as is when federating users. Otherwise, the username from the identity provider is lower-cased and might not match the original value if it is case-sensitive. This setting only affects the username associated with the federated identity as usernames in the server are always in lower-case.
|
||||
supportsClientAssertions=Supports client assertions
|
||||
supportsClientAssertionReuse=Allows client assertions to be re-used
|
||||
allowClientIdAsAudience=Allows Client ID as audience for assertions
|
||||
allowClientIdAsAudienceHelp=If enabled, the Client ID configured in the Identity Provider is the only valid audience for assertions used in Federated client authentication and in JWT Authorization Grants (Client Assertions and JWT Authorization Grant). The client ID is used instead of the token-url/issuer-url defined in the respective specifications. Note this behavior is not covered by any standard.
|
||||
organizationsExplain=Manage your organizations and members.
|
||||
emptyOrganizations=No organizations
|
||||
emptyOrganizationsInstructions=There is no organization yet. Please create an organization and manage it.
|
||||
|
|
|
|||
|
|
@ -125,12 +125,21 @@ export const AdvancedSettings = ({
|
|||
const isClientAuthFederatedEnabled = isFeatureEnabled(
|
||||
Feature.ClientAuthFederated,
|
||||
);
|
||||
const jwtAuthorizationGrant = isFeatureEnabled(Feature.JWTAuthorizationGrant);
|
||||
const transientUsers = useWatch({
|
||||
control,
|
||||
name: "config.doNotStoreUsers",
|
||||
defaultValue: "false",
|
||||
});
|
||||
const syncModeAvailable = transientUsers === "false";
|
||||
const jwtAuthorizationGrantEnabled = useWatch({
|
||||
control,
|
||||
name: "config.jwtAuthorizationGrantEnabled",
|
||||
});
|
||||
const supportsClientAssertions = useWatch({
|
||||
control,
|
||||
name: "config.supportsClientAssertions",
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{!isOIDC && !isSAML && !isOAuth2 && (
|
||||
|
|
@ -315,17 +324,29 @@ export const AdvancedSettings = ({
|
|||
label="caseSensitiveOriginalUsername"
|
||||
/>
|
||||
{isClientAuthFederatedEnabled && isOIDC && (
|
||||
<>
|
||||
<SwitchField
|
||||
field="config.supportsClientAssertions"
|
||||
label="supportsClientAssertions"
|
||||
/>
|
||||
<SwitchField
|
||||
field="config.supportsClientAssertions"
|
||||
label="supportsClientAssertions"
|
||||
/>
|
||||
)}
|
||||
{isClientAuthFederatedEnabled &&
|
||||
isOIDC &&
|
||||
supportsClientAssertions === "true" && (
|
||||
<SwitchField
|
||||
field="config.supportsClientAssertionReuse"
|
||||
label="supportsClientAssertionReuse"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
{isOIDC &&
|
||||
((isClientAuthFederatedEnabled &&
|
||||
supportsClientAssertions === "true") ||
|
||||
(jwtAuthorizationGrant &&
|
||||
jwtAuthorizationGrantEnabled === "true")) && (
|
||||
<SwitchField
|
||||
field="config.allowClientIdAsAudience"
|
||||
label="allowClientIdAsAudience"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,12 +12,15 @@ public class FederatedJWTClientValidator extends AbstractJWTClientValidator {
|
|||
private final int allowedClockSkew;
|
||||
private final boolean reusePermitted;
|
||||
private int maximumExpirationTime = 300;
|
||||
private final List<String> validAudiences;
|
||||
|
||||
public FederatedJWTClientValidator(ClientAuthenticationFlowContext context, SignatureValidator signatureValidator, String expectedTokenIssuer, int allowedClockSkew, boolean reusePermitted) throws Exception {
|
||||
public FederatedJWTClientValidator(ClientAuthenticationFlowContext context, SignatureValidator signatureValidator,
|
||||
String expectedTokenIssuer, int allowedClockSkew, boolean reusePermitted, String... validAudiences) throws Exception {
|
||||
super(context, signatureValidator, null);
|
||||
this.expectedTokenIssuer = expectedTokenIssuer;
|
||||
this.allowedClockSkew = allowedClockSkew;
|
||||
this.reusePermitted = reusePermitted;
|
||||
this.validAudiences = validAudiences == null ? Collections.emptyList() : List.of(validAudiences);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -27,7 +30,9 @@ public class FederatedJWTClientValidator extends AbstractJWTClientValidator {
|
|||
|
||||
@Override
|
||||
protected List<String> getExpectedAudiences() {
|
||||
return Collections.singletonList(Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName()));
|
||||
return validAudiences.isEmpty()
|
||||
? List.of(Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName()))
|
||||
: validAudiences;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1062,8 +1062,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
public boolean verifyClientAssertion(ClientAuthenticationFlowContext context) throws Exception {
|
||||
OIDCIdentityProviderConfig config = getConfig();
|
||||
|
||||
FederatedJWTClientValidator validator = new FederatedJWTClientValidator(context, v -> verifySignature(v.getJws()),
|
||||
config.getIssuer(), config.getAllowedClockSkew(), config.isSupportsClientAssertionReuse());
|
||||
FederatedJWTClientValidator validator = config.isAllowClientIdAsAudience() && config.getClientId() != null
|
||||
? new FederatedJWTClientValidator(context, v -> verifySignature(v.getJws()), config.getIssuer(),
|
||||
config.getAllowedClockSkew(), config.isSupportsClientAssertionReuse(), config.getClientId())
|
||||
: new FederatedJWTClientValidator(context, v -> verifySignature(v.getJws()), config.getIssuer(),
|
||||
config.getAllowedClockSkew(), config.isSupportsClientAssertionReuse());
|
||||
|
||||
if (!Profile.isFeatureEnabled(Profile.Feature.CLIENT_AUTH_FEDERATED)) {
|
||||
return false;
|
||||
|
|
@ -1104,7 +1107,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
|
||||
@Override
|
||||
public List<String> getAllowedAudienceForJWTGrant() {
|
||||
return new JWTAuthorizationGrantIdentityProvider(session, getConfig()).getAllowedAudienceForJWTGrant();
|
||||
return getConfig().isAllowClientIdAsAudience() && getConfig().getClientId() != null
|
||||
? List.of(getConfig().getClientId())
|
||||
: new JWTAuthorizationGrantIdentityProvider(session, getConfig()).getAllowedAudienceForJWTGrant();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig imp
|
|||
public static final String IS_ACCESS_TOKEN_JWT = "isAccessTokenJWT";
|
||||
public static final String SUPPORTS_CLIENT_ASSERTIONS = "supportsClientAssertions";
|
||||
public static final String SUPPORTS_CLIENT_ASSERTION_REUSE = "supportsClientAssertionReuse";
|
||||
public static final String ALLOW_CLIENT_ID_AS_AUDIENCE = "allowClientIdAsAudience";
|
||||
|
||||
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
|
||||
super(identityProviderModel);
|
||||
|
|
@ -153,6 +154,14 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig imp
|
|||
return Boolean.parseBoolean(getConfig().get(SUPPORTS_CLIENT_ASSERTION_REUSE));
|
||||
}
|
||||
|
||||
public boolean isAllowClientIdAsAudience() {
|
||||
return Boolean.parseBoolean(getConfig().getOrDefault(ALLOW_CLIENT_ID_AS_AUDIENCE, "false"));
|
||||
}
|
||||
|
||||
public void setAllowClientIdAsAudience(boolean allowClientIdAsAudience) {
|
||||
getConfig().put(ALLOW_CLIENT_ID_AS_AUDIENCE, String.valueOf(allowClientIdAsAudience));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(RealmModel realm) {
|
||||
super.validate(realm);
|
||||
|
|
|
|||
|
|
@ -96,11 +96,33 @@ public class BaseClientAuthTest extends AbstractBaseClientAuthTest {
|
|||
assertFailure(null, TOKEN_ISSUER, EXTERNAL_CLIENT_ID, jwt.getId(), "client_not_found", events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientIdAllowedAsAudience() {
|
||||
JsonWebToken jwt = createDefaultToken();
|
||||
jwt.audience("test-client");
|
||||
assertFailure("Invalid token audience", doClientGrant(jwt));
|
||||
assertFailure(INTERNAL_CLIENT_ID, TOKEN_ISSUER, EXTERNAL_CLIENT_ID, jwt.getId(), events.poll());
|
||||
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.ALLOW_CLIENT_ID_AS_AUDIENCE, Boolean.TRUE.toString());
|
||||
});
|
||||
|
||||
jwt = createDefaultToken();
|
||||
jwt.audience("test-client");
|
||||
assertSuccess(INTERNAL_CLIENT_ID, doClientGrant(jwt));
|
||||
assertSuccess(INTERNAL_CLIENT_ID, jwt.getId(), TOKEN_ISSUER, EXTERNAL_CLIENT_ID, events.poll());
|
||||
|
||||
jwt = createDefaultToken();
|
||||
assertFailure("Invalid token audience", doClientGrant(jwt));
|
||||
assertFailure(INTERNAL_CLIENT_ID, TOKEN_ISSUER, EXTERNAL_CLIENT_ID, jwt.getId(), events.poll());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OAuthIdentityProvider getIdentityProvider() {
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonWebToken createDefaultToken() {
|
||||
JsonWebToken token = new JsonWebToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
|
|
@ -120,6 +142,7 @@ public class BaseClientAuthTest extends AbstractBaseClientAuthTest {
|
|||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute("clientId", "test-client")
|
||||
.setAttribute("issuer", "http://127.0.0.1:8500")
|
||||
.setAttribute(OIDCIdentityProviderConfig.USE_JWKS_URL, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
|
||||
|
|
|
|||
|
|
@ -52,6 +52,25 @@ public class OIDCIdentityProviderJWTAuthorizationGrantTest extends AbstractJWTAu
|
|||
assertSuccess("test-app", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientIdAllowedAsAudience() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", "test-client", IDP_ISSUER));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.ALLOW_CLIENT_ID_AS_AUDIENCE, Boolean.TRUE.toString());
|
||||
});
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", "test-client", IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", response);
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantRealmConfig extends AbstractJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig {
|
||||
|
||||
@Override
|
||||
|
|
@ -61,6 +80,7 @@ public class OIDCIdentityProviderJWTAuthorizationGrantTest extends AbstractJWTAu
|
|||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute("clientId", "test-client")
|
||||
.setAttribute(IdentityProviderModel.ISSUER, IDP_ISSUER)
|
||||
.setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, Boolean.TRUE.toString())
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
|
||||
|
|
|
|||
Loading…
Reference in a new issue