KeyCloak - 获取Token流程源码解析
客户端代理入口 - TokenService
客户端代理接口,他的实际意义是让我们可以直接通过TokenService类似于Feign等远程调用一样,可以直接调用我们的目标功能,在实际上通过代理之后,实际请求的会是 TokenEndpoint 。
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public interface TokenService {
@POST
@Path("/realms/{realm}/protocol/openid-connect/token")
AccessTokenResponse grantToken(@PathParam("realm") String realm, MultivaluedMap<String, String> map);
@POST
@Path("/realms/{realm}/protocol/openid-connect/token")
AccessTokenResponse refreshToken(@PathParam("realm") String realm, MultivaluedMap<String, String> map);
@POST
@Path("/realms/{realm}/protocol/openid-connect/logout")
void logout(@PathParam("realm") String realm, MultivaluedMap<String, String> map);
}
// Client下的TokenManager
public TokenManager(Config config, Client client) {
this.config = config;
WebTarget target = client.target(config.getServerUrl());
if (!config.isPublicClient()) {
target.register(new BasicAuthFilter(config.getClientId(), config.getClientSecret()));
}
this.tokenService = Keycloak.getClientProvider().targetProxy(target, TokenService.class);
this.accessTokenGrantType = config.getGrantType();
}
tokenService 【JAX-RS 层】 - JAX-RS路由 -> OIDCLoginProtocolService 【协议服务层】 - token -> TokenEndpoint (还不知道为什么这样路由转过去,得看看JAX-RS Api)
OIDCLoginProtocolService
Token Api 实际上在该类下注册,实际上当我们项目中集成了 KeyCloak 之后,我们的请求是由 TokenEndpoint 处理的
public class OIDCLoginProtocolService {
/* ······ */
public static UriBuilder tokenServiceBaseUrl(UriBuilder baseUriBuilder) {
return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + OIDCLoginProtocol.LOGIN_PROTOCOL);
}
/* ······ */
public static UriBuilder tokenUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "token");
}
/* ······ */
/**
* Token endpoint
*/
@Path("token")
public Object token() {
return new TokenEndpoint(session, tokenManager, event);
}
/* ······ */
}
TokenEndpoint
processGrantRequest
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@POST
public Response processGrantRequest() {
cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
MultivaluedMap<String, String> formParameters = request.getDecodedFormParameters();
if (formParameters == null) {
formParameters = new MultivaluedHashMap<>();
}
formParams = formParameters;
// 这里拿到的 GrantType 其实是 SpringSecurity 中写死的,它来自于 OAuth2AuthorizationCodeGrantRequest 构建的时候
// 直接拿的 AuthorizationGrantType 的 AUTHORIZATION_CODE 常量,所以它恒定为 authorization_code
grantType = formParams.getFirst(OIDCLoginProtocol.GRANT_TYPE_PARAM);
// https://tools.ietf.org/html/rfc6749#section-5.1
// The authorization server MUST include the HTTP "Cache-Control" response header field
// with a value of "no-store" as well as the "Pragma" response header field with a value of "no-cache".
httpResponse.setHeader("Cache-Control", "no-store");
httpResponse.setHeader("Pragma", "no-cache");
checkSsl();
checkRealm();
checkGrantType();
if (!grantType.equals(OAuth2Constants.UMA_GRANT_TYPE)
// pre-authorized grants are not necessarily used by known clients.
&& !grantType.equals(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE)) {
checkClient();
checkParameterDuplicated();
}
/*
* To request an access token that is bound to a public key using DPoP, the client MUST provide a valid DPoP
* proof JWT in a DPoP header when making an access token request to the authorization server's token endpoint.
* This is applicable for all access token requests regardless of grant type (e.g., the common
* authorization_code and refresh_token grant types and extension grants such as the JWT
* authorization grant [RFC7523])
*/
DPoPUtil.retrieveDPoPHeaderIfPresent(session, clientConfig, event, cors).ifPresent(dPoP -> {
session.setAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, dPoP);
});
OAuth2GrantType.Context context = new OAuth2GrantType.Context(session, clientConfig, clientAuthAttributes,
formParams, event, cors, tokenManager);
// 因为上面 grant_type 恒定为 authorization_code 所以实际上 grant 其实是 AuthorizationCodeGrantType
return grant.process(context);
}
AuthorizationCodeGrantType
process
整个方法,从头到尾,其实都是在做校验 先是 授权码、会话、用户、重定向地址、客户端配置、PKCE、DPoP、Client作用域检查
public Response process(Context context) {
setContext(context);
/**
下面相当一大段是利用 token 获取客户端Session、用户Serssion、用户本身做检查
*/
// 对授权码做验证 ,也就是我们登录的时候拿到的 code 【有时间可以研究一下,授权码的生成和验证】
String code = formParams.getFirst(OAuth2Constants.CODE);
if (code == null) {
String errorMessage = "Missing parameter: " + OAuth2Constants.CODE;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, errorMessage, Response.Status.BAD_REQUEST);
}
OAuth2CodeParser.ParseResult parseResult = OAuth2CodeParser.parseCode(session, code, realm, event);
// 如果是错误的授权码 (可能是重复使用的授权码)
if (parseResult.isIllegalCode()) {
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
// Attempt to use same code twice should invalidate existing clientSession
if (clientSession != null) {
clientSession.detachFromUserSession();
}
event.error(Errors.INVALID_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST);
}
// 对Client Session做检查
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
// 如果授权码已经过期
if (parseResult.isExpiredCode()) {
event.error(Errors.EXPIRED_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST);
}
// 获取用户 Session 做检查
UserSessionModel userSession = null;
if (clientSession != null) {
userSession = clientSession.getUserSession();
}
if (userSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User session not found", Response.Status.BAD_REQUEST);
}
// 对用户做检查
UserModel user = userSession.getUser();
if (user == null) {
event.error(Errors.USER_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User not found", Response.Status.BAD_REQUEST);
}
event.user(userSession.getUser());
if (!user.isEnabled()) {
event.error(Errors.USER_DISABLED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST);
}
/**
直到这里为止,开始尝试对重定向的地址做检查,估计目的是防止中间者劫持 code 伪造信息攻击
*/
OAuth2Code codeData = parseResult.getCodeData();
String redirectUri = codeData.getRedirectUriParam();
String redirectUriParam = formParams.getFirst(OAuth2Constants.REDIRECT_URI);
// KEYCLOAK-4478 Backwards compatibility with the adapters earlier than KC 3.4.2
if (redirectUriParam != null && redirectUriParam.contains("session_state=") && !redirectUri.contains("session_state=")) {
redirectUriParam = KeycloakUriBuilder.fromUri(redirectUriParam)
.replaceQueryParam(OAuth2Constants.SESSION_STATE, null)
.build().toString();
}
// 对重定向地址做检查,如果不一致那么报错
if (redirectUri != null && !redirectUri.equals(redirectUriParam)) {
String errorMessage = "Parameter 'redirect_uri' did not match originally saved redirect URI used in initial OIDC request. Saved redirectUri: %s, redirectUri parameter: %s";
event.detail(Details.REASON, String.format(errorMessage, redirectUri, redirectUriParam));
event.error(Errors.INVALID_REDIRECT_URI);
logger.tracef(errorMessage, redirectUri, redirectUriParam);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Incorrect redirect_uri", Response.Status.BAD_REQUEST);
}
// 对客户端ID做检查
if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
String errorMessage = "Auth error: Found different client_id in clientSession";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CLIENT);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
// 客户端是否使用标准流程
if (!client.isStandardFlowEnabled()) {
String errorMessage = "Client not allowed to exchange code";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
String errorMessage = "Session not active";
event.detail(Details.REASON, errorMessage);
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
// https://tools.ietf.org/html/rfc7636#section-4.6
String codeVerifier = formParams.getFirst(OAuth2Constants.CODE_VERIFIER);
String codeChallenge = codeData.getCodeChallenge();
String codeChallengeMethod = codeData.getCodeChallengeMethod();
String authUserId = user.getId();
String authUsername = user.getUsername();
if (authUserId == null) {
authUserId = "unknown";
}
if (authUsername == null) {
authUsername = "unknown";
}
// PKCE 验证 (不知道是什么,估计是安全手段)
if (codeChallengeMethod != null && !codeChallengeMethod.isEmpty()) {
PkceUtils.checkParamsForPkceEnforcedClient(codeVerifier, codeChallenge, codeChallengeMethod, authUserId, authUsername, event, cors);
} else {
// PKCE Activation is OFF, execute the codes implemented in KEYCLOAK-2604
PkceUtils.checkParamsForPkceNotEnforcedClient(codeVerifier, codeChallenge, codeChallengeMethod, authUserId, authUsername, event, cors);
}
// DPoP 验证 (不知道是啥,估计是安全手段)
// https://datatracker.ietf.org/doc/html/rfc9449#section-10
DPoPUtil.validateDPoPJkt(codeData.getDpopJkt(), session, event, cors);
// 客户端自定义策略
try {
session.clientPolicy().triggerOnEvent(new TokenRequestContext(formParams, parseResult));
} catch (ClientPolicyException cpe) {
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
}
// 更新会话状态和授权情况
updateClientSession(clientSession);
updateUserSessionFromClientAuth(userSession);
// Compute client scopes again from scope parameter. Check if user still has them granted
// (but in code-to-token request, it could just theoretically happen that they are not available)
// 从授权码获取的数据中获取 scope
String scopeParam = codeData.getScope();
// 构造 Supplier 用于获取请求的客户端
/**
TokenManager
- getRequestedClientScopes 目的是获取请求的client scope (所有默认 client 作用域 + 自身client + 部分可选)
并且对 scope 参数做解析。keycloak上的配置 默认scope 有 openid、profile。phone、email则是可选的
- verifyConsentStillAvailable 目的是获取所有已经授权的 client scope 【怎么授权不知道】
*/
Supplier<Stream<ClientScopeModel>> clientScopesSupplier = () -> TokenManager.getRequestedClientScopes(session, scopeParam, client, user);
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopesSupplier.get())) {
String errorMessage = "Client no longer has requested consent from user";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, errorMessage, Response.Status.BAD_REQUEST);
}
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, scopeParam, session);
// 设置nonce参数,后续创建 token 会用到该参数
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, codeData.getNonce());
return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, s -> {return new TokenResponseContext(formParams, parseResult, clientSessionCtx, s);});
}
createTokenResponse
protected Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
String scopeParam, boolean code, Function<TokenManager.AccessTokenResponseBuilder, ClientPolicyContext> clientPolicyContextGenerator) {
// 设置授权类型
clientSessionCtx.setAttribute(Constants.GRANT_TYPE, context.getGrantType());
// 利用tokenManager 创建 Token
AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
// 初始化响应结果构造器
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
.responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(token);
// 判断是否需要 refreshToken
boolean useRefreshToken = clientConfig.isUseRefreshToken();
if (useRefreshToken) {
responseBuilder.generateRefreshToken();
// 如果是首次访问、离线下创建的在线会话,把Session删除 (不知道在说啥)
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(responseBuilder.getRefreshToken().getType())
&& clientSessionCtx.getClientSession().getNote(AuthenticationProcessor.FIRST_OFFLINE_ACCESS) != null) {
// the online session can be removed if first created for offline access
session.sessions().removeUserSession(realm, userSession);
}
}
// MTLS 绑定 (不知道是啥,估计是一种安全手段)
checkAndBindMtlsHoKToken(responseBuilder, useRefreshToken);
// 如果是 OIDC 请求,构造 Token 的 ID 令牌
if (TokenUtil.isOIDCRequest(scopeParam)) {
responseBuilder.generateIDToken().generateAccessTokenHash();
}
// Client 自定义的策略处理
if (clientPolicyContextGenerator != null) {
try {
session.clientPolicy().triggerOnEvent(clientPolicyContextGenerator.apply(responseBuilder));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, cpe.getErrorDetail());
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}
}
// 构造 Token Response 并且返回, build 方法会做实际编码处理
AccessTokenResponse res = null;
if (code) {
try {
res = responseBuilder.build();
} catch (RuntimeException re) {
if ("can not get encryption KEK".equals(re.getMessage())) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
"can not get encryption KEK", Response.Status.BAD_REQUEST);
} else {
throw re;
}
}
} else {
res = responseBuilder.build();
}
event.success();
// 利用 CORS 头对Response做跨域处理
return cors.add(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE));
}
TokenManager
createClientAccessToken
public AccessToken createClientAccessToken(KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession,
ClientSessionContext clientSessionCtx) {
AccessToken token = initToken(session, realm, client, user, userSession, clientSessionCtx, session.getContext().getUri());
token = transformAccessToken(session, token, userSession, clientSessionCtx);
return token;
}
initToken
protected AccessToken initToken(KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession,
ClientSessionContext clientSessionCtx, UriInfo uriInfo) {
AccessToken token = new AccessToken();
// 生成 Token 的 ID
TokenContextEncoderProvider encoder = session.getProvider(TokenContextEncoderProvider.class);
AccessTokenContext tokenCtx = encoder.getTokenContextFromClientSessionContext(clientSessionCtx, KeycloakModelUtils.generateId());
token.id(encoder.encodeTokenId(tokenCtx));
// 设置 Token 的类型
token.type(formatTokenType(client, token));
// 如果是单个请求用的临时Token(虚拟会话 - 注释说的),那么将 Subject 设置为用户的 Id
if (UserSessionModel.SessionPersistenceState.TRANSIENT.equals(userSession.getPersistenceState())) {
token.subject(user.getId());
}
// 设置 token 的 issued 时间
token.issuedNow();
// 在 token 中记录是哪一个 client 发的
token.issuedFor(client.getClientId());
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
token.issuer(clientSession.getNote(OIDCLoginProtocol.ISSUER));
// 设置 令牌作用域,这个跟 Client Scope 和前面方法的调用有关系
token.setScope(clientSessionCtx.getScopeString());
// 设置 acr (是否为单点登录认证,如果是0那么说明是 SSO) acr 的配置,估计跟 Client Scope 中 act 是有关系
// Backwards compatibility behaviour prior step-up authentication was introduced
// Protocol mapper is supposed to set this in case "step_up_authentication" feature enabled
if (!Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION)) {
String acr = AuthenticationManager.isSSOAuthentication(clientSession) ? "0" : "1";
token.setAcr(acr);
}
// 设置 token 中的 session id
token.setSessionId(userSession.getId());
// 检查是否设置了 Client Scope 中的 offline_access
ClientScopeModel offlineAccessScope = KeycloakModelUtils.getClientScopeByName(realm, OAuth2Constants.OFFLINE_ACCESS);
boolean offlineTokenRequested = offlineAccessScope == null ? false
: clientSessionCtx.getClientScopeIds().contains(offlineAccessScope.getId());
// 计算 token 的过期时间
token.exp(getTokenExpiration(realm, client, userSession, clientSession, offlineTokenRequested));
// 添加跟踪信息
// Tracing
var tracing = session.getProvider(TracingProvider.class);
var span = tracing.getCurrentSpan();
if (span.isRecording()) {
// 令牌发行者
span.setAttribute(TracingAttributes.TOKEN_ISSUER, token.getIssuer());
// 令牌的SessionID
span.setAttribute(TracingAttributes.TOKEN_SID, token.getSessionId());
// 令牌的Id
span.setAttribute(TracingAttributes.TOKEN_ID, token.getId());
}
return token;
}
transformAccessToken
public AccessToken transformAccessToken(KeycloakSession session, AccessToken token,
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
// 收集各种各样的OIDCAccesTokenMapper,并且按照利用他们对 token 做转换映射(mapper)
AccessToken accessToken = ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx, mapper -> mapper.getValue() instanceof OIDCAccessTokenMapper)
.collect(new TokenCollector<AccessToken>(token) {
@Override
protected AccessToken applyMapper(AccessToken token, Map.Entry<ProtocolMapperModel, ProtocolMapper> mapper) {
return ((OIDCAccessTokenMapper) mapper.getValue()).transformAccessToken(token, mapper.getKey(), session, userSession, clientSessionCtx);
}
});
// 获取限制访问的 Client ,并且将他们从 token 中移除
final ClientModel[] requestedAudienceClients = clientSessionCtx.getAttribute(Constants.REQUESTED_AUDIENCE_CLIENTS, ClientModel[].class);
if (requestedAudienceClients != null) {
restrictRequestedAudience(accessToken, Arrays.stream(requestedAudienceClients)
.map(ClientModel::getClientId)
.collect(Collectors.toSet()));
}
return accessToken;
}
但是实际上 OIDCAccessTokenMapper 涉及到的 mappers 有点多。在源码中涉及到的有22个。但实际有单独对transformAccessToken有实现的主要是以下的类。(不排除还有漏掉的,这些类基本都是针对一些属性做调整和修改,有需要再看)
AbstractPairwiseSubMapper
AllowedWebOriginsProtocolMapper
AudienceResolveProtocolMapper
HardcodedRole
NonceBackwardsCompatibleMapper
AccessTokenResponse
build
对accessToken和refreshToken做解析编码、构造TokenResponse并响应
public AccessTokenResponse build() {
if (response != null) return response;
/**
创建 evnet 做审计记录
*/
if (accessToken != null) {
event.detail(Details.TOKEN_ID, accessToken.getId());
}
if (refreshToken != null) {
if (event.getEvent().getDetails().containsKey(Details.REFRESH_TOKEN_ID)) {
event.detail(Details.UPDATED_REFRESH_TOKEN_ID, refreshToken.getId());
} else {
event.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
}
event.detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
}
// 对 accessToken 做编码,并设置过期时间等
AccessTokenResponse res = new AccessTokenResponse();
if (accessToken != null) {
// 需要注意的是这里的 encode 底层实现的时候,会对 token 本身做签名添加、非对称加密等处理,增加token的安全性
String encodedToken = session.tokens().encode(accessToken);
res.setToken(encodedToken);
res.setTokenType(responseTokenType);
res.setSessionState(accessToken.getSessionState());
if (accessToken.getExp() != 0) {
res.setExpiresIn(accessToken.getExp() - Time.currentTime());
}
}
// 生成 accessToken 哈希
if (generateAccessTokenHash) {
String atHash = generateOIDCHash(res.getToken());
idToken.setAccessTokenHash(atHash);
}
if (codeHash != null) {
idToken.setCodeHash(codeHash);
}
// Financial API - Part 2: Read and Write API Security Profile
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
if (stateHash != null) {
idToken.setStateHash(stateHash);
}
// 编码、设置ID令牌
if (idToken != null) {
String encodedToken = session.tokens().encodeAndEncrypt(idToken);
res.setIdToken(encodedToken);
}
// 编码设置 RefershToken
if (refreshToken != null) {
String encodedToken = session.tokens().encode(refreshToken);
res.setRefreshToken(encodedToken);
Long exp = refreshToken.getExp();
if (exp != null && exp > 0) {
res.setRefreshExpiresIn(exp - Time.currentTime());
}
}
// 设置令牌生效时间
int notBefore = realm.getNotBefore();
if (client.getNotBefore() > notBefore) notBefore = client.getNotBefore();
final UserModel user = userSession.getUser();
if (! isLightweightUser(user)) {
int userNotBefore = session.users().getNotBeforeOfUser(realm, user);
if (userNotBefore > notBefore) notBefore = userNotBefore;
}
res.setNotBeforePolicy(notBefore);
// 收集 tokenResponse 的 mapper 并调用处理
res = transformAccessTokenResponse(session, res, userSession, clientSessionCtx);
// OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
String responseScope = clientSessionCtx.getScopeString();
res.setScope(responseScope);
event.detail(Details.SCOPE, responseScope);
response = res;
// 最终返回 TokenResponse
return response;
}
transformAccessTokenResponse
public AccessTokenResponse transformAccessTokenResponse(KeycloakSession session, AccessTokenResponse accessTokenResponse,
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
return ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx, mapper -> mapper.getValue() instanceof OIDCAccessTokenResponseMapper)
.collect(new TokenCollector<AccessTokenResponse>(accessTokenResponse) {
@Override
protected AccessTokenResponse applyMapper(AccessTokenResponse token, Map.Entry<ProtocolMapperModel, ProtocolMapper> mapper) {
return ((OIDCAccessTokenResponseMapper) mapper.getValue()).transformAccessTokenResponse(token, mapper.getKey(), session, userSession, clientSessionCtx);
}
});
}
跟上面对 accessToken 做转换的类似,但是这里实际上只有一个类是匹配上的 AbstractOIDCProtocolMapper
AbstractOIDCProtocolMapper
public AccessTokenResponse transformAccessTokenResponse(AccessTokenResponse accessTokenResponse, ProtocolMapperModel mappingModel,
KeycloakSession session, UserSessionModel userSession,
ClientSessionContext clientSessionCtx) {
if (!OIDCAttributeMapperHelper.includeInAccessTokenResponse(mappingModel)) {
return accessTokenResponse;
}
setClaim(accessTokenResponse, mappingModel, userSession, session, clientSessionCtx);
return accessTokenResponse;
}