Faking keycloak-js Authentication
Notes on how to fake an authentication flow by keycloak-js to write end to end test using Cypress. Example using Cypress is available on GitHub: https://github.com/kesalohe/spoof-keycloakjs.
Although the example don’t need a Keycloak server it might be useful to have one to explore its configuration.
podman run --rm -it -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:18.0.2 start-dev
Authorisation Code Flow
Authorisation code flow is defined by RFC-6749, section 4.1.
Keycloak refers to that flow as Standard Flow
.
To simulate a valid authentication flow we just need 3 steps:
- Forge a
callbackState
. - Forge the keycloak redirection url from Keycloak.
- Intercept the authorization code exchange.
Forge callbackState
This part is straight forward, we just need to create a json object and store it in LocalStorage
.
The important part here is the value we give to the state
, it will be used in several places.
To keep things simple we don’t want to set the field nonce
so that we can generate random token without it.
Keycloak-js initialize the callbackState
at keycloak.js#L390:
var state = createUUID();
var nonce = createUUID();
var callbackState = {
state: state,
nonce: nonce,
redirectUri: encodeURIComponent(redirectUri)
};
The callbackState
is stored in LocalStorage
with an id looking like kc-callback-${callbackState.state}
, see keycloak.js#L1658.
cs.add = function(state) {
clearExpired();
var key = 'kc-callback-' + state.state;
state.expires = new Date().getTime() + (60 * 60 * 1000);
localStorage.setItem(key, JSON.stringify(state));
};
We also need to add an expires
field with a time in the future in milliseconds since epoch.
Keycloak-js will regularly remove all expired state from LocalStorage
.
function clearExpired() {
var time = new Date().getTime();
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.indexOf('kc-callback-') == 0) {
var value = localStorage.getItem(key);
if (value) {
try {
var expires = JSON.parse(value).expires;
if (!expires || expires < time) {
localStorage.removeItem(key);
}
} catch (err) {
localStorage.removeItem(key);
}
}
}
}
}
In the end we will have a callbackState
like below that is stored in LocalStorage
with the key kc-callback-a81a596b-7399-4990-84a0-bcd4bddc75b2
.
{
"state": "a81a596b-7399-4990-84a0-bcd4bddc75b2",
"expires": 4813465472523
}
Forge Keycloak Redirection
Unless configured otherwise, keycloak.js will use the url’s fragment part to get its attributes. According to the RFC-6749#section-4.1.2, we just need to create an URL to our application with 2 parameters.
code
: the authorization code which we won’t be using but still needs to be present.state
: thecallbackState.state
value, this one needs to match the uuid we generated when creating ourcallbackState
json.
In the end, the entry point for our application is http://ourapp#code=42&state=a81a596b-7399-4990-84a0-bcd4bddc75b2
.
Intercept Authorization Code Exchange
Keycloak-js will parse the redirection url and check LocalStorage
for the state
it received.
If it matches then it will try to exchange the authorization code for an access token, keycloak.js#L744.
Intercept this request and return a json payload containing the access token and refresh token with your own.
Note that the field expires_in
is never used by keycloak-js, it might be dropped?
Also, since we haven’t set a nonce
in our LocalStorage
we can generate any JWT we want.
Either create your own or fetch one from keycloak, keycloak-js will never validate that the JWT’s signature is valid.
The JWT validation is only performed by the backend service.
{
"access_token": "<your-access-token>",
"refresh_token": "<your-access-token>",
"token_type": "Bearer",
"expires_in": 3600
}
Nonce
The nonce
matters in real live scenario to prevent replay attacks.
Thankfully, the nonce
is only checked if it is present in the LocalStorage
.
That means we can spoof the authentication mechanism without changing its configuration.
The nonce
value is just a random UUID that is stored in LocalStorage
and sent to Keycloak during the first redirection.
Keycloak would then add the received nonce
into the access token.
Keycloak-js would just compare the LocalStorage
’s value with the one from either the access token, refresh token or id token.
If they don’t match then the authentication fails.
See: keycloak.js#L778.
function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) {
if (useNonce && ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
(kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
(kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce))) {
logInfo('[KEYCLOAK] Invalid nonce, clearing token');
kc.clearToken();
promise && promise.setError();
}}
}
Implicit Code Flow
The implicit code flow follow the same algorithm except that the access token is passed directly by the redirection url. Just be aware that this authentication flow is unsecure and won’t be present in OAuth 2.1.
The step about the LocalStorage
state stays the same.
Redirection url now has the parameters:
access_token
id_token
token_type
state
For example: http://yourapp#access_token=abc.abc.abc&id_token=abc.abc.abc&token_type=Bearer&state=a81a596b-7399-4990-84a0-bcd4bddc75b2
.