Custom SPI JWT Token Generation Issues in Keycloak (missing issuer)

Hello Keycloak Community,

I’m facing issues with token generation and validation in a custom SPI implementation on Keycloak version 24, where I authenticate users via a CAS validation flow. My custom SPI creates a user session and crafts tokens post-CAS validation.

Issue Description:

  • Manual Token Generation: Tokens are not generated automatically. I must manually invoke generateAccessToken() and generateRefreshToken() to produce them.
  • Missing Issuer Claim: Even after manual token generation, the tokens lack the ‘issuer’ (iss) claim, which should be automatically set by Keycloak, so I added the issuer manually.
  • Token Validity Post-Refresh: With thoses manual tokens generation and update, initially generated tokens function correctly (access and refresh tokens both work for their intended purposes), but tokens obtained from the refresh token endpoint subsequently do not have an issuer set and are invalid (can’t be use as the inital ones).

Setup Details:
This issue occurs only when using the custom SPI. The standard authentication flow in Keycloak, which does not use this SPI, functions correctly.
The client and realm configurations have been double-checked and appear correct.

Code Snippet:
This is the code responsible to create the user session and craft the tokens

public Response authenticate() throws Exception {
		ClientModel client = session.clients().getClientByClientId(realm, clientId);
		if (client == null) {
			logger.error("Client not found");
			throw new RuntimeException("Client not found");
		}

		session.getContext().setClient(client);

		// Create user session
		String ipAddress = session.getContext().getConnection().getRemoteAddr();
		UserSessionModel userSession = session.sessions().createUserSession(
				KeycloakModelUtils.generateId(), realm, user, user.getUsername(), ipAddress, "form", false, null, null,
				UserSessionModel.SessionPersistenceState.PERSISTENT);

		// Create an authentication session and link it
		AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm)
				.createAuthenticationSession(client);
		authSession.setAuthenticatedUser(user);
		authSession.setAuthNote(AuthenticationManager.SSO_AUTH, "true");

		// Create and link client session
		AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client,
				userSession);
		if (clientSession == null) {
			logger.error("Client session creation failed");
			throw new RuntimeException("Client session creation failed");
		}
		clientSession.setProtocol("openid-connect");

		// Set up context for the client session
		DefaultClientSessionContext clientSessionCtx = DefaultClientSessionContext
				.fromClientSessionScopeParameter(clientSession, session);

		// Generate tokens
		TokenManager tokenManager = new TokenManager();
		EventBuilder eventBuilder = new EventBuilder(realm, session, session.getContext().getConnection());

		TokenManager.AccessTokenResponseBuilder tokenResponseBuilder = tokenManager.responseBuilder(realm, client,
				eventBuilder, session, userSession, clientSessionCtx);

		tokenResponseBuilder.generateAccessToken();

		AccessToken accessToken = tokenManager.createClientAccessToken(session, realm, client, user, userSession,
				clientSessionCtx);
		accessToken = tokenManager.transformAccessToken(session, accessToken, userSession, clientSessionCtx);

		accessToken.issuer(getKeycloakBaseUrl());
		tokenResponseBuilder.accessToken(accessToken);

		tokenResponseBuilder.generateRefreshToken();
		RefreshToken refreshToken = tokenResponseBuilder.getRefreshToken();
		refreshToken.issuer(getKeycloakBaseUrl());
		tokenResponseBuilder.refreshToken(refreshToken);

		AccessTokenResponse response = tokenResponseBuilder.build();

		if (response == null || response.getToken() == null) {
			logger.error("Token response generation failed or no token generated");
			throw new RuntimeException("Token response generation failed or no token generated");
		}

		return Response.ok(response).build();
	}

Specific Questions:
How can I adjust my custom SPI or Keycloak settings to ensure tokens are generated automatically without manual intervention?

What might be causing the missing issuer claim in tokens generated through my custom SPI, and how can I ensure it’s included?

Why do tokens from the refresh token endpoint lack the issuer, and how can I resolve this to ensure all tokens are valid?

Any insights, suggestions, or guidance on these issues would be greatly appreciated. Thank you for your help!

The solution was to add the issuer inside the user session :slight_smile:

                // Define issuer
		clientSession.setNote(OIDCLoginProtocol.ISSUER, getKeycloakBaseUrl());

		// Set up context for the client session
		DefaultClientSessionContext clientSessionCtx = DefaultClientSessionContext
				.fromClientSessionScopeParameter(clientSession, session);

		// Generate tokens
		TokenManager tokenManager = new TokenManager();
		// EventBuilder eventBuilder = new EventBuilder(realm, session, session.getContext().getConnection());
		EventBuilder eventBuilder = new EventBuilder(this.realm, session, session.getContext().getConnection());

		AccessToken accessToken = tokenManager.createClientAccessToken(session, realm, client, user, userSession,
				clientSessionCtx);

		// Generate tokens using AccessTokenResponseBuilder
		AccessTokenResponseBuilder tokenResponseBuilder = tokenManager
				.responseBuilder(realm, client, eventBuilder, session, userSession, clientSessionCtx)
				.accessToken(accessToken)
				.generateRefreshToken();

		// Build the token response
		AccessTokenResponse response = tokenResponseBuilder.build();
1 Like