SSO — WSO2 API Manager and Keycloak
Implement SSO and see how to debug the WSO2 API Manager.
Join the DZone community and get the full member experience.
Join For FreeIn this article, I am going to show how to implement Single Sign-On (SSO) for WSO2 API Manager using Keycloak as a Federated Identity Provider. Also, I will go for a deep-dive showing how to debug the WSO2 API Manager code to check what happens inside when it's configured with a third-party identity provider (i.e Keycloak in this example).
High-Level Architecture
This is what we are going to do in this tutorial.
Software Needed
Please download the following software:
WSO2 API Manager (2.6.0)
Keycloak (6.0.1)
Note: We are not going to use WSO2 Identity Server as a middleman for this tutorial. The article is based on pure WSO2 API Manager 2.6.0 (all in one).
Before We Start
The tutorial will be divided into two parts. In the first part, I will explain how to install and configure Keycloak. In the second part, I will explain how to install and configure the WSO2 API Manager.
Before we start, let's modify the /etc/hosts file and add two hosts.
# APIM Test
127.0.0.1 apim.wso2.com
# Keycloak
127.0.0.1 idp.keycloak.com
Keycloak Installation and Configuration
Installation
The installation of the Keycloak is quite straightforward. Download the zip version of Keycloak (6.0.1) and unzip it in your preferred directory.
Keystore Creation
We need to create a Keystore with the hostname we created for the Keycloak. Browse to the following location.
$ keycloak-6.0.1/standalone/configuration
There, you can see an existing application.keystore. Delete it and create a new one. Follow the steps below.
$ keytool -genkey -alias server -keyalg RSA -keysize 2048 -validity 3650 -keystore application.keystore -dname "CN=*.keycloak.com" -storepass password -keypass password -noprompt
Export the public certificate from the Keystore.
$ keytool -export -alias server -file server.crt -keystore application.keystore -storepass password -noprompt
We are going to import this server.crt in the client-truststore.jks of the WSO2 API Manager later.
Execution
Browse to the bin directory and execute the following command.
$ sh standalone.sh
When executed, as shown above, it will use its default ports (e.g 8080, 8443). If you want to change the default ports, use the port offset as shown below.
$ sh standalone.sh -Djboss.socket.binding.port-offset=1
The ports will be shifted to 8081, 8444, etc. Once the server is up and running, access this URL.
Note: This is the first time it will ask you to create an admin user. For this tutorial, I have created the user admin with password admin.
Client Configuration
Once logged in with the admin user created in the previous step you can see a page like this. We are going to use the default master realm for this tutorial, but, feel free to create your own custom realm.
Let's create an OpenID-connect client as shown in the below screen-shot.
After creating the client let's configure it as shown below.
Now click the Credentials tab and you can see the client-secret.
At this point, we have configured our openid-connect client named wso2apim.
Let's Play
Let's check on what we have configured. Execute the following URL.
It will return you all the necessary URLs (as shown below) we will need later for configuring a Federated IDP in WSO2 API Manager.
{
"issuer": "http://idp.keycloak.com:8081/auth/realms/master",
"authorization_endpoint": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/auth",
"token_endpoint": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/token",
"token_introspection_endpoint": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/userinfo",
"end_session_endpoint": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/logout",
"jwks_uri": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/certs",
"check_session_iframe": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials"
],
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"subject_types_supported": [
"public",
"pairwise"
],
"id_token_signing_alg_values_supported": [
"PS384",
"ES384",
"RS384",
"HS256",
"HS512",
"ES256",
"RS256",
"HS384",
"ES512",
"PS256",
"PS512",
"RS512"
],
"userinfo_signing_alg_values_supported": [
"PS384",
"ES384",
"RS384",
"HS256",
"HS512",
"ES256",
"RS256",
"HS384",
"ES512",
"PS256",
"PS512",
"RS512",
"none"
],
"request_object_signing_alg_values_supported": [
"PS384",
"ES384",
"RS384",
"ES256",
"RS256",
"ES512",
"PS256",
"PS512",
"RS512",
"none"
],
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"registration_endpoint": "http://idp.keycloak.com:8081/auth/realms/master/clients-registrations/openid-connect",
"token_endpoint_auth_methods_supported": [
"private_key_jwt",
"client_secret_basic",
"client_secret_post",
"client_secret_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256"
],
"claims_supported": [
"aud",
"sub",
"iss",
"auth_time",
"name",
"given_name",
"family_name",
"preferred_username",
"email"
],
"claim_types_supported": [
"normal"
],
"claims_parameter_supported": false,
"scopes_supported": [
"openid",
"address",
"email",
"microprofile-jwt",
"offline_access",
"phone",
"profile",
"roles",
"web-origins"
],
"request_parameter_supported": true,
"request_uri_parameter_supported": true,
"code_challenge_methods_supported": [
"plain",
"S256"
],
"tls_client_certificate_bound_access_tokens": true,
"introspection_endpoint": "http://idp.keycloak.com:8081/auth/realms/master/protocol/openid-connect/token/introspect"
}
Pretty cool! Now, let's check the claims of the admin user when called through a /token URL. Here is the CURL command for it.
$ curl -k -X POST \
https://idp.keycloak.com:8444/auth/realms/master/protocol/openid-connect/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'cache-control: no-cache' \
-d 'username=admin&password=admin&grant_type=password&client_id=wso2apim&client_secret=f7e77e99-c283-4bdb-9030-9a836c66111b'
Important parameters:
username | admin |
password | admin |
grant_type | password |
client_id | wso2apim |
client_secret | The secret we saw in the Credentials tab of the client wso2apim. |
The response will be something like this:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ5cXlsRWwwcndLajR0Mm1vR3JwdG5lVHNPS3VWd05VbWw0NWc2Yms3LXFBIn0.eyJqdGkiOiI4MmZiZjc5NS0yYmQ0LTQ4NDYtOGJlMS1lZGE1NWVmOWM3NWYiLCJleHAiOjE1NjQ5Mjk1OTcsIm5iZiI6MCwiaWF0IjoxNTY0OTI5NTM3LCJpc3MiOiJodHRwczovL2lkcC5rZXljbG9hay5jb206ODQ0NC9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOlsibWFzdGVyLXJlYWxtIiwiYWNjb3VudCJdLCJzdWIiOiI0ODIyODJhNS00NjdmLTQ3OWUtOWQ0MC1jMGZhZGJjYjM2YmYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ3c28yYXBpbSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImNhN2U3YjY1LTYyMmMtNGJjNC04MGRiLTM0MTYxNDEyZTBiNiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiY3JlYXRlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.OsEICf4ORcyryWhUSxjMLG8FpTKCYPIqn04qieoyl3FxJiN-SkHVNLQddJbgfYQ71z1LLkfQJaa4TlsrVabtqMu4Uo9ENM8qD1nh_J5DF967SEnClTfsgahojNFSamUdNJRMiSSGkCQplqdDDs1_24VNa9OL3c0R-MeNMSpsJ2JGdF1AbcoUZ5y9Fr26cEIzRKNqi4qBJvtu8v15GZF64A5efDYDAA6juEcIm32UYaXP6xgWHY0jC11CXSwK-204dUPCW6tCxcFyuBxFvLI-Y8b03XWcBPhQtJSL3DqetkAwKi2frHJxmhhxVtApDU-YHV7QOj-lgEE2S3LsLqP3FQ",
"expires_in": 60,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkZDEzMGE3NC1iNDA1LTQ3NjUtOTc2Ni05ZjNjYjY1ZTJiZGUifQ.eyJqdGkiOiI5ZTU5NTdlMy1mMmFhLTQxOGYtOTM0OS02YjAyOTBmMTBhZDEiLCJleHAiOjE1NjQ5MzEzMzcsIm5iZiI6MCwiaWF0IjoxNTY0OTI5NTM3LCJpc3MiOiJodHRwczovL2lkcC5rZXljbG9hay5jb206ODQ0NC9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJodHRwczovL2lkcC5rZXljbG9hay5jb206ODQ0NC9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiI0ODIyODJhNS00NjdmLTQ3OWUtOWQ0MC1jMGZhZGJjYjM2YmYiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoid3NvMmFwaW0iLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiJjYTdlN2I2NS02MjJjLTRiYzQtODBkYi0zNDE2MTQxMmUwYjYiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiY3JlYXRlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUifQ.pv25UVfaDuXaQSNGmNXkdgawJ__K6RdAE4P4lrFtZ8k",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "ca7e7b65-622c-4bc4-80db-34161412e0b6",
"scope": "email profile"
}
Now, copy this access_token and decode it using https://jwt.io/. You will see something like this,
{
"jti": "82fbf795-2bd4-4846-8be1-eda55ef9c75f",
"exp": 1564929597,
"nbf": 0,
"iat": 1564929537,
"iss": "https://idp.keycloak.com:8444/auth/realms/master",
"aud": [
"master-realm",
"account"
],
"sub": "482282a5-467f-479e-9d40-c0fadbcb36bf",
"typ": "Bearer",
"azp": "wso2apim",
"auth_time": 0,
"session_state": "ca7e7b65-622c-4bc4-80db-34161412e0b6",
"acr": "1",
"realm_access": {
"roles": [
"create-realm",
"offline_access",
"admin",
"uma_authorization"
]
},
"resource_access": {
"master-realm": {
"roles": [
"view-realm",
"view-identity-providers",
"manage-identity-providers",
"impersonation",
"create-client",
"manage-users",
"query-realms",
"view-authorization",
"query-clients",
"query-users",
"manage-events",
"manage-realm",
"view-events",
"view-users",
"view-clients",
"manage-authorization",
"manage-clients",
"query-groups"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"email_verified": false,
"preferred_username": "admin"
}
Note the sub claim. It's something autogenerated and contains special characters. At the time of JIT provisioning, WSO2 API Manager will try to insert it in the user database table and throw an exception because it does not accept special characters.
To resolve this problem, we will need to use the Mappers facility of Keycloak as shown below.
Create a Mapper called userNameInSub (can be any name) and choose Mapper Type equal to Script Mapper. Add the line as marked in the screen-shot below.
Now, execute the same CURL command, and you should see admin (username) in the sub claim.
Role Creation
Let's create a role called subscriber in the wso2apim client.
Now, we will need this role claim in the access_token. To do that we will create another Mapper (SubscriberRoleMapper) as shown below.
Create User and Assign Role
Let's create some demo users.
The most important thing is to do Role Mappings as shown above. We are adding the subscriber role (defined in the client wso2apim) to the user kc_agogoi.
Verify The User
Let's verify the user using the CURL command.
$ curl -k -X POST \
https://idp.keycloak.com:8444/auth/realms/master/protocol/openid-connect/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'cache-control: no-cache' \
-d 'username=kc_agogoi&password=123456&grant_type=password&client_id=wso2apim&client_secret=f7e77e99-c283-4bdb-9030-9a836c66111b'
Important parameters in the request.
username | kc_agogoi |
password | 123456 |
grant_type | password |
client_id | wso2apim |
client_secret | The secret we saw in the Credentials tab of the client wso2apim. |
Here is a sample response (decoded),
{
"jti": "eabae823-f3f1-4c13-a687-bd837c0cf952",
"exp": 1564932485,
"nbf": 0,
"iat": 1564932425,
"iss": "https://idp.keycloak.com:8444/auth/realms/master",
"aud": "account",
"sub": "kc_agogoi",
"typ": "Bearer",
"azp": "wso2apim",
"auth_time": 0,
"session_state": "dc40d2f1-6190-49c8-a0f7-3c394add8f24",
"acr": "1",
"resource_access": {
"wso2apim": {
"roles": [
"subscriber"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"email_verified": false,
"role": [
"subscriber"
],
"name": "Anupam Gogoi",
"preferred_username": "kc_agogoi",
"given_name": "Anupam",
"family_name": "Gogoi"
}
Check the sub and role claim. We are good to go now for the next steps.
WSO2 API Manager Configuration
API Manager configuration is crucial for the SSO to work. In the section above, we have configured an OpenID-connect client in Keycloak. Now, we will have to configure a Federated Identity Provider in WSO2 API Manager that can authenticate with the OpenID-connect client (wso2apim) in Keycloak.
So, first, let's check which Federated Authenticators are available in the WSO2 API Manager (2.6.0) product.
Check that we have only SAML federated authenticator available by default. And that's the main reason why you should use the WSO2 Identity Server that comes bundled with all the necessary authenticators.
But for this tutorial, I am not using WSO2 IS. So, how will I achieve the goal?
Well, there is a workaround. If you have WSO2 IS (5.7.0) downloaded in your machine search for the following jar file,
$ wso2is-5.7.0/repository/components/dropins/org.wso2.carbon.identity.application.authenticator.oidc-5.1.23.jar
Copy it and paste it in the dropins folder of the WSO2 API Manager.
$ wso2am-2.6.0/repository/components/dropins/
Now, restart the API Manager and you should see the OAuth2/OpenID connector.
Configure Truststore
Before proceeding, let's add the public certificate (server.crt) of Keycloak to the client-truststore.jks of API Manager.
$ keytool -import -alias keycloak -file server.crt -keystore client-truststore.jks -storepass wso2carbon
Then, restart the server.
Configure Federated IDP
Create a federated IDP named APIM_KEYCLOAK, as shown below.
Make a note on the Claim Configuration and Role Configuration. We are mapping the role claim from Keycloak to the Local claim i.e WSO2 API Manager. Also, we are mapping the subscriber role from Keycloak side to Internal/subscriber role of WSO2 API Manager.
Now, make the OpenID authenticator configuration as shown below,
The information, such as Authorization, Token URL can be found from the well-known endpoint of the Keycloak, as shown in the Keycloak configuration part.
Configure Service Provider
Let's configure a service provider as shown below.
Configure site.conf for Store
Here is a sample configuration.
"oidcConfiguration" : {
"enabled" : "true",
"issuer" : "API_STORE",
"identityProviderURI" : "https://apim.wso2.com:9443/oauth2/token",
"authorizationEndpointURI" : "https://apim.wso2.com:9443/oauth2/authorize",
"tokenEndpointURI" : "https://apim.wso2.com:9443/oauth2/token",
"userInfoURI" : "https://apim.wso2.com:9443/oauth2/userinfo",
"jwksURI" : "https://apim.wso2.com:9443/oauth2/jwks",
"logoutEndpointURI" : "https://apim.wso2.com:9443/oidc/logout",
"authHttpMethod": "POST",
"clientConfiguration" : {
"clientId" : "XH5vjX1VIKBLlWq_2GmgeVHbDDga",
"clientSecret" : "RUZ39KRhy59AFx7f5b_zNfR8quMa",
"responseType" : "code",
"authorizationType" : "authorization_code",
"scope" : "phone email address openid profile",
"redirectURI" : "https://apim.wso2.com:9443/store/jagg/jaggery_oidc_acs.jag",
"postLogoutRedirectURI" : "https://apim.wso2.com:9443/store/",
"clientAlgorithm" : "RS256"
}
},
clientId and clientSecret are the credentials of the Service Provider we created in WSO2 API Manager. We have configured the site.json only for Store. It can be done for Publisher and Admin too.
Configure axis2.xml
Make a small modification in this file.
$ wso2am-2.6.0/repository/conf/axis2/axis2.xml
In <transportSender name="https"> add the following line.
<parameter name="HostnameVerifier">AllowAll</parameter>
Testing
We are good to go now.
In an incognito window, access the /store.
Click the Sign-in button and it will redirect to Keycloak.Login with Keycloak user.
Approve the claims.
Finally, you are logged in API Store with Keycloak user.
In /carbon, check the Users and you can see the user i.e kc_agogoi provisioned.
That's it. We are done.
Deep Diving
It's a good idea to know what goes inside. For that, I am going to show you the internals of the classes. We are going to debug the WSO2 API Manager.
Create a project
Let's create a simple hello-world project in IntelliJ. Then, let's import the following jars to the project.
$ wso2am-2.6.0/repository/components/dropins/org.wso2.carbon.identity.application.authenticator.oidc-5.1.23.jar
$ wso2am-2.6.0/repository/components/plugins/org.wso2.carbon.identity.application.authentication.framework_5.12.153.jar
$ wso2am-2.6.0/repository/components/plugins/org.wso2.carbon.identity.claim.metadata.mgt_5.12.153.jar
The federated IDP authentication happens in the following class.
org.wso2.carbon.identity.application.authenticator.oidc.OpenIDConnectAuthenticator
The provisioning happens in this class.
org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.JITProvisioningPostAuthenticationHandler
You can run WSO2 API Manager in debug mode and configure IntelliJ for remote debugging to know more details.
Conclusion
In this tutorial, I have shown how Keycloak can be used in WSO2 API Manager as a federated identity provider. Also, I did a hack to install Oauth2/OpenID connector in WSO2 API Manager. If in the future, the library dependencies of Oauth2/OpenID connector is changed this approach might not work. So, the best option is to use WSO2 Identity Server that comes bundled with all these connectors out of the box.
Thanks for reading and comment if you have any problem working on it.
Opinions expressed by DZone contributors are their own.
Comments