Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

SSO — WSO2 API Manager and Keycloak

DZone 's Guide to

SSO — WSO2 API Manager and Keycloak

Implement SSO and see how to debug the WSO2 API Manager.

· Security Zone ·
Free Resource

In 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.

High-level architecture

High-level architecture


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.

Master realm

Master realm


Let's create an OpenID-connect client as shown in the below screen-shot.

OpenID-connect client

OpenID-connect client


After creating the client let's configure it as shown below.

Configuring client

Configuring client


Now click the Credentials tab and you can see the client-secret.

Checking credentials

Checking credentials


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.

Adding userNameInSub Mapper

Adding userNameInSub Mapper



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.

Creating subscriber in wso2apim client

Creating subscriber in 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.

Creating SubscriberRoleMapper

Creating SubscriberRoleMapper


Create User and Assign Role

Let's create some demo users.

Creating demo users

Creating 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.

Checking available authenticators

Checking available authenticators


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. 

O Auth2/OpenID connector

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.

Creating deferated IDP

Creating deferated IDP


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,

OpenID authenticator configuration

OpenID authenticator configuration


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.

Configuring service provider

Configuring service provider


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.

Accessing /store

Accessing /store


Click the Sign-in button and it will redirect to Keycloak.Login with Keycloak user.

Log in page

Log in page


Approve the claims.

Approving user claims

Approving user claims


Finally, you are logged in API Store with Keycloak user.

API Store

API Store


In /carbon, check the Users and you can see the user i.e kc_agogoi provisioned.

API manager

API manager


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. 

Topics:
wso2 api manager ,keycloak ,jit provisioning ,api ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}