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: 'someone@domain.com',
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
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:
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:
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.