Skip to content
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

[JBEAP-28701] added use of nonce #2253

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from

Conversation

rsearls
Copy link
Contributor

@rsearls rsearls commented Feb 11, 2025

@@ -390,7 +395,11 @@ public enum AuthOutcome {
}

public static String generateId() {
return UUID.randomUUID().toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generateId method is also used for generating a unique identifier for the JWT ID claim and for the state code. I think we should leave this existing method unchanged and introduce a new method for the nonce generation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -222,7 +223,8 @@ protected String getRedirectUri(String state) {
String redirectUri = rewrittenRedirectUri(url);
URIBuilder redirectUriBuilder = new URIBuilder(deployment.getAuthUrl());
redirectUriBuilder.addParameter(RESPONSE_TYPE, CODE)
.addParameter(CLIENT_ID, deployment.getResourceName());
.addParameter(CLIENT_ID, deployment.getResourceName())
.addParameter(NONCE, String.valueOf(state.hashCode()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be re-using the state value here. We should be using a separate value for the nonce.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -456,6 +459,14 @@ protected AuthChallenge resolveCode(String code) {
log.error("Stale token");
return challenge(HttpStatus.SC_FORBIDDEN, AuthenticationError.Reason.STALE_TOKEN, null);
}

String stateCookieValue = getCookieValue(deployment.getStateCookieName());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separate cookie should be used for nonce purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

String stateCookieValue = getCookieValue(deployment.getStateCookieName());
String nonceValue = token.getClaimValueAsString(NONCE);
if (nonceValue == null || !nonceValue.equals(String.valueOf(stateCookieValue.hashCode()))) {
log.error("Invalid nonce");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its more straight forward to take the value from idToken which has been validated

@fjuma
Copy link
Contributor

fjuma commented Feb 11, 2025

@rsearls Thanks for working on this! It would also be good to add some unit tests for the case where the ID token contains the correct nonce and the case where the ID token contains the wrong nonce.

@fjuma
Copy link
Contributor

fjuma commented Feb 11, 2025

@rsearls Please update the base branch for this PR to the 2.6.x branch, thanks.

@rsearls rsearls force-pushed the JBEAP-28701-OIDC-Auth-Code branch from c4aec6e to 3f02a79 Compare February 17, 2025 13:59
@rsearls
Copy link
Contributor Author

rsearls commented Feb 17, 2025

All existing tests use the valid nonce case. I have add a test for the invalid case.

Once you are happy this the changes in this branch I will port them to 2.6.x

Copy link
Contributor

@fjuma fjuma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rsearls! I've added some comments, let me know if you have any questions.

@@ -96,6 +100,9 @@ public class OidcRequestAuthenticator {
protected String refreshToken;
protected String strippedOauthParametersRequestUri;

private int NONCE_SIZE = 36;
private final String sessionId = generateSessionId();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could name this sessionRandomValue or something like that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The random value can be generated when creating the authentication request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable renamed.

There are several reasons I made sessionId private final in class scope
rather than a method local variable. I wanted to keep the needed code
changes to this class to a minimum. The variable is used in 3 different
methods, getRedirectUri, createRequestWithRequestParameter, convertToRequestParameter.
Each method would require a signature change. The methods are called
in 5 different places also requiring a change.
I also thought that in the future some calling class might need access to the
value of sessionId. Having a class scope private final variable would enable
a consistent accurate reference.
However I can make the change you requested if you prefer.

@@ -222,7 +229,8 @@ protected String getRedirectUri(String state) {
String redirectUri = rewrittenRedirectUri(url);
URIBuilder redirectUriBuilder = new URIBuilder(deployment.getAuthUrl());
redirectUriBuilder.addParameter(RESPONSE_TYPE, CODE)
.addParameter(CLIENT_ID, deployment.getResourceName());
.addParameter(CLIENT_ID, deployment.getResourceName())
.addParameter(NONCE, String.valueOf(sessionId.hashCode()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we need to use a cryptographic hash of the random value. We can use MessageDigest for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -111,6 +111,7 @@ public class Oidc {
public static final String REDIRECT_URI = "redirect_uri";
public static final String REFRESH_TOKEN = "refresh_token";
public static final String RESPONSE_TYPE = "response_type";
public static final String SESSION_ID = "session_id";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can rename this to something like SESSION_RANDOM_VALUE="session_random_value"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -336,9 +346,16 @@ protected AuthChallenge checkStateCookie() {
log.warn("state parameter was null");
return challenge(HttpStatus.SC_BAD_REQUEST, AuthenticationError.Reason.INVALID_STATE_COOKIE, null);
}
if (!state.equals(stateCookieValue)) {

String sCookieValue = stateCookieValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these changes needed?

The checkStateCookie() method currently deals specifically with the deployment.getStateCookieName() (i.e., the OAuth_Token_Request_State cookie).

@@ -444,6 +461,22 @@ protected AuthChallenge resolveCode(String code) {
TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, tokenString);
idToken = verifiedTokens.getIdToken();
token = verifiedTokens.getAccessToken();

String stateCookieValue = getCookieValue(deployment.getStateCookieName());
Copy link
Contributor

@fjuma fjuma Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this get the value of the OAuth_Token_Request_State cookie instead of the session random value cookie?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to the issue described for line 350.

String sessionIdValue = "";
for(int i=0; i < cookieArr.length; i++) {
String cookie = cookieArr[i].trim();
if (cookie.startsWith(SESSION_ID+"=")) {
Copy link
Contributor

@fjuma fjuma Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check, is this needed? I don't see a similar check being done in the code that obtains the state cookie value.

Copy link
Contributor Author

@rsearls rsearls Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cookie state is performed by the challenge returned at line 428 (i.e checkStateCookie().
This is needed to get at the value fo the session_id as described for line 350

}
}
String nonceValue = idToken.getNonce();
if (nonceValue == null || !nonceValue.equals(String.valueOf(sessionIdValue.hashCode()))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we should use the cryptographic hash as mentioned above.

Copy link
Contributor

@fjuma fjuma Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the nonce validation is part of verifying the ID token, I think it makes sense for this check to be done in the OIDC TokenValidator#parseAndVerifyToken method. From the TokenValidator we have access to the oidcClientConfiguration from which we can get the value of the session random value cookie and we can get the nonce from idJwtClaims.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

jwtClaims.setClaim(STATE, state);
jwtClaims.setClaim(REDIRECT_URI, redirectUri);
jwtClaims.setClaim(RESPONSE_TYPE, CODE);
jwtClaims.setClaim(CLIENT_ID, deployment.getResourceName());
jwtClaims.setClaim(NONCE, String.valueOf(sessionId.hashCode()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, same comment as above regarding using a cryptographic hash.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

SecureRandom random = new SecureRandom();
byte[] nonceData = new byte[NONCE_SIZE];
random.nextBytes(nonceData);
return String.valueOf(ByteIterator.ofBytes(nonceData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be return ByteIterator.ofBytes(nonceData).base64Encode().drainToString();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Generate an invalid sessionId so that the nonce check fails
public void testSessionIdNonceMismatch() throws Exception {
performAuthentication(
getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), NONE, "", ""),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses the request parameter configuration. It would be good to have an additional test case that doesn't use this configuration to test the standard authentication request case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@rsearls rsearls force-pushed the JBEAP-28701-OIDC-Auth-Code branch from 3f02a79 to d6f32ce Compare February 25, 2025 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants