This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new faf40ba0c98 CXF-9225 - Enforce nonce for the hybrid case (#3328)
faf40ba0c98 is described below

commit faf40ba0c98c1621522cf8d9c1fff64a8cdb126d
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Jul 23 09:28:56 2026 +0100

    CXF-9225 - Enforce nonce for the hybrid case (#3328)
---
 .../oidc/rp/OidcClientCodeRequestFilter.java       |  84 ++++-
 .../rp/OidcClientCodeRequestFilterNonceTest.java   | 402 +++++++++++++++++++++
 2 files changed, 476 insertions(+), 10 deletions(-)

diff --git 
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClientCodeRequestFilter.java
 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClientCodeRequestFilter.java
index 0299217564c..1d9fa450ef5 100644
--- 
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClientCodeRequestFilter.java
+++ 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClientCodeRequestFilter.java
@@ -26,6 +26,7 @@ import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.SecurityContext;
 import jakarta.ws.rs.core.UriBuilder;
 import jakarta.ws.rs.core.UriInfo;
+import org.apache.cxf.common.util.Base64UrlUtility;
 import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter;
 import org.apache.cxf.jaxrs.utils.ExceptionUtils;
 import org.apache.cxf.rs.security.oauth2.client.ClientCodeRequestFilter;
@@ -35,6 +36,8 @@ import 
org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
 import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
 import org.apache.cxf.rs.security.oidc.common.ClaimsRequest;
 import org.apache.cxf.rs.security.oidc.common.IdToken;
+import org.apache.cxf.rs.security.oidc.utils.OidcUtils;
+import org.apache.cxf.rt.security.crypto.CryptoUtils;
 
 public class OidcClientCodeRequestFilter extends ClientCodeRequestFilter {
 
@@ -51,6 +54,14 @@ public class OidcClientCodeRequestFilter extends 
ClientCodeRequestFilter {
     private String claims;
     private String claimsLocales;
     private String roleClaim;
+    /**
+     * The OAuth 2.0 / OIDC response_type to request from the Authorization 
Endpoint.
+     * Defaults to {@code code} (Authorization Code Flow).
+     * Set to an Implicit or Hybrid value (e.g. {@code id_token}, {@code code 
id_token})
+     * to enable those flows; a nonce will be auto-generated and enforced for 
any
+     * response type that contains {@code id_token}, per OIDC Core §3.2.2.1 
and §3.3.2.1.
+     */
+    private String responseType;
 
     public OidcClientCodeRequestFilter() {
         super();
@@ -61,6 +72,27 @@ public class OidcClientCodeRequestFilter extends 
ClientCodeRequestFilter {
         this.authenticationContextRef = Arrays.asList(acr.split(" "));
     }
 
+    /**
+     * Set the {@code response_type} sent to the Authorization Endpoint.
+     * When set to a value that contains {@code id_token} (Implicit or Hybrid 
flows),
+     * the filter will automatically generate a nonce for each authorization 
request
+     * and enforce its presence in the returned ID Token.
+     * @param responseType e.g. {@code id_token}, {@code code id_token}, 
{@code id_token token}
+     */
+    public void setResponseType(String responseType) {
+        this.responseType = responseType;
+    }
+
+    /**
+     * Returns {@code true} when the configured response type causes an ID 
Token to be
+     * delivered directly from the Authorization Endpoint (Implicit or Hybrid 
flows),
+     * meaning a nonce is REQUIRED per OIDC Core §3.2.2.1 and §3.3.2.1.
+     */
+    private boolean isNonceRequired() {
+        return responseType != null
+            && responseType.contains(OidcUtils.ID_TOKEN_RESPONSE_TYPE);
+    }
+
     @Override
     protected ClientTokenContext createTokenContext(ContainerRequestContext rc,
                                                     ClientAccessToken at,
@@ -77,8 +109,13 @@ public class OidcClientCodeRequestFilter extends 
ClientCodeRequestFilter {
             IdToken idToken = idTokenReader.getIdToken(at,
                                   
requestParams.getFirst(OAuthConstants.AUTHORIZATION_CODE_VALUE),
                                   getConsumer());
+            // Hybrid flow can be detected from context: when the 
Authorization Endpoint
+            // returns an id_token directly in the callback parameters 
(response_type
+            // contains "id_token" together with "code"), a nonce is REQUIRED 
even if
+            // the caller has not explicitly configured a responseType on this 
filter.
+            boolean hybridFlowDetected = 
requestParams.containsKey(OidcUtils.ID_TOKEN);
             // Validate the properties set up at the redirection time.
-            validateIdToken(idToken, state);
+            validateIdToken(idToken, state, hybridFlowDetected);
 
             ctx.setIdToken(idToken);
             if (userInfoClient != null) {
@@ -100,12 +137,29 @@ public class OidcClientCodeRequestFilter extends 
ClientCodeRequestFilter {
         if (maxAgeOffset != null) {
             state.putSingle(MAX_AGE_PARAMETER, 
Long.toString(System.currentTimeMillis() + maxAgeOffset));
         }
+        // Per OIDC Core §3.2.2.1 and §3.3.2.1, a nonce is REQUIRED for 
Implicit and Hybrid flows
+        // (any response_type containing "id_token"). Auto-generate one if the 
caller has not
+        // already supplied it, so replay protection is always active for 
these flows.
+        if (isNonceRequired() && state.getFirst(IdToken.NONCE_CLAIM) == null) {
+            state.putSingle(IdToken.NONCE_CLAIM,
+                
Base64UrlUtility.encode(CryptoUtils.generateSecureRandomBytes(16)));
+        }
         return state;
     }
 
-    private void validateIdToken(IdToken idToken, MultivaluedMap<String, 
String> state) {
+    private void validateIdToken(IdToken idToken, MultivaluedMap<String, 
String> state,
+                                    boolean hybridFlowDetected) {
 
-        String nonce = state.getFirst(IdToken.NONCE_CLAIM);
+        String nonce = state != null ? state.getFirst(IdToken.NONCE_CLAIM) : 
null;
+        // A nonce is REQUIRED (OIDC Core §3.2.2.1 / §3.3.2.1) when:
+        //  (a) the configured responseType contains "id_token" (Implicit or 
Hybrid), OR
+        //  (b) an id_token was observed in the authorization callback 
parameters, which
+        //      indicates a Hybrid flow even without explicit responseType 
configuration.
+        // In either case, reject the response if no nonce was round-tripped — 
this means
+        // the authorization request was sent without one, removing replay 
protection.
+        if ((isNonceRequired() || hybridFlowDetected) && nonce == null) {
+            throw new OAuthServiceException(OAuthConstants.INVALID_REQUEST);
+        }
         String tokenNonce = idToken.getNonce();
         if (nonce != null && (tokenNonce == null || 
!nonce.equals(tokenNonce))) {
             throw new OAuthServiceException(OAuthConstants.INVALID_REQUEST);
@@ -145,13 +199,18 @@ public class OidcClientCodeRequestFilter extends 
ClientCodeRequestFilter {
     protected void setAdditionalCodeRequestParams(UriBuilder ub,
                                                   MultivaluedMap<String, 
String> redirectState,
                                                   MultivaluedMap<String, 
String> codeRequestState) {
-        if (redirectState != null) {
-            if (redirectState.getFirst(IdToken.NONCE_CLAIM) != null) {
-                ub.queryParam(IdToken.NONCE_CLAIM, 
redirectState.getFirst(IdToken.NONCE_CLAIM));
-            }
-            if (redirectState.getFirst(MAX_AGE_PARAMETER) != null) {
-                ub.queryParam(MAX_AGE_PARAMETER, 
redirectState.getFirst(MAX_AGE_PARAMETER));
-            }
+        // Prefer the nonce from redirectState (managed by the 
ClientCodeStateManager).
+        // Fall back to codeRequestState to cover the auto-generated nonce 
path used when the
+        // state manager does not copy the nonce into its redirect map.
+        String nonce = redirectState != null ? 
redirectState.getFirst(IdToken.NONCE_CLAIM) : null;
+        if (nonce == null && codeRequestState != null) {
+            nonce = codeRequestState.getFirst(IdToken.NONCE_CLAIM);
+        }
+        if (nonce != null) {
+            ub.queryParam(IdToken.NONCE_CLAIM, nonce);
+        }
+        if (redirectState != null && redirectState.getFirst(MAX_AGE_PARAMETER) 
!= null) {
+            ub.queryParam(MAX_AGE_PARAMETER, 
redirectState.getFirst(MAX_AGE_PARAMETER));
         }
         if (codeRequestState != null && 
codeRequestState.getFirst(LOGIN_HINT_PARAMETER) != null) {
             ub.queryParam(LOGIN_HINT_PARAMETER, 
codeRequestState.getFirst(LOGIN_HINT_PARAMETER));
@@ -168,6 +227,11 @@ public class OidcClientCodeRequestFilter extends 
ClientCodeRequestFilter {
         if (promptLogin != null) {
             ub.queryParam(PROMPT_PARAMETER, promptLogin);
         }
+        // Override the response_type set by the base filter (which defaults 
to "code").
+        // This is required to support Implicit (id_token) and Hybrid (code 
id_token, etc.) flows.
+        if (responseType != null && 
!OAuthConstants.CODE_RESPONSE_TYPE.equals(responseType)) {
+            ub.replaceQueryParam(OAuthConstants.RESPONSE_TYPE, responseType);
+        }
 
     }
 
diff --git 
a/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcClientCodeRequestFilterNonceTest.java
 
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcClientCodeRequestFilterNonceTest.java
new file mode 100644
index 00000000000..598edae2d35
--- /dev/null
+++ 
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcClientCodeRequestFilterNonceTest.java
@@ -0,0 +1,402 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.rs.security.oidc.rp;
+
+import java.lang.reflect.Method;
+import java.net.URI;
+
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.UriInfo;
+import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
+import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
+import org.apache.cxf.rs.security.oidc.common.IdToken;
+import org.apache.cxf.rs.security.oidc.utils.OidcUtils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for nonce enforcement in {@link OidcClientCodeRequestFilter}.
+ *
+ * OIDC Core §3.2.2.1 (Implicit Flow) and §3.3.2.1 (Hybrid Flow) require the
+ * {@code nonce} parameter whenever an ID Token may be returned directly from
+ * the Authorization Endpoint (i.e., the response_type contains {@code 
id_token}).
+ */
+public class OidcClientCodeRequestFilterNonceTest {
+
+    private static final URI ABSOLUTE_PATH = 
URI.create("https://app.example.com/rp/callback";);
+
+    // -----------------------------------------------------------------------
+    // validateIdToken – direct tests via reflection
+    // -----------------------------------------------------------------------
+
+    /**
+     * Code Flow (default): nonce is optional. A token with no nonce claim 
must be
+     * accepted even when the state carries no nonce.
+     */
+    @Test
+    public void testCodeFlowAcceptsTokenWithoutNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        // no responseType set → code flow
+
+        IdToken token = new IdToken();
+        // no nonce in token
+
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        // no nonce in state
+
+        invokeValidateIdToken(filter, token, state); // must not throw
+    }
+
+    /**
+     * Code Flow: if the RP sent a nonce it MUST be echoed back by the IdP.
+     */
+    @Test
+    public void testCodeFlowRejectsNonceMismatch() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+
+        IdToken token = new IdToken();
+        token.setNonce("wrong-nonce");
+
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        state.putSingle(IdToken.NONCE_CLAIM, "correct-nonce");
+
+        try {
+            invokeValidateIdToken(filter, token, state);
+            fail("Expected OAuthServiceException for nonce mismatch");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    /**
+     * Code Flow: token nonce absent when state carries one must be rejected.
+     */
+    @Test
+    public void testCodeFlowRejectsAbsentTokenNonceWhenStateHasOne() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+
+        IdToken token = new IdToken();
+        // no nonce in token
+
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        state.putSingle(IdToken.NONCE_CLAIM, "expected-nonce");
+
+        try {
+            invokeValidateIdToken(filter, token, state);
+            fail("Expected OAuthServiceException: token nonce absent");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    /**
+     * Implicit Flow (response_type=id_token): nonce is REQUIRED.
+     * A null state (no ClientCodeStateManager configured) must be rejected.
+     */
+    @Test
+    public void testImplicitFlowRejectsNullState() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.ID_TOKEN_RESPONSE_TYPE);
+
+        IdToken token = new IdToken();
+
+        try {
+            invokeValidateIdToken(filter, token, null);
+            fail("Expected OAuthServiceException: nonce required for implicit 
flow");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    /**
+     * Implicit Flow: nonce is REQUIRED; state without one must be rejected.
+     */
+    @Test
+    public void testImplicitFlowRejectsStateWithoutNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.ID_TOKEN_RESPONSE_TYPE);
+
+        IdToken token = new IdToken();
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        // no nonce in state
+
+        try {
+            invokeValidateIdToken(filter, token, state);
+            fail("Expected OAuthServiceException: nonce required for implicit 
flow");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    /**
+     * Implicit Flow: valid nonce round-trip must be accepted.
+     */
+    @Test
+    public void testImplicitFlowAcceptsMatchingNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.ID_TOKEN_RESPONSE_TYPE);
+
+        IdToken token = new IdToken();
+        token.setNonce("session-nonce-abc");
+
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        state.putSingle(IdToken.NONCE_CLAIM, "session-nonce-abc");
+
+        invokeValidateIdToken(filter, token, state); // must not throw
+    }
+
+    /**
+     * Implicit Flow: nonce mismatch must be rejected.
+     */
+    @Test
+    public void testImplicitFlowRejectsNonceMismatch() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.ID_TOKEN_RESPONSE_TYPE);
+
+        IdToken token = new IdToken();
+        token.setNonce("attacker-nonce");
+
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        state.putSingle(IdToken.NONCE_CLAIM, "session-nonce-abc");
+
+        try {
+            invokeValidateIdToken(filter, token, state);
+            fail("Expected OAuthServiceException for nonce mismatch");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    /**
+     * Hybrid Flow (response_type=code id_token): nonce is REQUIRED.
+     */
+    @Test
+    public void testHybridFlowRejectsStateWithoutNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.CODE_ID_TOKEN_RESPONSE_TYPE);
+
+        IdToken token = new IdToken();
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+
+        try {
+            invokeValidateIdToken(filter, token, state);
+            fail("Expected OAuthServiceException: nonce required for hybrid 
flow");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    /**
+     * Hybrid Flow (response_type=id_token token): nonce is REQUIRED.
+     */
+    @Test
+    public void testImplicitWithAccessTokenFlowRejectsStateWithoutNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.ID_TOKEN_AT_RESPONSE_TYPE);
+
+        IdToken token = new IdToken();
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+
+        try {
+            invokeValidateIdToken(filter, token, state);
+            fail("Expected OAuthServiceException: nonce required for id_token 
token flow");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    // -----------------------------------------------------------------------
+    // toCodeRequestState – nonce auto-generation
+    // -----------------------------------------------------------------------
+
+    /**
+     * For Implicit Flow, {@code toCodeRequestState} must auto-generate a 
nonce when the
+     * caller has not supplied one.
+     */
+    @Test
+    public void testImplicitFlowAutoGeneratesNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.ID_TOKEN_RESPONSE_TYPE);
+
+        MultivaluedMap<String, String> state = 
invokeToCodeRequestState(filter, new MultivaluedHashMap<>());
+
+        String nonce = state.getFirst(IdToken.NONCE_CLAIM);
+        assertNotNull("A nonce must be auto-generated for implicit flow", 
nonce);
+    }
+
+    /**
+     * For Implicit Flow, a caller-supplied nonce must be preserved (not 
replaced).
+     */
+    @Test
+    public void testImplicitFlowPreservesCallerNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        filter.setResponseType(OidcUtils.ID_TOKEN_RESPONSE_TYPE);
+
+        MultivaluedHashMap<String, String> query = new MultivaluedHashMap<>();
+        query.putSingle(IdToken.NONCE_CLAIM, "my-app-nonce");
+
+        MultivaluedMap<String, String> state = 
invokeToCodeRequestState(filter, query);
+
+        assertEquals("Caller-supplied nonce must not be overwritten",
+            "my-app-nonce", state.getFirst(IdToken.NONCE_CLAIM));
+    }
+
+    /**
+     * For Code Flow (default), no nonce should be auto-generated.
+     */
+    @Test
+    public void testCodeFlowDoesNotAutoGenerateNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        // no responseType set → code flow
+
+        MultivaluedMap<String, String> state = 
invokeToCodeRequestState(filter, new MultivaluedHashMap<>());
+
+        assertNull("Code flow must not auto-generate a nonce", 
state.getFirst(IdToken.NONCE_CLAIM));
+    }
+
+    /**
+     * Hybrid flow detected from context: an id_token in the callback 
requestParams means
+     * nonce is required even when responseType has NOT been explicitly 
configured.
+     */
+    @Test
+    public void testHybridFlowContextDetectionRequiresNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        // No responseType configured — the filter is code-flow by default.
+
+        IdToken token = new IdToken();
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        // No nonce in state.
+
+        // Simulate a hybrid callback: id_token was returned in the 
authorization response.
+        MultivaluedMap<String, String> requestParams = new MetadataMap<>();
+        requestParams.putSingle(OidcUtils.ID_TOKEN, "some.jwt.value");
+
+        try {
+            invokeValidateIdToken(filter, token, state, requestParams);
+            fail("Expected OAuthServiceException: nonce required for hybrid 
flow (context-detected)");
+        } catch (OAuthServiceException ex) {
+            assertEquals(OAuthConstants.INVALID_REQUEST, ex.getMessage());
+        }
+    }
+
+    /**
+     * Hybrid flow detected from context: valid nonce round-trip must be 
accepted.
+     */
+    @Test
+    public void testHybridFlowContextDetectionAcceptsMatchingNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        // No responseType configured.
+
+        IdToken token = new IdToken();
+        token.setNonce("session-nonce");
+
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        state.putSingle(IdToken.NONCE_CLAIM, "session-nonce");
+
+        MultivaluedMap<String, String> requestParams = new MetadataMap<>();
+        requestParams.putSingle(OidcUtils.ID_TOKEN, "some.jwt.value");
+
+        invokeValidateIdToken(filter, token, state, requestParams); // must 
not throw
+    }
+
+    /**
+     * Pure code flow (no id_token in callback) must not require a nonce even 
when
+     * context detection is active.
+     */
+    @Test
+    public void testCodeFlowContextDetectionDoesNotRequireNonce() {
+        OidcClientCodeRequestFilter filter = new OidcClientCodeRequestFilter();
+        // No responseType configured.
+
+        IdToken token = new IdToken();
+        MultivaluedMap<String, String> state = new MetadataMap<>();
+        // No id_token in the callback — plain code flow.
+        MultivaluedMap<String, String> requestParams = new MetadataMap<>();
+        requestParams.putSingle("code", "auth-code");
+
+        invokeValidateIdToken(filter, token, state, requestParams); // must 
not throw
+    }
+
+    /** Convenience overload: no id_token in callback (non-hybrid). */
+    private static void invokeValidateIdToken(OidcClientCodeRequestFilter 
filter,
+                                              IdToken idToken,
+                                              MultivaluedMap<String, String> 
state) {
+        invokeValidateIdToken(filter, idToken, state, new MetadataMap<>());
+    }
+
+    private static void invokeValidateIdToken(OidcClientCodeRequestFilter 
filter,
+                                              IdToken idToken,
+                                              MultivaluedMap<String, String> 
state,
+                                              MultivaluedMap<String, String> 
requestParams) {
+        try {
+            Method method = 
OidcClientCodeRequestFilter.class.getDeclaredMethod(
+                "validateIdToken", IdToken.class, MultivaluedMap.class, 
boolean.class);
+            method.setAccessible(true);
+            boolean hybridFlowDetected = requestParams != null
+                && requestParams.containsKey(OidcUtils.ID_TOKEN);
+            method.invoke(filter, idToken, state, hybridFlowDetected);
+        } catch (java.lang.reflect.InvocationTargetException ex) {
+            Throwable cause = ex.getCause();
+            if (cause instanceof OAuthServiceException) {
+                throw (OAuthServiceException) cause;
+            }
+            throw new IllegalStateException(cause);
+        } catch (ReflectiveOperationException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static MultivaluedMap<String, String> invokeToCodeRequestState(
+            OidcClientCodeRequestFilter filter, MultivaluedMap<String, String> 
queryParams) {
+        UriInfo uriInfo = mock(UriInfo.class);
+        when(uriInfo.getQueryParameters(anyBoolean())).thenReturn(queryParams);
+        when(uriInfo.getAbsolutePath()).thenReturn(ABSOLUTE_PATH);
+
+        ContainerRequestContext rc = mock(ContainerRequestContext.class);
+        when(rc.getUriInfo()).thenReturn(uriInfo);
+        when(rc.getMediaType()).thenReturn(null);
+
+        try {
+            Method method = 
OidcClientCodeRequestFilter.class.getDeclaredMethod(
+                "toCodeRequestState", ContainerRequestContext.class, 
UriInfo.class);
+            method.setAccessible(true);
+            return (MultivaluedMap<String, String>) method.invoke(filter, rc, 
uriInfo);
+        } catch (java.lang.reflect.InvocationTargetException ex) {
+            Throwable cause = ex.getCause();
+            if (cause instanceof RuntimeException) {
+                throw (RuntimeException) cause;
+            }
+            throw new IllegalStateException(cause);
+        } catch (ReflectiveOperationException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+}

Reply via email to