In previous post, we spoke about the migration of Refresh Token, where Token Exchange is playing a big role to have a seamless migration.
Keycloak is providing token exchange just by configuration, however, in our use case, we use custom identity provider due to our lack support to OpenID Connect.
In this article, I'll discuss an extension to our custom identity provider (OAuth 2.0) to support token exchange
.
Enable external exchange
In our OauthServiceIdentityProvider
, we override the supportsExternalExchange
to return true
.
public class OauthServiceIdentityProvider extends AbstractOAuth2IdentityProvider<OauthServiceIdentityProviderConfig>
{
...
@Override
protected boolean supportsExternalExchange()
{
return true;
}
...
}
Exchange implementation
To implement the exchange logic, override the method exchangeExternalImpl
.
In the below example, I added some validations then called the doGetFederatedIdentity(..)
to get the federation. Different than the doGetFederatedIdentity in Brokering non-OIDC OAuth 2.0 identities, I use useSessionContent
to decide from where we get the clientId
. In case of token exchange, we get the clientId from the token claim token.getOtherClaims().get(TOKEN_CLAIM_CLIENT_ID)
@Override
protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap < String, String > params) {
final String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
if (subjectToken == null) {
event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
}
String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
if (subjectTokenType == null) {
subjectTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE;
}
if (!OAuth2Constants.REFRESH_TOKEN_TYPE.equals(subjectTokenType)) {
event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " invalid");
event.error(Errors.INVALID_TOKEN_TYPE);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST);
}
final String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
if (requestedType != null && !requestedType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)) {
event.detail(Details.REASON, "requested_token_type unsupported");
event.error(Errors.INVALID_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid requested token type", Response.Status.BAD_REQUEST);
}
return doGetFederatedIdentity(subjectToken, false);
}
private BrokeredIdentityContext doGetFederatedIdentity(String accessToken, boolean useSessionContent) {
if (accessToken == null) {
throw new IdentityBrokerException("No token from server.");
}
JsonWebToken token;
try {
JWSInput jws = new JWSInput(accessToken);
token = jws.readJsonContent(JsonWebToken.class);
final String username = token.getSubject();
String clientId;
if (useSessionContent) {
clientId = session.getContext().getClient().getName();
} else {
clientId = String.valueOf(token.getOtherClaims().get(TOKEN_CLAIM_CLIENT_ID));
}
final String userId = String.valueOf(token.getOtherClaims().get(TOKEN_CLAIM_USER_ID));
final String userType = String.valueOf(token.getOtherClaims().get(TOKEN_CLAIM_USER_TYPE));
final BrokeredIdentityContext user = new BrokeredIdentityContext(userId);
user.setUsername(username);
user.setIdpConfig(getConfig());
user.setIdp(this);
user.setUserAttribute(TOKEN_CLAIM_USER_ID, userId);
user.getContextData().put(TOKEN_CLAIM_USER_TYPE, userType);
return user;
} catch (JWSInputException e) {
throw new IdentityBrokerException("Invalid token", e);
}
}
Add user attributes and mappings claims
Same as the authenticationFinished
method, for the token exchange, we need to override the exchangeExternalComplete
to enrich our user session with whatever needed add the required user attributes and session notes. Session notes can then be used later to map Keycloak token claims.
@Override
public void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap < String, String > params) {
final String value = (String) context.getContextData().get(TOKEN_CLAIM_USER_TYPE);
if (StringUtil.isNotBlank(value)) {
userSession.setNote(field, value);
}
String userId = context.getUserAttribute(TOKEN_CLAIM_USER_ID);
if (userId != null && !userId.isBlank()) {
userSession.getUser().setSingleAttribute(TOKEN_CLAIM_USER_ID, userId);
} else {
logger.warn("userId is null or blank");
}
super.exchangeExternalComplete(userSession, context, params);
}
By this article, I am done with our custom identity provider implementation to fully support OAuth 2.0 including token exchange.
Are you using Terraform to configure your Keycloak cluster? well, it is not easy to make it with Terraform when speaking about custom identity provider. In Custom Identity Providers in Keycloak with Terraform, I explained it in details to keep your platform Terraformed ;-)
I hope you find it useful.
Top comments (0)