Skip to content

Commit

Permalink
Feature toggle for decode state parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Jun 7, 2024
1 parent c923a72 commit 1e37b3d
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 15 deletions.
14 changes: 10 additions & 4 deletions src/main/java/oidc/endpoints/AuthorizationEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,16 @@ private ModelAndView doAuthorization(MultiValueMap<String, String> 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();
}
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/oidc/model/OpenIDClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -56,6 +52,7 @@ public class OpenIDClient {
private boolean includeUnspecifiedNameID;
private boolean consentRequired;
private boolean claimsInIdToken;
private boolean stateParameterDecodingDisabled;

public OpenIDClient(String clientId, List<String> redirectUrls, List<Scope> scopes, List<String> grants) {
this.clientId = clientId;
Expand Down Expand Up @@ -101,6 +98,7 @@ public OpenIDClient(Map<String, Object> 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)
Expand Down
46 changes: 41 additions & 5 deletions src/test/java/oidc/endpoints/AuthorizationEndpointTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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<String, String> 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",
Expand All @@ -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");
Expand All @@ -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<String, String> queryParams = new HashMap<>();
Expand Down Expand Up @@ -253,7 +278,7 @@ public void hybridFlowFragment() throws IOException, BadJOSEException, ParseExce
String fragment = url.substring(url.indexOf("#") + 1);
Map<String, String> 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);
Expand Down Expand Up @@ -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");
Expand All @@ -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<String, String> queryParameters = UriComponentsBuilder.fromUriString(url).build().getQueryParams().toSingleValueMap();
assertEquals(state, queryParameters.get("state"));
assertImplicitFlowResponse(queryParameters);
}

private JWTClaimsSet assertImplicitFlowResponse(Map<String, ? extends Object> parameters) throws ParseException, MalformedURLException, BadJOSEException, JOSEException {
String idToken = (String) parameters.get("id_token");
JWTClaimsSet claimsSet = processToken(idToken, port);
Expand Down Expand Up @@ -329,7 +365,7 @@ public void signedJwtAuthorization() throws Exception {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(location);
MultiValueMap<String, String> queryParams = builder.build().getQueryParams();
String state = queryParams.getFirst("state");
assertEquals("state", state);
assertEquals("new", state);

String code = queryParams.getFirst("code");
Map<String, Object> result = doToken(code);
Expand Down
6 changes: 5 additions & 1 deletion src/test/resources/manage/oidc10_rp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down

0 comments on commit 1e37b3d

Please sign in to comment.