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"); + } +}