VYPR
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.

PackageAffected versionsPatched versions
org.wildfly.security:wildfly-elytronMaven
>= 1.17.0.Final, < 2.2.9.Final2.2.9.Final
org.wildfly.security:wildfly-elytronMaven
>= 2.3.0.Final, < 2.6.2.Final2.6.2.Final
org.wildfly.security:wildfly-elytron-http-oidcMaven
>= 1.17.0.Final, < 2.2.9.Final2.2.9.Final
org.wildfly.security:wildfly-elytron-http-oidcMaven
>= 2.3.0.Final, < 2.6.2.Final2.6.2.Final

Patches

2
d7754f5a6a91

[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

News mentions

0

No linked articles in our index yet.