Skip to content

Commit

Permalink
Enhancing Light Weight Token(keycloak#22148)
Browse files Browse the repository at this point in the history
  • Loading branch information
skabano authored and mposolda committed Oct 17, 2023
1 parent c51060a commit 6112b25
Show file tree
Hide file tree
Showing 50 changed files with 1,091 additions and 304 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
* @version $Revision: 1 $
*/
public class JsonWebToken implements Serializable, Token {
public static final String AZP = "azp";
public static final String SUBJECT = "sub";

@JsonProperty("jti")
protected String id;

Expand All @@ -53,11 +56,11 @@ public class JsonWebToken implements Serializable, Token {
@JsonSerialize(using = StringOrArraySerializer.class)
@JsonDeserialize(using = StringOrArrayDeserializer.class)
protected String[] audience;
@JsonProperty("sub")
@JsonProperty(SUBJECT)
protected String subject;
@JsonProperty("typ")
protected String type;
@JsonProperty("azp")
@JsonProperty(AZP)
public String issuedFor;
protected Map<String, Object> otherClaims = new HashMap<>();

Expand Down Expand Up @@ -184,7 +187,7 @@ public JsonWebToken iat(Long iat) {
this.iat = iat;
return this;
}

/**
* @deprecated int will overflow with values after 2038. Use {@link #iat(Long)} ()} instead.
*/
Expand Down
4 changes: 4 additions & 0 deletions js/apps/admin-ui/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -3096,6 +3096,10 @@
"label": "Add to userinfo",
"tooltip": "Should the claim be added to the userinfo?"
},
"includeInIntrospection": {
"label": "Add to token introspection",
"tooltip": "Should the claim be added to the token introspection?"
},
"sectorIdentifierUri": {
"label": "Sector Identifier URI",
"tooltip": "Providers that use pairwise sub values and support Dynamic Client Registration SHOULD use the sector_identifier_uri parameter. It provides a way for a group of websites under common administrative control to have consistent pairwise sub values independent of the individual domain names. It also provides a way for Clients to change redirect_uri domains without having to reregister all their users."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ public void runUpdate(UserSessionEntity entity) {
update(task);
}

@Override
public SessionPersistenceState getPersistenceState() {
return persistenceState;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ public State getState() {
return State.valueOf(state);
}

@Override
public SessionPersistenceState getPersistenceState() {
return SessionPersistenceState.PERSISTENT;
}

@Override
public void setState(State state) {
String stateStr = state==null ? null : state.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.TimeAdapter;

Expand Down Expand Up @@ -289,4 +288,9 @@ public boolean equals(Object o) {
public int hashCode() {
return getId().hashCode();
}

@Override
public SessionPersistenceState getPersistenceState() {
return entity.getPersistenceState();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class SearchableFields {

/**
* Returns map where key is ID of the client (its UUID) and value is ID respective {@link AuthenticatedClientSessionModel} object.
* @return
* @return
*/
Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions();
/**
Expand Down Expand Up @@ -135,6 +135,8 @@ public static State valueOfInteger(Integer id) {
}
}

SessionPersistenceState getPersistenceState();

/**
* Flag used when creating user session
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.ImpersonationSessionNote;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.Urls;
import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.UserSessionUtil;
import org.keycloak.util.JsonSerialization;

import jakarta.ws.rs.core.MediaType;
Expand All @@ -55,6 +63,7 @@ public AccessTokenIntrospectionProvider(KeycloakSession session) {
public Response introspect(String token) {
try {
AccessToken accessToken = verifyAccessToken(token);
accessToken = transformAccessToken(accessToken);
ObjectNode tokenMetadata;

if (accessToken != null) {
Expand Down Expand Up @@ -106,6 +115,57 @@ public Response introspect(String token) {
}
}

private AccessToken transformAccessToken(AccessToken token) {
if (token == null) {
return null;
}

ClientModel client = realm.getClientByClientId(token.getIssuedFor());
EventBuilder event = new EventBuilder(realm, session, session.getContext().getConnection())
.event(EventType.INTROSPECT_TOKEN)
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN);
UserSessionModel userSession;
try {
userSession = UserSessionUtil.findValidSession(session, realm, token, event, client);
} catch (Exception e) {
logger.debugf("Can not get user session: %s", e.getMessage());
// Backwards compatibility
return token;
}
if (userSession.getUser() == null) {
logger.debugf("User not found");
// Backwards compatibility
return token;
}
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, session);
AccessToken smallToken = getAccessTokenFromStoredData(token, userSession);
return tokenManager.transformIntrospectionAccessToken(session, smallToken, userSession, clientSessionCtx);
}

private AccessToken getAccessTokenFromStoredData(AccessToken token, UserSessionModel userSession) {
// Copy just "basic" claims from the initial token. The same like filled in TokenManager.initToken. The rest should be possibly added by protocol mappers (only if configured for introspection response)
AccessToken newToken = new AccessToken();
newToken.id(token.getId());
newToken.type(token.getType());
newToken.subject(token.getSubject() != null ? token.getSubject() : userSession.getUser().getId());
newToken.iat(token.getIat());
newToken.exp(token.getExp());
newToken.issuedFor(token.getIssuedFor());
newToken.issuer(token.getIssuer());
newToken.setNonce(token.getNonce());
newToken.setScope(token.getScope());
newToken.setAuth_time(token.getAuth_time());
newToken.setSessionState(token.getSessionState());

// In the case of a refresh token, aud is a basic claim.
newToken.audience(token.getAudience());

// The cnf is not a claim controlled by the protocol mapper.
newToken.setConfirmation(token.getConfirmation());
return newToken;
}

protected AccessToken verifyAccessToken(String token) {
AccessToken accessToken;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,29 +133,29 @@ public Map<String, ProtocolMapperModel> getBuiltinMappers() {
private Map<String, ProtocolMapperModel> builtins = new HashMap<>();

void initBuiltIns() {
ProtocolMapperModel model;
ProtocolMapperModel model;
model = UserAttributeMapper.createClaimMapper(USERNAME,
"username",
"preferred_username", String.class.getSimpleName(),
true, true);
true, true, true);
builtins.put(USERNAME, model);

model = UserAttributeMapper.createClaimMapper(EMAIL,
"email",
"email", "String",
true, true);
true, true, true);
builtins.put(EMAIL, model);

model = UserAttributeMapper.createClaimMapper(GIVEN_NAME,
"firstName",
"given_name", "String",
true, true);
true, true, true);
builtins.put(GIVEN_NAME, model);

model = UserAttributeMapper.createClaimMapper(FAMILY_NAME,
"lastName",
"family_name", "String",
true, true);
true, true, true);
builtins.put(FAMILY_NAME, model);

createUserAttributeMapper(MIDDLE_NAME, "middleName", IDToken.MIDDLE_NAME, "String");
Expand All @@ -175,10 +175,10 @@ void initBuiltIns() {
model = UserPropertyMapper.createClaimMapper(EMAIL_VERIFIED,
"emailVerified",
"email_verified", "boolean",
true, true);
true, true, true);
builtins.put(EMAIL_VERIFIED, model);

ProtocolMapperModel fullName = FullNameMapper.create(FULL_NAME, true, true, true);
ProtocolMapperModel fullName = FullNameMapper.create(FULL_NAME, true, true, true, true);
builtins.put(FULL_NAME, fullName);

ProtocolMapperModel address = AddressMapper.createAddressMapper();
Expand All @@ -187,34 +187,34 @@ void initBuiltIns() {
model = UserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME,
KerberosConstants.GSS_DELEGATION_CREDENTIAL,
KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String",
true, false);
true, false, true);
builtins.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, model);

model = UserRealmRoleMappingMapper.create(null, REALM_ROLES, "realm_access.roles", true, false, true);
model = UserRealmRoleMappingMapper.create(null, REALM_ROLES, "realm_access.roles", true, false, true, true);
builtins.put(REALM_ROLES, model);

model = UserClientRoleMappingMapper.create(null, null, CLIENT_ROLES, "resource_access.${client_id}.roles", true, false, true);
model = UserClientRoleMappingMapper.create(null, null, CLIENT_ROLES, "resource_access.${client_id}.roles", true, false, true, true);
builtins.put(CLIENT_ROLES, model);

model = AudienceResolveProtocolMapper.createClaimMapper(AUDIENCE_RESOLVE);
model = AudienceResolveProtocolMapper.createClaimMapper(AUDIENCE_RESOLVE, true, true);
builtins.put(AUDIENCE_RESOLVE, model);

model = AllowedWebOriginsProtocolMapper.createClaimMapper(ALLOWED_WEB_ORIGINS);
model = AllowedWebOriginsProtocolMapper.createClaimMapper(ALLOWED_WEB_ORIGINS, true, true);
builtins.put(ALLOWED_WEB_ORIGINS, model);

builtins.put(IMPERSONATOR_ID.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
builtins.put(IMPERSONATOR_USERNAME.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));

model = UserAttributeMapper.createClaimMapper(UPN, "username",
"upn", "String",
true, true);
true, true, true);
builtins.put(UPN, model);

model = UserRealmRoleMappingMapper.create(null, GROUPS, GROUPS, true, true, true);
model = UserRealmRoleMappingMapper.create(null, GROUPS, GROUPS, true, true, true, true);
builtins.put(GROUPS, model);

if (Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION)) {
model = AcrProtocolMapper.create(ACR, true, true);
model = AcrProtocolMapper.create(ACR, true, true, true);
builtins.put(ACR, model);
}
}
Expand All @@ -223,7 +223,7 @@ private void createUserAttributeMapper(String name, String attrName, String clai
ProtocolMapperModel model = UserAttributeMapper.createClaimMapper(name,
attrName,
claimName, type,
true, true, false);
true, true, true, false);
builtins.put(name, model);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenResponseMapper;
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
Expand Down Expand Up @@ -785,6 +786,17 @@ protected AccessToken applyMapper(AccessToken token, Map.Entry<ProtocolMapperMod
});
}

public AccessToken transformIntrospectionAccessToken(KeycloakSession session, AccessToken token,
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
return ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx, mapper -> mapper.getValue() instanceof TokenIntrospectionTokenMapper)
.collect(new TokenCollector<AccessToken>(token) {
@Override
protected AccessToken applyMapper(AccessToken token, Map.Entry<ProtocolMapperModel, ProtocolMapper> mapper) {
return ((TokenIntrospectionTokenMapper) mapper.getValue()).transformIntrospectionToken(token, mapper.getKey(), session, userSession, clientSessionCtx);
}
});
}

public Map<String, Object> generateUserInfoClaims(AccessToken userInfo, UserModel userModel) {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", userInfo.getSubject() == null? userModel.getId() : userInfo.getSubject());
Expand Down
Loading

0 comments on commit 6112b25

Please sign in to comment.