diff --git a/src/main/java/oidc/endpoints/AuthorizationEndpoint.java b/src/main/java/oidc/endpoints/AuthorizationEndpoint.java index 58993714..84fe369b 100644 --- a/src/main/java/oidc/endpoints/AuthorizationEndpoint.java +++ b/src/main/java/oidc/endpoints/AuthorizationEndpoint.java @@ -135,10 +135,16 @@ private ModelAndView doAuthorization(MultiValueMap parameters, //swap reference authenticationRequest = oidcAuthenticationRequest; } - //Can't use authenticationRequest.getState(), because this is decoded - String stateValue = new QueryString(request).getStateValue(); - State state = StringUtils.hasText(stateValue) ? new State(stateValue) : null; - //The form post after consent has been asked / given contains the state + State state; + if (client.isStateParameterDecodingDisabled()) { + //Can't use authenticationRequest.getState(), because this is decoded and the client opted out for that + String stateValue = new QueryString(request).getStateValue(); + state = StringUtils.hasText(stateValue) ? new State(stateValue) : null; + } else { + //The authenticationRequest.getState() returns it decoded, which is the default behaviour we want + state = authenticationRequest.getState(); + } + //The form post after the user has granted consent contains the state in de body, instead of a query param if (state == null && authenticationRequest.getState() != null) { state = authenticationRequest.getState(); } diff --git a/src/main/java/oidc/model/OpenIDClient.java b/src/main/java/oidc/model/OpenIDClient.java index 5b982a97..bc0980b8 100644 --- a/src/main/java/oidc/model/OpenIDClient.java +++ b/src/main/java/oidc/model/OpenIDClient.java @@ -7,11 +7,7 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.util.StringUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import static oidc.manage.ServiceProviderTranslation.translateServiceProviderEntityId; @@ -56,6 +52,7 @@ public class OpenIDClient { private boolean includeUnspecifiedNameID; private boolean consentRequired; private boolean claimsInIdToken; + private boolean stateParameterDecodingDisabled; public OpenIDClient(String clientId, List redirectUrls, List scopes, List grants) { this.clientId = clientId; @@ -101,6 +98,7 @@ public OpenIDClient(Map root) { this.signingCertificateUrl = (String) metaDataFields.get("oidc:signingCertificateUrl"); this.consentRequired = parseBoolean(metaDataFields.get("oidc:consentRequired")); this.claimsInIdToken = parseBoolean(metaDataFields.get("oidc:claims_in_id_token")); + this.stateParameterDecodingDisabled = parseBoolean(metaDataFields.get("oidc:state_parameter_decoding_disabled")); this.includeUnspecifiedNameID = nameIdFormats.stream() .filter(metaDataFields::containsKey) diff --git a/src/test/java/oidc/endpoints/AuthorizationEndpointTest.java b/src/test/java/oidc/endpoints/AuthorizationEndpointTest.java index cb75f6e3..6c4f1b0e 100644 --- a/src/test/java/oidc/endpoints/AuthorizationEndpointTest.java +++ b/src/test/java/oidc/endpoints/AuthorizationEndpointTest.java @@ -109,7 +109,7 @@ public void authorizeCodeFlowWithNonce() throws IOException, BadJOSEException, P @Test public void oauth2NonOidcImplicitFlow() throws IOException { - String state = "https%3A%2F%2Fexample.com"; + String state = "https://example.com"; Response response = doAuthorizeWithClaimsAndScopes("mock-sp", "token", null, null, null, null, "groups", state); String url = response.getHeader("Location"); @@ -119,6 +119,19 @@ public void oauth2NonOidcImplicitFlow() throws IOException { assertEquals(state, fragmentParameters.get("state")); } + @Test + public void oauth2NonOidcImplicitFlowStateDecodeDisabled() throws IOException { + String state = "https%3A%2F%2Fexample.com"; + Response response = doAuthorizeWithClaimsAndScopes("student.mobility.rp.localhost", "token", + null, null, null, null, "groups", state); + String url = response.getHeader("Location"); + String fragment = url.substring(url.indexOf("#") + 1); + Map fragmentParameters = fragmentToMap(fragment); + assertFalse(fragmentParameters.containsKey("id_token")); + assertEquals(state, fragmentParameters.get("state")); + } + + @Test public void noScopeNoState() throws IOException { String code = getCode(doAuthorizeWithClaimsAndScopes("mock-sp", "code", @@ -129,7 +142,7 @@ public void noScopeNoState() throws IOException { @Test public void queryParamState() throws IOException { - String state = "https%3A%2F%2Fexample.com"; + String state = "https://example.com"; Response response = doAuthorizeWithClaimsAndScopes("mock-sp", "code", null, null, null, null, null, state); String location = response.getHeader("Location"); @@ -138,6 +151,18 @@ public void queryParamState() throws IOException { assertEquals(state, returnedState); } + @Test + public void queryParamStateDecodingDisabled() throws IOException { + String state = "https%3A%2F%2Fexample.com"; + //See src/test/resources/manage/oidc10_rp.json and metaData: oidc:state_parameter_decoding_disabled + Response response = doAuthorizeWithClaimsAndScopes("student.mobility.rp.localhost", "code", + null, null, null, null, null, state); + String location = response.getHeader("Location"); + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(location); + String returnedState = builder.build().getQueryParams().getFirst("state"); + assertEquals(state, returnedState); + } + @Test public void validationMissingParameter() { Map queryParams = new HashMap<>(); @@ -253,7 +278,7 @@ public void hybridFlowFragment() throws IOException, BadJOSEException, ParseExce String fragment = url.substring(url.indexOf("#") + 1); Map fragmentParameters = fragmentToMap(fragment); String code = fragmentParameters.get("code"); - assertEquals(state, fragmentParameters.get("state")); + assertEquals("https://example.com", fragmentParameters.get("state")); AuthorizationCode authorizationCode = mongoTemplate.findOne(Query.query(Criteria.where("code").is(code)), AuthorizationCode.class); User user = mongoTemplate.findOne(Query.query(Criteria.where("sub").is(authorizationCode.getSub())), User.class); @@ -287,7 +312,7 @@ public void hybridFlowFragment() throws IOException, BadJOSEException, ParseExce @Test public void implicitFlowQuery() throws IOException, BadJOSEException, ParseException, JOSEException { - String state = "https%3A%2F%2Fexample.com"; + String state = "https://example.com"; Response response = doAuthorizeWithClaimsAndScopes("mock-sp", "id_token token", ResponseMode.QUERY.getValue(), "nonce", null, Collections.emptyList(), "openid", state); String url = response.getHeader("Location"); @@ -296,6 +321,17 @@ public void implicitFlowQuery() throws IOException, BadJOSEException, ParseExcep assertImplicitFlowResponse(queryParameters); } + @Test + public void implicitFlowQueryStateDecodingDisabled() throws IOException, BadJOSEException, ParseException, JOSEException { + String state = "https%3A%2F%2Fexample.com"; + Response response = doAuthorizeWithClaimsAndScopes("student.mobility.rp.localhost", "id_token token", ResponseMode.QUERY.getValue(), "nonce", null, + Collections.emptyList(), "openid", state); + String url = response.getHeader("Location"); + Map queryParameters = UriComponentsBuilder.fromUriString(url).build().getQueryParams().toSingleValueMap(); + assertEquals(state, queryParameters.get("state")); + assertImplicitFlowResponse(queryParameters); + } + private JWTClaimsSet assertImplicitFlowResponse(Map parameters) throws ParseException, MalformedURLException, BadJOSEException, JOSEException { String idToken = (String) parameters.get("id_token"); JWTClaimsSet claimsSet = processToken(idToken, port); @@ -329,7 +365,7 @@ public void signedJwtAuthorization() throws Exception { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(location); MultiValueMap queryParams = builder.build().getQueryParams(); String state = queryParams.getFirst("state"); - assertEquals("state", state); + assertEquals("new", state); String code = queryParams.getFirst("code"); Map result = doToken(code); diff --git a/src/test/resources/manage/oidc10_rp.json b/src/test/resources/manage/oidc10_rp.json index c489dca0..50e94cc7 100644 --- a/src/test/resources/manage/oidc10_rp.json +++ b/src/test/resources/manage/oidc10_rp.json @@ -286,11 +286,15 @@ "groups" ], "grants": [ - "authorization_code" + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" ], "isResourceServer": "0", "isPublicClient": false, "oidc:claims_in_id_token": true, + "oidc:state_parameter_decoding_disabled": true, "NameIDFormats:0": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" }