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

Closes #33332

Signed-off-by: Venelin Cvetkov <venelin.tsvetkov@gmail.com>
(cherry picked from commit d388dc7936)
This commit is contained in:
Venelin Cvetkov
2025-03-20 12:42:12 +01:00
committed by Marek Posolda
parent a2deff172b
commit 4ae7d60784
5 changed files with 80 additions and 3 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
@@ -2797,6 +2798,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

@@ -863,7 +863,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

@@ -23,8 +23,6 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
import java.util.concurrent.TimeUnit;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
@@ -199,6 +197,67 @@ public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBase
}
}
@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);
});
OAuthClient.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName())
.clientId(brokerApp.getClientId())
.doGrantAccessTokenRequest(brokerApp.getSecret(), bc.getUserLogin(), bc.getUserPassword());
assertThat(tokenResponse.getIdToken(), notNullValue());
String idTokenString = tokenResponse.getIdToken();
oauth.realm(bc.providerRealmName());
String logoutUrl = oauth.getLogoutUrl()
.idTokenHint(idTokenString)
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build();
driver.navigate().to(logoutUrl);
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();
}
}
private static void setupRealm(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();
IdentityProviderModel idp = session.identityProviders().getByAlias(IDP_OIDC_ALIAS);