How to obtain an access token within a Keycloak SPI?
Asked Answered
H

2

5

Use case:
From inside a EventListenerProvider on an event I want to make an authenticated REST call to one of our keycloak secured service. For this I need a token.

First I just test printing the token to check whether it is succeeded.

  public void onEvent(final Event event) {
        Keycloak k = Keycloak.getInstance("http://localhost:8080/auth", "myrealm", "[email protected]", "password", "myclient");
        AccessTokenResponse t = k.tokenManager().getAccessToken();
        logger.info(t.getSessionState());
        logger.info(t.getToken());
   }

Unfortunatly both the session_state and token is NULL.

All the data are correct, the url,the realm..etc. Otherwise we would know about that. Keycloak doesnt log anything just silently returns null.
On the top of that I can use the above code from anywhere else and it works! I can use it from a plain java main() method and still works. Getting token by hand via postman also works.

What is wrong with the Keycloak Provider? How can I get an accesstoken for a particular user?

Hydrocortisone answered 4/9, 2020 at 13:32 Comment(0)
P
7

You can use the following example to create a AccessToken:

public String getAccessToken(UserModel userModel, KeycloakSession keycloakSession) {
    KeycloakContext keycloakContext = keycloakSession.getContext();

    AccessToken token = new AccessToken();
    token.subject(userModel.getId());
    token.issuer(Urls.realmIssuer(keycloakContext.getUri().getBaseUri(), keycloakContext.getRealm().getName()));
    token.issuedNow();
    token.expiration((int) (token.getIat() + 60L)); //Lifetime of 60 seconds

    KeyWrapper key = keycloakSession.keys().getActiveKey(keycloakContext.getRealm(), KeyUse.SIG, "RS256");

    return new JWSBuilder().kid(key.getKid()).type("JWT").jsonContent(token).sign(new AsymmetricSignatureSignerContext(key));
  }

To create an access token for a client:

public String getAccessToken(ClientModel clientModel, KeycloakSession keycloakSession) {
    KeycloakContext keycloakContext = keycloakSession.getContext();
    String clientId = clientModel.getClientId();
    String clientSecret = clientModel.getClientSecret();

    JsonWebToken token = new JsonWebToken();
    token.id(UUID.randomUUID().toString());
    token.issuer(clientId);
    token.subject(clientId);
    token.issuedNow();
    token.expiration((int) (token.getIat() + 60L)); //Lifetime of 60 seconds

    return new JWSBuilder().jsonContent(token).hmac512(clientSecret);
  }

Note that you also need to specify <module name="org.keycloak.keycloak-services"/> in your jboss-deployment-structure.

Purlieu answered 15/5, 2021 at 18:52 Comment(3)
is it possible to generate a token using client id and client secret? (grant type: client credentials). In the above example we are generating a JWT token which is not a keycloak token.Irresistible
@Irresistible It should work similar but instead of an AccessToken use JsonWebToken and set the clientId as issuer and subject. Also, use the client secret as signing key. I will edit the answer with a full example.Purlieu
your second code example is outdated.Zen
P
0

(Updated)

If you are developing an internal Spi, you can access the same private apis Keycloak uses to generate access token from token request. You can quikly look into keycloak default implementation of ClientCredentialsGrantType.java. But I should warn you that there are some drawbacks on using those internal APIs, besides the fact that keycloak could change it any moment, those apis are tricky and heavly coupled. Another point way more critial is that keycloak have no clue about the URI/domain that it is running without an active Http Connection. In short, if you are in a session not started from a browser it is impossible to catch the correct context.getUri().

So, to answer you the above code will work only if are in a Http based session. Otherwise you shall provide the correct issuer Uri or simulate an Connection on context.setConnection().

    var realm = context.getRealm();

    var clientUser = userProvider.getServiceAccount(client);
    RootAuthenticationSessionModel rootAuthSession = authSessionManager.createAuthenticationSession(realm, false);
    AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
    authSession.setAuthenticatedUser(clientUser);
    authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
    authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(context.getUri().getBaseUri(), realm.getName()));
    authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, "openid email");

    var userSession = session.sessions()
            .createUserSession(
                    authSession.getParentSession().getId(),
                    realm,
                    clientUser,
                    clientUser.getUsername(),
                    null,
                    ServiceAccountConstants.CLIENT_AUTH,
                    false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);

    var clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession);
    accessToken = ((TokenManager) session.tokens()).responseBuilder(
            realm, client, new EventBuilder(realm, session), session, userSession, clientSessionCtx)
            .generateAccessToken()
            .getAccessToken();

I strongly suggest you to just make an HttpRequest to obtain credentials from keycloak public apis like the follow code:

        var auth = client.getClientId() + ":" + client.getSecret();
        var encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
        var body = "grant_type=client_credentials";
        var request = HttpRequest
                .newBuilder(tokenEndpoint)
                .setHeader("Content-Type", "application/x-www-form-urlencoded")
                .setHeader("Authorization", "Basic " + encodedAuth)
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();

        var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        var jsonResponse = JsonSerialization.readValue(response.body(), JsonNode.class);

        accessToken = TokenVerifier.create(jsonResponse.get("access_token").asText(), AccessToken.class).getToken();
Popliteal answered 11/7 at 20:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.