Medium severity4.2NVD Advisory· Published Dec 9, 2024· Updated Apr 15, 2026
CVE-2024-12369
CVE-2024-12369
Description
A vulnerability was found in OIDC-Client. When using the RH SSO OIDC adapter with EAP 7.x or when using the elytron-oidc-client subsystem with EAP 8.x, authorization code injection attacks can occur, allowing an attacker to inject a stolen authorization code into the attacker's own session with the client with a victim's identity. This is usually done with a Man-in-the-Middle (MitM) or phishing attack.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.wildfly.security:wildfly-elytronMaven | >= 1.17.0.Final, < 2.2.9.Final | 2.2.9.Final |
org.wildfly.security:wildfly-elytronMaven | >= 2.3.0.Final, < 2.6.2.Final | 2.6.2.Final |
org.wildfly.security:wildfly-elytron-http-oidcMaven | >= 1.17.0.Final, < 2.2.9.Final | 2.2.9.Final |
org.wildfly.security:wildfly-elytron-http-oidcMaven | >= 2.3.0.Final, < 2.6.2.Final | 2.6.2.Final |
Patches
2d7754f5a6a91[ELY-2887] Add a nonce to OIDC requests for CVE-2024-12369
13 files changed · +175 −66
http/oidc/src/main/java/org/wildfly/security/http/oidc/AuthenticationError.java+2 −1 modified@@ -36,7 +36,8 @@ public enum Reason { INVALID_TOKEN, STALE_TOKEN, NO_AUTHORIZATION_HEADER, - NO_QUERY_PARAMETER_ACCESS_TOKEN + NO_QUERY_PARAMETER_ACCESS_TOKEN, + INVALID_NONCE } private Reason reason;
http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java+8 −0 modified@@ -238,5 +238,13 @@ interface ElytronMessages extends BasicLogger { @Message(id = 23057, value = "principal-attribute '%s' claim does not exist, falling back to 'sub'") void principalAttributeClaimDoesNotExist(String principalAttributeClaim); + @Message(id = 23071, value = "Invalid ID token nonce: %s") + String invalidNonceValue(String name); + + @Message(id = 23072, value = "No such algorithm: '%s'") + IllegalArgumentException noSuchAlgorithm(String algorithm); + + @Message(id = 23073, value = "Nonce cookie does not exist") + String nonceCookieDoesNotExist(); }
http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java+4 −0 modified@@ -51,6 +51,7 @@ public class IDToken extends JsonWebToken { public static final String CLAIMS_LOCALES = "claims_locales"; public static final String ACR = "acr"; public static final String S_HASH = "s_hash"; + public static final String NONCE = "nonce"; /** * Construct a new instance. @@ -220,4 +221,7 @@ public String getAcr() { return getClaimValueAsString(ACR); } + public String getNonce() { + return getClaimValueAsString(NONCE); + } }
http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcCookieTokenStore.java+2 −1 modified@@ -21,6 +21,7 @@ import static org.wildfly.security.http.oidc.ElytronMessages.log; import static org.wildfly.security.http.oidc.Oidc.OIDC_STATE_COOKIE; import static org.wildfly.security.http.oidc.Oidc.checkCachedAccountMatchesRequest; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import java.net.URISyntaxException; import java.util.List; @@ -227,7 +228,7 @@ public static OidcPrincipal<RefreshableOidcSecurityContext> getPrincipalFromCook idToken = new IDToken(new JwtConsumerBuilder().setSkipSignatureVerification().setSkipAllValidators().build().processToClaims(idTokenString)); } log.debug("Token obtained from cookie"); - RefreshableOidcSecurityContext secContext = new RefreshableOidcSecurityContext(deployment, tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString); + RefreshableOidcSecurityContext secContext = new RefreshableOidcSecurityContext(deployment, facade.getRequest().getCookie(SESSION_RANDOM_VALUE), tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString); return new OidcPrincipal<>(idToken.getPrincipalName(deployment), secContext); } catch (InvalidJwtException e) { log.failedToParseTokenFromCookie(e);
http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java+17 −0 modified@@ -19,11 +19,15 @@ package org.wildfly.security.http.oidc; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.jose.jwk.JWKUtil.BASE64_URL; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.StringTokenizer; import java.util.UUID; @@ -34,6 +38,7 @@ import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpRequestBase; import org.jose4j.jws.AlgorithmIdentifiers; +import org.wildfly.common.iteration.ByteIterator; import org.wildfly.security.jose.util.JsonSerialization; /** @@ -81,13 +86,15 @@ 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_RANDOM_VALUE="session_random_value"; public static final String SESSION_STATE = "session_state"; public static final String SOAP_ACTION = "SOAPAction"; public static final String STALE_TOKEN = "Stale token"; public static final String STATE = "state"; public static final int INVALID_ISSUED_FOR_CLAIM = -1; public static final int INVALID_AT_HASH_CLAIM = -2; public static final int INVALID_TYPE_CLAIM = -3; + public static final int INVALID_SESSION_RANDOM_VALUE = -4; static final String OIDC_CLIENT_CONFIG_RESOLVER = "oidc.config.resolver"; static final String OIDC_CONFIG_FILE_LOCATION = "oidc.config.file"; static final String OIDC_JSON_FILE = "/WEB-INF/oidc.json"; @@ -368,4 +375,14 @@ protected static boolean checkCachedAccountMatchesRequest(OidcAccount account, O return true; } + protected static String getCryptographicValue(final String src) { + try { + MessageDigest md = MessageDigest.getInstance(SHA256); + md.update(src.getBytes(StandardCharsets.UTF_8)); + return ByteIterator.ofBytes(md.digest()) + .base64Encode(BASE64_URL, false).drainToString(); + } catch (NoSuchAlgorithmException e) { + throw log.noSuchAlgorithm(e.getMessage()); + } + } }
http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java+20 −5 modified@@ -19,6 +19,7 @@ package org.wildfly.security.http.oidc; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.IDToken.NONCE; import static org.wildfly.security.http.oidc.Oidc.ALLOW_QUERY_PARAMS_PROPERTY_NAME; import static org.wildfly.security.http.oidc.Oidc.CLIENT_ID; import static org.wildfly.security.http.oidc.Oidc.CODE; @@ -33,6 +34,7 @@ import static org.wildfly.security.http.oidc.Oidc.REDIRECT_URI; import static org.wildfly.security.http.oidc.Oidc.RESPONSE_TYPE; import static org.wildfly.security.http.oidc.Oidc.SCOPE; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import static org.wildfly.security.http.oidc.Oidc.SESSION_STATE; import static org.wildfly.security.http.oidc.Oidc.STATE; import static org.wildfly.security.http.oidc.Oidc.UI_LOCALES; @@ -47,6 +49,7 @@ import java.net.URL; import java.security.AccessController; import java.security.PrivilegedAction; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -56,6 +59,7 @@ import org.apache.http.NameValuePair; import org.apache.http.client.utils.URIBuilder; import org.apache.http.message.BasicNameValuePair; +import org.wildfly.common.iteration.ByteIterator; import org.wildfly.security.http.HttpConstants; /** @@ -75,7 +79,7 @@ public class OidcRequestAuthenticator { protected AuthChallenge challenge; protected String refreshToken; protected String strippedOauthParametersRequestUri; - + private int NONCE_SIZE = 36; static final boolean ALLOW_QUERY_PARAMS_PROPERTY; static { @@ -161,7 +165,7 @@ protected String getCode() { return getQueryParamValue(facade, CODE); } - protected String getRedirectUri(String state) { + protected String getRedirectUri(String state, String sessionRandomValueHash) { String url = getRequestUrl(); log.debugf("callback uri: %s", url); @@ -199,7 +203,8 @@ protected String getRedirectUri(String state) { .addParameter(RESPONSE_TYPE, CODE) .addParameter(CLIENT_ID, deployment.getResourceName()) .addParameter(REDIRECT_URI, rewrittenRedirectUri(url)) - .addParameter(STATE, state); + .addParameter(STATE, state) + .addParameter(NONCE, sessionRandomValueHash); redirectUriBuilder.addParameters(forwardedQueryParams); return redirectUriBuilder.build().toString(); } catch (URISyntaxException e) { @@ -217,7 +222,8 @@ protected String getStateCode() { protected AuthChallenge loginRedirect() { final String state = getStateCode(); - final String redirect = getRedirectUri(state); + final String sessionRandomValue = generateSessionRandomValue(); + final String redirect = getRedirectUri(state, Oidc.getCryptographicValue(sessionRandomValue)); if (redirect == null) { return challenge(HttpStatus.SC_FORBIDDEN, AuthenticationError.Reason.NO_REDIRECT_URI, null); } @@ -235,6 +241,7 @@ public boolean challenge(OidcHttpFacade exchange) { exchange.getResponse().setStatus(HttpStatus.SC_MOVED_TEMPORARILY); exchange.getResponse().setCookie(deployment.getStateCookieName(), state, "/", null, -1, deployment.getSSLRequired().isRequired(facade.getRequest().getRemoteAddr()), true); exchange.getResponse().setHeader(HttpConstants.LOCATION, redirect); + exchange.getResponse().setCookie(SESSION_RANDOM_VALUE, sessionRandomValue, "/", null, -1, deployment.getSSLRequired().isRequired(facade.getRequest().getRemoteAddr()), true); return true; } }; @@ -362,7 +369,8 @@ protected AuthChallenge resolveCode(String code) { try { TokenValidator tokenValidator = TokenValidator.builder(deployment).build(); - TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, tokenString); + TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, tokenString, + facade.getRequest().getCookie(SESSION_RANDOM_VALUE)); idToken = verifiedTokens.getIdToken(); token = verifiedTokens.getAccessToken(); log.debug("Token Verification succeeded!"); @@ -435,4 +443,11 @@ private static boolean hasScope(String scopeParam, String targetScope) { } return false; } + + private String generateSessionRandomValue() { + SecureRandom random = new SecureRandom(); + byte[] nonceData = new byte[NONCE_SIZE]; + random.nextBytes(nonceData); + return ByteIterator.ofBytes(nonceData).base64Encode().drainToString(); + } }
http/oidc/src/main/java/org/wildfly/security/http/oidc/RefreshableOidcSecurityContext.java+4 −2 modified@@ -34,16 +34,18 @@ public class RefreshableOidcSecurityContext extends OidcSecurityContext { protected transient OidcClientConfiguration clientConfiguration; protected transient OidcTokenStore tokenStore; protected String refreshToken; + protected transient OidcHttpFacade.Cookie cookie; public RefreshableOidcSecurityContext() { } - public RefreshableOidcSecurityContext(OidcClientConfiguration clientConfiguration, OidcTokenStore tokenStore, String tokenString, + public RefreshableOidcSecurityContext(OidcClientConfiguration clientConfiguration, OidcHttpFacade.Cookie cookie, OidcTokenStore tokenStore, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) { super(tokenString, token, idTokenString, idToken); this.clientConfiguration = clientConfiguration; this.tokenStore = tokenStore; this.refreshToken = refreshToken; + this.cookie = cookie; } @Override @@ -149,7 +151,7 @@ public boolean refreshToken(boolean checkActive) { IDToken idToken; try { TokenValidator tokenValidator = TokenValidator.builder(clientConfiguration).build(); - TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, accessTokenString); + TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, accessTokenString, cookie); idToken = verifiedTokens.getIdToken(); accessToken = verifiedTokens.getAccessToken(); log.debug("Token Verification succeeded!");
http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java+3 −2 modified@@ -24,6 +24,7 @@ import static org.wildfly.security.http.oidc.Oidc.FACES_REQUEST; import static org.wildfly.security.http.oidc.Oidc.HTML_CONTENT_TYPE; import static org.wildfly.security.http.oidc.Oidc.PARTIAL; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import static org.wildfly.security.http.oidc.Oidc.SOAP_ACTION; import static org.wildfly.security.http.oidc.Oidc.TEXT_CONTENT_TYPE; import static org.wildfly.security.http.oidc.Oidc.WILDCARD_CONTENT_TYPE; @@ -202,14 +203,14 @@ protected boolean verifySSL() { } protected void completeAuthentication(OidcRequestAuthenticator oidc) { - RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, facade.getTokenStore(), oidc.getTokenString(), oidc.getToken(), oidc.getIDTokenString(), oidc.getIDToken(), oidc.getRefreshToken()); + RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, facade.getRequest().getCookie(SESSION_RANDOM_VALUE), facade.getTokenStore(), oidc.getTokenString(), oidc.getToken(), oidc.getIDTokenString(), oidc.getIDToken(), oidc.getRefreshToken()); final OidcPrincipal<RefreshableOidcSecurityContext> principal = new OidcPrincipal<>(oidc.getIDToken().getPrincipalName(deployment), session); completeOidcAuthentication(principal); log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName()); } protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) { - RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null); + RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, facade.getRequest().getCookie(SESSION_RANDOM_VALUE), null, bearer.getTokenString(), bearer.getToken(), null, null, null); final OidcPrincipal<RefreshableOidcSecurityContext> principal = new OidcPrincipal<>(bearer.getToken().getPrincipalName(deployment), session); completeBearerAuthentication(principal); log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName());
http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java+30 −1 modified@@ -24,6 +24,7 @@ import static org.wildfly.security.http.oidc.Oidc.INVALID_AT_HASH_CLAIM; import static org.wildfly.security.http.oidc.Oidc.INVALID_ISSUED_FOR_CLAIM; import static org.wildfly.security.http.oidc.Oidc.INVALID_TYPE_CLAIM; +import static org.wildfly.security.http.oidc.Oidc.INVALID_SESSION_RANDOM_VALUE; import static org.wildfly.security.http.oidc.Oidc.getJavaAlgorithmForHash; import static org.wildfly.security.jose.jwk.JWKUtil.BASE64_URL; @@ -82,12 +83,14 @@ private TokenValidator(Builder builder) { * @return the {@code VerifiedTokens} if the ID token was valid * @throws OidcException if the ID token is invalid */ - public VerifiedTokens parseAndVerifyToken(final String idToken, final String accessToken) throws OidcException { + public VerifiedTokens parseAndVerifyToken(final String idToken, final String accessToken, OidcHttpFacade.Cookie cookie) throws OidcException { try { JwtContext idJwtContext = setVerificationKey(idToken, jwtConsumerBuilder); jwtConsumerBuilder.setExpectedAudience(clientConfiguration.getResourceName()); jwtConsumerBuilder.registerValidator(new AzpValidator(clientConfiguration.getResourceName())); jwtConsumerBuilder.registerValidator(new AtHashValidator(accessToken, clientConfiguration.getTokenSignatureAlgorithm())); + jwtConsumerBuilder.registerValidator(new NonceValidator(cookie)); + // second pass to validate jwtConsumerBuilder.build().processContext(idJwtContext); JwtClaims idJwtClaims = idJwtContext.getJwtClaims(); @@ -276,6 +279,32 @@ public ErrorCodeValidator.Error validate(JwtContext jwtContext) throws Malformed } } + private static class NonceValidator implements ErrorCodeValidator { + private OidcHttpFacade.Cookie cookie; + + public NonceValidator(OidcHttpFacade.Cookie cookie) { + this.cookie = cookie; + } + + public ErrorCodeValidator.Error validate(JwtContext jwtContext) throws MalformedClaimException { + JwtClaims idJwtClaims = jwtContext.getJwtClaims(); + IDToken idToken = new IDToken(idJwtClaims); + + if (cookie != null) { + String sessionRandomValue = Oidc.getCryptographicValue(cookie.getValue()); + String nonceValue = idToken.getNonce(); + if (!sessionRandomValue.equals(nonceValue)) { + return new ErrorCodeValidator.Error(INVALID_SESSION_RANDOM_VALUE, + log.invalidNonceValue(nonceValue)); + } + } else { + return new ErrorCodeValidator.Error(INVALID_SESSION_RANDOM_VALUE, + log.nonceCookieDoesNotExist()); + } + return null; + } + } + private static class TypeValidator implements ErrorCodeValidator { public static final String TYPE = "typ"; private final String expectedType;
http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java+62 −6 modified@@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -185,6 +186,32 @@ public MockResponse dispatch(RecordedRequest recordedRequest) throws Interrupted }; } + protected static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, + int expectedStatusCode, String expectedLocation, + String clientPageText, List<HttpServerCookie> cookies) { + return new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException { + String path = recordedRequest.getPath(); + if (path.contains("/" + CLIENT_APP) && path.contains("&code=")) { + try { + TestingHttpServerRequest request = new TestingHttpServerRequest(new String[0], + new URI(recordedRequest.getRequestUrl().toString()), cookies); + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + assertEquals(expectedStatusCode, response.getStatusCode()); + assertEquals(expectedLocation, response.getLocation()); + return new MockResponse().setBody(clientPageText); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return new MockResponse() + .setBody(""); + } + }; + } + protected static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, int expectedStatusCode, String expectedLocation, String clientPageText, Map<String, Object> sessionScopeAttachments) { return new Dispatcher() { @@ -305,27 +332,43 @@ protected String getCookieString(HttpServerCookie cookie) { return header.toString(); } + protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, + boolean changeSessionId) throws Exception { + performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), + expectedLocation, clientPageText, changeSessionId); + } + protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String expectedLocation, String clientPageText) throws Exception { - performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), expectedLocation, clientPageText); + performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), expectedLocation, clientPageText, false); } protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, CallbackHandler callbackHandler) throws Exception { performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), expectedLocation, clientPageText, - callbackHandler); + callbackHandler, false); + } + + protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String clientUrl, String expectedLocation, + String clientPageText) throws Exception { + performAuthentication(oidcConfig, username, password, loginToKeycloak, + expectedDispatcherStatusCode, clientUrl, expectedLocation, + clientPageText, false); } protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, - int expectedDispatcherStatusCode, String clientUrl, String expectedLocation, String clientPageText) throws Exception { + int expectedDispatcherStatusCode, String clientUrl, String expectedLocation, + String clientPageText, boolean changeSessionId) throws Exception { performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, clientUrl, expectedLocation, clientPageText, - getCallbackHandler()); + getCallbackHandler(), changeSessionId); } protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String clientUrl, String expectedLocation, String clientPageText, - CallbackHandler callbackHandler) throws Exception { + CallbackHandler callbackHandler, boolean changeSessionId) throws Exception { try { Map<String, Object> props = new HashMap<>(); OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); @@ -343,7 +386,20 @@ protected void performAuthentication(InputStream oidcConfig, String username, St assertEquals(Status.NO_AUTH, request.getResult()); if (loginToKeycloak) { - client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, expectedLocation, clientPageText)); + // change the sessionRandomValue value so that the compare to nonce will fail. + List<HttpServerCookie> tmpCookies = response.getCookies(); + if (changeSessionId) { + for (HttpServerCookie c : tmpCookies) { + if (c.getName().equals(SESSION_RANDOM_VALUE)) { + HttpServerCookie tmpCookie = HttpServerCookie.getInstance(c.getName(), + "9999" + c.getValue(), c.getDomain(), c.getMaxAge(), c.getPath(), + c.isSecure(), c.getVersion(), c.isHttpOnly()); + tmpCookies.remove(c); + tmpCookies.add(tmpCookie); + } + } + } + client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, expectedLocation, clientPageText, tmpCookies)); TextPage page = loginToKeycloak(username, password, requestUri, response.getLocation(), response.getCookies()).click(); assertTrue(page.getContent().contains(clientPageText));
http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcSecurityRealmTest.java+6 −6 modified@@ -72,7 +72,7 @@ public void testGetRealmIdentityWithNonOidcPrincipal() throws RealmUnavailableEx @Test public void testGetRealmIdentityNoRoles() throws RealmUnavailableException { // setup - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(new OidcClientConfiguration(), + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(new OidcClientConfiguration(), null, null, null, new AccessToken(new JwtClaims()), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -108,7 +108,7 @@ public void testGetRealmIdentityRolesCombined() throws RealmUnavailableException jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("realm_access", createRoles("roleC", "roleD")); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -137,7 +137,7 @@ public void testGetRealmIdentityOnlyRealmRoles() throws RealmUnavailableExceptio jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("realm_access", createRoles("roleC", "roleD")); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -165,7 +165,7 @@ public void testGetRealmIdentityOnlyResourceRoles() throws RealmUnavailableExcep jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("", new RealmAccessClaim(createRoles("roleC", "roleD"))); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -193,7 +193,7 @@ public void testGetRealmIdentityNoMappings() throws RealmUnavailableException { jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("", new RealmAccessClaim(createRoles("roleC", "roleD"))); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -390,7 +390,7 @@ private static String getRealmAndResourceRolesClaims(boolean includeRealmAndReso } private Attributes.Entry getRealmIdentityRoles(OidcClientConfiguration clientConfiguration, JwtClaims jwtClaims) throws RealmUnavailableException { - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext);
http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java+7 −0 modified@@ -206,6 +206,13 @@ KeycloakConfiguration.ALICE_PASSWORD, true, HttpStatus.SC_MOVED_TEMPORARILY, get getCallbackHandler()); } + @Test + public void testStandardConfigWithNonceMismatch() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), + KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, true, + HttpStatus.SC_FORBIDDEN, null, CLIENT_PAGE_TEXT, true); + } + /***************************************************************************************************************************************** * Tests for multi-tenancy. *
tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java+10 −42 modified@@ -191,49 +191,17 @@ public TestingHttpServerRequest(String[] authorization, URI requestURI, String c this.requestURI = requestURI; this.cookies = new ArrayList<>(); if (cookie != null) { - final String cookieName = cookie.substring(0, cookie.indexOf('=')); - final String cookieValue = cookie.substring(cookie.indexOf('=') + 1); - cookies.add(new HttpServerCookie() { - @Override - public String getName() { - return cookieName; - } - - @Override - public String getValue() { - return cookieValue; - } - - @Override - public String getDomain() { - return null; - } - - @Override - public int getMaxAge() { - return -1; - } - - @Override - public String getPath() { - return "/"; - } - - @Override - public boolean isSecure() { - return false; - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public boolean isHttpOnly() { - return true; + String[] cookiesArr = cookie.split(";"); + if (cookiesArr.length == 0) { + cookiesArr[0] = cookie; + } + for (int i = 0; i < cookiesArr.length; i++) { + String[] cookiePair = cookiesArr[i].trim().split("="); + if (cookiePair.length == 2) { + this.cookies.add(HttpServerCookie.getInstance( + cookiePair[0], cookiePair[1], null, -1, "/", false, 0, true)); } - }); + } } }
5ac5e6bbcba5[ELY-2887] Add a nonce to OIDC requests for CVE-2024-12369
13 files changed · +209 −34
http/oidc/src/main/java/org/wildfly/security/http/oidc/AuthenticationError.java+2 −1 modified@@ -36,7 +36,8 @@ public enum Reason { INVALID_TOKEN, STALE_TOKEN, NO_AUTHORIZATION_HEADER, - NO_QUERY_PARAMETER_ACCESS_TOKEN + NO_QUERY_PARAMETER_ACCESS_TOKEN, + INVALID_NONCE } private Reason reason;
http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java+9 −0 modified@@ -278,5 +278,14 @@ interface ElytronMessages extends BasicLogger { @Message(id = 23070, value = "Authentication request format must be one of the following: oauth2, request, request_uri.") RuntimeException invalidAuthenticationRequestFormat(); + + @Message(id = 23071, value = "Invalid ID token nonce: %s") + String invalidNonceValue(String name); + + @Message(id = 23072, value = "No such algorithm: '%s'") + IllegalArgumentException noSuchAlgorithm(String algorithm); + + @Message(id = 23073, value = "Nonce cookie does not exist") + String nonceCookieDoesNotExist(); }
http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java+4 −0 modified@@ -53,6 +53,7 @@ public class IDToken extends JsonWebToken { public static final String CLAIMS_LOCALES = "claims_locales"; public static final String ACR = "acr"; public static final String S_HASH = "s_hash"; + public static final String NONCE = "nonce"; /** * Construct a new instance. @@ -228,4 +229,7 @@ public String getAcr() { return getClaimValueAsString(ACR); } + public String getNonce() { + return getClaimValueAsString(NONCE); + } }
http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcCookieTokenStore.java+2 −1 modified@@ -21,6 +21,7 @@ import static org.wildfly.security.http.oidc.ElytronMessages.log; import static org.wildfly.security.http.oidc.Oidc.OIDC_STATE_COOKIE; import static org.wildfly.security.http.oidc.Oidc.checkCachedAccountMatchesRequest; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import java.net.URISyntaxException; import java.util.List; @@ -227,7 +228,7 @@ public static OidcPrincipal<RefreshableOidcSecurityContext> getPrincipalFromCook idToken = new IDToken(new JwtConsumerBuilder().setSkipSignatureVerification().setSkipAllValidators().build().processToClaims(idTokenString)); } log.debug("Token obtained from cookie"); - RefreshableOidcSecurityContext secContext = new RefreshableOidcSecurityContext(deployment, tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString); + RefreshableOidcSecurityContext secContext = new RefreshableOidcSecurityContext(deployment, facade.getRequest().getCookie(SESSION_RANDOM_VALUE), tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString); return new OidcPrincipal<>(idToken.getPrincipalName(deployment), secContext); } catch (InvalidJwtException e) { log.failedToParseTokenFromCookie(e);
http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java+17 −0 modified@@ -19,11 +19,15 @@ package org.wildfly.security.http.oidc; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.jose.jwk.JWKUtil.BASE64_URL; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.StringTokenizer; import java.util.UUID; @@ -34,6 +38,7 @@ import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpRequestBase; import org.jose4j.jws.AlgorithmIdentifiers; +import org.wildfly.common.iteration.ByteIterator; import org.wildfly.security.jose.util.JsonSerialization; /** @@ -111,6 +116,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_RANDOM_VALUE="session_random_value"; public static final String SESSION_STATE = "session_state"; public static final String SOAP_ACTION = "SOAPAction"; public static final String SSL_REQUIRED = "ssl-required"; @@ -119,6 +125,7 @@ public class Oidc { public static final int INVALID_ISSUED_FOR_CLAIM = -1; public static final int INVALID_AT_HASH_CLAIM = -2; public static final int INVALID_TYPE_CLAIM = -3; + public static final int INVALID_SESSION_RANDOM_VALUE = -4; static final String OIDC_CLIENT_CONFIG_RESOLVER = "oidc.config.resolver"; static final String OIDC_CONFIG_FILE_LOCATION = "oidc.config.file"; static final String OIDC_JSON_FILE = "/WEB-INF/oidc.json"; @@ -445,4 +452,14 @@ protected static boolean checkCachedAccountMatchesRequest(OidcAccount account, O return true; } + protected static String getCryptographicValue(final String src) { + try { + MessageDigest md = MessageDigest.getInstance(SHA256); + md.update(src.getBytes(StandardCharsets.UTF_8)); + return ByteIterator.ofBytes(md.digest()) + .base64Encode(BASE64_URL, false).drainToString(); + } catch (NoSuchAlgorithmException e) { + throw log.noSuchAlgorithm(e.getMessage()); + } + } }
http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java+37 −13 modified@@ -23,6 +23,7 @@ import static org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA512; import static org.jose4j.jws.AlgorithmIdentifiers.NONE; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.IDToken.NONCE; import static org.wildfly.security.http.oidc.Oidc.ALLOW_QUERY_PARAMS_PROPERTY_NAME; import static org.wildfly.security.http.oidc.Oidc.CLIENT_ID; import static org.wildfly.security.http.oidc.Oidc.CODE; @@ -39,6 +40,7 @@ import static org.wildfly.security.http.oidc.Oidc.REQUEST; import static org.wildfly.security.http.oidc.Oidc.REQUEST_URI; import static org.wildfly.security.http.oidc.Oidc.SCOPE; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import static org.wildfly.security.http.oidc.Oidc.SESSION_STATE; import static org.wildfly.security.http.oidc.Oidc.STATE; import static org.wildfly.security.http.oidc.Oidc.UI_LOCALES; @@ -59,6 +61,7 @@ import java.security.Key; import java.security.KeyPair; import java.security.PublicKey; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -76,6 +79,7 @@ import org.jose4j.jwt.JwtClaims; import org.jose4j.keys.HmacKey; import org.jose4j.lang.JoseException; +import org.wildfly.common.iteration.ByteIterator; import org.wildfly.security.http.HttpConstants; /** @@ -96,6 +100,8 @@ public class OidcRequestAuthenticator { protected String refreshToken; protected String strippedOauthParametersRequestUri; + private int NONCE_SIZE = 36; + static final boolean ALLOW_QUERY_PARAMS_PROPERTY; static { @@ -181,7 +187,7 @@ protected String getCode() { return getQueryParamValue(facade, CODE); } - protected String getRedirectUri(String state) { + protected String getRedirectUri(String state, String sessionRandomValueHash) { String url = getRequestUrl(); log.debugf("callback uri: %s", url); @@ -229,31 +235,31 @@ protected String getRedirectUri(String state) { if (deployment.getRequestParameterSupported()) { // add request objects into request parameter try { - createRequestWithRequestParameter(REQUEST, redirectUriBuilder, redirectUri, state, forwardedQueryParams); + createRequestWithRequestParameter(REQUEST, redirectUriBuilder, redirectUri, state, forwardedQueryParams, sessionRandomValueHash); } catch (IOException | JoseException e) { throw log.unableToCreateRequestWithRequestParameter(e); } } else { // send request as usual - createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams, sessionRandomValueHash); log.requestParameterNotSupported(); } break; case REQUEST_URI: if (deployment.getRequestUriParameterSupported()) { try { - createRequestWithRequestParameter(REQUEST_URI, redirectUriBuilder, redirectUri, state, forwardedQueryParams); + createRequestWithRequestParameter(REQUEST_URI, redirectUriBuilder, redirectUri, state, forwardedQueryParams, sessionRandomValueHash); } catch (IOException | JoseException e) { throw log.unableToCreateRequestUriWithRequestParameter(e); } } else { // send request as usual - createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams, sessionRandomValueHash); log.requestParameterNotSupported(); } break; default: - createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams, sessionRandomValueHash); break; } return redirectUriBuilder.build().toString(); @@ -262,15 +268,16 @@ protected String getRedirectUri(String state) { } } - protected URIBuilder createOAuthRequest(URIBuilder redirectUriBuilder, String redirectUri, String state, List<NameValuePair> forwardedQueryParams) { + protected URIBuilder createOAuthRequest(URIBuilder redirectUriBuilder, String redirectUri, String state, List<NameValuePair> forwardedQueryParams, String sessionRandomValueHash) { redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri) .addParameter(STATE, state) - .addParameters(forwardedQueryParams); + .addParameters(forwardedQueryParams) + .addParameter(NONCE, sessionRandomValueHash); return redirectUriBuilder; } - protected URIBuilder createRequestWithRequestParameter(String requestFormat, URIBuilder redirectUriBuilder, String redirectUri, String state, List<NameValuePair> forwardedQueryParams) throws JoseException, IOException { - String request = convertToRequestParameter(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + protected URIBuilder createRequestWithRequestParameter(String requestFormat, URIBuilder redirectUriBuilder, String redirectUri, String state, List<NameValuePair> forwardedQueryParams, String sessionRandomValueHash) throws JoseException, IOException { + String request = convertToRequestParameter(redirectUriBuilder, redirectUri, state, forwardedQueryParams, sessionRandomValueHash); switch (requestFormat) { case REQUEST: @@ -296,7 +303,8 @@ protected String getStateCode() { protected AuthChallenge loginRedirect() { final String state = getStateCode(); - final String redirect = getRedirectUri(state); + final String sessionRandomValue = generateSessionRandomValue(); + final String redirect = getRedirectUri(state, Oidc.getCryptographicValue(sessionRandomValue)); if (redirect == null) { return challenge(HttpStatus.SC_FORBIDDEN, AuthenticationError.Reason.NO_REDIRECT_URI, null); } @@ -314,6 +322,8 @@ public boolean challenge(OidcHttpFacade exchange) { exchange.getResponse().setStatus(HttpStatus.SC_MOVED_TEMPORARILY); exchange.getResponse().setCookie(deployment.getStateCookieName(), state, "/", null, -1, deployment.getSSLRequired().isRequired(facade.getRequest().getRemoteAddr()), true); exchange.getResponse().setHeader(HttpConstants.LOCATION, redirect); + exchange.getResponse().setCookie(SESSION_RANDOM_VALUE, sessionRandomValue, "/", null, -1, deployment.getSSLRequired().isRequired(facade.getRequest().getRemoteAddr()), true); + return true; } }; @@ -336,6 +346,7 @@ 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)) { log.warn("state parameter invalid"); log.warn("cookie: " + stateCookieValue); @@ -441,9 +452,12 @@ protected AuthChallenge resolveCode(String code) { try { TokenValidator tokenValidator = TokenValidator.builder(deployment).build(); - TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, tokenString); + + TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, tokenString, + facade.getRequest().getCookie(SESSION_RANDOM_VALUE)); idToken = verifiedTokens.getIdToken(); token = verifiedTokens.getAccessToken(); + log.debug("Token Verification succeeded!"); } catch (OidcException e) { log.failedVerificationOfToken(e.getMessage()); @@ -456,6 +470,7 @@ protected AuthChallenge resolveCode(String code) { log.error("Stale token"); return challenge(HttpStatus.SC_FORBIDDEN, AuthenticationError.Reason.STALE_TOKEN, null); } + log.debug("successfully authenticated"); return null; } @@ -535,7 +550,7 @@ private void addScopes(String scopes, Set<String> allScopes) { } } - private String convertToRequestParameter(URIBuilder redirectUriBuilder, String redirectUri, String state, List<NameValuePair> forwardedQueryParams) throws JoseException, IOException { + private String convertToRequestParameter(URIBuilder redirectUriBuilder, String redirectUri, String state, List<NameValuePair> forwardedQueryParams, String sessionRandomValueHash) throws JoseException, IOException { redirectUriBuilder.addParameter(SCOPE, OIDC_SCOPE); JwtClaims jwtClaims = new JwtClaims(); @@ -545,10 +560,12 @@ private String convertToRequestParameter(URIBuilder redirectUriBuilder, String r for ( NameValuePair parameter: forwardedQueryParams) { jwtClaims.setClaim(parameter.getName(), parameter.getValue()); } + jwtClaims.setClaim(STATE, state); jwtClaims.setClaim(REDIRECT_URI, redirectUri); jwtClaims.setClaim(RESPONSE_TYPE, CODE); jwtClaims.setClaim(CLIENT_ID, deployment.getResourceName()); + jwtClaims.setClaim(NONCE, sessionRandomValueHash); // sign JWT first before encrypting JsonWebSignature signedRequest = signRequest(jwtClaims, deployment); @@ -622,4 +639,11 @@ private JsonWebEncryption encryptRequest(JsonWebSignature signedRequest) throws return jsonEncryption; } } + + private String generateSessionRandomValue() { + SecureRandom random = new SecureRandom(); + byte[] nonceData = new byte[NONCE_SIZE]; + random.nextBytes(nonceData); + return ByteIterator.ofBytes(nonceData).base64Encode().drainToString(); + } }
http/oidc/src/main/java/org/wildfly/security/http/oidc/RefreshableOidcSecurityContext.java+4 −2 modified@@ -34,16 +34,18 @@ public class RefreshableOidcSecurityContext extends OidcSecurityContext { protected transient OidcClientConfiguration clientConfiguration; protected transient OidcTokenStore tokenStore; protected String refreshToken; + protected transient OidcHttpFacade.Cookie cookie; public RefreshableOidcSecurityContext() { } - public RefreshableOidcSecurityContext(OidcClientConfiguration clientConfiguration, OidcTokenStore tokenStore, String tokenString, + public RefreshableOidcSecurityContext(OidcClientConfiguration clientConfiguration, OidcHttpFacade.Cookie cookie, OidcTokenStore tokenStore, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) { super(tokenString, token, idTokenString, idToken); this.clientConfiguration = clientConfiguration; this.tokenStore = tokenStore; this.refreshToken = refreshToken; + this.cookie = cookie; } @Override @@ -149,7 +151,7 @@ public boolean refreshToken(boolean checkActive) { IDToken idToken; try { TokenValidator tokenValidator = TokenValidator.builder(clientConfiguration).build(); - TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, accessTokenString); + TokenValidator.VerifiedTokens verifiedTokens = tokenValidator.parseAndVerifyToken(idTokenString, accessTokenString, cookie); idToken = verifiedTokens.getIdToken(); accessToken = verifiedTokens.getAccessToken(); log.debug("Token Verification succeeded!");
http/oidc/src/main/java/org/wildfly/security/http/oidc/RequestAuthenticator.java+3 −2 modified@@ -24,6 +24,7 @@ import static org.wildfly.security.http.oidc.Oidc.FACES_REQUEST; import static org.wildfly.security.http.oidc.Oidc.HTML_CONTENT_TYPE; import static org.wildfly.security.http.oidc.Oidc.PARTIAL; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import static org.wildfly.security.http.oidc.Oidc.SOAP_ACTION; import static org.wildfly.security.http.oidc.Oidc.TEXT_CONTENT_TYPE; import static org.wildfly.security.http.oidc.Oidc.WILDCARD_CONTENT_TYPE; @@ -200,14 +201,14 @@ protected boolean verifySSL() { } protected void completeAuthentication(OidcRequestAuthenticator oidc) { - RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, facade.getTokenStore(), oidc.getTokenString(), oidc.getToken(), oidc.getIDTokenString(), oidc.getIDToken(), oidc.getRefreshToken()); + RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, facade.getRequest().getCookie(SESSION_RANDOM_VALUE), facade.getTokenStore(), oidc.getTokenString(), oidc.getToken(), oidc.getIDTokenString(), oidc.getIDToken(), oidc.getRefreshToken()); final OidcPrincipal<RefreshableOidcSecurityContext> principal = new OidcPrincipal<>(oidc.getIDToken().getPrincipalName(deployment), session); completeOidcAuthentication(principal); log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName()); } protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) { - RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null); + RefreshableOidcSecurityContext session = new RefreshableOidcSecurityContext(deployment, facade.getRequest().getCookie(SESSION_RANDOM_VALUE), null, bearer.getTokenString(), bearer.getToken(), null, null, null); final OidcPrincipal<RefreshableOidcSecurityContext> principal = new OidcPrincipal<>(bearer.getToken().getPrincipalName(deployment), session); completeBearerAuthentication(principal); log.debugv("User ''{0}'' invoking ''{1}'' on client ''{2}''", principal.getName(), facade.getRequest().getURI(), deployment.getResourceName());
http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java+31 −1 modified@@ -23,6 +23,7 @@ import static org.wildfly.security.http.oidc.Oidc.DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME; import static org.wildfly.security.http.oidc.Oidc.INVALID_AT_HASH_CLAIM; import static org.wildfly.security.http.oidc.Oidc.INVALID_ISSUED_FOR_CLAIM; +import static org.wildfly.security.http.oidc.Oidc.INVALID_SESSION_RANDOM_VALUE; import static org.wildfly.security.http.oidc.Oidc.INVALID_TYPE_CLAIM; import static org.wildfly.security.http.oidc.Oidc.getJavaAlgorithmForHash; import static org.wildfly.security.jose.jwk.JWKUtil.BASE64_URL; @@ -82,12 +83,14 @@ private TokenValidator(Builder builder) { * @return the {@code VerifiedTokens} if the ID token was valid * @throws OidcException if the ID token is invalid */ - public VerifiedTokens parseAndVerifyToken(final String idToken, final String accessToken) throws OidcException { + public VerifiedTokens parseAndVerifyToken(final String idToken, final String accessToken, OidcHttpFacade.Cookie cookie) throws OidcException { try { JwtContext idJwtContext = setVerificationKey(idToken, jwtConsumerBuilder); jwtConsumerBuilder.setExpectedAudience(clientConfiguration.getResourceName()); jwtConsumerBuilder.registerValidator(new AzpValidator(clientConfiguration.getResourceName())); jwtConsumerBuilder.registerValidator(new AtHashValidator(accessToken, clientConfiguration.getTokenSignatureAlgorithm())); + jwtConsumerBuilder.registerValidator(new NonceValidator(cookie)); + // second pass to validate jwtConsumerBuilder.build().processContext(idJwtContext); JwtClaims idJwtClaims = idJwtContext.getJwtClaims(); @@ -288,6 +291,33 @@ private static String getAccessTokenHash(String accessTokenString, String jwsAlg } + + private static class NonceValidator implements ErrorCodeValidator { + private OidcHttpFacade.Cookie cookie; + + public NonceValidator(OidcHttpFacade.Cookie cookie) { + this.cookie = cookie; + } + + public ErrorCodeValidator.Error validate(JwtContext jwtContext) throws MalformedClaimException { + JwtClaims idJwtClaims = jwtContext.getJwtClaims(); + IDToken idToken = new IDToken(idJwtClaims); + + if (cookie != null) { + String sessionRandomValue = Oidc.getCryptographicValue(cookie.getValue()); + String nonceValue = idToken.getNonce(); + if (!sessionRandomValue.equals(nonceValue)) { + return new ErrorCodeValidator.Error(INVALID_SESSION_RANDOM_VALUE, + log.invalidNonceValue(nonceValue)); + } + } else { + return new ErrorCodeValidator.Error(INVALID_SESSION_RANDOM_VALUE, + log.nonceCookieDoesNotExist()); + } + return null; + } + } + private static class TypeValidator implements ErrorCodeValidator { public static final String TYPE = "typ"; private final String expectedType;
http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java+60 −5 modified@@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME; +import static org.wildfly.security.http.oidc.Oidc.SESSION_RANDOM_VALUE; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -211,6 +212,33 @@ public MockResponse dispatch(RecordedRequest recordedRequest) throws Interrupted }; } + protected static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, + int expectedStatusCode, String expectedLocation, + String clientPageText, List<HttpServerCookie> cookies) { + return new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException { + String path = recordedRequest.getPath(); + if (path.contains("/" + CLIENT_APP) && path.contains("&code=")) { + try { + + TestingHttpServerRequest request = new TestingHttpServerRequest(new String[0], + new URI(recordedRequest.getRequestUrl().toString()), cookies); + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + assertEquals(expectedStatusCode, response.getStatusCode()); + assertEquals(expectedLocation, response.getLocation()); + return new MockResponse().setBody(clientPageText); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return new MockResponse() + .setBody(""); + } + }; + } + protected static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, int expectedStatusCode, String expectedLocation, String clientPageText, Map<String, Object> sessionScopeAttachments) { return new Dispatcher() { @@ -351,24 +379,37 @@ protected void checkForScopeClaims(Callback callback, String expectedScopes) thr protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String expectedLocation, String clientPageText) throws Exception { performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), expectedLocation, - clientPageText, null, false); + clientPageText, null, false, false ); + } + + protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, + boolean changeSessionId) throws Exception { + performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), expectedLocation, + clientPageText, null, false, changeSessionId); } protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String clientUrl, String expectedLocation, String clientPageText) throws Exception { performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, clientUrl, expectedLocation, - clientPageText, null, false); + clientPageText, null, false, false); } + protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, + String expectedLocation, String clientPageText, String expectedScope, boolean checkInvalidScopeError, + boolean changeSessionId) throws Exception { + performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), expectedLocation, clientPageText, + expectedScope, checkInvalidScopeError, changeSessionId); + } protected void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, String expectedScope, boolean checkInvalidScopeError) throws Exception { performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, getClientUrl(), expectedLocation, clientPageText, - expectedScope, checkInvalidScopeError); + expectedScope, checkInvalidScopeError, false); } private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String clientUrl, String expectedLocation, String clientPageText, - String expectedScope, boolean checkInvalidScopeError) throws Exception { + String expectedScope, boolean checkInvalidScopeError, boolean changeSessionId) throws Exception { try { Map<String, Object> props = new HashMap<>(); OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); @@ -394,7 +435,21 @@ private void performAuthentication(InputStream oidcConfig, String username, Stri } if (loginToKeycloak) { - client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, expectedLocation, clientPageText)); + // change the sessionRandomValue value so that the compare to nonce will fail. + List<HttpServerCookie> tmpCookies = response.getCookies(); + if (changeSessionId) { + for (HttpServerCookie c : tmpCookies) { + if (c.getName().equals(SESSION_RANDOM_VALUE)) { + HttpServerCookie tmpCookie = HttpServerCookie.getInstance(c.getName(), + "9999" + c.getValue(), c.getDomain(), c.getMaxAge(), c.getPath(), + c.isSecure(), c.getVersion(), c.isHttpOnly()); + tmpCookies.remove(c); + tmpCookies.add(tmpCookie); + } + } + } + client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, + expectedLocation, clientPageText, tmpCookies)); if (checkInvalidScopeError) { WebClient webClient = getWebClient();
http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcSecurityRealmTest.java+6 −6 modified@@ -72,7 +72,7 @@ public void testGetRealmIdentityWithNonOidcPrincipal() throws RealmUnavailableEx @Test public void testGetRealmIdentityNoRoles() throws RealmUnavailableException { // setup - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(new OidcClientConfiguration(), + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(new OidcClientConfiguration(), null, null, null, new AccessToken(new JwtClaims()), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -108,7 +108,7 @@ public void testGetRealmIdentityRolesCombined() throws RealmUnavailableException jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("realm_access", createRoles("roleC", "roleD")); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -137,7 +137,7 @@ public void testGetRealmIdentityOnlyRealmRoles() throws RealmUnavailableExceptio jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("realm_access", createRoles("roleC", "roleD")); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -165,7 +165,7 @@ public void testGetRealmIdentityOnlyResourceRoles() throws RealmUnavailableExcep jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("", new RealmAccessClaim(createRoles("roleC", "roleD"))); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -193,7 +193,7 @@ public void testGetRealmIdentityNoMappings() throws RealmUnavailableException { jwtClaims.setClaim("resource_access", resourceAccess); jwtClaims.setClaim("", new RealmAccessClaim(createRoles("roleC", "roleD"))); - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext); @@ -390,7 +390,7 @@ private static String getRealmAndResourceRolesClaims(boolean includeRealmAndReso } private Attributes.Entry getRealmIdentityRoles(OidcClientConfiguration clientConfiguration, JwtClaims jwtClaims) throws RealmUnavailableException { - RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration, + RefreshableOidcSecurityContext securityContext = new RefreshableOidcSecurityContext(clientConfiguration,null, null, null, new AccessToken(jwtClaims), null, null, null); OidcPrincipal principal = new OidcPrincipal("john", securityContext);
http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java+22 −0 modified@@ -367,6 +367,28 @@ public void testRequestObjectConfigMissingENCValue() throws Exception { testRequestObjectInvalidConfiguration(getOidcConfigurationInputStreamWithoutEncValue(REQUEST.getValue(), RSA_OAEP), RequestObjectErrorType.MISSING_ENC_VALUE); } + @Test + // Generate an invalid sessionRandomValue so that the nonce check fails + public void testRequestParameterNonceMismatch() throws Exception { + performAuthentication( + getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), NONE, "", ""), + KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,true, + HttpStatus.SC_FORBIDDEN,null, CLIENT_PAGE_TEXT, true); + } + + @Test + public void testRequestUriParameterNonceMismatch() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_URI.getValue(), NONE, "", ""), + KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, true, + HttpStatus.SC_FORBIDDEN, null, CLIENT_PAGE_TEXT, true); + } + + @Test + public void testStandardConfigWithNonceMismatch() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_FORBIDDEN, null, CLIENT_PAGE_TEXT, true); + } + /***************************************************************************************************************************************** * Tests for multi-tenancy. *
tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java+12 −3 modified@@ -213,11 +213,20 @@ public TestingHttpServerRequest(String[] authorization, URI requestURI, String c } this.remoteUser = null; this.requestURI = requestURI; + this.cookies = new ArrayList<>(); if (cookie != null) { - final String cookieName = cookie.substring(0, cookie.indexOf('=')); - final String cookieValue = cookie.substring(cookie.indexOf('=') + 1); - cookies.add(HttpServerCookie.getInstance(cookieName, cookieValue, null, -1, "/", false, 0, true)); + String[] cookiesArr = cookie.split(";"); + if (cookiesArr.length == 0) { + cookiesArr[0] = cookie; + } + for (int i = 0; i < cookiesArr.length; i++) { + String[] cookiePair = cookiesArr[i].trim().split("="); + if (cookiePair.length == 2) { + this.cookies.add(HttpServerCookie.getInstance( + cookiePair[0], cookiePair[1], null, -1, "/", false, 0, true)); + } + } } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
13- github.com/advisories/GHSA-5565-3c98-g6jcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-12369ghsaADVISORY
- access.redhat.com/security/cve/CVE-2024-12369nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/wildfly-security/wildfly-elytron/commit/5ac5e6bbcba58883b3cebb2ddbcec4de140c5cebnvdWEB
- github.com/wildfly-security/wildfly-elytron/commit/d7754f5a6a91ceb0f4dbbbfe301991f6a55404cbnvdWEB
- github.com/wildfly-security/wildfly-elytron/pull/2253nvdWEB
- github.com/wildfly-security/wildfly-elytron/pull/2261nvdWEB
- github.com/wildfly-security/wildfly-elytron/security/advisories/GHSA-5565-3c98-g6jcghsaWEB
- issues.redhat.com/browse/ELY-2887ghsaWEB
- access.redhat.com/errata/RHSA-2025:3989nvd
- access.redhat.com/errata/RHSA-2025:3990nvd
- access.redhat.com/errata/RHSA-2025:3992nvd
News mentions
0No linked articles in our index yet.