Search code examples
oauth-2.0keycloaktoken-exchange

Keycloak token exchange does not return refresh token


I have an application with regular REST api, and I'm usink Keycloak for the authentication. The frontend is a React application with its own Client ID (no secret because it's a public page), the backend is a Node application with another Client ID (and secret, obviously).

I'm trying to put up a simple OTP login. When the code is verified the API does a token exchange from it's own token (obtained through Client Credentials flow) to the frontend one, on behalf of the user; the permissions on Keycloak are OK, and I get the access token for the user, but I don't see the refresh token even if I've requested it.

Here's the code I'm using:


export const doUserLogin = async (user: User) => {
    return axios
        .post(
            `https://${Conf.keycloak.domain}/realms/${Conf.keycloak.realm}/protocol/openid-connect/token`,
            new URLSearchParams({
                client_id: Conf.keycloak.credentials.clientId,
                client_secret: Conf.keycloak.credentials.clientSecret,
                grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
                requested_token_type: 'urn:ietf:params:oauth:token-type:refresh_token',
                requested_subject: user.keycloakId,
                audience: 'FRONTEND_CLIENT_ID',
            }),
        )
        .then((res) => res.data);
};

I imagine there is some configuration I have to enable on Keycloak, but I cannot figure out what to do


Solution

  • Turns out the refresh token generaton must be enabled in the REST client, even it's working on behalf of the frontend.

    Client --> Client details --> Advanced --> Open ID Connect Compatibility Modes --> "Use refresh tokens" ON

    Note that the subsequent option (Use refresh tokens for client credentials grant) is not required, at least for my use case.

    In addition, if you need also the ID token you must add the scope "openid" to the token exchange call, even if the official documentation says that's unmanaged at the moment:

    export const doUserLogin = async (user: User) => {
        return axios
            .post(
                `https://${Conf.keycloak.domain}/realms/${Conf.keycloak.realm}/protocol/openid-connect/token`,
                new URLSearchParams({
                    client_id: Conf.keycloak.credentials.clientId,
                    client_secret: Conf.keycloak.credentials.clientSecret,
                    grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
                    requested_token_type: 'urn:ietf:params:oauth:token-type:refresh_token',
                    requested_subject: user.keycloakId,
                    audience: 'FRONTEND_CLIENT_ID',
                    scope: 'openid',
                }),
            )
            .then((res) => res.data);
    };