From 4f417f01a71336444fdbaef059217350bf75eb8c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 10:25:55 -0500 Subject: [PATCH] BearerTokenServerAuthenticationEntryPoint Issue: gh-5605 --- ...erTokenServerAuthenticationEntryPoint.java | 121 ++++++++++++++++++ ...enServerAuthenticationEntryPointTests.java | 97 ++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java new file mode 100644 index 0000000000..d266e55efc --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 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 + * + * 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.springframework.security.oauth2.server.resource.web.server; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * An {@link AuthenticationEntryPoint} implementation used to commence authentication of protected resource requests + * using {@link BearerTokenAuthenticationFilter}. + *

+ * Uses information provided by {@link BearerTokenError} to set HTTP response status code and populate + * {@code WWW-Authenticate} HTTP header. + * + * @author Rob Winch + * @since 5.1 + * @see BearerTokenError + * @see RFC 6750 Section 3: The WWW-Authenticate + * Response Header Field + */ +public final class BearerTokenServerAuthenticationEntryPoint implements + ServerAuthenticationEntryPoint { + + private String realmName; + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + @Override + public Mono commence(ServerWebExchange exchange, AuthenticationException authException) { + HttpStatus status = getStatus(authException); + + Map parameters = createParameters(authException); + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + ServerHttpResponse response = exchange.getResponse(); + response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatusCode(status); + return response.setComplete(); + } + + private Map createParameters(AuthenticationException authException) { + Map parameters = new LinkedHashMap<>(); + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + + parameters.put("error", error.getErrorCode()); + + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + + if (error instanceof BearerTokenError) { + BearerTokenError bearerTokenError = (BearerTokenError) error; + + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + } + } + return parameters; + } + + private HttpStatus getStatus(AuthenticationException authException) { + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + if (error instanceof BearerTokenError) { + return ((BearerTokenError) error).getHttpStatus(); + } + } + return HttpStatus.UNAUTHORIZED; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java new file mode 100644 index 0000000000..cb60fd0131 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2018 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 + * + * 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.springframework.security.oauth2.server.resource.web.server; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.BadCredentialsException; +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.server.resource.BearerTokenError; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class BearerTokenServerAuthenticationEntryPointTests { + private BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); + + private MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + + @Test + public void commenceWhenNotOAuth2AuthenticationExceptionThenBearer() { + this.entryPoint.commence(this.exchange, new BadCredentialsException("")).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer"); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenRealmNameThenHasRealmName() { + this.entryPoint.setRealmName("Realm"); + + this.entryPoint.commence(this.exchange, new BadCredentialsException("")).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer realm=\"Realm\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenOAuth2AuthenticationExceptionThenContainsErrorInformation() { + OAuth2Error oauthError = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenOAuth2ErrorCompleteThenContainsErrorInformation() { + OAuth2Error oauthError = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "Oops", "https://example.com"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\", error_description=\"Oops\", error_uri=\"https://example.com\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenBearerTokenThenErrorInformation() { + OAuth2Error oauthError = new BearerTokenError(OAuth2ErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, "Oops", "https://example.com"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\", error_description=\"Oops\", error_uri=\"https://example.com\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + private MockServerHttpResponse getResponse() { + return this.exchange.getResponse(); + } +}