Intro
Many articles have references to SSO capabilities of Keycloak, but they often don’t explain how it works under the hood. In this article we have simple environment in form of Docker Compose with Keycloak and two Angular applications that will help you to understand how cookies based authentication works with Keycloak. We will dive deep into this topic to explore the details and gain a better understanding of the key concepts. I believe this will be helpful for:
- SSO/Keycloak architects
- developers who customize Keycloak using custom SPIs
- companies looking for leveraging SSO functionality
You can find the full GitHub repository, along with instructions to clone and run it, here: https://github.com/TorinKS/sso-mfe-keycloak
Our Docker Compose setup consists of the following:
- An Angular
mfe-a
application, hosted on the local domainmfe-a.home.arpa
. - An Angular
mfe-b
application, hosted on the local domainmfe-b.home.arpa
. - Keycloak, hosted on the domain
sso.home.arpa
. - HAProxy, configured to enable working with these domains on localhost using FQDNs.
Cookie-based authentication
Before we understand what cookie-based SSO in Keycloak is, let’s take a look at an example architecture of an application that uses cookies for authentication.
Below are the details of how it works:
- the browser (web client/user agent) sends a GET request to retrieve the authentication form.
- the web server (framework) generates the form and adds a special random value (“nonce”), which is used to protect against replay attacks. Typically, this “nonce” is returned as a hidden element in the HTML form. Additionally, the server creates a session, which is persisted in a database, file system, cache, etc., and is not yet authenticated. The session is mapped to the “nonce” so that the server can later verify during authentication that the same browser, with the corresponding session cookies, initially received the authentication form.
- during authentication, the client submits the authentication form along with the “nonce.” If everything is valid, the web server marks the session as authenticated. From this point onward, the web client can interact with the web server (or other backends) that support this session-based authentication mechanism until the cookies expire.
We can say that this cookie-based authentication allows us to avoid repeating the process of entering a login and password each time we want to access a web server page. This is how authentication cookies work in Spring Security (https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html) and other similar frameworks in different stacks.
Additionally, cookies are typically HTTP-only and tied to a particular domain, which mitigates an attacker’s ability to manipulate these cookies in the browser.
Now imagine that in your organization you have a hundred of application each of them use its own cookies, even with the same name, for authentication. Basically, you would end up with the session cookies for different domain names, such as:
- app1.company.com
- jira.company.com
- gitlab.company.com, and so on.
This is where a centralized authentication solution with SSO capabilities (like Keycloak) enters the scene. The industry trend is to shift from separate authentication on each service to a centralized solution that authenticates all users. This provides several significant benefits for us:
- having one service for authentication, we can federate users to this service from other services like AD, OpenLDAP.
- we can apply security policies (MFA, strong password policies, etc.) in one place.
- we can eliminate the need to write authentication logic in other services by shifting this responsibility to one centralized service and so on.
- we can set authentication cookies on our centralized authentication service and reuse them for authentication on other services, leveraging cookie-based SSO capabilities.
From a service integration perspective, implementing SSO-enabled authentication requires just two key steps:
- Integrate an OAuth/OIDC client library appropriate for your application framework
- Register your application as an OIDC client within Keycloak (your centralized authentication service)
When a user attempts to access a protected resource, the OIDC client library automatically initiates the authorization code flow (recommended in most cases flow), redirecting the user’s browser to Keycloak’s authentication endpoints. Upon successful authentication, Keycloak establishes persistent SSO sessions through secure cookies and issues standard OAuth tokens to the requesting application.
The true power of this architecture becomes evident in subsequent authentication attempts. When a user navigates to another application, the existing Keycloak SSO session cookies are automatically included in the request. Keycloak recognizes these valid session cookies, bypassing the username/password challenge and seamlessly issuing appropriate tokens to the requesting application.
Moreover, the same approach works on mobile platforms, where mobile applications can reuse external user agents and their capabilities to work with cookies, just like browsers, to implement SSO for multiple mobile applications on the same device. See https://datatracker.ietf.org/doc/html/rfc8252 for additional details. When it comes to desktop applications, the situation is similar, you just reuse desktop browser.
Keycloak cookie-based SSO
So, let’s take a look at Keycloak cookie based authentiation. But before we start, take a look at simplified Authorization Code (the authentication flow typically used by default for browser-based application – https://datatracker.ietf.org/doc/html/rfc6749) flow UML sequence diagram
There are 3 steps in this flow:
- the client application generates a code verifier and its derived code challenge
- the web client authenticates and receives an authorization code in a 302 HTTP redirect, which is then reused for exchanging it for access token, refresh and id token
- web client redeems the authorization code for a token.
This flow is slightly simplified here, so in another article, we will be able to go into more details.
Code example
Our Docker compose https://github.com/TorinKS/sso-mfe-keycloak/blob/main/docker-compose.yml has these components:
- haproxy
- angular app mfe-a on mfe-a.home.arpa domain
- angular app mfe-b on mfe-b.home.arpa domain
- Keycloak
- PostgreSQL
The https://github.com/TorinKS/sso-mfe-keycloak/blob/main/haproxy/haproxy.cfg HAProxy configuration file defines how incoming HTTP requests are routed to different backend services based on the requested hostname. You can find the breakdown of the main part of the configuration below.
The “frontend
section” defines how incoming requests are handled and routed to backends:
frontend http-in
: defines a frontend namedhttp-in
.mode http
: specifies that this frontend operates in HTTP mode.bind *:80
: binds the frontend to port 80 on all available network interfaces.use_backend mfe-a.home.arpa if { req.hdr(host) -i mfe-a.home.arpa }
: routes requests to themfe-a.home.arpa
backend if thehost
header matchesmfe-a.home.arpa
(case-insensitive).use_backend mfe-b.home.arpa if { req.hdr(host) -i mfe-b.home.arpa }
: routes requests to themfe-b.home.arpa
backend if thehost
header matchesmfe-b.home.arpa
.use_backend sso.home.arpa if { req.hdr(host) -i sso.home.arpa }
: routes requests to thesso.home.arpa
backend if thehost
header matchessso.home.arpa
.
The "backend
sections” define how requests are forwarded to specific servers:
backend mfe-a.home.arpa
: defines a backend namedmfe-a.home.arpa
.mode http
: specifies that this backend operates in HTTP mode.server mfe-a.home.arpa ${MFE_A_IP}:${MFE_A_EXPOSED_PORT} check
: forwards requests to the server at${MFE_A_IP}:${MFE_A_EXPOSED_PORT}
and performs health checks.
The same applies to the mfe-b.home.arpa backend. Both configurations are similar, as each points to an Angular application hosted on a Node.js web server. The key difference is that the two applications use different OIDC client configurations in keycloak-init.factory.ts: one application is configured with the ‘mfe-a’ OIDC client, while the other uses ‘mfe-b’. This was done specially to allow you to see how SSO works in Keycloak for the same user and different OIDC clients (applications), and this can be scaled unlimited, one user and multiple sessions and tokens for differrent OIDC clients.
We also have a special script here that creates all the needed configuration in Keycloak to avoid manual work – https://github.com/TorinKS/sso-mfe-keycloak/blob/main/configure.sh :
- realm “company-external”
- OIDC clients with their configuration (we use public OIDC clients, which are recommended for frontend SPA, mobile applications)
- user test-user with the same password
Each Angular application uses keycloak-angular lib and the configuration as shown below
const KC_OPTIONS: KeycloakOptions = {
config: {
url: 'http://sso.home.arpa/auth',
realm: 'company-external',
clientId: 'mfe-a',
},
initOptions: {
onLoad: 'login-required',
flow: 'standard',
checkLoginIframe: false,
enableLogging: true,
useNonce: true,
adapter: 'default',
pkceMethod: 'S256',
},
loadUserProfileAtStartUp: true
};
This is an OIDC configuration for the Angular Library, which was mentioned above as a needed part to enable SSO authentication in your application. Basically, these few line of code is everything that you need to make centralized authentication work and to support cookies based SSO.
Our Keycloak configuration has:
- two public OIDC clients mfe-a, mfe-b
- for each of OIDC client, we configure Valid Redirect URIs and Web origins
A valid Redirect URI parameter is needed so Keycloak can redirect the authorization code to the proper location, and web origins parameters configure CORS for the frontends properly.

Another aspect is that for browser-based authentication, Keycloak uses predefined steps involved in the authentication process used by Browser Built-in flow

That flow includes Cookie based authentication as the first step in the flow.

The “Alternative” parameter tells Keycloak to use one of the available authentication:
- Cookie
- Identity Provider Redirector
- Forms
If authentication cookies are available in the request to /auth endpoints the authentication will be done by this “Cookie” step.
Just clone, configure, and start the project with Docker Compose. Open http://mfe-a.home.arpa address and Dev Tools in Chrome and in Network tab check “Preserve log” and “Disable cache” checkboxes, you will see that Angular application redirects browser to sso.home.arpa

This is the first request to /auth endpoint: http://sso.home.arpa/auth/realms/company-external/protocol/openid-connect/auth
During authentication with ‘test-user’ login and ‘test-user’ password, you will reach 2-nd /authenticate endpoints as shown below

http://sso.home.arpa/auth/realms/company-external/login-actions/authenticate?client_id=mfe-a&tab_id=7vH0Q8mRL44&client_data=eyJydSI6Imh0dHA6Ly9tZmUtYS5ob21lLmFycGEvYXNzZXRzLyIsInJ0IjoiY29kZSIsInJtIjoiZnJhZ21lbnQiLCJzdCI6IjQ4OTE3NWQ2LTk1NjYtNGMwZS1hYTQwLTFlZGQ3YjhlYmQ5NyJ9
and you get back 302 Redirect with Authorizatin Code in the Location field (code parameter) as in the example below
http://mfe-a.home.arpa/assets/#state=2427e05f-269a-488c-a1d9-8c3a8ae5fa6c&session_state=94b63071-cc84-4c08-a1ec-4e6e18d59b1e&iss=http%3A%2F%2Fsso.home.arpa%2Fauth%2Frealms%2Fcompany-external&code=43dae82f-8f83-4e6b-8203-84e0575cf329.94b63071-cc84-4c08-a1ec-4e6e18d59b1e.acd095ce-53c4-47fc-8886-545b2890b684
And the last request is the POST request to /token endpoint
http://sso.home.arpa/auth/realms/company-external/protocol/openid-connect/token
and finally, we get back our tokens
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJHbXZ1Sjh2ZmdVd1BCSDZFS2FaQTB4M1djMWNFT1BxR01yc2d2WmNIWGRvIn0.eyJleHAiOjE3NDI5MzA2NzMsImlhdCI6MTc0MjkzMDM3MiwiYXV0aF90aW1lIjoxNzQyOTMwMzU3LCJqdGkiOiIwYzlkZjE0NS0xYjQ3LTQyZjctYWNmNS0zYmMzNDZjMjkyOGIiLCJpc3MiOiJodHRwOi8vc3NvLmhvbWUuYXJwYS9hdXRoL3JlYWxtcy9jb21wYW55LWV4dGVybmFsIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImVmM2ZmMWVjLWY0NjctNGFhZC05OWIyLTMwNDY2ZjdkNmQ0MCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im1mZS1hIiwic2lkIjoiOTRiNjMwNzEtY2M4NC00YzA4LWExZWMtNGU2ZTE4ZDU5YjFlIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbWZlLWEuaG9tZS5hcnBhIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWNvbXBhbnktZXh0ZXJuYWwiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdC11c2VyIn0.J_HIqMzYEAD7WOXaFnog55R6Q-0uRQVIWIjjHW6gpOKMqdSwJD1mJMCZaO7n9D_Kz2dMxdvifw8Pi4Twgscdapt0ZKXfDuY7zaR4g6NN-w-Y3rxKPu1XrB_OSgr4HNr4B-LhuDra1elRMCnHYFM5CWdEHNADBKYqPTb2ndTh6jQfpVW7ExsLLORWgCC4Dj1EJvQLf9U6ygXEf_XM3ZwAsqFaWhVEPTVLxhYCrtwV9TEBItEwJmRQvbicrqUt-gNVLnsN7kQRAemNovWP6pWx_UbahmXsLFQXIeQ7C6TS9eTLyMk1Bo7kXHAH1p-mqICA9gYZdQ8l0wfn1KksCdIBoQ",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4Mzk3OGVmYy00NmZlLTQ0MjgtOWY0MS1jYTlkOTE0NWEzNGYifQ.eyJleHAiOjE3NDI5MzIxNzMsImlhdCI6MTc0MjkzMDM3MywianRpIjoiY2Y5MGMyMzYtNWRiZS00NzAxLWFmZmQtZTUwMDJhMmVkODk0IiwiaXNzIjoiaHR0cDovL3Nzby5ob21lLmFycGEvYXV0aC9yZWFsbXMvY29tcGFueS1leHRlcm5hbCIsImF1ZCI6Imh0dHA6Ly9zc28uaG9tZS5hcnBhL2F1dGgvcmVhbG1zL2NvbXBhbnktZXh0ZXJuYWwiLCJzdWIiOiJlZjNmZjFlYy1mNDY3LTRhYWQtOTliMi0zMDQ2NmY3ZDZkNDAiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibWZlLWEiLCJzaWQiOiI5NGI2MzA3MS1jYzg0LTRjMDgtYTFlYy00ZTZlMThkNTliMWUiLCJzY29wZSI6Im9wZW5pZCBhY3Igcm9sZXMgd2ViLW9yaWdpbnMgYmFzaWMgZW1haWwgcHJvZmlsZSJ9.xeVL_uJTAQxIc59sUXtc4Ogn1uT8KCTmvIwOmmBgcXnYzxpR3aaEPe59ppP7QwASu5iW-_IIVuis8FYzGok-4Q",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJHbXZ1Sjh2ZmdVd1BCSDZFS2FaQTB4M1djMWNFT1BxR01yc2d2WmNIWGRvIn0.eyJleHAiOjE3NDI5MzA2NzMsImlhdCI6MTc0MjkzMDM3MywiYXV0aF90aW1lIjoxNzQyOTMwMzU3LCJqdGkiOiI4ZjQ1MWJkZC00NTc0LTRjMjctODJkMC1kNGM5OWQ3NjE2ODkiLCJpc3MiOiJodHRwOi8vc3NvLmhvbWUuYXJwYS9hdXRoL3JlYWxtcy9jb21wYW55LWV4dGVybmFsIiwiYXVkIjoibWZlLWEiLCJzdWIiOiJlZjNmZjFlYy1mNDY3LTRhYWQtOTliMi0zMDQ2NmY3ZDZkNDAiLCJ0eXAiOiJJRCIsImF6cCI6Im1mZS1hIiwibm9uY2UiOiIyN2ZkM2UyYS03ZjcwLTRkOWYtODJiYi01MzQyNGJmMzA2Y2EiLCJzaWQiOiI5NGI2MzA3MS1jYzg0LTRjMDgtYTFlYy00ZTZlMThkNTliMWUiLCJhdF9oYXNoIjoiVUY5WnhnblFlUWhSZ0UyUGxMZDhhUSIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QtdXNlciJ9.Gdy3EZCGgidI2fdachUu6WGiFKT3ZFLliaRvlrvFlzNVUBkPXWmecYHLawte9xo134kNCEzCsASpxQYiQVXYmth5_qCmoY93g6KJ4UX2BVCWGFFrxIO2i_R3NwTJ0PLw5FLu_2vpahmd9yqw3QVFEaVojEtSY5NH8jH_nbcOtNMFKWLcNB0ycwE7r8Uu-QyfUeDMUY76HAu9_aMaKgm5gBunVaR3mp1btu_3W6vuhuUNZRI2iGpQhWGZejg4xBgXAT7pLYUC83G80aNc_Gf_-eIpjIM570asWbmxf8fxsaOyH1X3Lqyh6rqoljOQlYBQ2UFfqFNmBawSc4MRpyEw7w",
"not-before-policy": 0,
"session_state": "94b63071-cc84-4c08-a1ec-4e6e18d59b1e",
"scope": "openid email profile"
}
Here we are successfully authenticated in our first Angular applciation and get back tokens. Keycloak also created the session for the user ‘test-user’ and OIDC client ‘mfe-a’

The JWT token is issued to the user test-user (preffered_username claim) and OIDC client mfe-a (azp claim), as you can see, according to the JWT token decoded payload below
{
"exp": 1742930673,
"iat": 1742930372,
"auth_time": 1742930357,
"jti": "0c9df145-1b47-42f7-acf5-3bc346c2928b",
"iss": "http://sso.home.arpa/auth/realms/company-external",
"aud": "account",
"sub": "ef3ff1ec-f467-4aad-99b2-30466f7d6d40",
"typ": "Bearer",
"azp": "mfe-a",
"sid": "94b63071-cc84-4c08-a1ec-4e6e18d59b1e",
"acr": "1",
"allowed-origins": [
"http://mfe-a.home.arpa"
],
"realm_access": {
"roles": [
"default-roles-company-external",
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email profile",
"email_verified": false,
"preferred_username": "test-user"
}
The other side of this authentication is that we got SSO cookies from sso.home.arpa:
- AUTH_SESSION_ID
- KEYCLOAK_IDENTITY
- KEYCLOAK_SESSION

Now, when we open mfe-b.home.arpa in the browser OIDC library redirects us to /auth endpoint
http://sso.home.arpa/auth/realms/company-external/protocol/openid-connect/auth?client_id=mfe-b&redirect_uri=http%3A%2F%2Fmfe-b.home.arpa%2Fassets%2F&state=7b6a9cb5-f715-4674-b674-a222fd7850a1&response_mode=fragment&response_type=code&scope=openid&nonce=a0301b05-ba5d-4ebd-94f3-84a261416538&code_challenge=bIH090-u76HEZm_2HH5U2OEgbEr98TqPd0CWCh4Daio&code_challenge_method=S256
and browser automatically adds SSO cookies to the request, as shown below

and Keycloak during this request will authenticate the user by SSO cookies set previously during authentication on mfe-a.home.arpa.
If we check the sessions of the our user in Keycloak under admin, we will see that there are two sessions now, and this is the nutshell of cookie based SSO authentication.

Having as many applications and corresponding OIDC clients as you need, we can configure just integrations and delegate the whole authentication to Keycloak.
Here’s how the previously mentioned authorization code flow looks now with SSO cookies authentication:
I think that’s all I’d like to share with you in this article. I hope it was interesting and feel free to ask any questions in the comments.