@ -232,6 +232,9 @@ public class OAuth2AuthorizationCodeGrantTests {
@@ -232,6 +232,9 @@ public class OAuth2AuthorizationCodeGrantTests {
@Autowired
private OAuth2AuthorizationService authorizationService ;
@Autowired
private OAuth2AuthorizationConsentService authorizationConsentService ;
@Autowired
private JwtDecoder jwtDecoder ;
@ -689,6 +692,38 @@ public class OAuth2AuthorizationCodeGrantTests {
@@ -689,6 +692,38 @@ public class OAuth2AuthorizationCodeGrantTests {
assertThat ( consentPage ) . contains ( scopeCheckbox ( "message.write" ) ) ;
}
@Test
public void requestWhenRequiresConsentThenDisplaysConsentPageWithOnlyNewScope ( ) throws Exception {
this . spring . register ( AuthorizationServerConfiguration . class ) . autowire ( ) ;
RegisteredClient registeredClient = TestRegisteredClients . registeredClient ( ) . scopes ( ( scopes ) - > {
scopes . clear ( ) ;
scopes . add ( "message.read" ) ;
scopes . add ( "message.write" ) ;
} ) . clientSettings ( ClientSettings . builder ( ) . requireAuthorizationConsent ( true ) . build ( ) ) . build ( ) ;
this . registeredClientRepository . save ( registeredClient ) ;
OAuth2AuthorizationConsent authorizationConsent = OAuth2AuthorizationConsent
. withId ( registeredClient . getId ( ) , "user" )
. scope ( "message.write" )
. build ( ) ;
this . authorizationConsentService . save ( authorizationConsent ) ;
String consentPage = this . mvc
. perform ( get ( DEFAULT_AUTHORIZATION_ENDPOINT_URI )
. queryParams ( getAuthorizationRequestParameters ( registeredClient ) )
. with ( user ( "user" ) ) )
. andExpect ( status ( ) . is2xxSuccessful ( ) )
. andReturn ( )
. getResponse ( )
. getContentAsString ( ) ;
assertThat ( consentPage ) . contains ( "Consent required" ) ;
assertThat ( consentPage ) . contains ( scopeCheckbox ( "message.read" ) ) ;
assertThat ( consentPage ) . contains ( disabledScopeCheckbox ( "message.write" ) ) ;
}
@Test
public void requestWhenConsentRequestThenReturnAccessTokenResponse ( ) throws Exception {
this . spring . register ( AuthorizationServerConfiguration . class ) . autowire ( ) ;
@ -746,6 +781,47 @@ public class OAuth2AuthorizationCodeGrantTests {
@@ -746,6 +781,47 @@ public class OAuth2AuthorizationCodeGrantTests {
. andReturn ( ) ;
}
@Test
public void requestWhenCustomConsentPageConfiguredThenRedirectWithAllScopes ( ) throws Exception {
this . spring . register ( AuthorizationServerConfigurationCustomConsentPage . class ) . autowire ( ) ;
RegisteredClient registeredClient = TestRegisteredClients . registeredClient ( ) . scopes ( ( scopes ) - > {
scopes . clear ( ) ;
scopes . add ( "message.read" ) ;
scopes . add ( "message.write" ) ;
} ) . clientSettings ( ClientSettings . builder ( ) . requireAuthorizationConsent ( true ) . build ( ) ) . build ( ) ;
this . registeredClientRepository . save ( registeredClient ) ;
OAuth2AuthorizationConsent authorizationConsent = OAuth2AuthorizationConsent
. withId ( registeredClient . getId ( ) , "user" )
. scope ( "message.write" )
. build ( ) ;
this . authorizationConsentService . save ( authorizationConsent ) ;
MvcResult mvcResult = this . mvc
. perform ( get ( DEFAULT_AUTHORIZATION_ENDPOINT_URI )
. queryParams ( getAuthorizationRequestParameters ( registeredClient ) )
. with ( user ( "user" ) ) )
. andExpect ( status ( ) . is3xxRedirection ( ) )
. andReturn ( ) ;
String redirectedUrl = mvcResult . getResponse ( ) . getRedirectedUrl ( ) ;
assertThat ( redirectedUrl ) . matches ( "http://localhost/oauth2/consent\\?scope=.+&client_id=.+&state=.+" ) ;
String locationHeader = URLDecoder . decode ( redirectedUrl , StandardCharsets . UTF_8 ) ;
UriComponents uriComponents = UriComponentsBuilder . fromUriString ( locationHeader ) . build ( ) ;
MultiValueMap < String , String > redirectQueryParams = uriComponents . getQueryParams ( ) ;
assertThat ( uriComponents . getPath ( ) ) . isEqualTo ( consentPage ) ;
assertThat ( redirectQueryParams . getFirst ( OAuth2ParameterNames . SCOPE ) ) . isEqualTo ( "message.read message.write" ) ;
assertThat ( redirectQueryParams . getFirst ( OAuth2ParameterNames . CLIENT_ID ) )
. isEqualTo ( registeredClient . getClientId ( ) ) ;
String state = extractParameterFromRedirectUri ( redirectedUrl , "state" ) ;
OAuth2Authorization authorization = this . authorizationService . findByToken ( state , STATE_TOKEN_TYPE ) ;
assertThat ( authorization ) . isNotNull ( ) ;
}
@Test
public void requestWhenCustomConsentPageConfiguredThenRedirect ( ) throws Exception {
this . spring . register ( AuthorizationServerConfigurationCustomConsentPage . class ) . autowire ( ) ;
@ -1076,6 +1152,202 @@ public class OAuth2AuthorizationCodeGrantTests {
@@ -1076,6 +1152,202 @@ public class OAuth2AuthorizationCodeGrantTests {
. isEqualTo ( true ) ;
}
@Test
public void requestWhenPushedAuthorizationRequestAndRequiresConsentThenDisplaysConsentPage ( ) throws Exception {
this . spring . register ( AuthorizationServerConfigurationWithPushedAuthorizationRequests . class ) . autowire ( ) ;
RegisteredClient registeredClient = TestRegisteredClients . registeredClient ( ) . scopes ( ( scopes ) - > {
scopes . clear ( ) ;
scopes . add ( "message.read" ) ;
scopes . add ( "message.write" ) ;
} ) . clientSettings ( ClientSettings . builder ( ) . requireAuthorizationConsent ( true ) . build ( ) ) . build ( ) ;
this . registeredClientRepository . save ( registeredClient ) ;
MvcResult mvcResult = this . mvc
. perform ( post ( "/oauth2/par" ) . params ( getAuthorizationRequestParameters ( registeredClient ) )
. param ( PkceParameterNames . CODE_CHALLENGE , S256_CODE_CHALLENGE )
. param ( PkceParameterNames . CODE_CHALLENGE_METHOD , "S256" )
. header ( HttpHeaders . AUTHORIZATION , getAuthorizationHeader ( registeredClient ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . CACHE_CONTROL , containsString ( "no-store" ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . PRAGMA , containsString ( "no-cache" ) ) )
. andExpect ( status ( ) . isCreated ( ) )
. andExpect ( jsonPath ( "$.request_uri" ) . isNotEmpty ( ) )
. andExpect ( jsonPath ( "$.expires_in" ) . isNotEmpty ( ) )
. andReturn ( ) ;
String requestUri = JsonPath . read ( mvcResult . getResponse ( ) . getContentAsString ( ) , "$.request_uri" ) ;
String consentPage = this . mvc
. perform ( get ( DEFAULT_AUTHORIZATION_ENDPOINT_URI )
. queryParam ( OAuth2ParameterNames . CLIENT_ID , registeredClient . getClientId ( ) )
. queryParam ( OAuth2ParameterNames . REQUEST_URI , requestUri )
. with ( user ( "user" ) ) )
. andExpect ( status ( ) . is2xxSuccessful ( ) )
. andReturn ( )
. getResponse ( )
. getContentAsString ( ) ;
assertThat ( consentPage ) . contains ( "Consent required" ) ;
assertThat ( consentPage ) . contains ( scopeCheckbox ( "message.read" ) ) ;
assertThat ( consentPage ) . contains ( scopeCheckbox ( "message.write" ) ) ;
}
@Test
public void requestWhenPushedAuthorizationRequestAndRequiresConsentThenDisplaysConsentPageWithOnlyNewScope ( )
throws Exception {
this . spring . register ( AuthorizationServerConfigurationWithPushedAuthorizationRequests . class ) . autowire ( ) ;
RegisteredClient registeredClient = TestRegisteredClients . registeredClient ( ) . scopes ( ( scopes ) - > {
scopes . clear ( ) ;
scopes . add ( "message.read" ) ;
scopes . add ( "message.write" ) ;
} ) . clientSettings ( ClientSettings . builder ( ) . requireAuthorizationConsent ( true ) . build ( ) ) . build ( ) ;
this . registeredClientRepository . save ( registeredClient ) ;
OAuth2AuthorizationConsent authorizationConsent = OAuth2AuthorizationConsent
. withId ( registeredClient . getId ( ) , "user" )
. scope ( "message.write" )
. build ( ) ;
this . authorizationConsentService . save ( authorizationConsent ) ;
MvcResult mvcResult = this . mvc
. perform ( post ( "/oauth2/par" ) . params ( getAuthorizationRequestParameters ( registeredClient ) )
. param ( PkceParameterNames . CODE_CHALLENGE , S256_CODE_CHALLENGE )
. param ( PkceParameterNames . CODE_CHALLENGE_METHOD , "S256" )
. header ( HttpHeaders . AUTHORIZATION , getAuthorizationHeader ( registeredClient ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . CACHE_CONTROL , containsString ( "no-store" ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . PRAGMA , containsString ( "no-cache" ) ) )
. andExpect ( status ( ) . isCreated ( ) )
. andExpect ( jsonPath ( "$.request_uri" ) . isNotEmpty ( ) )
. andExpect ( jsonPath ( "$.expires_in" ) . isNotEmpty ( ) )
. andReturn ( ) ;
String requestUri = JsonPath . read ( mvcResult . getResponse ( ) . getContentAsString ( ) , "$.request_uri" ) ;
String consentPage = this . mvc
. perform ( get ( DEFAULT_AUTHORIZATION_ENDPOINT_URI )
. queryParam ( OAuth2ParameterNames . CLIENT_ID , registeredClient . getClientId ( ) )
. queryParam ( OAuth2ParameterNames . REQUEST_URI , requestUri )
. with ( user ( "user" ) ) )
. andExpect ( status ( ) . is2xxSuccessful ( ) )
. andReturn ( )
. getResponse ( )
. getContentAsString ( ) ;
assertThat ( consentPage ) . contains ( "Consent required" ) ;
assertThat ( consentPage ) . contains ( scopeCheckbox ( "message.read" ) ) ;
assertThat ( consentPage ) . contains ( disabledScopeCheckbox ( "message.write" ) ) ;
}
@Test
public void requestWhenPushedAuthorizationRequestAndCustomConsentPageConfiguredThenRedirect ( ) throws Exception {
this . spring . register ( AuthorizationServerConfigurationWithPushedAuthorizationRequestsAndCustomConsentPage . class )
. autowire ( ) ;
RegisteredClient registeredClient = TestRegisteredClients . registeredClient ( ) . scopes ( ( scopes ) - > {
scopes . clear ( ) ;
scopes . add ( "message.read" ) ;
scopes . add ( "message.write" ) ;
} ) . clientSettings ( ClientSettings . builder ( ) . requireAuthorizationConsent ( true ) . build ( ) ) . build ( ) ;
this . registeredClientRepository . save ( registeredClient ) ;
MvcResult mvcResult = this . mvc
. perform ( post ( "/oauth2/par" ) . params ( getAuthorizationRequestParameters ( registeredClient ) )
. param ( PkceParameterNames . CODE_CHALLENGE , S256_CODE_CHALLENGE )
. param ( PkceParameterNames . CODE_CHALLENGE_METHOD , "S256" )
. header ( HttpHeaders . AUTHORIZATION , getAuthorizationHeader ( registeredClient ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . CACHE_CONTROL , containsString ( "no-store" ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . PRAGMA , containsString ( "no-cache" ) ) )
. andExpect ( status ( ) . isCreated ( ) )
. andExpect ( jsonPath ( "$.request_uri" ) . isNotEmpty ( ) )
. andExpect ( jsonPath ( "$.expires_in" ) . isNotEmpty ( ) )
. andReturn ( ) ;
String requestUri = JsonPath . read ( mvcResult . getResponse ( ) . getContentAsString ( ) , "$.request_uri" ) ;
mvcResult = this . mvc
. perform ( get ( DEFAULT_AUTHORIZATION_ENDPOINT_URI )
. queryParam ( OAuth2ParameterNames . CLIENT_ID , registeredClient . getClientId ( ) )
. queryParam ( OAuth2ParameterNames . REQUEST_URI , requestUri )
. with ( user ( "user" ) ) )
. andExpect ( status ( ) . is3xxRedirection ( ) )
. andReturn ( ) ;
String redirectedUrl = mvcResult . getResponse ( ) . getRedirectedUrl ( ) ;
assertThat ( redirectedUrl ) . matches ( "http://localhost/oauth2/consent\\?scope=.+&client_id=.+&state=.+" ) ;
String locationHeader = URLDecoder . decode ( redirectedUrl , StandardCharsets . UTF_8 . name ( ) ) ;
UriComponents uriComponents = UriComponentsBuilder . fromUriString ( locationHeader ) . build ( ) ;
MultiValueMap < String , String > redirectQueryParams = uriComponents . getQueryParams ( ) ;
assertThat ( uriComponents . getPath ( ) ) . isEqualTo ( consentPage ) ;
assertThat ( redirectQueryParams . getFirst ( OAuth2ParameterNames . SCOPE ) ) . isEqualTo ( "message.read message.write" ) ;
assertThat ( redirectQueryParams . getFirst ( OAuth2ParameterNames . CLIENT_ID ) )
. isEqualTo ( registeredClient . getClientId ( ) ) ;
String state = extractParameterFromRedirectUri ( redirectedUrl , "state" ) ;
OAuth2Authorization authorization = this . authorizationService . findByToken ( state , STATE_TOKEN_TYPE ) ;
assertThat ( authorization ) . isNotNull ( ) ;
}
@Test
public void requestWhenPushedAuthorizationRequestAndCustomConsentPageConfiguredThenRedirectWithAllScopes ( )
throws Exception {
this . spring . register ( AuthorizationServerConfigurationWithPushedAuthorizationRequestsAndCustomConsentPage . class )
. autowire ( ) ;
RegisteredClient registeredClient = TestRegisteredClients . registeredClient ( ) . scopes ( ( scopes ) - > {
scopes . clear ( ) ;
scopes . add ( "message.read" ) ;
scopes . add ( "message.write" ) ;
} ) . clientSettings ( ClientSettings . builder ( ) . requireAuthorizationConsent ( true ) . build ( ) ) . build ( ) ;
this . registeredClientRepository . save ( registeredClient ) ;
OAuth2AuthorizationConsent authorizationConsent = OAuth2AuthorizationConsent
. withId ( registeredClient . getId ( ) , "user" )
. scope ( "message.write" )
. build ( ) ;
this . authorizationConsentService . save ( authorizationConsent ) ;
MvcResult mvcResult = this . mvc
. perform ( post ( "/oauth2/par" ) . params ( getAuthorizationRequestParameters ( registeredClient ) )
. param ( PkceParameterNames . CODE_CHALLENGE , S256_CODE_CHALLENGE )
. param ( PkceParameterNames . CODE_CHALLENGE_METHOD , "S256" )
. header ( HttpHeaders . AUTHORIZATION , getAuthorizationHeader ( registeredClient ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . CACHE_CONTROL , containsString ( "no-store" ) ) )
. andExpect ( header ( ) . string ( HttpHeaders . PRAGMA , containsString ( "no-cache" ) ) )
. andExpect ( status ( ) . isCreated ( ) )
. andExpect ( jsonPath ( "$.request_uri" ) . isNotEmpty ( ) )
. andExpect ( jsonPath ( "$.expires_in" ) . isNotEmpty ( ) )
. andReturn ( ) ;
String requestUri = JsonPath . read ( mvcResult . getResponse ( ) . getContentAsString ( ) , "$.request_uri" ) ;
mvcResult = this . mvc
. perform ( get ( DEFAULT_AUTHORIZATION_ENDPOINT_URI )
. queryParam ( OAuth2ParameterNames . CLIENT_ID , registeredClient . getClientId ( ) )
. queryParam ( OAuth2ParameterNames . REQUEST_URI , requestUri )
. with ( user ( "user" ) ) )
. andExpect ( status ( ) . is3xxRedirection ( ) )
. andReturn ( ) ;
String redirectedUrl = mvcResult . getResponse ( ) . getRedirectedUrl ( ) ;
assertThat ( redirectedUrl ) . matches ( "http://localhost/oauth2/consent\\?scope=.+&client_id=.+&state=.+" ) ;
String locationHeader = URLDecoder . decode ( redirectedUrl , StandardCharsets . UTF_8 ) ;
UriComponents uriComponents = UriComponentsBuilder . fromUriString ( locationHeader ) . build ( ) ;
MultiValueMap < String , String > redirectQueryParams = uriComponents . getQueryParams ( ) ;
assertThat ( uriComponents . getPath ( ) ) . isEqualTo ( consentPage ) ;
assertThat ( redirectQueryParams . getFirst ( OAuth2ParameterNames . SCOPE ) ) . isEqualTo ( "message.read message.write" ) ;
assertThat ( redirectQueryParams . getFirst ( OAuth2ParameterNames . CLIENT_ID ) )
. isEqualTo ( registeredClient . getClientId ( ) ) ;
String state = extractParameterFromRedirectUri ( redirectedUrl , "state" ) ;
OAuth2Authorization authorization = this . authorizationService . findByToken ( state , STATE_TOKEN_TYPE ) ;
assertThat ( authorization ) . isNotNull ( ) ;
}
private static String generateDPoPProof ( String tokenEndpointUri ) {
// @formatter:off
Map < String , Object > publicJwk = TestJwks . DEFAULT_EC_JWK
@ -1120,8 +1392,8 @@ public class OAuth2AuthorizationCodeGrantTests {
@@ -1120,8 +1392,8 @@ public class OAuth2AuthorizationCodeGrantTests {
private static String getAuthorizationHeader ( RegisteredClient registeredClient ) throws Exception {
String clientId = registeredClient . getClientId ( ) ;
String clientSecret = registeredClient . getClientSecret ( ) ;
clientId = URLEncoder . encode ( clientId , StandardCharsets . UTF_8 . name ( ) ) ;
clientSecret = URLEncoder . encode ( clientSecret , StandardCharsets . UTF_8 . name ( ) ) ;
clientId = URLEncoder . encode ( clientId , StandardCharsets . UTF_8 ) ;
clientSecret = URLEncoder . encode ( clientSecret , StandardCharsets . UTF_8 ) ;
String credentialsString = clientId + ":" + clientSecret ;
byte [ ] encodedBytes = Base64 . getEncoder ( ) . encode ( credentialsString . getBytes ( StandardCharsets . UTF_8 ) ) ;
return "Basic " + new String ( encodedBytes , StandardCharsets . UTF_8 ) ;
@ -1132,6 +1404,12 @@ public class OAuth2AuthorizationCodeGrantTests {
@@ -1132,6 +1404,12 @@ public class OAuth2AuthorizationCodeGrantTests {
"<input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"{0}\" id=\"{0}\">" , scope ) ;
}
private static String disabledScopeCheckbox ( String scope ) {
return MessageFormat . format (
"<input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" id=\"{0}\" checked disabled>" ,
scope ) ;
}
private String extractParameterFromRedirectUri ( String redirectUri , String param )
throws UnsupportedEncodingException {
String locationHeader = URLDecoder . decode ( redirectUri , StandardCharsets . UTF_8 . name ( ) ) ;
@ -1506,4 +1784,31 @@ public class OAuth2AuthorizationCodeGrantTests {
@@ -1506,4 +1784,31 @@ public class OAuth2AuthorizationCodeGrantTests {
}
@EnableWebSecurity
@Configuration ( proxyBeanMethods = false )
static class AuthorizationServerConfigurationWithPushedAuthorizationRequestsAndCustomConsentPage
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain ( HttpSecurity http ) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer . authorizationServer ( ) ;
http
. securityMatcher ( authorizationServerConfigurer . getEndpointsMatcher ( ) )
. with ( authorizationServerConfigurer , ( authorizationServer ) - >
authorizationServer
. pushedAuthorizationRequestEndpoint ( Customizer . withDefaults ( ) )
. authorizationEndpoint ( ( authorizationEndpoint ) - >
authorizationEndpoint . consentPage ( consentPage ) )
)
. authorizeHttpRequests ( ( authorize ) - >
authorize . anyRequest ( ) . authenticated ( )
) ;
return http . build ( ) ;
}
// @formatter:on
}
}