diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java index 41a3206202..da90d58a16 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java @@ -212,7 +212,7 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient defaultScopes(T grantRequest) { - return scopes(grantRequest); + return Collections.emptySet(); } /** diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClient.java index db43cb47bf..a6aafb7a13 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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. @@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; @@ -76,19 +75,12 @@ public final class DefaultAuthorizationCodeTokenResponseClient Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null"); RequestEntity request = this.requestEntityConverter.convert(authorizationCodeGrantRequest); ResponseEntity response = getResponse(request); - OAuth2AccessTokenResponse tokenResponse = response.getBody(); - if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { - // As per spec, in Section 5.1 Successful Access Token Response - // https://tools.ietf.org/html/rfc6749#section-5.1 - // If AccessTokenResponse.scope is empty, then default to the scope - // originally requested by the client in the Token Request - // @formatter:off - tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) - .scopes(authorizationCodeGrantRequest.getClientRegistration().getScopes()) - .build(); - // @formatter:on - } - return tokenResponse; + // As per spec, in Section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessTokenResponse.scope is empty, then we assume all requested scopes were + // granted. + // However, we use the explicit scopes returned in the response (if any). + return response.getBody(); } private ResponseEntity getResponse(RequestEntity request) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java index 168a85a9da..a1ee225a94 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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. @@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; @@ -76,19 +75,12 @@ public final class DefaultClientCredentialsTokenResponseClient Assert.notNull(clientCredentialsGrantRequest, "clientCredentialsGrantRequest cannot be null"); RequestEntity request = this.requestEntityConverter.convert(clientCredentialsGrantRequest); ResponseEntity response = getResponse(request); - OAuth2AccessTokenResponse tokenResponse = response.getBody(); - if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { - // As per spec, in Section 5.1 Successful Access Token Response - // https://tools.ietf.org/html/rfc6749#section-5.1 - // If AccessTokenResponse.scope is empty, then default to the scope - // originally requested by the client in the Token Request - // @formatter:off - tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) - .scopes(clientCredentialsGrantRequest.getClientRegistration().getScopes()) - .build(); - // @formatter:on - } - return tokenResponse; + // As per spec, in Section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessTokenResponse.scope is empty, then we assume all requested scopes were + // granted. + // However, we use the explicit scopes returned in the response (if any). + return response.getBody(); } private ResponseEntity getResponse(RequestEntity request) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java index 419b5f91d6..feae305eda 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java @@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; @@ -73,19 +72,12 @@ public final class DefaultJwtBearerTokenResponseClient Assert.notNull(jwtBearerGrantRequest, "jwtBearerGrantRequest cannot be null"); RequestEntity request = this.requestEntityConverter.convert(jwtBearerGrantRequest); ResponseEntity response = getResponse(request); - OAuth2AccessTokenResponse tokenResponse = response.getBody(); - if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { - // As per spec, in Section 5.1 Successful Access Token Response - // https://tools.ietf.org/html/rfc6749#section-5.1 - // If AccessTokenResponse.scope is empty, then default to the scope - // originally requested by the client in the Token Request - // @formatter:off - tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) - .scopes(jwtBearerGrantRequest.getClientRegistration().getScopes()) - .build(); - // @formatter:on - } - return tokenResponse; + // As per spec, in Section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessTokenResponse.scope is empty, then we assume all requested scopes were + // granted. + // However, we use the explicit scopes returned in the response (if any). + return response.getBody(); } private ResponseEntity getResponse(RequestEntity request) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java index 2afdde28d6..0fd6145a34 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java @@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; @@ -80,16 +79,12 @@ public final class DefaultPasswordTokenResponseClient Assert.notNull(passwordGrantRequest, "passwordGrantRequest cannot be null"); RequestEntity request = this.requestEntityConverter.convert(passwordGrantRequest); ResponseEntity response = getResponse(request); - OAuth2AccessTokenResponse tokenResponse = response.getBody(); - if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { - // As per spec, in Section 5.1 Successful Access Token Response - // https://tools.ietf.org/html/rfc6749#section-5.1 - // If AccessTokenResponse.scope is empty, then default to the scope - // originally requested by the client in the Token Request - tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) - .scopes(passwordGrantRequest.getClientRegistration().getScopes()).build(); - } - return tokenResponse; + // As per spec, in Section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessTokenResponse.scope is empty, then we assume all requested scopes were + // granted. + // However, we use the explicit scopes returned in the response (if any). + return response.getBody(); } private ResponseEntity getResponse(RequestEntity request) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java index 77926000d4..82d873619e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -65,11 +65,6 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient return Collections.emptySet(); } - @Override - Set defaultScopes(OAuth2AuthorizationCodeGrantRequest grantRequest) { - return grantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes(); - } - @Override BodyInserters.FormInserter populateTokenRequestBody(OAuth2AuthorizationCodeGrantRequest grantRequest, BodyInserters.FormInserter body) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java index 7ad39faf0c..f7df261b6b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java index 251bc685f2..202c581093 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java @@ -295,7 +295,7 @@ public class DefaultAuthorizationCodeTokenResponseClientTests { } @Test - public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasDefaultScope() { + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasNoScope() { // @formatter:off String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" @@ -307,7 +307,7 @@ public class DefaultAuthorizationCodeTokenResponseClientTests { this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build())); - assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + assertThat(accessTokenResponse.getAccessToken().getScopes()).isEmpty(); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java index f69616cc0c..cbc39a4321 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java @@ -304,7 +304,7 @@ public class DefaultClientCredentialsTokenResponseClientTests { } @Test - public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasDefaultScope() { + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasNoScope() { // @formatter:off String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" @@ -317,7 +317,7 @@ public class DefaultClientCredentialsTokenResponseClientTests { this.clientRegistration.build()); OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient .getTokenResponse(clientCredentialsGrantRequest); - assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + assertThat(accessTokenResponse.getAccessToken().getScopes()).isEmpty(); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java index 84bb3d88a2..14c27c993e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java @@ -102,7 +102,8 @@ public class DefaultJwtBearerTokenResponseClientTests { String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" + " \"token_type\": \"bearer\",\n" - + " \"expires_in\": \"3600\"\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\"\n" + "}\n"; // @formatter:on this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); @@ -204,7 +205,7 @@ public class DefaultJwtBearerTokenResponseClientTests { } @Test - public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasDefaultScope() { + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasNoScope() { // @formatter:off String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" @@ -217,7 +218,7 @@ public class DefaultJwtBearerTokenResponseClientTests { this.jwtAssertion); OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient .getTokenResponse(jwtBearerGrantRequest); - assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + assertThat(accessTokenResponse.getAccessToken().getScopes()).isEmpty(); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java index b227f77a90..55c5d4f311 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java @@ -102,7 +102,8 @@ public class DefaultPasswordTokenResponseClientTests { String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" + " \"token_type\": \"bearer\",\n" - + " \"expires_in\": \"3600\"\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\"\n" + "}\n"; // @formatter:on this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); @@ -136,7 +137,8 @@ public class DefaultPasswordTokenResponseClientTests { String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" + " \"token_type\": \"bearer\",\n" - + " \"expires_in\": \"3600\"\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read\"\n" + "}\n"; // @formatter:on this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); @@ -268,6 +270,22 @@ public class DefaultPasswordTokenResponseClientTests { assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read"); } + @Test + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasNoScope() { + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest( + this.clientRegistration.build(), this.username, this.password); + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(passwordGrantRequest); + assertThat(accessTokenResponse.getAccessToken().getScopes()).isEmpty(); + } + @Test public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthorizationException() { String accessTokenErrorResponse = "{\n" + " \"error\": \"unauthorized_client\"\n" + "}\n"; diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultRefreshTokenTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultRefreshTokenTokenResponseClientTests.java index ef90d900dc..be563fe3d2 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultRefreshTokenTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultRefreshTokenTokenResponseClientTests.java @@ -104,7 +104,8 @@ public class DefaultRefreshTokenTokenResponseClientTests { String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" + " \"token_type\": \"bearer\",\n" - + " \"expires_in\": \"3600\"\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\"\n" + "}\n"; // @formatter:on this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); @@ -131,6 +132,26 @@ public class DefaultRefreshTokenTokenResponseClientTests { assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo(this.refreshToken.getTokenValue()); } + @Test + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasOriginalScope() { + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + ClientRegistration clientRegistration = this.clientRegistration + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST).build(); + OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(clientRegistration, + this.accessToken, this.refreshToken); + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient + .getTokenResponse(refreshTokenGrantRequest); + assertThat(accessTokenResponse.getAccessToken().getScopes()) + .containsExactly(this.accessToken.getScopes().toArray(new String[0])); + } + @Test public void getTokenResponseWhenAuthenticationClientSecretPostThenFormParametersAreSent() throws Exception { // @formatter:off diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index 444f83de60..bdc682a269 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -246,7 +246,7 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests { } @Test - public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenReturnAccessTokenResponseUsingRequestedScope() { + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenReturnAccessTokenResponseWithNoScopes() { // @formatter:off String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" @@ -258,8 +258,7 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests { this.clientRegistration.scope("openid", "profile", "email", "address"); OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient .getTokenResponse(authorizationCodeGrantRequest()).block(); - assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("openid", "profile", "email", - "address"); + assertThat(accessTokenResponse.getAccessToken().getScopes()).isEmpty(); } private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest() { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java index 0b458ac7ce..16933ab2ef 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -103,6 +103,7 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests { RecordedRequest actualRequest = this.server.takeRequest(); String body = actualRequest.getUtf8Body(); assertThat(response.getAccessToken()).isNotNull(); + assertThat(response.getAccessToken().getScopes()).containsExactly("create"); assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)) .isEqualTo("Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ="); assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser"); @@ -128,6 +129,7 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests { RecordedRequest actualRequest = this.server.takeRequest(); String body = actualRequest.getBody().readUtf8(); assertThat(response.getAccessToken()).isNotNull(); + assertThat(response.getAccessToken().getScopes()).containsExactly("create"); String urlEncodedClientCredentialecret = URLEncoder.encode(clientCredentialWithAnsiKeyboardSpecialCharacters, StandardCharsets.UTF_8.toString()); String clientCredentials = Base64.getEncoder() @@ -155,6 +157,7 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests { RecordedRequest actualRequest = this.server.takeRequest(); String body = actualRequest.getUtf8Body(); assertThat(response.getAccessToken()).isNotNull(); + assertThat(response.getAccessToken().getScopes()).containsExactly("create"); assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); assertThat(body).isEqualTo( "grant_type=client_credentials&client_id=client-id&client_secret=client-secret&scope=read%3Auser"); @@ -230,7 +233,7 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests { } @Test - public void getTokenResponseWhenNoScopeThenClientRegistrationScopesDefaulted() { + public void getTokenResponseWhenNoScopeThenReturnAccessTokenResponseWithNoScopes() { ClientRegistration registration = this.clientRegistration.build(); // @formatter:off enqueueJson("{\n" @@ -242,7 +245,7 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests { // @formatter:on OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration); OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block(); - assertThat(response.getAccessToken().getScopes()).isEqualTo(registration.getScopes()); + assertThat(response.getAccessToken().getScopes()).isEmpty(); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java index 3199966539..d221511cc3 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -293,9 +293,17 @@ public class WebClientReactiveJwtBearerTokenResponseClientTests { @Test public void getTokenResponseWhenClientSecretBasicThenSuccess() throws Exception { + // @formatter:off + String accessTokenResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": 3600,\n" + + " \"scope\": \"read write\"" + + "}\n"; + // @formatter:on ClientRegistration clientRegistration = this.clientRegistration.build(); JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); - enqueueJson(DEFAULT_ACCESS_TOKEN_RESPONSE); + enqueueJson(accessTokenResponse); OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block(); assertThat(response).isNotNull(); assertThat(response.getAccessToken().getScopes()).containsExactly("read", "write"); @@ -309,12 +317,18 @@ public class WebClientReactiveJwtBearerTokenResponseClientTests { @Test public void getTokenResponseWhenClientSecretPostThenSuccess() throws Exception { // @formatter:off + String accessTokenResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": 3600,\n" + + " \"scope\": \"read write\"" + + "}\n"; ClientRegistration clientRegistration = this.clientRegistration .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) .build(); // @formatter:on JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); - enqueueJson(DEFAULT_ACCESS_TOKEN_RESPONSE); + enqueueJson(accessTokenResponse); OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block(); assertThat(response).isNotNull(); assertThat(response.getAccessToken().getScopes()).containsExactly("read", "write"); @@ -333,6 +347,7 @@ public class WebClientReactiveJwtBearerTokenResponseClientTests { + " \"expires_in\": 3600,\n" + " \"scope\": \"read\"\n" + "}\n"; + // @formatter:on ClientRegistration clientRegistration = this.clientRegistration.build(); JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); enqueueJson(accessTokenResponse); @@ -341,6 +356,17 @@ public class WebClientReactiveJwtBearerTokenResponseClientTests { assertThat(response.getAccessToken().getScopes()).containsExactly("read"); } + @Test + public void getTokenResponseWhenResponseDoesNotIncludeScopeThenReturnAccessTokenResponseWithNoScopes() + throws Exception { + ClientRegistration clientRegistration = this.clientRegistration.build(); + JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); + enqueueJson(DEFAULT_ACCESS_TOKEN_RESPONSE); + OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block(); + assertThat(response).isNotNull(); + assertThat(response.getAccessToken().getScopes()).isEmpty(); + } + private void enqueueJson(String body) { MockResponse response = new MockResponse().setBody(body).setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java index b6d1cd186f..d8570c58c2 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -99,7 +99,8 @@ public class WebClientReactivePasswordTokenResponseClientTests { } @Test - public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() throws Exception { + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenReturnAccessTokenResponseWithNoScope() + throws Exception { // @formatter:off String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" @@ -128,6 +129,41 @@ public class WebClientReactivePasswordTokenResponseClientTests { assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234"); assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); + assertThat(accessTokenResponse.getAccessToken().getScopes()).isEmpty(); + assertThat(accessTokenResponse.getRefreshToken()).isNull(); + } + + @Test + public void getTokenResponseWhenSuccessResponseIncludesScopeThenReturnAccessTokenResponse() throws Exception { + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + Instant expiresAtBefore = Instant.now().plusSeconds(3600); + ClientRegistration clientRegistration = this.clientRegistrationBuilder.build(); + OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, + this.username, this.password); + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(passwordGrantRequest) + .block(); + Instant expiresAtAfter = Instant.now().plusSeconds(3600); + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.POST.toString()); + assertThat(recordedRequest.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE)) + .isEqualTo(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"); + String formParameters = recordedRequest.getBody().readUtf8(); + assertThat(formParameters).contains("grant_type=password"); + assertThat(formParameters).contains("username=user1"); + assertThat(formParameters).contains("password=password"); + assertThat(formParameters).contains("scope=read+write"); + assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234"); + assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); assertThat(accessTokenResponse.getAccessToken().getScopes()) .containsExactly(clientRegistration.getScopes().toArray(new String[0])); assertThat(accessTokenResponse.getRefreshToken()).isNull(); diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java index 6fd89a4f10..ae88d84239 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java @@ -19,8 +19,11 @@ package org.springframework.security.web.access.intercept; import java.io.IOException; import java.util.function.Supplier; +import jakarta.servlet.DispatcherType; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -36,7 +39,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.util.Assert; -import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.filter.GenericFilterBean; /** * An authorization filter that restricts access to the URL using @@ -45,7 +48,7 @@ import org.springframework.web.filter.OncePerRequestFilter; * @author Evgeniy Cheban * @since 5.5 */ -public class AuthorizationFilter extends OncePerRequestFilter { +public class AuthorizationFilter extends GenericFilterBean { private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); @@ -54,7 +57,11 @@ public class AuthorizationFilter extends OncePerRequestFilter { private AuthorizationEventPublisher eventPublisher = AuthorizationFilter::noPublish; - private boolean shouldFilterAllDispatcherTypes = true; + private boolean observeOncePerRequest = false; + + private boolean filterErrorDispatch = true; + + private boolean filterAsyncDispatch = true; /** * Creates an instance. @@ -66,15 +73,57 @@ public class AuthorizationFilter extends OncePerRequestFilter { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException { - AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request); - this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision); - if (decision != null && !decision.isGranted()) { - throw new AccessDeniedException("Access Denied"); + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + if (this.observeOncePerRequest && isApplied(request)) { + chain.doFilter(request, response); + return; } - filterChain.doFilter(request, response); + + if (skipDispatch(request)) { + chain.doFilter(request, response); + return; + } + + String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); + request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); + try { + AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request); + this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision); + if (decision != null && !decision.isGranted()) { + throw new AccessDeniedException("Access Denied"); + } + chain.doFilter(request, response); + } + finally { + request.removeAttribute(alreadyFilteredAttributeName); + } + } + + private boolean skipDispatch(HttpServletRequest request) { + if (DispatcherType.ERROR.equals(request.getDispatcherType()) && !this.filterErrorDispatch) { + return true; + } + if (DispatcherType.ASYNC.equals(request.getDispatcherType()) && !this.filterAsyncDispatch) { + return true; + } + return false; + } + + private boolean isApplied(HttpServletRequest request) { + return request.getAttribute(getAlreadyFilteredAttributeName()) != null; + } + + private String getAlreadyFilteredAttributeName() { + String name = getFilterName(); + if (name == null) { + name = getClass().getName(); + } + return name + ".APPLIED"; } /** @@ -97,22 +146,6 @@ public class AuthorizationFilter extends OncePerRequestFilter { return authentication; } - @Override - protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - doFilterInternal(request, response, filterChain); - } - - @Override - protected boolean shouldNotFilterAsyncDispatch() { - return !this.shouldFilterAllDispatcherTypes; - } - - @Override - protected boolean shouldNotFilterErrorDispatch() { - return !this.shouldFilterAllDispatcherTypes; - } - /** * Use this {@link AuthorizationEventPublisher} to publish * {@link AuthorizationDeniedEvent}s and {@link AuthorizationGrantedEvent}s. @@ -139,7 +172,9 @@ public class AuthorizationFilter extends OncePerRequestFilter { * @since 5.7 */ public void setShouldFilterAllDispatcherTypes(boolean shouldFilterAllDispatcherTypes) { - this.shouldFilterAllDispatcherTypes = shouldFilterAllDispatcherTypes; + this.observeOncePerRequest = !shouldFilterAllDispatcherTypes; + this.filterErrorDispatch = shouldFilterAllDispatcherTypes; + this.filterAsyncDispatch = shouldFilterAllDispatcherTypes; } private static void noPublish(Supplier authentication, T object, @@ -147,4 +182,38 @@ public class AuthorizationFilter extends OncePerRequestFilter { } + public boolean isObserveOncePerRequest() { + return this.observeOncePerRequest; + } + + /** + * Sets whether this filter apply only once per request. By default, this is + * true, meaning the filter will only execute once per request. Sometimes + * users may wish it to execute more than once per request, such as when JSP forwards + * are being used and filter security is desired on each included fragment of the HTTP + * request. + * @param observeOncePerRequest whether the filter should only be applied once per + * request + */ + public void setObserveOncePerRequest(boolean observeOncePerRequest) { + this.observeOncePerRequest = observeOncePerRequest; + } + + /** + * If set to true, the filter will be applied to error dispatcher. Defaults to false. + * @param filterErrorDispatch whether the filter should be applied to error dispatcher + */ + public void setFilterErrorDispatch(boolean filterErrorDispatch) { + this.filterErrorDispatch = filterErrorDispatch; + } + + /** + * If set to true, the filter will be applied to the async dispatcher. Defaults to + * false. + * @param filterAsyncDispatch whether the filter should be applied to async dispatch + */ + public void setFilterAsyncDispatch(boolean filterAsyncDispatch) { + this.filterAsyncDispatch = filterAsyncDispatch; + } + } diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java index 0b4d640236..c87116a6b0 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java @@ -16,15 +16,19 @@ package org.springframework.security.web.access.intercept; +import java.io.IOException; import java.util.function.Supplier; import jakarta.servlet.DispatcherType; import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.access.AccessDeniedException; @@ -39,6 +43,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.util.WebUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -49,6 +54,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -59,6 +65,24 @@ import static org.mockito.Mockito.verifyNoInteractions; */ public class AuthorizationFilterTests { + private static final String ALREADY_FILTERED_ATTRIBUTE_NAME = "org.springframework.security.web.access.intercept.AuthorizationFilter.APPLIED"; + + private AuthorizationFilter filter; + + private AuthorizationManager authorizationManager; + + private MockHttpServletRequest request = new MockHttpServletRequest(); + + private final MockHttpServletResponse response = new MockHttpServletResponse(); + + private final FilterChain chain = new MockFilterChain(); + + @BeforeEach + public void setup() { + this.authorizationManager = mock(AuthorizationManager.class); + this.filter = new AuthorizationFilter(this.authorizationManager); + } + @AfterEach public void tearDown() { SecurityContextHolder.clearContext(); @@ -198,37 +222,101 @@ public class AuthorizationFilterTests { } @Test - public void doFilterNestedErrorDispatchWhenAuthorizationManagerThenUses() throws Exception { - AuthorizationManager authorizationManager = mock(AuthorizationManager.class); - AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); - authorizationFilter.setShouldFilterAllDispatcherTypes(true); - MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); - mockRequest.setDispatcherType(DispatcherType.ERROR); - mockRequest.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error"); - MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - FilterChain mockFilterChain = mock(FilterChain.class); - - authorizationFilter.doFilterNestedErrorDispatch(mockRequest, mockResponse, mockFilterChain); - verify(authorizationManager).check(any(Supplier.class), any(HttpServletRequest.class)); + public void doFilterWhenObserveOncePerRequestTrueAndIsAppliedThenNotInvoked() throws ServletException, IOException { + setIsAppliedTrue(); + this.filter.setObserveOncePerRequest(true); + this.filter.doFilter(this.request, this.response, this.chain); + verifyNoInteractions(this.authorizationManager); } @Test - public void doFilterNestedErrorDispatchWhenAuthorizationEventPublisherThenUses() throws Exception { - AuthorizationFilter authorizationFilter = new AuthorizationFilter( - AuthenticatedAuthorizationManager.authenticated()); - MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); - MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - FilterChain mockFilterChain = mock(FilterChain.class); + public void doFilterWhenObserveOncePerRequestTrueAndNotAppliedThenInvoked() throws ServletException, IOException { + this.filter.setObserveOncePerRequest(true); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.authorizationManager).check(any(), any()); + } - SecurityContext securityContext = new SecurityContextImpl(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")); - SecurityContextHolder.setContext(securityContext); + @Test + public void doFilterWhenObserveOncePerRequestFalseAndIsAppliedThenInvoked() throws ServletException, IOException { + setIsAppliedTrue(); + this.filter.setObserveOncePerRequest(false); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.authorizationManager).check(any(), any()); + } - AuthorizationEventPublisher eventPublisher = mock(AuthorizationEventPublisher.class); - authorizationFilter.setAuthorizationEventPublisher(eventPublisher); - authorizationFilter.doFilterNestedErrorDispatch(mockRequest, mockResponse, mockFilterChain); - verify(eventPublisher).publishAuthorizationEvent(any(Supplier.class), any(HttpServletRequest.class), - any(AuthorizationDecision.class)); + @Test + public void doFilterWhenObserveOncePerRequestFalseAndNotAppliedThenInvoked() throws ServletException, IOException { + this.filter.setObserveOncePerRequest(false); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.authorizationManager).check(any(), any()); + } + + @Test + public void doFilterWhenFilterErrorDispatchFalseAndIsErrorThenNotInvoked() throws ServletException, IOException { + this.request.setDispatcherType(DispatcherType.ERROR); + this.filter.setFilterErrorDispatch(false); + this.filter.doFilter(this.request, this.response, this.chain); + verifyNoInteractions(this.authorizationManager); + } + + @Test + public void doFilterWhenFilterErrorDispatchTrueAndIsErrorThenInvoked() throws ServletException, IOException { + this.request.setDispatcherType(DispatcherType.ERROR); + this.filter.setFilterErrorDispatch(true); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.authorizationManager).check(any(), any()); + } + + @Test + public void doFilterWhenFilterThenSetAlreadyFilteredAttribute() throws ServletException, IOException { + this.request = mock(MockHttpServletRequest.class); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.request).setAttribute(ALREADY_FILTERED_ATTRIBUTE_NAME, Boolean.TRUE); + } + + @Test + public void doFilterWhenFilterThenRemoveAlreadyFilteredAttribute() throws ServletException, IOException { + this.request = spy(MockHttpServletRequest.class); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.request).setAttribute(ALREADY_FILTERED_ATTRIBUTE_NAME, Boolean.TRUE); + assertThat(this.request.getAttribute(ALREADY_FILTERED_ATTRIBUTE_NAME)).isNull(); + } + + @Test + public void doFilterWhenFilterAsyncDispatchTrueAndIsAsyncThenInvoked() throws ServletException, IOException { + this.request.setDispatcherType(DispatcherType.ASYNC); + this.filter.setFilterAsyncDispatch(true); + this.filter.doFilter(this.request, this.response, this.chain); + verify(this.authorizationManager).check(any(), any()); + } + + @Test + public void doFilterWhenFilterAsyncDispatchFalseAndIsAsyncThenNotInvoked() throws ServletException, IOException { + this.request.setDispatcherType(DispatcherType.ASYNC); + this.filter.setFilterAsyncDispatch(false); + this.filter.doFilter(this.request, this.response, this.chain); + verifyNoInteractions(this.authorizationManager); + } + + @Test + public void filterWhenFilterErrorDispatchDefaultThenTrue() { + Boolean filterErrorDispatch = (Boolean) ReflectionTestUtils.getField(this.filter, "filterErrorDispatch"); + assertThat(filterErrorDispatch).isTrue(); + } + + @Test + public void filterWhenFilterAsyncDispatchDefaultThenTrue() { + Boolean filterAsyncDispatch = (Boolean) ReflectionTestUtils.getField(this.filter, "filterAsyncDispatch"); + assertThat(filterAsyncDispatch).isTrue(); + } + + @Test + public void filterWhenObserveOncePerRequestDefaultThenFalse() { + assertThat(this.filter.isObserveOncePerRequest()).isFalse(); + } + + private void setIsAppliedTrue() { + this.request.setAttribute(ALREADY_FILTERED_ATTRIBUTE_NAME, Boolean.TRUE); } }