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