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.

normal-authentication-flow

To simulate a valid authentication flow we just need 3 steps:

  1. Forge a callbackState.
  2. Forge the keycloak redirection url from Keycloak.
  3. Intercept the authorization code exchange.

faked-authentication-flow

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.

  1. code: the authorization code which we won’t be using but still needs to be present.
  2. state: the callbackState.state value, this one needs to match the uuid we generated when creating our callbackState 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:

For example: http://yourapp#access_token=abc.abc.abc&id_token=abc.abc.abc&token_type=Bearer&state=a81a596b-7399-4990-84a0-bcd4bddc75b2.