-
Notifications
You must be signed in to change notification settings - Fork 281
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
base: 2.x
Are you sure you want to change the base?
Conversation
@@ -390,7 +395,11 @@ public enum AuthOutcome { | |||
} | |||
|
|||
public static String generateId() { | |||
return UUID.randomUUID().toString(); |
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 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.
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.
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())); |
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.
We shouldn't be re-using the state value here. We should be using a separate value for the nonce.
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.
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()); |
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.
A separate cookie should be used for nonce purposes.
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.
done
String stateCookieValue = getCookieValue(deployment.getStateCookieName()); | ||
String nonceValue = token.getClaimValueAsString(NONCE); | ||
if (nonceValue == null || !nonceValue.equals(String.valueOf(stateCookieValue.hashCode()))) { | ||
log.error("Invalid nonce"); |
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.
I think it would be better to validate the nonce in the TokenValidator#parseAndVerifyToken
method here:
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.
Its more straight forward to take the value from idToken which has been validated
@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. |
@rsearls Please update the base branch for this PR to the 2.6.x branch, thanks. |
c4aec6e
to
3f02a79
Compare
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 |
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.
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(); |
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.
Maybe we could name this sessionRandomValue
or something like that?
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 random value can be generated when creating the authentication request.
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.
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())); |
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.
Here, we need to use a cryptographic hash of the random value. We can use MessageDigest
for this.
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.
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"; |
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.
Maybe we can rename this to something like SESSION_RANDOM_VALUE="session_random_value"
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.
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; |
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.
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()); |
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.
Won't this get the value of the OAuth_Token_Request_State
cookie instead of the session random value cookie?
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.
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+"=")) { |
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.
Just to check, is this needed? I don't see a similar check being done in the code that obtains the state cookie value.
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.
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()))) { |
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.
Here, we should use the cryptographic hash as mentioned above.
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.
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
.
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.
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())); |
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.
Note, same comment as above regarding using a cryptographic hash.
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.
done
SecureRandom random = new SecureRandom(); | ||
byte[] nonceData = new byte[NONCE_SIZE]; | ||
random.nextBytes(nonceData); | ||
return String.valueOf(ByteIterator.ofBytes(nonceData) |
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.
I think this can be return ByteIterator.ofBytes(nonceData).base64Encode().drainToString();
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.
done
// Generate an invalid sessionId so that the nonce check fails | ||
public void testSessionIdNonceMismatch() throws Exception { | ||
performAuthentication( | ||
getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), NONE, "", ""), |
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.
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.
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.
done
3f02a79
to
d6f32ce
Compare
https://issues.redhat.com/browse/JBEAP-28701