Keycloak is an open source identity and access management application. Put it simply, it can be used to add authentication to applications and secure services.
I've just started taking a look at it, so I will write about how I tested OpenID Connect's (OIDC) Authorization Code Flow using Keycloak as the authorization server. This is a very common flow we see when applications allow us to login with our Google or Facebook credentials.
Explaining OpenID Connect is not the goal of this article, so I will leave an extremely succinct and well explained article about the subject:
https://www.simpleorientedarchitecture.com/openid-connect-in-a-nutshell/
Running Keycloak
I've run Keycloak on Docker on my local machine. Make sure Docker is installed and start Keycloak with the following command (please adapt it to your needs):
$ docker run -d --name=keycloak --restart unless-stopped -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:15.0.2
Just access http://localhost:8080 to be presented with the welcome page:
Just click on Administration Console and you can login with username admin
and password admin
as set in the Docker command. In this console, we can add users and register applications to be secured by Keycloak.
Realms
A realm represents the top level grouping mechanism in Keycloak. It is a space where we can create isolated groups of users and applications. By default, we have the master
realm that contains the admin user we used to login to the console. But, we should not use it for our own applications and only use it to create other realms.
So, let's create our realm by hovering over the Master
menu and clicking Add realm
:
Type demo
in the Name field and click Create
.
Users
Let's create a user in the newly created realm. Click Users
in the sidebar and then Add user
in the top-right corner of the page.
And create a user as shown in the image above. In the user details, select the Credentials
tab to set its password. We can disable the Temporary
option to make it permanent since we are just testing the functionality. If this is enabled, the user needs to set a new password after logging in.
Clients
Now, we're going to create a new client. Clients are the applications that will use Keycloak as their authentication system. Click Clients
in the sidebar and Create
in the top-right corner of the page.
And add a client called test_client_app
as shown above. I've set an arbitrary Root URL
since I just want to simulate the login flow without actually having an application for that.
We want to make sure the Standard Flow Enabled
option is turned on to make sure we can use the Authorization Code Flow. Let's also enable the Consent Required
option, so the end-user needs to explicitly consent to sharing his/her information to the client application. Also, set Access Type
as confidential
, so our client application needs to provide a secret when interacting with the OIDC endpoints.
Testing the Authorization Code Flow
We can finally test the Authorization Code Flow. Here, we are simulating the user we created clicking in the login button in the test_client_app. A client app will redirect the user to a link like below, the authorization endpoint, so the person can login to Keycloak. For our test, let's open this URL in the browser.
http://localhost:8080/auth/realms/demo/protocol/openid-connect/auth?client_id=test_client_app&response_type=code&state=a_random_state_to_validate_the_callback
Then we can type our credentials to sign in:
The consent screen will be displayed as we are used to when using Google or Facebook to login to other services.
Clicking Yes
will redirect us to an error page. This is expected since Keycloak redirect us to the Root URL
we configured previously and we don't have an actual client application to test. The URL of the page we are redirect to will be something like below:
http://localhost:9090/?state=a_random_state_to_validate_the_callback&session_state=f526307d-8bf0-4706-b308-2309ddc1c95a&code=25f7896f-12ef-499b-a8eb-85364e068379.f526307d-8bf0-4706-b308-2309ddc1c95a.b0f608c3-ccd2-4923-9ade-f785c6237cc3
state
is the random value we set when accessing the login page. Our client application can check it now to make sure it matches the initial request. The most significant parameter here is the code
parameter we can exchange by an access token.
Access Token
We can exchange the code
received previously by an access token. We do that by making a request to the token endpoint. To check the available endpoints in our realm, we can open the following URL in the browser. This is the identity provider metadata endpoint.
http://localhost:8080/auth/realms/demo/.well-known/openid-configuration
So, to call the token endpoint, let's run the following request in a terminal:
curl -X POST 'http://localhost:8080/auth/realms/demo/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'client_id=test_client_app' \
-d 'client_secret=9797d273-a3e2-4269-85b6-6deec57bdc40' \
-d 'code=25f7896f-12ef-499b-a8eb-85364e068379.f526307d-8bf0-4706-b308-2309ddc1c95a.b0f608c3-ccd2-4923-9ade-f785c6237cc3' \
-d 'redirect_uri=http://localhost:9090/callback'
The client_secret
can be found in the Credentials
tab in the client details page as below, if the Access Type
was set to confidential
as described in the initial steps.
The response will be a JSON and contain the access_token
:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItUHVFWHk4N09ucko5OFpzZjRXRlEyY1VhTmFlQ2JtTXFxSGY5TnpRVEdNIn0.eyJleHAiOjE2Mzg0NjAzMzIsImlhdCI6MTYzODQ2MDAzMiwiYXV0aF90aW1lIjoxNjM4NDYwMDA0LCJqdGkiOiIyMmY5MDg5Mi01MDE1LTRiMmItYjkxNS03YWEyYTlkZGRhMmUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJjMjk2N2JmNy0zYmFmLTQzYzQtOGFlYS1kZmVlZDAxODUyZWIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0ZXN0X2NsaWVudF9hcHAiLCJzZXNzaW9uX3N0YXRlIjoiODQzYWM5NWUtOTE5YS00ZmVhLTlkM2QtODhiMzY0MjRlZmI3IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjkwOTAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiZGVmYXVsdC1yb2xlcy1kZW1vIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI4NDNhYzk1ZS05MTlhLTRmZWEtOWQzZC04OGIzNjQyNGVmYjciLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJUZXN0IFVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0X3VzZXIiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciJ9.g_v6DA9foM1TBUUV46wclDbkSYrFWJ1S6VWXuA1lj6AY1T4SIFcGrGDibCdT1N1s01HsU1qDwJohLcQpBmojIbgeLHuIK_K2AeDC99lE0yVFvxxoH-rfeAyHDJf0Vqqk_GWQU0hl3VYhDEb-ZBstkFwv0Vuu1C9nw6a_w1POoEACMppz-0WROeXhQa5_D8yqGHSC70jZ0vUVFCL7RiVeXDaV9J7kyoSr4bQYhJ1bHU9kQwq-yo0eSLxCnmQmYFa3yEDJKSCTups7A1FrXTuNTG8XUT1pfa_YVh51g4_u_HEsahwl8mSSs_Y56TLI5LNOBKj-tH56aDxCoLx8XMWYJQ",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyYmUwYjMxYS0wZTUwLTQzMWEtYjZiNi1jMmJkZDFhZDk0MTgifQ.eyJleHAiOjE2Mzg0NjE4MzIsImlhdCI6MTYzODQ2MDAzMiwianRpIjoiOGJlYjA5NDktODRkYi00NzI1LTlmN2MtMjc0YzU5NWFmYzkxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2RlbW8iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsInN1YiI6ImMyOTY3YmY3LTNiYWYtNDNjNC04YWVhLWRmZWVkMDE4NTJlYiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJ0ZXN0X2NsaWVudF9hcHAiLCJzZXNzaW9uX3N0YXRlIjoiODQzYWM5NWUtOTE5YS00ZmVhLTlkM2QtODhiMzY0MjRlZmI3Iiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiODQzYWM5NWUtOTE5YS00ZmVhLTlkM2QtODhiMzY0MjRlZmI3In0.sDfZRV6snD8wGkSZ1h-Asn2FFXDS0DPAXKUss4WgBos",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "843ac95e-919a-4fea-9d3d-88b36424efb7",
"scope": "email profile"
}
Userinfo
Now that we have an access token, we can use it to make a request to the userinfo endpoint:
curl 'http://localhost:8080/auth/realms/demo/protocol/openid-connect/userinfo' \
-H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItUHVFWHk4N09ucko5OFpzZjRXRlEyY1VhTmFlQ2JtTXFxSGY5TnpRVEdNIn0.eyJleHAiOjE2Mzg0NjAzMzIsImlhdCI6MTYzODQ2MDAzMiwiYXV0aF90aW1lIjoxNjM4NDYwMDA0LCJqdGkiOiIyMmY5MDg5Mi01MDE1LTRiMmItYjkxNS03YWEyYTlkZGRhMmUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJjMjk2N2JmNy0zYmFmLTQzYzQtOGFlYS1kZmVlZDAxODUyZWIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0ZXN0X2NsaWVudF9hcHAiLCJzZXNzaW9uX3N0YXRlIjoiODQzYWM5NWUtOTE5YS00ZmVhLTlkM2QtODhiMzY0MjRlZmI3IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjkwOTAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiZGVmYXVsdC1yb2xlcy1kZW1vIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI4NDNhYzk1ZS05MTlhLTRmZWEtOWQzZC04OGIzNjQyNGVmYjciLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJUZXN0IFVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0X3VzZXIiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciJ9.g_v6DA9foM1TBUUV46wclDbkSYrFWJ1S6VWXuA1lj6AY1T4SIFcGrGDibCdT1N1s01HsU1qDwJohLcQpBmojIbgeLHuIK_K2AeDC99lE0yVFvxxoH-rfeAyHDJf0Vqqk_GWQU0hl3VYhDEb-ZBstkFwv0Vuu1C9nw6a_w1POoEACMppz-0WROeXhQa5_D8yqGHSC70jZ0vUVFCL7RiVeXDaV9J7kyoSr4bQYhJ1bHU9kQwq-yo0eSLxCnmQmYFa3yEDJKSCTups7A1FrXTuNTG8XUT1pfa_YVh51g4_u_HEsahwl8mSSs_Y56TLI5LNOBKj-tH56aDxCoLx8XMWYJQ'
This returns details about the user as expected so, in a real application, we could proceed to registering or logging in the user.
{
"sub": "c2967bf7-3baf-43c4-8aea-dfeed01852eb",
"email_verified": false,
"name": "Test User",
"preferred_username": "test_user",
"given_name": "Test",
"family_name": "User"
}
This was a very short test of Keycloak. It's extremely fully-featured and flexible, so I plan to learn more about it since it seems to be a very good option to have in our toolkit.
References
- https://www.keycloak.org/
- https://www.keycloak.org/getting-started/getting-started-docker
- https://www.keycloak.org/docs/latest/getting_started/index.html
- https://www.keycloak.org/docs/latest/securing_apps/index.html
- https://www.keycloak.org/docs/latest/securing_apps/index.html#other-openid-connect-libraries
- https://www.appsdeveloperblog.com/keycloak-authorization-code-grant-example/