From 3c461b704c9a261c6ba34cce0c35b2737790f642 Mon Sep 17 00:00:00 2001 From: "mhyeon.lee" Date: Wed, 11 Jul 2018 23:52:00 +0900 Subject: [PATCH] Add AuthenticationMethod type This section defines three methods of sending bearer access tokens in resource requests to resource servers. Clients MUST NOT use more than one method to transmit the token in each request. RFC6750 Section 2 Authenticated Requests https://tools.ietf.org/html/rfc6750#section-2 Add AuthenticationMethod in ClientRegistration UserInfoEndpoint. Add AuthenticationMethod for OAuth2UserService to get User. To support the use of the POST method. https://tools.ietf.org/html/rfc6750#section-2.2 gh-5500 --- .../registration/ClientRegistration.java | 26 ++++++ .../DefaultReactiveOAuth2UserService.java | 21 ++++- .../NimbusUserInfoResponseClient.java | 6 +- .../oidc/userinfo/OidcUserServiceTests.java | 74 ++++++++++++++++- .../registration/ClientRegistrationTests.java | 23 ++++++ .../DefaultOAuth2UserServiceTests.java | 80 ++++++++++++++++++- ...DefaultReactiveOAuth2UserServiceTests.java | 50 ++++++++++++ .../oauth2/core/AuthenticationMethod.java | 72 +++++++++++++++++ .../core/AuthenticationMethodTests.java | 48 +++++++++++ 9 files changed, 394 insertions(+), 6 deletions(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index bf390f8b94..4b9bb1e24b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.client.registration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; @@ -197,6 +198,7 @@ public final class ClientRegistration { */ public class UserInfoEndpoint { private String uri; + private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER; private String userNameAttributeName; private UserInfoEndpoint() { @@ -211,6 +213,16 @@ public final class ClientRegistration { return this.uri; } + /** + * Returns the authentication method for the user info endpoint. + * + * @since 5.1 + * @return the {@link AuthenticationMethod} for the user info endpoint. + */ + public AuthenticationMethod getAuthenticationMethod() { + return this.authenticationMethod; + } + /** * Returns the attribute name used to access the user's name from the user info response. * @@ -247,6 +259,7 @@ public final class ClientRegistration { private String authorizationUri; private String tokenUri; private String userInfoUri; + private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER; private String userNameAttributeName; private String jwkSetUri; private String clientName; @@ -383,6 +396,18 @@ public final class ClientRegistration { return this; } + /** + * Sets the authentication method for the user info endpoint. + * + * @since 5.1 + * @param userInfoAuthenticationMethod the authentication method for the user info endpoint + * @return the {@link Builder} + */ + public Builder userInfoAuthenticationMethod(AuthenticationMethod userInfoAuthenticationMethod) { + this.userInfoAuthenticationMethod = userInfoAuthenticationMethod; + return this; + } + /** * Sets the attribute name used to access the user's name from the user info response. * @@ -446,6 +471,7 @@ public final class ClientRegistration { providerDetails.authorizationUri = this.authorizationUri; providerDetails.tokenUri = this.tokenUri; providerDetails.userInfoEndpoint.uri = this.userInfoUri; + providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod; providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName; providerDetails.jwkSetUri = this.jwkSetUri; clientRegistration.providerDetails = providerDetails; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java index 1f9bc10c14..533400c391 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java @@ -26,8 +26,10 @@ import java.util.Set; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; @@ -99,9 +101,22 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() { }; - Mono> userAttributes = this.webClient.get() - .uri(userInfoUri) - .headers(bearerToken(userRequest.getAccessToken().getTokenValue())) + AuthenticationMethod authenticationMethod = userRequest.getClientRegistration().getProviderDetails() + .getUserInfoEndpoint().getAuthenticationMethod(); + WebClient.RequestHeadersSpec requestHeadersSpec; + if (AuthenticationMethod.FORM.equals(authenticationMethod)) { + requestHeadersSpec = this.webClient.post() + .uri(userInfoUri) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .syncBody("access_token=" + userRequest.getAccessToken().getTokenValue()); + } else { + requestHeadersSpec = this.webClient.get() + .uri(userInfoUri) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .headers(bearerToken(userRequest.getAccessToken().getTokenValue())); + } + Mono> userAttributes = requestHeadersSpec .retrieve() .onStatus(s -> s != HttpStatus.OK, response -> { return parse(response).map(userInfoErrorResponse -> { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java index 52929a04a7..cbbe759788 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java @@ -32,6 +32,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -79,8 +80,11 @@ final class NimbusUserInfoResponseClient { OAuth2AccessToken oauth2AccessToken) throws OAuth2AuthenticationException { URI userInfoUri = URI.create(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); BearerAccessToken accessToken = new BearerAccessToken(oauth2AccessToken.getTokenValue()); + AuthenticationMethod authenticationMethod = clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod(); + HTTPRequest.Method httpMethod = AuthenticationMethod.FORM.equals(authenticationMethod) + ? HTTPRequest.Method.POST : HTTPRequest.Method.GET; - UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken); + UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, httpMethod, accessToken); HTTPRequest httpRequest = userInfoRequest.toHTTPRequest(); httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE); httpRequest.setConnectTimeout(30000); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index 6164022292..1586292af6 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -17,6 +17,8 @@ package org.springframework.security.oauth2.client.oidc.userinfo; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -26,9 +28,11 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -56,7 +60,7 @@ import static org.mockito.Mockito.when; * * @author Joe Grandja */ -@PowerMockIgnore("okhttp3.*") +@PowerMockIgnore({"okhttp3.*", "okio.Buffer"}) @PrepareForTest(ClientRegistration.class) @RunWith(PowerMockRunner.class) public class OidcUserServiceTests { @@ -79,6 +83,7 @@ public class OidcUserServiceTests { when(this.providerDetails.getUserInfoEndpoint()).thenReturn(this.userInfoEndpoint); when(this.clientRegistration.getAuthorizationGrantType()).thenReturn(AuthorizationGrantType.AUTHORIZATION_CODE); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn(StandardClaimNames.SUB); this.accessToken = mock(OAuth2AccessToken.class); @@ -354,4 +359,71 @@ public class OidcUserServiceTests { assertThat(server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT)) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"sub\": \"subject1\",\n" + + " \"name\": \"first last\",\n" + + " \"given_name\": \"first\",\n" + + " \"family_name\": \"last\",\n" + + " \"preferred_username\": \"user1\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"sub\": \"subject1\",\n" + + " \"name\": \"first last\",\n" + + " \"given_name\": \"first\",\n" + + " \"family_name\": \"last\",\n" + + " \"preferred_username\": \"user1\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.FORM); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index 0a9c096950..93a30b0505 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.registration; import org.junit.Test; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; @@ -52,6 +53,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -68,6 +70,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -81,6 +84,7 @@ public class ClientRegistrationTests { assertThat(registration.getScopes()).isEqualTo(SCOPES); assertThat(registration.getProviderDetails().getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); assertThat(registration.getProviderDetails().getTokenUri()).isEqualTo(TOKEN_URI); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()).isEqualTo(AuthenticationMethod.FORM); assertThat(registration.getProviderDetails().getJwkSetUri()).isEqualTo(JWK_SET_URI); assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); } @@ -96,6 +100,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -112,6 +117,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -128,6 +134,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -144,6 +151,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -160,6 +168,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -177,6 +186,7 @@ public class ClientRegistrationTests { .scope((String[]) null) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -193,6 +203,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(null) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -209,6 +220,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(null) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -225,6 +237,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(null) .clientName(CLIENT_NAME) .build(); @@ -241,6 +254,7 @@ public class ClientRegistrationTests { .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(null) .build(); @@ -256,6 +270,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(REDIRECT_URI) .scope("scope1") .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .tokenUri(TOKEN_URI) .clientName(CLIENT_NAME) .build(); @@ -284,6 +299,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); @@ -293,6 +309,7 @@ public class ClientRegistrationTests { assertThat(registration.getRedirectUriTemplate()).isEqualTo(REDIRECT_URI); assertThat(registration.getScopes()).isEqualTo(SCOPES); assertThat(registration.getProviderDetails().getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()).isEqualTo(AuthenticationMethod.FORM); assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); } @@ -304,6 +321,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -316,6 +334,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -328,6 +347,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(null) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -341,6 +361,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(REDIRECT_URI) .scope((String[]) null) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -353,6 +374,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(null) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -365,6 +387,7 @@ public class ClientRegistrationTests { .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(null) .build(); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java index a85a99f215..99ca960c2e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java @@ -17,6 +17,8 @@ package org.springframework.security.oauth2.client.userinfo; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -26,9 +28,11 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -46,7 +50,7 @@ import static org.mockito.Mockito.when; * * @author Joe Grandja */ -@PowerMockIgnore("okhttp3.*") +@PowerMockIgnore({"okhttp3.*", "okio.Buffer"}) @PrepareForTest(ClientRegistration.class) @RunWith(PowerMockRunner.class) public class DefaultOAuth2UserServiceTests { @@ -115,6 +119,7 @@ public class DefaultOAuth2UserServiceTests { String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -162,6 +167,7 @@ public class DefaultOAuth2UserServiceTests { String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -184,6 +190,7 @@ public class DefaultOAuth2UserServiceTests { String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -201,6 +208,7 @@ public class DefaultOAuth2UserServiceTests { String userInfoUri = "http://invalid-provider.com/user"; when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -229,6 +237,7 @@ public class DefaultOAuth2UserServiceTests { String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -237,4 +246,73 @@ public class DefaultOAuth2UserServiceTests { assertThat(server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT)) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.FORM); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java index 70090f3d89..be57a92114 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java @@ -22,15 +22,19 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +import okhttp3.mockwebserver.RecordedRequest; import reactor.test.StepVerifier; import java.time.Duration; @@ -67,6 +71,7 @@ public class DefaultReactiveOAuth2UserServiceTests { .authorizationUri("https://github.com/login/oauth/authorize") .tokenUri("https://github.com/login/oauth/access_token") .userInfoUri(userInfoUri) + .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) .userNameAttributeName("user-name") .clientName("GitHub") .clientId("clientId") @@ -140,6 +145,51 @@ public class DefaultReactiveOAuth2UserServiceTests { assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); } + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + this.clientRegistration.userInfoAuthenticationMethod(AuthenticationMethod.HEADER); + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + enqueueApplicationJsonBody(userInfoResponse); + + this.userService.loadUser(oauth2UserRequest()).block(); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + this.clientRegistration.userInfoAuthenticationMethod( AuthenticationMethod.FORM); + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + enqueueApplicationJsonBody(userInfoResponse); + + this.userService.loadUser(oauth2UserRequest()).block(); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } + @Test public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() throws Exception { String userInfoResponse = "{\n" + diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java new file mode 100644 index 0000000000..e74ee7ef98 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java @@ -0,0 +1,72 @@ +/* + * 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.core; + +import java.io.Serializable; + +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; + +/** + * The authentication method used when sending bearer access tokens in resource requests to resource servers. + * + * @author MyeongHyeon Lee + * @since 5.1 + * @see Section 2 Authenticated Requests + */ +public final class AuthenticationMethod implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + public static final AuthenticationMethod HEADER = new AuthenticationMethod("header"); + public static final AuthenticationMethod FORM = new AuthenticationMethod("form"); + public static final AuthenticationMethod QUERY = new AuthenticationMethod("query"); + private final String value; + + /** + * Constructs an {@code AuthenticationMethod} using the provided value. + * + * @param value the value of the authentication method type + */ + public AuthenticationMethod(String value) { + Assert.hasText(value, "value cannot be empty"); + this.value = value; + } + + /** + * Returns the value of the authentication method type. + * + * @return the value of the authentication method type + */ + public String getValue() { + return this.value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + AuthenticationMethod that = (AuthenticationMethod) obj; + return this.getValue().equals(that.getValue()); + } + + @Override + public int hashCode() { + return this.getValue().hashCode(); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java new file mode 100644 index 0000000000..4c541cf150 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java @@ -0,0 +1,48 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Tests for {@link AuthenticationMethod}. + * + * @author MyeongHyeon Lee + */ +public class AuthenticationMethodTests { + + @Test + public void constructorWhenValueIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new AuthenticationMethod(null)).hasMessage("value cannot be empty"); + } + + @Test + public void getValueWhenHeaderAuthenticationTypeThenReturnHeader() { + assertThat(AuthenticationMethod.HEADER.getValue()).isEqualTo("header"); + } + + @Test + public void getValueWhenFormAuthenticationTypeThenReturnForm() { + assertThat(AuthenticationMethod.FORM.getValue()).isEqualTo("form"); + } + + @Test + public void getValueWhenFormAuthenticationTypeThenReturnQuery() { + assertThat(AuthenticationMethod.QUERY.getValue()).isEqualTo("query"); + } +}