21 changed files with 774 additions and 533 deletions
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
/* |
||||
* Copyright 2020 the original author or authors. |
||||
* |
||||
* Licensed 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 |
||||
* |
||||
* https://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.springframework.security.oauth2.server.authorization.web; |
||||
|
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.web.authentication.AuthenticationConverter; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import java.util.Collections; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* An {@link AuthenticationConverter} that simply delegates to it's |
||||
* internal {@code List} of {@link AuthenticationConverter}(s). |
||||
* <p> |
||||
* Each {@link AuthenticationConverter} is given a chance to |
||||
* {@link AuthenticationConverter#convert(HttpServletRequest)} |
||||
* with the first {@code non-null} {@link Authentication} being returned. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 0.0.2 |
||||
* @see AuthenticationConverter |
||||
*/ |
||||
public final class DelegatingAuthenticationConverter implements AuthenticationConverter { |
||||
private final List<AuthenticationConverter> converters; |
||||
|
||||
/** |
||||
* Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters. |
||||
* |
||||
* @param converters a {@code List} of {@link AuthenticationConverter}(s) |
||||
*/ |
||||
public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) { |
||||
Assert.notEmpty(converters, "converters cannot be empty"); |
||||
this.converters = Collections.unmodifiableList(new LinkedList<>(converters)); |
||||
} |
||||
|
||||
@Override |
||||
public Authentication convert(HttpServletRequest request) { |
||||
Assert.notNull(request, "request cannot be null"); |
||||
// @formatter:off
|
||||
return this.converters.stream() |
||||
.map(converter -> converter.convert(request)) |
||||
.filter(Objects::nonNull) |
||||
.findFirst() |
||||
.orElse(null); |
||||
// @formatter:on
|
||||
} |
||||
} |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
/* |
||||
* Copyright 2020 the original author or authors. |
||||
* |
||||
* Licensed 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 |
||||
* |
||||
* https://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.springframework.security.oauth2.server.authorization.web; |
||||
|
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
||||
import org.springframework.security.oauth2.core.OAuth2Error; |
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; |
||||
import org.springframework.security.web.authentication.AuthenticationConverter; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import java.util.HashMap; |
||||
|
||||
/** |
||||
* Attempts to extract the parameters from {@link HttpServletRequest} |
||||
* used for authenticating public clients using Proof Key for Code Exchange (PKCE). |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 0.0.2 |
||||
* @see AuthenticationConverter |
||||
* @see OAuth2ClientAuthenticationToken |
||||
* @see OAuth2ClientAuthenticationFilter |
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636">Proof Key for Code Exchange by OAuth Public Clients</a> |
||||
*/ |
||||
public class PublicClientAuthenticationConverter implements AuthenticationConverter { |
||||
|
||||
@Override |
||||
public Authentication convert(HttpServletRequest request) { |
||||
if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) { |
||||
return null; |
||||
} |
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request); |
||||
|
||||
// client_id (REQUIRED for public clients)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); |
||||
if (!StringUtils.hasText(clientId)) { |
||||
return null; |
||||
} |
||||
if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { |
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); |
||||
} |
||||
|
||||
// code_verifier (REQUIRED)
|
||||
if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) { |
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); |
||||
} |
||||
|
||||
parameters.remove(OAuth2ParameterNames.CLIENT_ID); |
||||
|
||||
return new OAuth2ClientAuthenticationToken( |
||||
clientId, new HashMap<>(parameters.toSingleValueMap())); |
||||
} |
||||
} |
||||
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
/* |
||||
* Copyright 2020 the original author or authors. |
||||
* |
||||
* Licensed 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 |
||||
* |
||||
* https://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.springframework.security.oauth2.server.authorization.web; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
import static org.assertj.core.api.Assertions.entry; |
||||
|
||||
/** |
||||
* Tests for {@link PublicClientAuthenticationConverter}. |
||||
* |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class PublicClientAuthenticationConverterTests { |
||||
private PublicClientAuthenticationConverter converter = new PublicClientAuthenticationConverter(); |
||||
|
||||
@Test |
||||
public void convertWhenNotPublicClientThenReturnNull() { |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
Authentication authentication = this.converter.convert(request); |
||||
assertThat(authentication).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void convertWhenMissingClientIdThenReturnNull() { |
||||
MockHttpServletRequest request = createPkceTokenRequest(); |
||||
request.removeParameter(OAuth2ParameterNames.CLIENT_ID); |
||||
Authentication authentication = this.converter.convert(request); |
||||
assertThat(authentication).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void convertWhenMultipleClientIdThenInvalidRequestError() { |
||||
MockHttpServletRequest request = createPkceTokenRequest(); |
||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"); |
||||
assertThatThrownBy(() -> this.converter.convert(request)) |
||||
.isInstanceOf(OAuth2AuthenticationException.class) |
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) |
||||
.extracting("errorCode") |
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); |
||||
} |
||||
|
||||
@Test |
||||
public void convertWhenMultipleCodeVerifierThenInvalidRequestError() { |
||||
MockHttpServletRequest request = createPkceTokenRequest(); |
||||
request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-2"); |
||||
assertThatThrownBy(() -> this.converter.convert(request)) |
||||
.isInstanceOf(OAuth2AuthenticationException.class) |
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) |
||||
.extracting("errorCode") |
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); |
||||
} |
||||
|
||||
@Test |
||||
public void convertWhenPublicClientThenReturnClientAuthenticationToken() { |
||||
MockHttpServletRequest request = createPkceTokenRequest(); |
||||
OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); |
||||
assertThat(authentication.getPrincipal()).isEqualTo("client-1"); |
||||
assertThat(authentication.getAdditionalParameters()) |
||||
.containsOnly( |
||||
entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), |
||||
entry(OAuth2ParameterNames.CODE, "code"), |
||||
entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1")); |
||||
} |
||||
|
||||
private static MockHttpServletRequest createPkceTokenRequest() { |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); |
||||
request.addParameter(OAuth2ParameterNames.CODE, "code"); |
||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); |
||||
request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"); |
||||
return request; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue