1
votes

I have to move a legacy authentication system to Keycloak and I cannot change the actual workflow on the client. As such, I need to provide with my api (in node.js) a user creation and login system that in turns create and get access tokens from Keycloak on behalf of the user.

I'm able to create a user but I've not been able to find a way to generate an access token for that user. The only workaround I found is to create a user and set a random password, then asking to grant the user providing username and password but this means that I have to store a password on my side, which is exactly the reason why I wanted to move to Keycloak.

const KcAdminClient   = require('keycloak-admin').default;
const Keycloak        = require('keycloak-connect');

const _keycloakAdmin = new KcAdminClient({
  baseUrl: process.env.KEYCLOAK_SERVER_AUTH_URL,
  realm: process.env.KEYCLOAK_REALM
});
await _keycloakAdmin.auth({
  realm: process.env.KEYCLOAK_REALM,
  username: process.env.KEYCLOAK_USER,
  password: process.env.KEYCLOAK_PASSWORD,
  grantType: 'password',
  clientId: process.env.KEYCLOAK_CLIENT_ID,
});

//Create a user and set password 
const newUser = await _keycloakAdmin.users.create({
  realm: process.env.KEYCLOAK_REALM,
  username: 'something',
  email: '[email protected]',
  firstName: 'Some',
  lastName: 'One',
  emailVerified: true,
  enabled: true,
});

await _keycloakAdmin.users.resetPassword({
    realm: process.env.KEYCLOAK_REALM,
    id: newUser.id,
    credential: {
        temporary: false,
        type: 'password',
        value: 'randompassword'
    }
});

//generate a token for the user
const _keycloak = new Keycloak({}, {
  clientId: process.env.KEYCLOAK_CLIENT_ID,
  serverUrl: process.env.KEYCLOAK_SERVER_AUTH_URL,
  realm: process.env.KEYCLOAK_REALM,
  credentials: {
      secret: process.env.KEYCLOAK_CLIENT_SECRET
  }
});
const grant = await _keycloak.grantManager.obtainDirectly('something', 'randompassword');
const access_token = grant.access_token.token;

I cannot believe doesn't exist a more elegant way to do it so I think I'm missing something fundamental in the configuration of my Keycloak client and in understanding some basic concept and naming convention. I would have expected something like

await _keycloakAdmin.users.generateAccessToken(userId, realm, clientId, ...)

but I wasn't able to find it. I only found here on SO this unanswered question: Keycloak :REST API call to get access token of a user through admin username and password

1

1 Answers

0
votes

The solution is quite complex and needs (at the time of writing) the activation of a "preview" feature of Keycloak named Token Exchange. The process is described at https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange and for my specific case I followed the instructions at https://www.keycloak.org/docs/latest/securing_apps/index.html#internal-token-to-internal-token-exchange.

First of all you need to enable the Token Exchange feature adding the switch -Dkeycloak.profile=preview to JAVA_OPTS when you run Keycloak. To check that Keycloak loaded preview features, have a look at your server info at /auth/admin/master/console/#/server-info under the section profile: Keycloak Server Info Profile

The idea of the Token Exchange is that you get a token for an administrator of your realm and then you exchange it for a token of a "normal" user. To do so you have to create (if you don't already have) two different clients for your Keycloak realm: the first one is the "starting client" used by the administrator to get the token and the second is the "target client" for which you want the token for the "normal" user.

After that you need to create an admin user for your realm. You can follow the instructions at Keycloak - Create Admin User in a Realm

Then you need to enable the target client to accept the token exchange. You should follow carefully the instructions at https://www.keycloak.org/docs/latest/securing_apps/index.html#_client_to_client_permission It's a two-steps process: create the client policy that specifies which "starting clients" can exchange a token and then enable the permissions for the target client and attach the policy just created to the token-exchange permission:

Policy attached to token-exchange permission for target client

Having finished with setting up Keycloak, you can actually issue the two calls to first get the token for the administrator of the realm and then get the token for the user with a specific userId.

Get admin token

curl --location --request POST '<your_url>/auth/realms/<your_realm>/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=<your_starting_client>' \
--data-urlencode 'username=<your_admin_username>' \
--data-urlencode 'password=<your_admin_password>' \
--data-urlencode 'realm=<your_realm>' \
--data-urlencode 'scope=openid'

Exchange admin token for "normal" user token

curl --location --request POST '<your_url>/auth/realms/<your_realm>/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'client_id=<your_starting_client>' \
--data-urlencode 'subject_token=<your_admin_token>' \
--data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:refresh_token' \
--data-urlencode 'audience=<your_target_client>' \
--data-urlencode 'requested_subject=<your_target_user_id>'

Depending on the configuration of your clients you could have to eventually specify a client_secret in this second call.