Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
### 代码内容：
package org.keycloak.protocol.oidc;

import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.approveOAuth2DeviceAuthorization;
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.denyOAuth2DeviceAuthorization;
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow;

import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenIdGenerator;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.common.util.Time;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
import org.keycloak.protocol.oidc.utils.LogoutUtil;
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.ImplicitHybridTokenResponse;
import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;

import java.io.IOException;
import java.net.URI;
import java.util.Optional;
import java.util.UUID;

import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

/**
 * @author Bill Burke
 * @author Stian Thorgersen
 */
public class OIDCLoginProtocol implements LoginProtocol {

    public static final String LOGIN_PROTOCOL = "openid-connect";
    public static final String STATE_PARAM = "state";
    public static final String SCOPE_PARAM = "scope";
    public static final String CODE_PARAM = "code";
    public static final String RESPONSE_TYPE_PARAM = "response_type";
    public static final String GRANT_TYPE_PARAM = "grant_type";
    public static final String REDIRECT_URI_PARAM = "redirect_uri";
    public static final String POST_LOGOUT_REDIRECT_URI_PARAM = "post_logout_redirect_uri";
    public static final String CLIENT_ID_PARAM = "client_id";
    public static final String NONCE_PARAM = "nonce";
    public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
    public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
    public static final String LOGIN_HINT_PARAM = "login_hint";
    public static final String REQUEST_PARAM = "request";
    public static final String REQUEST_URI_PARAM = "request_uri";
    public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
    public static final String CLAIMS_PARAM = "claims";
    public static final String ACR_PARAM = "acr_values";
    public static final String ID_TOKEN_HINT = "id_token_hint";

    public static final String LOGOUT_STATE_PARAM = "OIDC_LOGOUT_STATE_PARAM";
    public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI";
    public static final String LOGOUT_VALIDATED_ID_TOKEN_SESSION_STATE = "OIDC_LOGOUT_VALIDATED_ID_TOKEN_SESSION_STATE";
    public static final String LOGOUT_VALIDATED_ID_TOKEN_ISSUED_AT = "OIDC_LOGOUT_VALIDATED_ID_TOKEN_ISSUED_AT";

    public static final String ISSUER = "iss";

    public static final String RESPONSE_MODE_PARAM = "response_mode";

    public static final String PROMPT_VALUE_NONE = "none";
    public static final String PROMPT_VALUE_LOGIN = "login";
    public static final String PROMPT_VALUE_CONSENT = "consent";
    public static final String PROMPT_VALUE_SELECT_ACCOUNT = "select_account";

    // Client authentication methods
    public static final String CLIENT_SECRET_BASIC = "client_secret_basic";
    public static final String CLIENT_SECRET_POST = "client_secret_post";
    public static final String CLIENT_SECRET_JWT = "client_secret_jwt";
    public static final String PRIVATE_KEY_JWT = "private_key_jwt";
    public static final String TLS_CLIENT_AUTH = "tls_client_auth";

    // https://tools.ietf.org/html/rfc7636#section-4.3
    public static final String CODE_CHALLENGE_PARAM = "code_challenge";
    public static final String CODE_CHALLENGE_METHOD_PARAM = "code_challenge_method";

    // https://tools.ietf.org/html/rfc7636#section-4.2
    public static final int PKCE_CODE_CHALLENGE_MIN_LENGTH = 43;
    public static final int PKCE_CODE_CHALLENGE_MAX_LENGTH = 128;

    // https://tools.ietf.org/html/rfc7636#section-4.1
    public static final int PKCE_CODE_VERIFIER_MIN_LENGTH = 43;
    public static final int PKCE_CODE_VERIFIER_MAX_LENGTH = 128;

    // https://tools.ietf.org/html/rfc7636#section-6.2.2
    public static final String PKCE_METHOD_PLAIN = "plain";
    public static final String PKCE_METHOD_S256 = "S256";

    private static final Logger logger = Logger.getLogger(OIDCLoginProtocol.class);

    protected KeycloakSession session;

    protected RealmModel realm;

    protected UriInfo uriInfo;

    protected HttpHeaders headers;

    protected EventBuilder event;

    protected OIDCResponseType responseType;
    protected OIDCResponseMode responseMode;

    public OIDCLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event) {
        this.session = session;
        this.realm = realm;
        this.uriInfo = uriInfo;
        this.headers = headers;
        this.event = event;
    }

    public OIDCLoginProtocol() {

    }

    private void setupResponseTypeAndMode(String responseType, String responseMode) {
        this.responseType = OIDCResponseType.parse(responseType);
        this.responseMode = OIDCResponseMode.parse(responseMode, this.responseType);
        this.event.detail(Details.RESPONSE_TYPE, responseType);
        this.event.detail(Details.RESPONSE_MODE, this.responseMode.toString().toLowerCase());
    }

    @Override
    public OIDCLoginProtocol setSession(KeycloakSession session) {
        this.session = session;
        return this;
    }

    @Override
    public OIDCLoginProtocol setRealm(RealmModel realm) {
        this.realm = realm;
        return this;
    }

    @Override
    public OIDCLoginProtocol setUriInfo(UriInfo uriInfo) {
        this.uriInfo = uriInfo;
        return this;
    }

    @Override
    public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers) {
        this.headers = headers;
        return this;
    }

    @Override
    public OIDCLoginProtocol setEventBuilder(EventBuilder event) {
        this.event = event;
        return this;
    }

    @Override
    public Response authenticated(AuthenticationSessionModel authSession, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
        AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();

        if (isOAuth2DeviceVerificationFlow(authSession)) {
            return approveOAuth2DeviceAuthorization(authSession, clientSession, session);
        }

        String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
        String responseModeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
        setupResponseTypeAndMode(responseTypeParam, responseModeParam);

        String redirect = authSession.getRedirectUri();
        OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, clientSession);
        String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
        logger.debugv("redirectAccessCode: state: {0}", state);
        if (state != null)
            redirectUri.addParam(OAuth2Constants.STATE, state);

        OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientModel(clientSession.getClient());
        if (!clientConfig.isExcludeSessionStateFromAuthResponse()) {
            redirectUri.addParam(OAuth2Constants.SESSION_STATE, userSession.getId());
        }

        String nonce = authSession.getClientNote(OIDCLoginProtocol.NONCE_PARAM);
        clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, nonce);

        String kcActionStatus = authSession.getClientNote(Constants.KC_ACTION_STATUS);
        if (kcActionStatus != null) {
            redirectUri.addParam(Constants.KC_ACTION_STATUS, kcActionStatus);
        }

        // Standard or hybrid flow
        String code = null;
        if (responseType.hasResponseType(OIDCResponseType.CODE)) {
            OAuth2Code codeData = new OAuth2Code(UUID.randomUUID().toString(),
                    Time.currentTime() + userSession.getRealm().getAccessCodeLifespan(),
                    nonce,
                    authSession.getClientNote(OAuth2Constants.SCOPE),
                    authSession.getClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM),
                    authSession.getClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM),
                    authSession.getClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM));

            code = OAuth2CodeParser.persistCode(session, clientSession, codeData);
            redirectUri.addParam(OAuth2Constants.CODE, code);
        }

        // Implicit or hybrid flow
        if (responseType.isImplicitOrHybridFlow()) {
            org.keycloak.protocol.oidc.TokenManager tokenManager = new org.keycloak.protocol.oidc.TokenManager();
            org.keycloak.protocol.oidc.TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, clientSession.getClient(), event, session, userSession, clientSessionCtx)
                    .generateAccessToken();

            if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {

                responseBuilder.generateIDToken(isIdTokenAsDetachedSignature(clientSession.getClient()));

                if (responseType.hasResponseType(OIDCResponseType.TOKEN)) {
                    responseBuilder.generateAccessTokenHash();
                }

                if (responseType.hasResponseType(OIDCResponseType.CODE)) {
                    responseBuilder.generateCodeHash(code);
                }

                // Financial API - Part 2: Read and Write API Security Profile
                // http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
                if (state != null && !state.isEmpty())
                    responseBuilder.generateStateHash(state);
            }

            try {
                session.clientPolicy().triggerOnEvent(new ImplicitHybridTokenResponse(authSession, clientSessionCtx, responseBuilder));
            } catch (ClientPolicyException cpe) {
                event.error(cpe.getError());
                new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
                redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, cpe.getError());
                return redirectUri.build();
            }

            AccessTokenResponse res = responseBuilder.build();

            if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {
                redirectUri.addParam(OAuth2Constants.ID_TOKEN, res.getIdToken());
            }

            if (responseType.hasResponseType(OIDCResponseType.TOKEN)) {
                redirectUri.addParam(OAuth2Constants.ACCESS_TOKEN, res.getToken());
                redirectUri.addParam(OAuth2Constants.TOKEN_TYPE, res.getTokenType());
                redirectUri.addParam(OAuth2Constants.EXPIRES_IN, String.valueOf(res.getExpiresIn()));
            }
        }

        return redirectUri.build();
    }

    // For FAPI 1.0 Advanced
    private boolean isIdTokenAsDetachedSignature(ClientModel client) {
        if (client == null) return false;
        return Boolean.valueOf(Optional.ofNullable(client.getAttribute(OIDCConfigAttributes.ID_TOKEN_AS_DETACHED_SIGNATURE)).orElse(Boolean.FALSE.toString())).booleanValue();
    }

    @Override
    public Response sendError(AuthenticationSessionModel authSession, Error error) {
        if (isOAuth2DeviceVerificationFlow(authSession)) {
            return denyOAuth2DeviceAuthorization(authSession, error, session);
        }
        String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
        String responseModeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
        setupResponseTypeAndMode(responseTypeParam, responseModeParam);

        String redirect = authSession.getRedirectUri();
        String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);

        OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, null);

        if (error != Error.CANCELLED_AIA_SILENT) {
            redirectUri.addParam(OAuth2Constants.ERROR, translateError(error));
        }
        if (error == Error.CANCELLED_AIA) {
            redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, "User cancelled aplication-initiated action.");
        }
        if (state != null) {
            redirectUri.addParam(OAuth2Constants.STATE, state);
        }

        new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
        return redirectUri.build();
    }

    private String translateError(Error error) {
        switch (error) {
            case CANCELLED_BY_USER:
            case CANCELLED_AIA:
            case CONSENT_DENIED:
                return OAuthErrorException.ACCESS_DENIED;
            case PASSIVE_INTERACTION_REQUIRED:
                return OAuthErrorException.INTERACTION_REQUIRED;
            case PASSIVE_LOGIN_REQUIRED:
                return OAuthErrorException.LOGIN_REQUIRED;
            default:
                ServicesLogger.LOGGER.untranslatedProtocol(error.name());
                return OAuthErrorException.SERVER_ERROR;
        }
    }

    @Override
    public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
        ClientModel client = clientSession.getClient();
        if (OIDCAdvancedConfigWrapper.fromClientModel(clientSession.getClient()).getBackchannelLogoutUrl() != null) {
            return new ResourceAdminManager(session).logoutClientSessionWithBackchannelLogoutUrl(client, clientSession);
        } else {
            return new ResourceAdminManager(session).logoutClientSession(realm, client, clientSession);
        }
    }

    @Override
    public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
        if (clientSession != null) {
            ClientModel client = clientSession.getClient();
            if (OIDCAdvancedConfigWrapper.fromClientModel(client).isFrontChannelLogoutEnabled()) {
                FrontChannelLogoutHandler logoutInfo = FrontChannelLogoutHandler.currentOrCreate(session, clientSession);
                logoutInfo.addClient(client);
            }
            clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
        }
        return null;
    }

    @Override
    public Response finishBrowserLogout(UserSessionModel userSession, AuthenticationSessionModel logoutSession) {
        event.event(EventType.LOGOUT);

        String redirectUri = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
        if (redirectUri != null) {
            event.detail(Details.REDIRECT_URI, redirectUri);
        }
        event.user(userSession.getUser()).session(userSession).success();
        FrontChannelLogoutHandler frontChannelLogoutHandler = FrontChannelLogoutHandler.current(session);
        if (frontChannelLogoutHandler != null) {
            String finalRedirectUri = redirectUri == null ? null : LogoutUtil.getRedirectUriWithAttachedState(redirectUri, logoutSession).toString();
            return frontChannelLogoutHandler.renderLogoutPage(finalRedirectUri);
        }

        return LogoutUtil.sendResponseAfterLogoutFinished(session, logoutSession);
    }

    @Override
    public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) {
        return isPromptLogin(authSession) || isAuthTimeExpired(userSession, authSession) || isReAuthRequiredForKcAction(userSession, authSession);
    }

    protected boolean isPromptLogin(AuthenticationSessionModel authSession) {
        String prompt = authSession.getClientNote(OIDCLoginProtocol.PROMPT_PARAM);
        return TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_LOGIN);
    }

    protected boolean isAuthTimeExpired(UserSessionModel userSession, AuthenticationSessionModel authSession) {
        String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME);
        String maxAge = authSession.getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
        if (maxAge == null) {
            return false;
        }

        int authTimeInt = authTime==null ? 0 : Integer.parseInt(authTime);
        int maxAgeInt = Integer.parseInt(maxAge);

        if (authTimeInt + maxAgeInt < Time.currentTime()) {
            logger.debugf("Authentication time is expired, needs to reauthenticate. userSession=%s, clientId=%s, maxAge=%d, authTime=%d", userSession.getId(),
                    authSession.getClient().getId(), maxAgeInt, authTimeInt);
            return true;
        }

        return false;
    }

    protected boolean isReAuthRequiredForKcAction(UserSessionModel userSession, AuthenticationSessionModel authSession) {
        if (authSession.getClientNote(Constants.KC_ACTION) != null) {
            String providerId = authSession.getClientNote(Constants.KC_ACTION);
            RequiredActionProvider requiredActionProvider = this.session.getProvider(RequiredActionProvider.class, providerId);
            String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME);
            int authTimeInt = authTime == null ? 0 : Integer.parseInt(authTime);
            int maxAgeInt = requiredActionProvider.getMaxAuthAge();
            return authTimeInt + maxAgeInt < Time.currentTime();
        } else {
            return false;
        }
    }

    @Override
    public boolean sendPushRevocationPolicyRequest(RealmModel realm, ClientModel resource, int notBefore, String managementUrl) {
        PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), notBefore);
        String token = session.tokens().encode(adminAction);
        logger.debugv("pushRevocation resource: {0} url: {1}", resource.getClientId(), managementUrl);
        URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build();
        try {
            int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token);
            boolean success = status == 204 || status == 200;
            logger.debugf("pushRevocation success for %s: %s", managementUrl, success);
            return success;
        } catch (IOException e) {
            ServicesLogger.LOGGER.failedToSendRevocation(e);
            return false;
        }
    }

    @Override
    public void close() {

    }
}

## 内容2
### 文件路径：
services/src/main/java/org/keycloak/protocol/oidc/utils/OAuth2Code.java
### 代码内容：
package org.keycloak.protocol.oidc.utils;

import java.util.HashMap;
import java.util.Map;

/**
 * Data associated with the oauth2 code.
 *
 * Those data are typically valid just for the very short time - they're created at the point before we redirect to the application
 * after successful and they're removed when application sends requests to the token endpoint (code-to-token endpoint) to exchange the
 * single-use OAuth2 code parameter for those data.
 *
 * @author Marek Posolda
 */
public class OAuth2Code {

    private static final String ID_NOTE = "id";
    private static final String EXPIRATION_NOTE = "exp";
    private static final String NONCE_NOTE = "nonce";
    private static final String SCOPE_NOTE = "scope";
    private static final String REDIRECT_URI_PARAM_NOTE = "redirectUri";
    private static final String CODE_CHALLENGE_NOTE = "code_challenge";
    private static final String CODE_CHALLENGE_METHOD_NOTE = "code_challenge_method";

    private final String id;

    private final int expiration;

    private final String nonce;

    private final String scope;

    private final String redirectUriParam;

    private final String codeChallenge;

    private final String codeChallengeMethod;

    public OAuth2Code(String id, int expiration, String nonce, String scope, String redirectUriParam,
                      String codeChallenge, String codeChallengeMethod) {
        this.id = id;
        this.expiration = expiration;
        this.nonce = nonce;
        this.scope = scope;
        this.redirectUriParam = redirectUriParam;
        this.codeChallenge = codeChallenge;
        this.codeChallengeMethod = codeChallengeMethod;
    }

    private OAuth2Code(Map<String, String> data) {
        id = data.get(ID_NOTE);
        expiration = Integer.parseInt(data.get(EXPIRATION_NOTE));
        nonce = data.get(NONCE_NOTE);
        scope = data.get(SCOPE_NOTE);
        redirectUriParam = data.get(REDIRECT_URI_PARAM_NOTE);
        codeChallenge = data.get(CODE_CHALLENGE_NOTE);
        codeChallengeMethod = data.get(CODE_CHALLENGE_METHOD_NOTE);
    }

    public static final OAuth2Code deserializeCode(Map<String, String> data) {
        return new OAuth2Code(data);
    }

    public Map<String, String> serializeCode() {
        Map<String, String> result = new HashMap<>();

        result.put(ID_NOTE, id.toString());
        result.put(EXPIRATION_NOTE, String.valueOf(expiration));
        result.put(NONCE_NOTE, nonce);
        result.put(SCOPE_NOTE, scope);
        result.put(REDIRECT_URI_PARAM_NOTE, redirectUriParam);
        result.put(CODE_CHALLENGE_NOTE, codeChallenge);
        result.put(CODE_CHALLENGE_METHOD_NOTE, codeChallengeMethod);

        return result;
    }

    public String getId() {
        return id;
    }

    public int getExpiration() {
        return expiration;
    }

    public String getNonce() {
        return nonce;
    }

    public String getScope() {
        return scope;
    }

    public String getRedirectUriParam() {
        return redirectUriParam;
    }

    public String getCodeChallenge() {
        return codeChallenge;
    }

    public String getCodeChallengeMethod() {
        return codeChallengeMethod;
    }
}

## 内容3
### 文件路径：
services/src/main/java/org/keycloak/protocol/oidc/utils/OAuth2CodeParser.java
### 代码内容：
package org.keycloak.protocol.oidc.utils;

import java.util.Map;
import java.util.regex.Pattern;

import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.UserSessionCrossDCManager;

import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification;

/**
 * @author Marek Posolda
 */
public class OAuth2CodeParser {

    private static final Logger logger = Logger.getLogger(OAuth2CodeParser.class);

    private static final Pattern DOT = Pattern.compile("\\.");

    /**
     * Will persist the code to the cache and return the object with the codeData and code correctly set
     *
     * @param session
     * @param clientSession
     * @param codeData
     * @return code parameter to be used in OAuth2 handshake
     */
    public static String persistCode(KeycloakSession session, AuthenticatedClientSessionModel clientSession, OAuth2Code codeData) {
        SingleUseObjectProvider codeStore = session.getProvider(SingleUseObjectProvider.class);

        String key = codeData.getId();
        if (key == null) {
            throw new IllegalStateException("ID not present in the data");
        }

        Map<String, String> serialized = codeData.serializeCode();
        codeStore.put(key, clientSession.getUserSession().getRealm().getAccessCodeLifespan(), serialized);
        return key + "." + clientSession.getUserSession().getId() + "." + clientSession.getClient().getId();
    }

    /**
     * Will parse the code and retrieve the corresponding OAuth2Code and AuthenticatedClientSessionModel. Will also check if code wasn't already
     * used and if it wasn't expired. If it was already used (or other error happened during parsing), then returned parser will have "isIllegalHash"
     * set to true. If it was expired, the parser will have "isExpired" set to true
     *
     * @param session
     * @param code
     * @param realm
     * @param event
     * @return
     */
    public static ParseResult parseCode(KeycloakSession session, String code, RealmModel realm, EventBuilder event) {
        ParseResult result = new ParseResult(code);

        String[] parsed = DOT.split(code, 3);
        if (parsed.length < 3) {
            logger.warn("Invalid format of the code");
            return result.illegalCode();
        }

        String userSessionId = parsed[1];
        String clientUUID = parsed[2];

        event.detail(Details.CODE_ID, userSessionId);
        event.session(userSessionId);

        // Parse UUID
        String codeUUID;
        try {
            codeUUID = parsed[0];
        } catch (IllegalArgumentException re) {
            logger.warn("Invalid format of the UUID in the code");
            return result.illegalCode();
        }

        // Retrieve UserSession
        UserSessionModel userSession = lockUserSessionsForModification(session, () -> new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, clientUUID));
        if (userSession == null) {
            // Needed to track if code is invalid or was already used.
            userSession = lockUserSessionsForModification(session, () -> session.sessions().getUserSession(realm, userSessionId));
            if (userSession == null) {
                return result.illegalCode();
            }
        }

        result.clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID);

        SingleUseObjectProvider codeStore = session.getProvider(SingleUseObjectProvider.class);
        Map<String, String> codeData = codeStore.remove(codeUUID);

        // Either code not available or was already used
        if (codeData == null) {
            logger.warnf("Code '%s' already used for userSession '%s' and client '%s'.", codeUUID, userSessionId, clientUUID);
            return result.illegalCode();
        }

        logger.tracef("Successfully verified code '%s'. User session: '%s', client: '%s'", codeUUID, userSessionId, clientUUID);

        result.codeData = OAuth2Code.deserializeCode(codeData);

        // Finally doublecheck if code is not expired
        int currentTime = Time.currentTime();
        if (currentTime > result.codeData.getExpiration()) {
            return result.expiredCode();
        }

        return result;
    }

    public static class ParseResult {

        private final String code;
        private OAuth2Code codeData;
        private AuthenticatedClientSessionModel clientSession;

        private boolean isIllegalCode = false;
        private boolean isExpiredCode = false;

        private ParseResult(String code, OAuth2Code codeData, AuthenticatedClientSessionModel clientSession) {
            this.code = code;
            this.codeData = codeData;
            this.clientSession = clientSession;

            this.isIllegalCode = false;
            this.isExpiredCode = false;
        }

        private ParseResult(String code) {
            this.code = code;
        }

        public String getCode() {
            return code;
        }

        public OAuth2Code getCodeData() {
            return codeData;
        }

        public AuthenticatedClientSessionModel getClientSession() {
            return clientSession;
        }

        public boolean isIllegalCode() {
            return isIllegalCode;
        }

        public boolean isExpiredCode() {
            return isExpiredCode;
        }

        private ParseResult illegalCode() {
            this.isIllegalCode = true;
            return this;
        }

        private ParseResult expiredCode() {
            this.isExpiredCode = true;
            return this;
        }
    }

}

Output:
【漏洞类型】会话固定漏洞
【严重性】High
【漏洞摘要】此代码存在会话固定漏洞。`OAuth2Code` 类在生成 OAuth2 代码时，未将用户会话 ID 绑定到代码中。攻击者可以通过劫持用户的 OAuth2 代码并替换其中的用户会话 ID，从而将用户的会话固定到攻击者控制的会话中。具体问题出在 `OIDCLoginProtocol` 类的 `authenticated` 方法中，生成 `OAuth2Code` 对象时未包含用户会话 ID。
【关键代码模式】`OAuth2Code` 类的构造函数和 `deserializeCode` 方法，`persistCode` 和 `parseCode` 方法中的代码生成和解析逻辑。
【修复方法说明】修复方法通过在生成 `OAuth2Code` 对象时包含用户会话 ID，并在解析代码时验证该 ID，确保代码只能用于其对应的用户会话。这解决了会话固定漏洞的问题。