Skip to content

Commit c873872

Browse files
authored
Encoding context to access token IDs (#37634)
closes #37118 Signed-off-by: mposolda <[email protected]>
1 parent b545339 commit c873872

28 files changed

+722
-26
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,7 @@ public final class Constants {
203203
public static final String REQUESTED_AUDIENCE_CLIENTS = "req-aud-clients";
204204
// claim used in refresh token to know the requested audience
205205
public static final String REQUESTED_AUDIENCE = "req-aud";
206+
// Note in clientSessionContext specifying token grant type used
207+
public static final String GRANT_TYPE = OAuth2Constants.GRANT_TYPE;
206208

207209
}

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

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Map;
2727
import java.util.Set;
2828

29+
import org.keycloak.OAuth2Constants;
2930
import org.keycloak.common.ClientConnection;
3031
import org.keycloak.events.EventBuilder;
3132
import org.keycloak.events.EventType;
@@ -81,6 +82,7 @@ public static class Context {
8182
protected EventBuilder event;
8283
protected Cors cors;
8384
protected Object tokenManager;
85+
protected String grantType;
8486

8587
public Context(KeycloakSession session, Object clientConfig, Map<String, String> clientAuthAttributes,
8688
MultivaluedMap<String, String> formParams, EventBuilder event, Cors cors, Object tokenManager) {
@@ -97,22 +99,7 @@ public Context(KeycloakSession session, Object clientConfig, Map<String, String>
9799
this.event = event;
98100
this.cors = cors;
99101
this.tokenManager = tokenManager;
100-
}
101-
102-
public Context(Context context) {
103-
this.session = context.session;
104-
this.realm = context.realm;
105-
this.client = context.client;
106-
this.clientConfig = context.clientConfig;
107-
this.clientConnection = context.clientConnection;
108-
this.clientAuthAttributes = context.clientAuthAttributes;
109-
this.request = context.request;
110-
this.response = context.response;
111-
this.headers = context.headers;
112-
this.formParams = context.formParams;
113-
this.event = context.event;
114-
this.cors = context.cors;
115-
this.tokenManager = context.tokenManager;
102+
this.grantType = formParams.getFirst(OAuth2Constants.GRANT_TYPE);
116103
}
117104

118105
public void setFormParams(MultivaluedHashMap<String, String> formParams) {
@@ -182,6 +169,10 @@ public KeycloakSession getSession() {
182169
public Object getTokenManager() {
183170
return tokenManager;
184171
}
172+
173+
public String getGrantType() {
174+
return grantType;
175+
}
185176
}
186177

187178
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@
2626
*/
2727
public interface OAuth2GrantTypeFactory extends ProviderFactory<OAuth2GrantType> {
2828

29+
/**
30+
* @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
31+
* is not ideal. Shortcut should be unique across grants.
32+
*/
33+
String getShortcut();
2934
}

services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
import org.keycloak.organization.protocol.mappers.oidc.OrganizationScope;
6565
import org.keycloak.protocol.ProtocolMapper;
6666
import org.keycloak.protocol.ProtocolMapperUtils;
67+
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
68+
import org.keycloak.protocol.oidc.encode.TokenContextEncoderProvider;
6769
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
6870
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
6971
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenResponseMapper;
@@ -243,6 +245,7 @@ public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, C
243245
if (oldToken.getNonce() != null) {
244246
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, oldToken.getNonce());
245247
}
248+
clientSessionCtx.setAttribute(Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN);
246249

247250
// recreate token.
248251
AccessToken newToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
@@ -861,9 +864,9 @@ protected AccessToken applyMapper(AccessToken token, Map.Entry<ProtocolMapperMod
861864
return ((OIDCAccessTokenMapper) mapper.getValue()).transformAccessToken(token, mapper.getKey(), session, userSession, clientSessionCtx);
862865
}
863866
});
864-
final ClientModel[] requestedAucienceClients = clientSessionCtx.getAttribute(Constants.REQUESTED_AUDIENCE_CLIENTS, ClientModel[].class);
865-
if (requestedAucienceClients != null) {
866-
restrictRequestedAudience(accessToken, Arrays.stream(requestedAucienceClients)
867+
final ClientModel[] requestedAudienceClients = clientSessionCtx.getAttribute(Constants.REQUESTED_AUDIENCE_CLIENTS, ClientModel[].class);
868+
if (requestedAudienceClients != null) {
869+
restrictRequestedAudience(accessToken, Arrays.stream(requestedAudienceClients)
867870
.map(ClientModel::getClientId)
868871
.collect(Collectors.toSet()));
869872
}
@@ -1045,7 +1048,11 @@ protected IDToken applyMapper(IDToken token, Map.Entry<ProtocolMapperModel, Prot
10451048
protected AccessToken initToken(KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession,
10461049
ClientSessionContext clientSessionCtx, UriInfo uriInfo) {
10471050
AccessToken token = new AccessToken();
1048-
token.id(KeycloakModelUtils.generateId());
1051+
1052+
TokenContextEncoderProvider encoder = session.getProvider(TokenContextEncoderProvider.class);
1053+
AccessTokenContext tokenCtx = encoder.getTokenContextFromClientSessionContext(clientSessionCtx, KeycloakModelUtils.generateId());
1054+
token.id(encoder.encodeTokenId(tokenCtx));
1055+
10491056
token.type(formatTokenType(client, token));
10501057
if (UserSessionModel.SessionPersistenceState.TRANSIENT.equals(userSession.getPersistenceState())) {
10511058
token.subject(user.getId());
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2025 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
*
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
*/
19+
20+
package org.keycloak.protocol.oidc.encode;
21+
22+
import java.util.Objects;
23+
24+
/**
25+
* Some context info about the token
26+
*
27+
* @author <a href="mailto:[email protected]">Marek Posolda</a>
28+
*/
29+
public class AccessTokenContext {
30+
31+
private final SessionType sessionType;
32+
private final TokenType tokenType;
33+
private final String grantType;
34+
private final String rawTokenId;
35+
36+
public enum SessionType {
37+
ONLINE("on"),
38+
OFFLINE("of"),
39+
TRANSIENT("tr"),
40+
UNKNOWN("un");
41+
42+
private final String shortcut;
43+
44+
SessionType(String shortcut) {
45+
this.shortcut = shortcut;
46+
}
47+
48+
public String getShortcut() {
49+
return shortcut;
50+
}
51+
}
52+
53+
public enum TokenType {
54+
REGULAR("rt"),
55+
LIGHTWEIGHT("lt"),
56+
UNKNOWN("un");
57+
58+
private final String shortcut;
59+
60+
TokenType(String shortcut) {
61+
this.shortcut = shortcut;
62+
}
63+
64+
public String getShortcut() {
65+
return shortcut;
66+
}
67+
}
68+
69+
public AccessTokenContext(SessionType sessionType, TokenType tokenType, String grantType, String rawTokenId) {
70+
Objects.requireNonNull(sessionType, "Null sessionType not allowed");
71+
Objects.requireNonNull(tokenType, "Null tokenType not allowed");
72+
Objects.requireNonNull(grantType, "Null grantType not allowed");
73+
Objects.requireNonNull(grantType, "Null rawTokenId not allowed");
74+
this.sessionType = sessionType;
75+
this.tokenType = tokenType;
76+
this.grantType = grantType;
77+
this.rawTokenId = rawTokenId;
78+
}
79+
80+
public SessionType getSessionType() {
81+
return sessionType;
82+
}
83+
84+
public TokenType getTokenType() {
85+
return tokenType;
86+
}
87+
88+
public String getGrantType() {
89+
return grantType;
90+
}
91+
92+
public String getRawTokenId() {
93+
return rawTokenId;
94+
}
95+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2025 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
*
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
*/
19+
20+
package org.keycloak.protocol.oidc.encode;
21+
22+
import org.keycloak.models.ClientSessionContext;
23+
import org.keycloak.models.Constants;
24+
import org.keycloak.models.KeycloakSession;
25+
import org.keycloak.models.UserSessionModel;
26+
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
27+
28+
/**
29+
* @author <a href="mailto:[email protected]">Marek Posolda</a>
30+
*/
31+
public class DefaultTokenContextEncoderProvider implements TokenContextEncoderProvider {
32+
33+
public static final String UNKNOWN = "na";
34+
35+
private final KeycloakSession session;
36+
private final DefaultTokenContextEncoderProviderFactory factory;
37+
38+
public DefaultTokenContextEncoderProvider(KeycloakSession session,
39+
DefaultTokenContextEncoderProviderFactory factory) {
40+
this.session = session;
41+
this.factory = factory;
42+
}
43+
44+
@Override
45+
public AccessTokenContext getTokenContextFromClientSessionContext(ClientSessionContext clientSessionContext, String rawTokenId) {
46+
AccessTokenContext.SessionType sessionType;
47+
UserSessionModel userSession = clientSessionContext.getClientSession().getUserSession();
48+
if (userSession.getPersistenceState() == UserSessionModel.SessionPersistenceState.TRANSIENT) {
49+
sessionType = AccessTokenContext.SessionType.TRANSIENT;
50+
} else {
51+
sessionType = userSession.isOffline() ? AccessTokenContext.SessionType.OFFLINE : AccessTokenContext.SessionType.ONLINE;
52+
}
53+
54+
boolean useLightweightToken = AbstractOIDCProtocolMapper.getShouldUseLightweightToken(session);
55+
AccessTokenContext.TokenType tokenType = useLightweightToken ? AccessTokenContext.TokenType.LIGHTWEIGHT : AccessTokenContext.TokenType.REGULAR;
56+
57+
String grantType = clientSessionContext.getAttribute(Constants.GRANT_TYPE, String.class);
58+
if (grantType == null) {
59+
grantType = UNKNOWN;
60+
}
61+
62+
return new AccessTokenContext(sessionType, tokenType, grantType, rawTokenId);
63+
}
64+
65+
@Override
66+
public AccessTokenContext getTokenContextFromTokenId(String encodedTokenId) {
67+
int indexOf = encodedTokenId.indexOf(':');
68+
if (indexOf == -1) {
69+
return new AccessTokenContext(AccessTokenContext.SessionType.UNKNOWN, AccessTokenContext.TokenType.UNKNOWN, UNKNOWN, encodedTokenId);
70+
} else {
71+
String encodedContext = encodedTokenId.substring(0, indexOf);
72+
String rawId = encodedTokenId.substring(indexOf + 1);
73+
74+
if (encodedContext.length() != 6) {
75+
throw new IllegalArgumentException("Incorrect token id: '" + encodedTokenId + "'. Expected length of 6.");
76+
}
77+
78+
// First 2 chars are "sessionType", next 2 chars "tokenType", last 2 chars "grantType"
79+
String stShortcut = encodedContext.substring(0, 2);
80+
String ttShortcut = encodedContext.substring(2, 4);
81+
String gtShortcut = encodedContext.substring(4, 6);
82+
83+
AccessTokenContext.SessionType st = factory.getSessionTypeByShortcut(stShortcut);
84+
if (st == null) {
85+
throw new IllegalArgumentException("Incorrect token id: " + encodedTokenId + ". Unknown value '" + stShortcut + "' for session type");
86+
}
87+
AccessTokenContext.TokenType tt = factory.getTokenTypeByShortcut(ttShortcut);
88+
if (tt == null) {
89+
throw new IllegalArgumentException("Incorrect token id: " + encodedTokenId + ". Unknown value '" + ttShortcut + "' for token type");
90+
}
91+
String gt = factory.getGrantTypeByShortcut(gtShortcut);
92+
if (gt == null) {
93+
throw new IllegalArgumentException("Incorrect token id: " + encodedTokenId + ". Unknown value '" + gtShortcut + "' for grant type");
94+
}
95+
96+
return new AccessTokenContext(st, tt, gt, rawId);
97+
}
98+
}
99+
100+
@Override
101+
public String encodeTokenId(AccessTokenContext tokenContext) {
102+
if (tokenContext.getSessionType() == AccessTokenContext.SessionType.UNKNOWN) {
103+
throw new IllegalStateException("Cannot encode token with unknown sessionType");
104+
}
105+
if (tokenContext.getTokenType() == AccessTokenContext.TokenType.UNKNOWN) {
106+
throw new IllegalStateException("Cannot encode token with unknown tokenType");
107+
}
108+
109+
String grantShort = factory.getShortcutByGrantType(tokenContext.getGrantType());
110+
if (grantShort == null) {
111+
throw new IllegalStateException("Cannot encode token with unknown grantType: " + tokenContext.getGrantType());
112+
}
113+
114+
return tokenContext.getSessionType().getShortcut() +
115+
tokenContext.getTokenType().getShortcut() +
116+
grantShort +
117+
':' + tokenContext.getRawTokenId();
118+
}
119+
120+
@Override
121+
public void close() {
122+
123+
}
124+
}

0 commit comments

Comments
 (0)