This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 3.6.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit d9eef291a1cd213049ed28836c7860604a9f3db4 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Jul 23 09:28:56 2026 +0100 CXF-9225 - Enforce nonce for the hybrid case (#3328) (cherry picked from commit faf40ba0c98c1621522cf8d9c1fff64a8cdb126d) --- .../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 9a0d1c0412b..6e15f980fda 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 @@ -27,6 +27,7 @@ import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; import javax.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; @@ -36,6 +37,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 { @@ -52,6 +55,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(); @@ -62,6 +73,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, @@ -78,8 +110,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) { @@ -101,12 +138,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); @@ -146,13 +200,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)); @@ -169,6 +228,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); + } + } +}
