I am working at this moment on a project that requires you to log in. More technical, it requires you to login via keycloak. How does it work?

It’s pretty simple. Instead of logging into every single application, users do it once on the keycloak authentication page. After they provide valid identification data, the technology verifies their identity. It creates a one-time verification code that gets extracted by the app and exchanged for an ID, access and refresh token. Based on that, the system either grants or denies the user access.

If you log in via the web interface, all is straight forward. Fill in the user and password fields and press the login button. Things get more complicate when you want to log in via the api with cy.request. Our API tests are also present in our cypress project. This means, the login via the UI will not work. We do want the bearer token that is created. How did I solve that?

To authenticate, we have to call keycloak several times. First we should call the auth endpoint. We give it some parameters like the client_id and redirect_uri. If this call succeeds, it returns an html page, that is the login page. The first call is done with following code:

function authenticate() {
    const realm: string = Cypress.env("realm");
    const root: string = Cypress.env("keycloakserver");
    const client_id = Cypress.env("clientId");
    const redirect = Cypress.config()["baseUrl"];
    return cy.request({
        url: `${root}/auth/realms/${realm}/protocol/openid-connect/auth`,
        qs: {
            client_id: client_id,
            redirect_uri: redirect,
            scope: 'openid',
            state: uuidv4(),
            nonce: uuidv4(),
            response_type: 'code',
            response_mode: 'fragment',
        }
    })
}

The login page that we receive contains a form with some data in it, we should call that form and provides the username and password of the user that will be used to do the API calls.

function sendUserPasswordFromForm({ htmldata, role }: UserFormData) {
    const username = Cypress.env("users")[role]["user"];
    const password = Cypress.env("users")[role]["password"];
    const html = document.createElement('html');

    html.innerHTML = htmldata;
    const form = html.getElementsByTagName('form');
    const isAuthorized = !form.length;
    if (!isAuthorized)
        return cy.request({
            form: true,
            method: 'POST',
            url: form[0].action,
            followRedirect: false,
            body: {
                username,
                password,
            }
        })
    return undefined;
}

If the credentials are correctly, you are logged in. But we still do not have the bearer token. We need that, because we have to put it in the header of our api calls each time. To receive the bearer token, we should do one more call. The function that I create here has an input parameter. The reply headers of the previous call.

function getBearerToken(locationHeader: string) {
    const queryParams = new URLSearchParams(locationHeader.split('#')[1]);
    const code = queryParams.get('code');
    const realm: string = Cypress.env("realm");
    const root: string = Cypress.env("keycloakserver");
    const client_id = Cypress.env("clientId");
    const redirect = Cypress.config()["baseUrl"];
    return cy.request({
        method: 'POST',
        url: `${root}/auth/realms/${realm}/protocol/openid-connect/token`,
        form: true,
        body: {
            grant_type: 'authorization_code',
            client_id: client_id,
            redirect_uri: redirect,
            code: code,
        }
    });
}

The received token is stored in a session object. Together with the real_uuid. We do this because then the same login script can be used to log in via the user interface. Then there is no need to fill in the form with the ‘slow’ GUI testitself. Therefore there is a session object created, so that cypress will use it again for each test until the token is invalid.

cy.session(sessionName, () => {
    authenticate()
        .then((response) => {
            sendUserPasswordFromForm({
                htmldata: response.body,
                role: role
            })
                .then((formReply) => {
                    getBearerToken(
                        arrayStringToString(formReply.headers['location'])
                    ).then((tokenresponse) => {
                        window.sessionStorage.setItem('realm_uuid', Cypress.env("realm"));
                        window.sessionStorage.setItem('bearer', tokenresponse.body.access_token);
                    });
                });
        });
}, {
    validate() {
        cy.log("Validate the session");
    }
});

Now I can grab the bearer token out of the sessionstorage and fill it in the API request header. It is that simple once you know it.