Skip to content

Implement access token context encoding framework#8

Open
ShashankFC wants to merge 1 commit into
feature-token-context-baselinefrom
feature-token-context-implementation
Open

Implement access token context encoding framework#8
ShashankFC wants to merge 1 commit into
feature-token-context-baselinefrom
feature-token-context-implementation

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Test 8

Summary by CodeRabbit

  • New Features

    • Introduced token context encoding system enabling metadata encoding and retrieval for access tokens.
    • Added grant type shortcut support across OAuth2 grant types for improved token processing.
  • Bug Fixes

    • Fixed variable naming error in token audience processing logic.

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/keycloak-coderabbit#8

closes #37118

Signed-off-by: mposolda <mposolda@gmail.com>
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change introduces a token context encoding system for OIDC access tokens, storing session type, token type, and grant type information as compact shortcuts within token IDs, enabling context retrieval without relying on non-standard token claims.

Changes

Cohort / File(s) Summary
Core Token Context Infrastructure
services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java, TokenContextEncoderProvider.java, TokenContextEncoderProviderFactory.java, TokenContextEncoderSpi.java
Introduces new public classes and interfaces for token context encapsulation. AccessTokenContext defines SessionType and TokenType enums with shortcut accessors, plus fields for grantType and rawTokenId. Provider and Factory interfaces define the contract for encoding/decoding token contexts. Note: Constructor bug in AccessTokenContext duplicates grantType null-check instead of validating rawTokenId.
Token Context Encoder Implementation
services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java, DefaultTokenContextEncoderProviderFactory.java
Implements token context encoding/decoding logic with session type determination (TRANSIENT/OFFLINE/ONLINE), token type selection (LIGHTWEIGHT/REGULAR), and grant type mapping via shortcuts. Factory manages bidirectional mappings between grant shortcuts and grant IDs with dynamic discovery and caching. Encodes token IDs as SS TT GT:rawId format.
Grant Type Factory Shortcuts
services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantTypeFactory.java, ClientCredentialsGrantTypeFactory.java, PermissionGrantTypeFactory.java, PreAuthorizedCodeGrantTypeFactory.java, RefreshTokenGrantTypeFactory.java, ResourceOwnerPasswordCredentialsGrantTypeFactory.java, TokenExchangeGrantTypeFactory.java, ciba/CibaGrantTypeFactory.java, device/DeviceGrantTypeFactory.java
Each grant type factory adds a new getShortcut() method returning a 2-3 letter abbreviation (e.g., "ac", "cc", "rt", "pc", "pg", "ro", "te", "ci", "dg"). Some factories also add a static GRANT_SHORTCUT constant.
Grant Type Context Integration
server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantType.java
Adds protected field grantType and public getter getGrantType() to Context class, initialized from formParams. Removes copy constructor and integrates grant type extraction into primary constructor.
Grant Type Factory Base
server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeFactory.java
Adds new abstract method getShortcut() returning String, defining the contract for all grant type factories to provide their shortcut aliases.
Token Manager and OIDC Integration
services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java, OAuth2GrantTypeBase.java, PreAuthorizedCodeGrantType.java, ResourceOwnerPasswordCredentialsGrantType.java, tokenexchange/StandardTokenExchangeProvider.java
Integrates grant type tracking into client session contexts via Constants.GRANT_TYPE attribute. TokenManager replaces direct token.id generation with TokenContextEncoderProvider. Fixes variable name typo: requestedAucienceClients → requestedAudienceClients.
Constants and Models
server-spi-private/src/main/java/org/keycloak/models/Constants.java
Adds public constant GRANT_TYPE aliasing OAuth2Constants.GRANT_TYPE.
Mapper Updates
services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java
Changes getShouldUseLightweightToken() from package-private to public static for external accessibility.
Service Provider Registration
services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.encode.TokenContextEncoderProviderFactory, org.keycloak.provider.Spi
Registers DefaultTokenContextEncoderProviderFactory and TokenContextEncoderSpi in service provider manifest files.
Test Support
services/src/test/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderTest.java, testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
Adds comprehensive unit tests for encoding/decoding flows with various session and token types, plus helper method isAccessTokenId() for test assertions validating token IDs against expected grant shortcuts.

Sequence Diagram(s)

sequenceDiagram
    participant TokenMgr as Token Manager
    participant Encoder as TokenContextEncoderProvider
    participant Factory as DefaultTokenContextEncoderProviderFactory
    participant GrantFactory as OAuth2GrantTypeFactory
    participant Context as AccessTokenContext
    
    TokenMgr->>Encoder: encodeTokenId(AccessTokenContext)
    Encoder->>Factory: getShortcutByGrantType(grantType)
    Factory->>GrantFactory: getShortcut()
    GrantFactory-->>Factory: "ac"
    Factory-->>Encoder: "ac"
    Encoder->>Context: getSessionType(), getTokenType()
    Context-->>Encoder: SessionType, TokenType
    Encoder->>Encoder: format "SS TT GT:rawId"
    Encoder-->>TokenMgr: encodedTokenId
    
    Note over TokenMgr,Context: Later: Decode token
    TokenMgr->>Encoder: getTokenContextFromTokenId(encodedTokenId)
    Encoder->>Encoder: parse "SS TT GT:rawId"
    Encoder->>Factory: getSessionTypeByShortcut("SS")
    Factory-->>Encoder: SessionType
    Encoder->>Factory: getTokenTypeByShortcut("TT")
    Factory-->>Encoder: TokenType
    Encoder->>Factory: getGrantTypeByShortcut("GT")
    Factory-->>Encoder: grantType
    Encoder->>Context: new AccessTokenContext(...)
    Context-->>Encoder: AccessTokenContext
    Encoder-->>TokenMgr: AccessTokenContext
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Poem

🐰 Hops and shortcuts, tokens encoded so small,
Session types dance, grant types in all,
With SS, TT, GT woven within,
Context rides tokens—let encoding begin!
A factory of factories maps shortcuts with care,
Token IDs bloom with metadata to share! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete and lacks substantive content. It only contains 'Test 8' and a replication note, missing any explanation of changes, motivation, testing, or impact. Provide a detailed description explaining the feature's purpose, implementation approach, changes made, testing performed, and any relevant context for reviewers.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Implement access token context encoding framework' directly and clearly summarizes the main changes: adding a comprehensive token context encoding framework with new classes, interfaces, and factory implementations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
@services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java:
- Around line 69-78: The constructor AccessTokenContext currently calls
Objects.requireNonNull(grantType) twice and fails to validate rawTokenId, so
replace the duplicate null-check with Objects.requireNonNull(rawTokenId, "Null
rawTokenId not allowed") to ensure rawTokenId cannot be null; update the
AccessTokenContext(SessionType, TokenType, String, String) constructor to
validate rawTokenId before assigning this.rawTokenId and leave the other checks
unchanged.

In
@services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java:
- Around line 44-63: In getTokenContextFromClientSessionContext, avoid NPEs by
checking clientSessionContext.getClientSession() and the returned
AuthenticatedClientSessionModel.getUserSession() for null before dereferencing;
if clientSession is null throw an IllegalArgumentException stating
"clientSessionContext must have a valid client session", and if userSession is
null throw an IllegalArgumentException stating "client session must have a user
session", then proceed to compute sessionType, useLightweightToken (via
AbstractOIDCProtocolMapper.getShouldUseLightweightToken(session)), tokenType,
grantType and return the new AccessTokenContext as before.

In
@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java:
- Around line 476-492: The matcher isAccessTokenId has inverted logic and wrong
substring indices: in matchesSafely(String item) ensure you first split and
confirm items.length == 2, then extract the grant shortcut using
items[0].substring(4, 6) (chars at indices 4-5) and return false if that
substring does NOT equal expectedGrantShortcut (i.e., if (!items[0].substring(4,
6).equals(expectedGrantShortcut)) return false), finally return
isUUID().matches(items[1]); update the inline comment to reflect the correct
indices and grant position.
🧹 Nitpick comments (10)
services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java (1)

867-871: Good typo fix.

The variable rename from requestedAucienceClients to requestedAudienceClients improves code clarity.

Note: Line 1272 has a similar variable resquestedAudienceClients with a different typo ("resquested" instead of "requested"). Consider fixing it in the same PR for consistency.

services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java (1)

50-50: Minor inconsistency in constant usage.

The code uses OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE directly, while PreAuthorizedCodeGrantType uses PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE. Although functionally equivalent, consider using TokenExchangeGrantTypeFactory.GRANT_TYPE for consistency across grant type implementations (since TokenExchangeGrantTypeFactory is already imported).

♻️ Suggested consistency improvement
-        clientSessionCtx.setAttribute(Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE);
+        clientSessionCtx.setAttribute(Constants.GRANT_TYPE, TokenExchangeGrantTypeFactory.GRANT_TYPE);

Also applies to: 237-237

server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeFactory.java (1)

29-33: Minor Javadoc inaccuracy regarding shortcut length.

The Javadoc states "usually like 3-letters shortcut" but the implementations consistently use 2-letter shortcuts (e.g., "ac", "cc", "rt", "dg", "te"). Consider updating the documentation to reflect the actual convention.

📝 Suggested documentation fix
     /**
-     * @return usually like 3-letters shortcut of specific grants. It can be useful for example in the tokens when the amount of characters should be limited and hence using full grant name
+     * @return usually a 2-letter shortcut of specific grants. It can be useful for example in the tokens when the amount of characters should be limited and hence using full grant name
      * is not ideal. Shortcut should be unique across grants.
      */
     String getShortcut();

Additionally, since this interface is part of the SPI, adding a new abstract method is a breaking change for any external OAuth2GrantTypeFactory implementations. Verify this is the intended behavior or consider providing a default implementation if backward compatibility is required.

services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantTypeFactory.java (1)

58-61: Consider defining a constant for the shortcut value.

Other factories like CibaGrantTypeFactory define a GRANT_SHORTCUT constant (e.g., public static final String GRANT_SHORTCUT = "ci") and return it from getShortcut(). Using a constant here would improve consistency across the codebase and allow tests/dependent code to reference the shortcut symbolically.

Suggested refactor
 public class PreAuthorizedCodeGrantTypeFactory implements OAuth2GrantTypeFactory, EnvironmentDependentProviderFactory {

     public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
     public static final String CODE_REQUEST_PARAM = "pre-authorized_code";
+    public static final String GRANT_SHORTCUT = "pc";

     ...

     @Override
     public String getShortcut() {
-        return "pc";
+        return GRANT_SHORTCUT;
     }
services/src/main/java/org/keycloak/protocol/oidc/grants/TokenExchangeGrantTypeFactory.java (1)

40-43: Consider defining a constant for consistency with CibaGrantTypeFactory.

Same as PreAuthorizedCodeGrantTypeFactory — extracting the shortcut into a public static final String GRANT_SHORTCUT = "te" constant would align with the pattern used in CibaGrantTypeFactory and improve testability.

Suggested refactor
 public class TokenExchangeGrantTypeFactory implements OAuth2GrantTypeFactory, EnvironmentDependentProviderFactory {

+    public static final String GRANT_SHORTCUT = "te";
+
     @Override
     public String getId() {
         return OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE;
     }

     @Override
     public String getShortcut() {
-        return "te";
+        return GRANT_SHORTCUT;
     }
services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantTypeFactory.java (1)

38-41: Consider extracting the shortcut to a public constant for consistency.

The implementation is correct. However, AuthorizationCodeGrantTypeFactory exposes its shortcut via a public static final String GRANT_SHORTCUT constant, which allows external code (e.g., tests, token encoding logic) to reference the shortcut without hardcoding the string. Consider applying the same pattern here for consistency and to avoid magic strings in consuming code.

Suggested change
 public class ResourceOwnerPasswordCredentialsGrantTypeFactory implements OAuth2GrantTypeFactory {

+    public static final String GRANT_SHORTCUT = "ro";
+
     @Override
     public String getId() {
         return OAuth2Constants.PASSWORD;
     }

     @Override
     public String getShortcut() {
-        return "ro";
+        return GRANT_SHORTCUT;
     }
services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantTypeFactory.java (1)

38-41: Consider extracting the shortcut to a public constant for consistency.

Similar to ResourceOwnerPasswordCredentialsGrantTypeFactory, consider extracting "cc" to a public static final String GRANT_SHORTCUT constant for consistency with AuthorizationCodeGrantTypeFactory and to enable type-safe references from tests and other consumers.

Suggested change
 public class ClientCredentialsGrantTypeFactory implements OAuth2GrantTypeFactory {

+    public static final String GRANT_SHORTCUT = "cc";
+
     @Override
     public String getId() {
         return OAuth2Constants.CLIENT_CREDENTIALS;
     }

     @Override
     public String getShortcut() {
-        return "cc";
+        return GRANT_SHORTCUT;
     }
services/src/test/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderTest.java (2)

38-50: Test setup directly manipulates factory fields.

The setup directly assigns to factory.grantsByShortcuts and factory.grantsToShortcuts. This works but couples the test to the internal field structure. Consider adding a test-friendly constructor or factory method if these fields are intended to be encapsulated.


56-59: JUnit assertEquals argument order is reversed.

JUnit convention is assertEquals(expected, actual), but the assertions use (actual, expected). This won't break tests, but failure messages will show "expected X but was Y" with the values swapped, making debugging confusing.

♻️ Suggested fix (apply to all assertEquals calls in this file)
-        Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.TRANSIENT);
-        Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.LIGHTWEIGHT);
-        Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.CLIENT_CREDENTIALS);
-        Assert.assertEquals(ctx.getRawTokenId(), "1234");
+        Assert.assertEquals(AccessTokenContext.SessionType.TRANSIENT, ctx.getSessionType());
+        Assert.assertEquals(AccessTokenContext.TokenType.LIGHTWEIGHT, ctx.getTokenType());
+        Assert.assertEquals(OAuth2Constants.CLIENT_CREDENTIALS, ctx.getGrantType());
+        Assert.assertEquals("1234", ctx.getRawTokenId());
services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderFactory.java (1)

38-42: Consider restricting field visibility.

grantsByShortcuts and grantsToShortcuts are package-private. If this is intentional for testing, consider adding a comment. Otherwise, these should be private to encapsulate internal state.

♻️ Suggested change
-    Map<String, String> grantsByShortcuts;
-    Map<String, String> grantsToShortcuts;
+    private Map<String, String> grantsByShortcuts;
+    private Map<String, String> grantsToShortcuts;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b545339 and c873872.

📒 Files selected for processing (28)
  • server-spi-private/src/main/java/org/keycloak/models/Constants.java
  • server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantType.java
  • server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
  • services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java
  • services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java
  • services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/encode/TokenContextEncoderProvider.java
  • services/src/main/java/org/keycloak/protocol/oidc/encode/TokenContextEncoderProviderFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/encode/TokenContextEncoderSpi.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/TokenExchangeGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantTypeFactory.java
  • services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java
  • services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java
  • services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.encode.TokenContextEncoderProviderFactory
  • services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
  • services/src/test/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderTest.java
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
🧰 Additional context used
🧬 Code graph analysis (7)
services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java (1)
server-spi-private/src/main/java/org/keycloak/models/Constants.java (1)
  • Constants (31-209)
services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java (2)
services/src/main/java/org/keycloak/protocol/oidc/grants/TokenExchangeGrantTypeFactory.java (1)
  • TokenExchangeGrantTypeFactory (33-70)
server-spi-private/src/main/java/org/keycloak/models/Constants.java (1)
  • Constants (31-209)
services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java (3)
services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java (1)
  • AccessTokenContext (29-95)
server-spi-private/src/main/java/org/keycloak/models/Constants.java (1)
  • Constants (31-209)
server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java (1)
  • KeycloakModelUtils (103-1219)
services/src/test/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderTest.java (3)
tests/utils/src/main/java/org/keycloak/tests/utils/Assert.java (1)
  • Assert (52-176)
services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java (1)
  • DefaultTokenContextEncoderProvider (31-124)
services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java (1)
  • AccessTokenContext (29-95)
services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderFactory.java (2)
services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java (1)
  • AccessTokenContext (29-95)
services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java (1)
  • DefaultTokenContextEncoderProvider (31-124)
services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java (1)
server-spi-private/src/main/java/org/keycloak/models/Constants.java (1)
  • Constants (31-209)
services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java (1)
server-spi-private/src/main/java/org/keycloak/models/Constants.java (1)
  • Constants (31-209)
🔇 Additional comments (29)
services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantTypeFactory.java (1)

38-41: Implementation is correct and follows the established pattern.

The getShortcut() method returns "pg" (permission grant) and the shortcut is unique across all grant type factories in the codebase. All shortcuts are properly distributed: ac, rt, dg, ci, ro, te, pc, pg, cc.

services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java (1)

84-87: The change from instance to public static method is safe and appropriate.

Verification confirms no subclass overrides this method. The static modifier is correct since the method only uses the session parameter without accessing instance state. Making it public enables access from DefaultTokenContextEncoderProvider and the new token context encoding framework, which is the intended use case shown in the codebase.

services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java (3)

67-68: LGTM!

New imports for the token context encoding framework are appropriately added.


248-248: LGTM!

Setting the grant type attribute during refresh token validation is appropriate. This enables the token context encoder to capture the correct grant type when the new access token is created via createClientAccessToken at line 251.


1051-1055: No changes needed—SPI is properly configured and provider is always available.

The TokenContextEncoderProvider SPI is correctly registered in META-INF/services with DefaultTokenContextEncoderProviderFactory, and the factory's create() method always returns a non-null instance. This is a mandatory core provider with no alternative implementations and cannot be disabled or unavailable. The code at lines 1051-1055 is safe without a null check.

services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantTypeFactory.java (1)

37-47: LGTM!

The implementation correctly adds the "dg" shortcut for Device Grant, following the established 2-letter convention used by other grant type factories (e.g., "ac" for authorization code, "cc" for client credentials). The getShortcut() method properly implements the interface contract.

server-spi-private/src/main/java/org/keycloak/models/Constants.java (1)

206-207: LGTM!

The GRANT_TYPE constant appropriately centralizes access to OAuth2Constants.GRANT_TYPE for session context usage, following the established pattern in this file (similar to how other OAuth2-related constants are aliased).

services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java (1)

32-32: LGTM!

The grant type is correctly set on the session context using Constants.GRANT_TYPE as the key, enabling the token context encoding framework to track and encode the grant type used for token issuance.

Also applies to: 82-82

services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java (1)

237-237: No action needed - all token exchange code paths set the grant type.

The exchangeClientToClient method delegates to either exchangeClientToOIDCClient (which sets GRANT_TYPE at line 237) or exchangeClientToSAML2Client. In StandardTokenExchangeProvider, the SAML2 path explicitly throws an exception rejecting SAML2 as unsupported. Thus, only the OIDC path executes successfully, and it always sets the grant type. The concern is not applicable.

services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantTypeFactory.java (1)

37-47: LGTM!

Good pattern: defining GRANT_SHORTCUT as a constant allows tests and dependent code to reference it symbolically, and the getShortcut() method correctly returns this constant.

services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java (1)

109-109: LGTM — grant type correctly stored before token creation.

Setting the GRANT_TYPE attribute on clientSessionCtx before calling createClientAccessToken ensures the token context encoder can access this information. The placement is correct.

services/src/main/resources/META-INF/services/org.keycloak.provider.Spi (1)

28-28: LGTM!

The TokenContextEncoderSpi is correctly registered in the SPI service file, enabling Keycloak's provider discovery mechanism to load the new token context encoding SPI.

services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java (1)

133-135: LGTM!

The grant type attribute is correctly set on the ClientSessionContext immediately after session attachment, making it available for downstream token context encoding. The placement before getUserSession() and token generation ensures the context is properly populated for the token building phase.

services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantTypeFactory.java (1)

32-42: LGTM!

Good implementation pattern with the public constant GRANT_SHORTCUT. This allows tests and token encoding logic to reference AuthorizationCodeGrantTypeFactory.GRANT_SHORTCUT directly rather than hardcoding the string. This is the preferred approach that other grant type factories should follow.

services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.encode.TokenContextEncoderProviderFactory (1)

1-20: LGTM!

Standard Java SPI service provider registration. The file correctly registers DefaultTokenContextEncoderProviderFactory as the implementation for the TokenContextEncoderProviderFactory interface, enabling discovery by the Keycloak provider framework. The implementation class exists in the expected location and correctly implements the required interface.

services/src/main/java/org/keycloak/protocol/oidc/encode/TokenContextEncoderProviderFactory.java (1)

27-28: LGTM!

Clean SPI factory interface definition following established Keycloak patterns.

services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantTypeFactory.java (1)

33-43: LGTM!

The grant shortcut implementation follows the established pattern and integrates cleanly with the token context encoding framework.

server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantType.java (2)

85-103: LGTM!

The grantType field is properly initialized from form parameters. The nullable behavior is acceptable since downstream token context encoding handles missing grant types by defaulting to UNKNOWN.


172-175: LGTM!

Simple getter following the established pattern in this class.

services/src/main/java/org/keycloak/protocol/oidc/encode/TokenContextEncoderSpi.java (1)

29-50: LGTM!

Clean SPI definition following Keycloak conventions. Marking as internal is appropriate for this token encoding infrastructure.

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java (2)

33-36: LGTM!

Imports for grant type factories are correctly added to support the new token ID validation.


145-146: Usage is correct, but depends on the matcher fix above.

The grant shortcut constant from AuthorizationCodeGrantTypeFactory.GRANT_SHORTCUT is appropriately used here.

services/src/main/java/org/keycloak/protocol/oidc/encode/TokenContextEncoderProvider.java (1)

31-38: LGTM!

Well-designed provider interface with clear method contracts for encoding/decoding token context. The Javadoc provides useful context about the purpose.

services/src/test/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderTest.java (1)

52-113: Test coverage is comprehensive.

Good coverage of success cases (client credentials, offline tokens), error handling (incorrect grant type), edge cases (unknown grant type, old token format), and round-trip encoding verification.

services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java (2)

65-98: LGTM!

The decoding logic is well-structured:

  • Gracefully handles legacy/unencoded token IDs by returning UNKNOWN context
  • Properly validates the encoded context length
  • Clear error messages for invalid shortcuts

100-118: LGTM!

Encoding logic correctly validates that UNKNOWN session/token types cannot be encoded, ensuring only valid contexts are persisted. The token format SSTTGT:rawId is clear and consistent with the decoding logic.

services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java (1)

36-67: LGTM!

The enums are well-designed with 2-character shortcuts that enable compact token ID encoding. The UNKNOWN values provide a clear fallback for legacy or unrecognized tokens.

services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderFactory.java (2)

64-86: LGTM!

The initialization logic correctly builds bidirectional mappings and the validation at lines 82-85 effectively catches configuration errors (duplicate shortcuts or IDs) early at startup rather than letting them cause subtle runtime bugs.


106-137: Thread-safe lazy refresh pattern is appropriate.

The dynamic refresh logic handles hot-deployed grant types gracefully. While multiple threads could trigger a refresh simultaneously, the use of ConcurrentHashMap and idempotent put operations ensures correctness without requiring explicit synchronization.

Comment on lines +69 to +78
public AccessTokenContext(SessionType sessionType, TokenType tokenType, String grantType, String rawTokenId) {
Objects.requireNonNull(sessionType, "Null sessionType not allowed");
Objects.requireNonNull(tokenType, "Null tokenType not allowed");
Objects.requireNonNull(grantType, "Null grantType not allowed");
Objects.requireNonNull(grantType, "Null rawTokenId not allowed");
this.sessionType = sessionType;
this.tokenType = tokenType;
this.grantType = grantType;
this.rawTokenId = rawTokenId;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: rawTokenId is not validated for null.

Line 73 validates grantType a second time instead of rawTokenId. This allows null rawTokenId values to be stored, which would likely cause NPEs when getRawTokenId() is used downstream.

🐛 Proposed fix
     public AccessTokenContext(SessionType sessionType, TokenType tokenType, String grantType, String rawTokenId) {
         Objects.requireNonNull(sessionType, "Null sessionType not allowed");
         Objects.requireNonNull(tokenType, "Null tokenType not allowed");
         Objects.requireNonNull(grantType, "Null grantType not allowed");
-        Objects.requireNonNull(grantType, "Null rawTokenId not allowed");
+        Objects.requireNonNull(rawTokenId, "Null rawTokenId not allowed");
         this.sessionType = sessionType;
         this.tokenType = tokenType;
         this.grantType = grantType;
         this.rawTokenId = rawTokenId;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public AccessTokenContext(SessionType sessionType, TokenType tokenType, String grantType, String rawTokenId) {
Objects.requireNonNull(sessionType, "Null sessionType not allowed");
Objects.requireNonNull(tokenType, "Null tokenType not allowed");
Objects.requireNonNull(grantType, "Null grantType not allowed");
Objects.requireNonNull(grantType, "Null rawTokenId not allowed");
this.sessionType = sessionType;
this.tokenType = tokenType;
this.grantType = grantType;
this.rawTokenId = rawTokenId;
}
public AccessTokenContext(SessionType sessionType, TokenType tokenType, String grantType, String rawTokenId) {
Objects.requireNonNull(sessionType, "Null sessionType not allowed");
Objects.requireNonNull(tokenType, "Null tokenType not allowed");
Objects.requireNonNull(grantType, "Null grantType not allowed");
Objects.requireNonNull(rawTokenId, "Null rawTokenId not allowed");
this.sessionType = sessionType;
this.tokenType = tokenType;
this.grantType = grantType;
this.rawTokenId = rawTokenId;
}
🤖 Prompt for AI Agents
In
@services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java
around lines 69 - 78, The constructor AccessTokenContext currently calls
Objects.requireNonNull(grantType) twice and fails to validate rawTokenId, so
replace the duplicate null-check with Objects.requireNonNull(rawTokenId, "Null
rawTokenId not allowed") to ensure rawTokenId cannot be null; update the
AccessTokenContext(SessionType, TokenType, String, String) constructor to
validate rawTokenId before assigning this.rawTokenId and leave the other checks
unchanged.

Comment on lines +44 to +63
@Override
public AccessTokenContext getTokenContextFromClientSessionContext(ClientSessionContext clientSessionContext, String rawTokenId) {
AccessTokenContext.SessionType sessionType;
UserSessionModel userSession = clientSessionContext.getClientSession().getUserSession();
if (userSession.getPersistenceState() == UserSessionModel.SessionPersistenceState.TRANSIENT) {
sessionType = AccessTokenContext.SessionType.TRANSIENT;
} else {
sessionType = userSession.isOffline() ? AccessTokenContext.SessionType.OFFLINE : AccessTokenContext.SessionType.ONLINE;
}

boolean useLightweightToken = AbstractOIDCProtocolMapper.getShouldUseLightweightToken(session);
AccessTokenContext.TokenType tokenType = useLightweightToken ? AccessTokenContext.TokenType.LIGHTWEIGHT : AccessTokenContext.TokenType.REGULAR;

String grantType = clientSessionContext.getAttribute(Constants.GRANT_TYPE, String.class);
if (grantType == null) {
grantType = UNKNOWN;
}

return new AccessTokenContext(sessionType, tokenType, grantType, rawTokenId);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for calls to getTokenContextFromClientSessionContext
rg -n 'getTokenContextFromClientSessionContext' --type java -B 3 -A 3

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 3021


🏁 Script executed:

# Find the ClientSessionContext interface to understand the contract
fd -e java | xargs grep -l 'interface ClientSessionContext\|class ClientSessionContext' | head -5

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 154


🏁 Script executed:

# Check the UserSessionModel interface for getPersistenceState and isOffline nullability
rg -n 'getUserSession\(\)' --type java -B 2 -A 2 | head -40

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 6500


🏁 Script executed:

# Read the ClientSessionContext interface
cat services/src/main/java/org/keycloak/models/ClientSessionContext.java | head -60

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 184


🏁 Script executed:

# Check the context in TokenManager where clientSessionCtx is obtained (around line 1053)
sed -n '1030,1070p' services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 2577


🏁 Script executed:

# Look for the method that creates or passes clientSessionCtx to understand null checks
rg -n 'clientSessionCtx' services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java -B 5 | head -50

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 2799


🏁 Script executed:

# Find ClientSessionContext interface
find . -name "*.java" -type f | xargs grep -l 'interface ClientSessionContext' 2>/dev/null

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 158


🏁 Script executed:

# Find DefaultClientSessionContext implementation
find . -name "DefaultClientSessionContext.java" -type f

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 168


🏁 Script executed:

# Search for getClientSession method definition
rg -n 'getClientSession\(\)' --type java -A 2 | grep -E '(interface|class|ClientSession|getClientSession)' | head -30

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 4845


🏁 Script executed:

# Read the ClientSessionContext interface
cat ./server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 2018


🏁 Script executed:

# Read the DefaultClientSessionContext implementation
cat ./services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 12539


🏁 Script executed:

# Check if there are @Nullable or @NonNull annotations in the interfaces/classes
rg -n '@Nullable|@NonNull|@Nonnull' services/src/main/java/org/keycloak/protocol/oidc/encode/ -A 2 -B 2

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 66


Add null checks for chained calls on potentially null objects.

The chained call clientSessionContext.getClientSession().getUserSession() at line 47 could throw NPE if getClientSession() returns null. Since getClientSession() has no nullability guarantees and other parts of the codebase explicitly check for null returns, add defensive null checks:

AuthenticatedClientSessionModel clientSession = clientSessionContext.getClientSession();
if (clientSession == null) {
    throw new IllegalArgumentException("clientSessionContext must have a valid client session");
}
UserSessionModel userSession = clientSession.getUserSession();
if (userSession == null) {
    throw new IllegalArgumentException("client session must have a user session");
}

Alternatively, document the precondition that both clientSessionContext.getClientSession() and its user session must be non-null.

🤖 Prompt for AI Agents
In
@services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java
around lines 44 - 63, In getTokenContextFromClientSessionContext, avoid NPEs by
checking clientSessionContext.getClientSession() and the returned
AuthenticatedClientSessionModel.getUserSession() for null before dereferencing;
if clientSession is null throw an IllegalArgumentException stating
"clientSessionContext must have a valid client session", and if userSession is
null throw an IllegalArgumentException stating "client session must have a user
session", then proceed to compute sessionType, useLightweightToken (via
AbstractOIDCProtocolMapper.getShouldUseLightweightToken(session)), tokenType,
grantType and return the new AccessTokenContext as before.

Comment on lines +476 to +492
public static Matcher<String> isAccessTokenId(String expectedGrantShortcut) {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String item) {
String[] items = item.split(":");
if (items.length != 2) return false;
// Grant type shortcut starts at character 4th char and is 2-chars long
if (items[0].substring(3, 5).equals(expectedGrantShortcut)) return false;
return isUUID().matches(items[1]);
}

@Override
public void describeTo(Description description) {
description.appendText("Not a Token ID with expected grant: " + expectedGrantShortcut);
}
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: Inverted logic and incorrect substring indices.

The isAccessTokenId matcher has two issues:

  1. Inverted condition: Line 483 returns false when the grant shortcut matches, but should return false when it doesn't match.

  2. Wrong substring indices: Based on the token format (trltcc:uuid = session(2) + tokenType(2) + grantType(2) + ":" + rawId), the grant type shortcut is at indices 4-5, not 3-4. substring(3, 5) extracts "tc" from "trltcc", not "cc".

These bugs will cause tests to pass incorrectly (false positives) since the matcher returns true when the grant shortcut does not match.

🐛 Proposed fix
     public static Matcher<String> isAccessTokenId(String expectedGrantShortcut) {
         return new TypeSafeMatcher<String>() {
             @Override
             protected boolean matchesSafely(String item) {
                 String[] items = item.split(":");
                 if (items.length != 2) return false;
-                // Grant type shortcut starts at character 4th char and is 2-chars long
-                if (items[0].substring(3, 5).equals(expectedGrantShortcut)) return false;
+                // Grant type shortcut starts at index 4 and is 2-chars long
+                if (!items[0].substring(4, 6).equals(expectedGrantShortcut)) return false;
                 return isUUID().matches(items[1]);
             }

             @Override
             public void describeTo(Description description) {
                 description.appendText("Not a Token ID with expected grant: " + expectedGrantShortcut);
             }
         };
     }
🤖 Prompt for AI Agents
In
@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
around lines 476 - 492, The matcher isAccessTokenId has inverted logic and wrong
substring indices: in matchesSafely(String item) ensure you first split and
confirm items.length == 2, then extract the grant shortcut using
items[0].substring(4, 6) (chars at indices 4-5) and return false if that
substring does NOT equal expectedGrantShortcut (i.e., if (!items[0].substring(4,
6).equals(expectedGrantShortcut)) return false), finally return
isUUID().matches(items[1]); update the inline comment to reflect the correct
indices and grant position.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants