Add config param disableTypeClaimCheck in order to validate external tokens without typ claim

Closes #33332

Signed-off-by: Venelin Cvetkov <venelin.tsvetkov@gmail.com>
This commit is contained in:
Venelin Cvetkov
2025-03-20 12:42:12 +01:00
committed by GitHub
parent 45344ef65f
commit d388dc7936
5 changed files with 75 additions and 1 deletions

View File

@@ -315,6 +315,7 @@ revert=Revert
eventTypes.IDENTITY_PROVIDER_RETRIEVE_TOKEN.description=Identity provider retrieve token
dependentPermission=Dependent permission
disableNonce=Disable nonce
disableTypeClaimCheck=Disable type claim check
addAssociatedRolesSuccess=Associated roles have been added
groupDeleted_one=Group deleted
userHelp=Optionally select user, for whom the example access token will be generated. If you do not select a user, example access token will not be generated during evaluation.
@@ -2831,6 +2832,7 @@ eventTypes.OAUTH2_DEVICE_CODE_TO_TOKEN.name=OAuth2 device code to token
searchGroups=Search groups
trusted-hosts.tooltip=List of Hosts, which are trusted and are allowed to invoke Client Registration Service and/or be used as values of Client URIs. You can use hostnames or IP addresses. If you use star at the beginning (for example '*.example.com' ) then whole domain example.com will be trusted.
disableNonceHelp=Do not send the nonce parameter in the authentication request. The nonce parameter is sent and verified by default.
disableTypeClaimCheckHelp=Disables the validation of the `typ` claim of tokens received from the Identity Provider. If this is `off` the type claim is validated (default).
deleteClientProfile=Delete this client profile
none=None
type=Type

View File

@@ -57,6 +57,10 @@ export const ExtendedNonDiscoverySettings = () => {
/>
<SwitchField field="config.disableUserInfo" label="disableUserInfo" />
<SwitchField field="config.disableNonce" label="disableNonce" />
<SwitchField
field="config.disableTypeClaimCheck"
label="disableTypeClaimCheck"
/>
<TextField field="config.defaultScope" label="scopes" />
<FormGroupField label="prompt">
<Controller

View File

@@ -896,7 +896,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
try {
if (!isTokenTypeSupported(parsedToken)) {
if (!getConfig().isDisableTypeClaimCheck() && !isTokenTypeSupported(parsedToken)) {
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token type not supported", Response.Status.BAD_REQUEST);
}
boolean idTokenType = OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType);

View File

@@ -168,6 +168,18 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
}
}
public boolean isDisableTypeClaimCheck() {
return Boolean.parseBoolean(getConfig().get("disableTypeClaimCheck"));
}
public void setDisableTypeClaimCheck(boolean disableTypeClaimCheck) {
if (disableTypeClaimCheck) {
getConfig().put("disableTypeClaimCheck", Boolean.TRUE.toString());
} else {
getConfig().remove("disableTypeClaimCheck");
}
}
@Override
public void validate(RealmModel realm) {
super.validate(realm);

View File

@@ -223,6 +223,62 @@ public class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBaseBroker
}
}
@Test
public void testIgnoredTokenTypesValidationWhenExplicitlyConfigured() throws Exception {
testingClient.server(BrokerTestConstants.REALM_CONS_NAME).run(KcOidcBrokerTokenExchangeTest::setupRealm);
RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName());
ClientsResource clients = providerRealm.clients();
ClientRepresentation brokerApp = clients.findByClientId("brokerapp").get(0);
brokerApp.setDirectAccessGrantsEnabled(true);
ClientResource brokerAppResource = providerRealm.clients().get(brokerApp.getId());
brokerAppResource.update(brokerApp);
RealmResource consumerRealm = realmsResouce().realm(bc.consumerRealmName());
IdentityProviderResource identityProviderResource = consumerRealm.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation idpRep = identityProviderResource.toRepresentation();
idpRep.getConfig().put("disableUserInfo", "true");
idpRep.getConfig().put("disableTypeClaimCheck", "true");
identityProviderResource.update(idpRep);
getCleanup().addCleanup(() -> {
idpRep.getConfig().put("disableUserInfo", "false");
idpRep.getConfig().put("disableTypeClaimCheck", "false");
identityProviderResource.update(idpRep);
});
org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName()).client(brokerApp.getClientId(), brokerApp.getSecret()).doPasswordGrantRequest(bc.getUserLogin(), bc.getUserPassword());
assertThat(tokenResponse.getIdToken(), notNullValue());
String idTokenString = tokenResponse.getIdToken();
oauth.realm(bc.providerRealmName());
oauth.logoutForm().idTokenHint(idTokenString)
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT).open();
String logoutToken = testingClient.testApp().getBackChannelRawLogoutToken();
Assert.assertNotNull(logoutToken);
Client httpClient = AdminClientUtil.createResteasyClient();
try {
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
.path(bc.consumerRealmName())
.path("protocol/openid-connect/token");
// test user info validation.
try (Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(
"test-app", "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, logoutToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, bc.getIDPAlias())
.param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
))) {
assertThat(response.getStatus(), equalTo(Status.OK.getStatusCode()));
}
} finally {
httpClient.close();
}
}
@Test
public void testInternalExternalTokenExchangeSessionToken() throws Exception {
testingClient.server(bc.consumerRealmName()).run(KcOidcBrokerTokenExchangeTest::setupRealm);