diff --git a/spring-boot-project/spring-boot-autoconfigure/pom.xml b/spring-boot-project/spring-boot-autoconfigure/pom.xml index ad95125bec..ff4e84fdbc 100755 --- a/spring-boot-project/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-project/spring-boot-autoconfigure/pom.xml @@ -725,6 +725,11 @@ json-path test + + com.squareup.okhttp3 + mockwebserver + test + com.sun.xml.messaging.saaj saaj-impl diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java index bfbe10884c..6afaf988ef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java @@ -208,6 +208,15 @@ public class OAuth2ClientProperties { */ private String jwkSetUri; + /** + * URI that an OpenID Connect Provider asserts as its Issuer Identifier. If the + * issuer provided is "https://example.com", then an "OpenID Provider + * Configuration Request" will be made to + * "https://example.com/.well-known/openid-configuration". The result is expected + * to be an "OpenID Provider Configuration Response". + */ + private String issuerUri; + public String getAuthorizationUri() { return this.authorizationUri; } @@ -248,6 +257,14 @@ public class OAuth2ClientProperties { this.jwkSetUri = jwkSetUri; } + public String getIssuerUri() { + return this.issuerUri; + } + + public void setIssuerUri(String issuerUri) { + this.issuerUri = issuerUri; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java index 4927c26059..36ca11d0a7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java @@ -25,6 +25,7 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.core.convert.ConversionException; import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; +import org.springframework.security.config.oauth2.client.oidc.OidcConfigurationProvider; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -37,6 +38,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Thiago Hirata + * @author Madhura Bhave * @since 2.1.0 */ public final class OAuth2ClientPropertiesRegistrationAdapter { @@ -54,6 +56,13 @@ public final class OAuth2ClientPropertiesRegistrationAdapter { private static ClientRegistration getClientRegistration(String registrationId, Registration properties, Map providers) { + String issuer = getIssuerIfPossible(registrationId, properties.getProvider(), + providers); + if (issuer != null) { + return OidcConfigurationProvider.issuer(issuer).registrationId(registrationId) + .clientId(properties.getClientId()) + .clientSecret(properties.getClientSecret()).build(); + } Builder builder = getBuilder(registrationId, properties.getProvider(), providers); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(properties::getClientId).to(builder::clientId); @@ -70,6 +79,27 @@ public final class OAuth2ClientPropertiesRegistrationAdapter { return builder.build(); } + private static String getIssuerIfPossible(String registrationId, + String configuredProviderId, Map providers) { + String providerId = (configuredProviderId != null ? configuredProviderId + : registrationId); + if (providers.containsKey(providerId)) { + Provider provider = providers.get(providerId); + String issuer = provider.getIssuerUri(); + if (issuer != null) { + return cleanIssuerPath(issuer); + } + } + return null; + } + + private static String cleanIssuerPath(String issuer) { + if (issuer.endsWith("/")) { + return issuer.substring(0, issuer.length() - 1); + } + return issuer; + } + private static Builder getBuilder(String registrationId, String configuredProviderId, Map providers) { String providerId = (configuredProviderId != null ? configuredProviderId diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java index 353c769091..4a6f6eb41c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java @@ -17,16 +17,26 @@ package org.springframework.boot.autoconfigure.security.oauth2.client; import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +50,15 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class OAuth2ClientPropertiesRegistrationAdapterTests { + private MockWebServer server; + + @After + public void cleanup() throws Exception { + if (this.server != null) { + this.server.shutdown(); + } + } + @Rule public ExpectedException thrown = ExpectedException.none(); @@ -217,4 +236,92 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties); } + @Test + public void oidcProviderConfigurationWhenProviderNotSpecifiedOnRegistration() + throws Exception { + Registration registration = new Registration(); + registration.setClientId("clientId"); + registration.setClientSecret("clientSecret"); + testOidcConfiguration(registration, "okta"); + } + + @Test + public void oidcProviderConfigurationWhenProviderSpecifiedOnRegistration() + throws Exception { + Registration registration = new Registration(); + registration.setProvider("okta-oidc"); + registration.setClientId("clientId"); + registration.setClientSecret("clientSecret"); + testOidcConfiguration(registration, "okta-oidc"); + } + + private void testOidcConfiguration(Registration registration, String providerId) + throws Exception { + this.server = new MockWebServer(); + this.server.start(); + String issuer = this.server.url("").toString(); + String cleanIssuerPath = cleanIssuerPath(issuer); + setupMockResponse(cleanIssuerPath); + OAuth2ClientProperties properties = new OAuth2ClientProperties(); + Provider provider = new Provider(); + provider.setIssuerUri(issuer); + properties.getProvider().put(providerId, provider); + properties.getRegistration().put("okta", registration); + Map registrations = OAuth2ClientPropertiesRegistrationAdapter + .getClientRegistrations(properties); + ClientRegistration adapted = registrations.get("okta"); + ProviderDetails providerDetails = adapted.getProviderDetails(); + assertThat(adapted.getClientAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(adapted.getAuthorizationGrantType()) + .isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(adapted.getRegistrationId()).isEqualTo("okta"); + assertThat(adapted.getClientName()).isEqualTo(cleanIssuerPath); + assertThat(adapted.getScopes()).containsOnly("openid"); + assertThat(providerDetails.getAuthorizationUri()) + .isEqualTo("https://example.com/o/oauth2/v2/auth"); + assertThat(providerDetails.getTokenUri()) + .isEqualTo("https://example.com/oauth2/v4/token"); + assertThat(providerDetails.getJwkSetUri()) + .isEqualTo("https://example.com/oauth2/v3/certs"); + assertThat(providerDetails.getUserInfoEndpoint().getUri()) + .isEqualTo("https://example.com/oauth2/v3/userinfo"); + } + + private String cleanIssuerPath(String issuer) { + if (issuer.endsWith("/")) { + return issuer.substring(0, issuer.length() - 1); + } + return issuer; + } + + private void setupMockResponse(String issuer) throws Exception { + MockResponse mockResponse = new MockResponse() + .setResponseCode(HttpStatus.OK.value()) + .setBody(new ObjectMapper().writeValueAsString(getResponse(issuer))) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + } + + private Map getResponse(String issuer) { + Map response = new HashMap<>(); + response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth"); + response.put("claims_supported", Collections.emptyList()); + response.put("code_challenge_methods_supported", Collections.emptyList()); + response.put("id_token_signing_alg_values_supported", Collections.emptyList()); + response.put("issuer", issuer); + response.put("jwks_uri", "https://example.com/oauth2/v3/certs"); + response.put("response_types_supported", Collections.emptyList()); + response.put("revocation_endpoint", "https://example.com/o/oauth2/revoke"); + response.put("scopes_supported", Collections.singletonList("openid")); + response.put("subject_types_supported", Collections.singletonList("public")); + response.put("grant_types_supported", + Collections.singletonList("authorization_code")); + response.put("token_endpoint", "https://example.com/oauth2/v4/token"); + response.put("token_endpoint_auth_methods_supported", + Collections.singletonList("client_secret_basic")); + response.put("userinfo_endpoint", "https://example.com/oauth2/v3/userinfo"); + return response; + } + } diff --git a/spring-boot-project/spring-boot-dependencies/pom.xml b/spring-boot-project/spring-boot-dependencies/pom.xml index 5747aa72c9..31d10e23e7 100644 --- a/spring-boot-project/spring-boot-dependencies/pom.xml +++ b/spring-boot-project/spring-boot-dependencies/pom.xml @@ -163,7 +163,7 @@ 1.2.0.RELEASE 2.0.2.BUILD-SNAPSHOT 1.2.2.RELEASE - 5.1.0.M1 + 5.1.0.BUILD-SNAPSHOT Apple-SR3 3.0.1.RELEASE 3.23.1 diff --git a/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml b/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml index 220007d7e4..e0a70da4e0 100644 --- a/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml +++ b/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml @@ -16,4 +16,10 @@ spring: client-name: Github email provider: github scope: user:email - redirect-uri-template: http://localhost:8080/login/oauth2/code/github \ No newline at end of file + redirect-uri-template: http://localhost:8080/login/oauth2/code/github + google-oidc: + client-id: ${GOOGLE-CLIENT-ID} + client-secret: ${GOOGLE-CLIENT-SECRET} + provider: + google-oidc: + issuer-uri: https://accounts.google.com \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java b/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java index 52ef0cd319..8df3b29edf 100644 --- a/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java @@ -33,7 +33,9 @@ import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { - "APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret" }) + "APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret", + "GOOGLE-CLIENT-ID=my-google-client-id", + "GOOGLE-CLIENT-SECRET=my-google-client-secret" }) public class SampleOAuth2ClientApplicationTests { @LocalServerPort @@ -55,7 +57,8 @@ public class SampleOAuth2ClientApplicationTests { ResponseEntity entity = this.restTemplate.getForEntity("/login", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-1"); + assertThat(entity.getBody()).contains("/oauth2/authorization/google"); + assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-2"); assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-2"); } diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml b/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml index 220007d7e4..e0a70da4e0 100644 --- a/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml @@ -16,4 +16,10 @@ spring: client-name: Github email provider: github scope: user:email - redirect-uri-template: http://localhost:8080/login/oauth2/code/github \ No newline at end of file + redirect-uri-template: http://localhost:8080/login/oauth2/code/github + google-oidc: + client-id: ${GOOGLE-CLIENT-ID} + client-secret: ${GOOGLE-CLIENT-SECRET} + provider: + google-oidc: + issuer-uri: https://accounts.google.com \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/test/java/sample/oauth2/client/SampleReactiveOAuth2ClientApplicationTests.java b/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/test/java/sample/oauth2/client/SampleReactiveOAuth2ClientApplicationTests.java index 74fd195dac..66fce8a191 100644 --- a/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/test/java/sample/oauth2/client/SampleReactiveOAuth2ClientApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/test/java/sample/oauth2/client/SampleReactiveOAuth2ClientApplicationTests.java @@ -28,7 +28,9 @@ import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { - "APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret" }) + "APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret", + "GOOGLE-CLIENT-ID=my-google-client-id", + "GOOGLE-CLIENT-SECRET=my-google-client-secret" }) public class SampleReactiveOAuth2ClientApplicationTests { @Autowired @@ -36,17 +38,16 @@ public class SampleReactiveOAuth2ClientApplicationTests { @Test public void everythingShouldRedirectToLogin() { - this.webTestClient.get().uri("/").exchange() - .expectStatus().isFound() - .expectHeader().valueEquals("Location", "/login"); + this.webTestClient.get().uri("/").exchange().expectStatus().isFound() + .expectHeader().valueEquals("Location", "/login"); } @Test public void loginShouldHaveBothOAuthClientsToChooseFrom() { - byte[] body = this.webTestClient.get().uri("/login").exchange() - .expectStatus().isOk() - .returnResult(String.class).getResponseBodyContent(); + byte[] body = this.webTestClient.get().uri("/login").exchange().expectStatus() + .isOk().returnResult(String.class).getResponseBodyContent(); String bodyString = new String(body); + assertThat(bodyString).contains("/oauth2/authorization/google"); assertThat(bodyString).contains("/oauth2/authorization/github-client-1"); assertThat(bodyString).contains("/oauth2/authorization/github-client-2"); }