Brokering OAuth2.0 is essential to our migration from Spring Security OAuth to Keycloak as Spring Security OAuth doesn't support OpenID Connect by default.
While Keycloak offers an extensive array of integrated brokering functionalities, such as OpenID Connect and SAML v2.0, it lacks support for OAuth 2.0 brokering.
Thanks to Keycloak Service Provider Interfaces (SPI), it is possible to write a custom SPI to broker OAuth2.0.
It worth to mention that, reading the source code of Keycloak and use it as a reference is good complement to the official documentation. For instance, It would be much harder to figure out the required implementation to make the Custom OAuth2.0 SPI work without debugging the OpenID Connect SPI provided by Keycloak.
In this article, I will also describe on how to add keycloak user attributes based on the remote identity token and also map remote identity token to Keycloak token.
Problem with built-in OIDC Provider
The main issue when OIDC is not supported by the remote identity provider is the missing of the idToken
. In this case, Keycloak fails to validate it.
Now you expect what is supposed to be considered to have the proper OAuth2.0 SPI. Yes, overcome the idToken
checks and all logic around it.
OAuth2.0 SPI
To implement an SPI, you need to implement its Provider Factory and Provider Interfaces. You also need to create a service configuration file.
First let us define our dependencies.
Dependencies
At least, you need below dependencies in your pom.xml
file.
<dependency>
<artifactId>keycloak-server-spi</artifactId>
<groupId>org.keycloak</groupId>
</dependency>
<dependency>
<artifactId>keycloak-server-spi-private</artifactId>
<groupId>org.keycloak</groupId>
</dependency>
<dependency>
<artifactId>keycloak-services</artifactId>
<groupId>org.keycloak</groupId>
</dependency>
SPI configuration file
For Keycloak server to identity the custom SPI during the starting up, add a new file named org.keycloak.broker.provider.IdentityProviderFactory
under src/main/resources/META-INF/services/
directory with below content.
com.example.keycloak.extensions.broker.oauth2.OauthServiceProviderFactory
Provider Configuration
Mainly, the Identity Provider Config for OAuth2.0 requires the Authorize
and Token
URLs. For sake of simplicity, I'll add both statically in the config but definitely in production make it configurable.
public class OauthServiceIdentityProviderConfig extends OAuth2IdentityProviderConfig
{
public static final String OAUTH_AUTHORIZE = "https://domain.com/oauth2/oauth/token";
public static final String OAUTH_TOKEN = "https://domain.com/oauth2/oauth/token";
public OauthServiceIdentityProviderConfig(IdentityProviderModel model)
{
super(model);
}
public OauthServiceIdentityProviderConfig()
{
}
public String getAuthPath()
{
return OAUTH_AUTHORIZE;
}
public String getTokenPath()
{
return OAUTH_TOKEN;
}
}
OAuth Service Identity Provider
Extend the built-in AbstractOAuth2IdentityProvider
to get all defaults from Keycloak OAuth2.0 provider built-in implementation.
public class OauthServiceIdentityProvider extends AbstractOAuth2IdentityProvider<OauthServiceIdentityProviderConfig>
{
public static final String DEFAULT_SCOPE = "myscope";
public static final String TOKEN_CLAIM_USER_ID = "userId";
public static final String TOKEN_CLAIM_USER_TYPE = "userType";
public OauthServiceIdentityProvider(KeycloakSession session, OauthServiceIdentityProviderConfig config)
{
super(session, config);
config.setAuthorizationUrl(config.getAuthPath());
config.setTokenUrl(config.getTokenPath());
config.setClientAuthMethod(CLIENT_SECRET_BASIC);
String defaultScope = config.getDefaultScope();
if (defaultScope == null || defaultScope.trim().isEmpty())
{
config.setDefaultScope(DEFAULT_SCOPE);
}
}
@Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken)
{
return doGetFederatedIdentity(accessToken, true);
}
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();
final String clientId = session.getContext().getClient().getName();
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);
}
}
/**
* This is required for the OAuth brokering
*
* @param authSession The AuthenticationSessionModel
* @param context The BrokeredIdentityContext
*/
@Override
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context)
{
final String value = (String) context.getContextData().get(TOKEN_CLAIM_USER_TYPE);
if (StringUtil.isNotBlank(value))
{
authSession.setUserSessionNote(field, value);
}
super.authenticationFinished(authSession, context);
}
@Override
protected String getDefaultScopes()
{
return DEFAULT_SCOPE;
}
}
Provider Factory
SPI requires a provider factory which mainly to instantiate the OauthServiceIdentityProvider
.
public class OauthServiceProviderFactory extends AbstractIdentityProviderFactory<OauthServiceIdentityProvider>
{
public static final String PROVIDER_ID = "OAUTH";
@Override
public String getName()
{
return "OAuth";
}
@Override
public OauthServiceIdentityProvider create(KeycloakSession session, IdentityProviderModel model)
{
return new OauthServiceIdentityProvider(session, new OauthServiceIdentityProviderConfig(model));
}
@Override
public OauthServiceIdentityProviderConfig createConfig()
{
return new OauthServiceIdentityProviderConfig();
}
@Override
public String getId()
{
return PROVIDER_ID;
}
}
Add attributes to Keycloak user
You can define a user attribute(s) extracted from the token claim. In the above example, the TOKEN_CLAIM_USER_ID
is an attribute that I extracted from the token and added as a user attributes in doGetFederatedIdentity
:
private BrokeredIdentityContext doGetFederatedIdentity(String accessToken, boolean useSessionContent)
{
....
JWSInput jws = new JWSInput(accessToken);
token = jws.readJsonContent(JsonWebToken.class);
final String userId = String.valueOf(token.getOtherClaims().get(TOKEN_CLAIM_USER_ID));
// define the BrokeredIdentityContext as user
user.setUserAttribute(TOKEN_CLAIM_USER_ID, userId);
....
}
Map remote token claims to Keycloak token
It is a common use case when there are claims in the remote identity token to map to Keycloak token claims. Especially when you stick to provide the same token claims as the remote identity.
To achieve that, we have to make the data available in brokered identity context data as:
private BrokeredIdentityContext doGetFederatedIdentity(String accessToken, boolean useSessionContent)
{
JWSInput jws = new JWSInput(accessToken);
token = jws.readJsonContent(JsonWebToken.class);
final String userType = String.valueOf(token.getOtherClaims().get(TOKEN_CLAIM_USER_TYPE));
// define the BrokeredIdentityContext as user
user.getContextData().put(TOKEN_CLAIM_USER_TYPE, userType);
...
}
Then override the authenticationFinished
method to add the data in the user session note:
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context)
{
final String value = (String) context.getContextData().get(TOKEN_CLAIM_USER_TYPE);
if (StringUtil.isNotBlank(value))
{
authSession.setUserSessionNote(field, value);
}
super.authenticationFinished(authSession, context);
}
Finally, use the scope mappers to map the Session Note as below:
Once you deploy the custom identity provider to the Keycloak cluster, you should find it listed in the identity providers
page in Keycloak Admin and ready to use!
Is Terraform your preference when it comes to manage platforms? I have a complete guide on configuring custom identity providers using Terraform for you :)
I hope you find it useful.
Top comments (0)