-
Notifications
You must be signed in to change notification settings - Fork 842
Refactor: Create Login State Values Once #3713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,8 +82,7 @@ public String getIdpAuthenticationUrl( | |
| var responseType = URLEncoder.encode(definition.getResponseType(), StandardCharsets.UTF_8); | ||
| var relyingPartyId = definition.getRelyingPartyId(); | ||
|
|
||
| var state = generateStateParam(); | ||
| SessionUtils.setStateParam(request.getSession(), SessionUtils.stateParameterAttributeKeyForIdp(idpOriginKey), state); | ||
| var state = SessionUtils.setStateParam(request.getSession(), SessionUtils.stateParameterAttributeKeyForIdp(idpOriginKey), generateStateParam()); | ||
|
|
||
| UriComponentsBuilder uriBuilder = UriComponentsBuilder | ||
| .fromUriString(idpUrlBase) | ||
|
|
@@ -96,9 +95,8 @@ public String getIdpAuthenticationUrl( | |
| // https://docs.spring.io/spring-security/site/docs/5.3.1.RELEASE/reference/html5/#initiating-the-authorization-request | ||
| if (isPkceNeeded(definition)) { | ||
| var pkceVerifier = new S256PkceVerifier(); | ||
| var codeVerifier = generateCodeVerifier(); | ||
| var codeChallenge = pkceVerifier.compute(codeVerifier); | ||
| SessionUtils.setStateParam(request.getSession(), SessionUtils.codeVerifierParameterAttributeKeyForIdp(idpOriginKey), codeVerifier); | ||
| var codeVerifier = SessionUtils.setStateParam(request.getSession(), SessionUtils.codeVerifierParameterAttributeKeyForIdp(idpOriginKey), generateCodeVerifier()); | ||
| var codeChallenge = SessionUtils.setStateParam(request.getSession(), SessionUtils.codeChallengeParameterAttributeKeyForIdp(idpOriginKey), pkceVerifier.compute(codeVerifier)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we replace |
||
| uriBuilder.queryParam("code_challenge", codeChallenge); | ||
| uriBuilder.queryParam("code_challenge_method", pkceVerifier.getCodeChallengeMethod()); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,7 @@ public final class SessionUtils { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String EXTERNAL_OAUTH_STATE_ATTRIBUTE_PREFIX = "external-oauth-state-"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String EXTERNAL_OAUTH_CODE_VERIFIER_ATTRIBUTE_PREFIX = "external-oauth-verifier-"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String EXTERNAL_OAUTH_CODE_CHALLENGE_ATTRIBUTE_PREFIX = "external-oauth-challenge-"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private SessionUtils() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -56,8 +57,13 @@ public static UaaAuthentication getForcePasswordExpiredUser(HttpSession session) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (UaaAuthentication) session.getAttribute(FORCE_PASSWORD_EXPIRED_USER); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static void setStateParam(HttpSession session, String stateParamKey, String state) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| session.setAttribute(stateParamKey, state); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static String setStateParam(HttpSession session, String stateParamKey, String state) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (session.isNew() || session.getAttribute(stateParamKey) == null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| session.setAttribute(stateParamKey, state); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return state; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return session.getAttribute(stateParamKey) instanceof String existingState ? existingState : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
+65
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (session.isNew() || session.getAttribute(stateParamKey) == null) { | |
| session.setAttribute(stateParamKey, state); | |
| return state; | |
| } else { | |
| return session.getAttribute(stateParamKey) instanceof String existingState ? existingState : null; | |
| Object existingAttribute = session.getAttribute(stateParamKey); | |
| if (session.isNew() || existingAttribute == null) { | |
| session.setAttribute(stateParamKey, state); | |
| return state; | |
| } else if (existingAttribute instanceof String) { | |
| return (String) existingAttribute; | |
| } else { | |
| throw new IllegalStateException( | |
| "Expected session attribute '" + stateParamKey + "' to be a String but found: " + | |
| existingAttribute.getClass().getName() | |
| ); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition 'session.isNew()' may not correctly identify all cases where state parameters should be regenerated. According to the Servlet specification, isNew() returns true only for newly created sessions where the client hasn't yet acknowledged the session (i.e., hasn't sent back the session ID). This means:
- If a session exists but the state attribute is null (e.g., after session timeout and recreation), isNew() could be false, and a new state would be generated correctly via the second condition.
- However, if isNew() is true but the attribute already exists (which shouldn't normally happen), the existing value would be overwritten.
The logic appears functional but could be clearer. Consider simplifying to just check if the attribute is null, which is the actual condition that matters for determining whether to generate or reuse state values.
| if (session.isNew() || session.getAttribute(stateParamKey) == null) { | |
| session.setAttribute(stateParamKey, state); | |
| return state; | |
| } else { | |
| return session.getAttribute(stateParamKey) instanceof String existingState ? existingState : null; | |
| Object existing = session.getAttribute(stateParamKey); | |
| if (existing == null) { | |
| session.setAttribute(stateParamKey, state); | |
| return state; | |
| } else { | |
| return existing instanceof String existingState ? existingState : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a comment or change the method's name to represent this behavior? If a value already exists, we do not update it.
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method signature has been changed from void to String without updating the method's documentation or JavaDoc. This is a breaking API change that affects how the method is used. Callers now need to use the return value instead of relying on side effects alone.
Consider adding JavaDoc to document:
- The return value represents the state parameter value (either newly generated or retrieved from session)
- The behavior of reusing cached values when they exist in the session
- That null may be returned if the cached value is not a String (though this should be addressed separately)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code challenge is being computed from a potentially cached code verifier, but the code challenge itself is also being cached. This creates an inconsistency problem: if the code verifier already exists in the session, the code will compute a new challenge from that existing verifier but then attempt to cache this newly computed challenge. However, if a code challenge already exists in the session, it will be returned instead of the newly computed one, leading to a mismatch between the cached verifier and cached challenge.
The issue arises because pkceVerifier.compute() always computes a fresh challenge, but setStateParam may return a previously cached challenge that was computed from a different execution path or even a different verifier value.
To fix this, the code challenge should be computed once when the code verifier is first generated, and both should be cached together atomically. If the code verifier already exists in the session, the cached code challenge should be retrieved directly without recomputation.