Commit f88ebc06 authored by Madhura Bhave's avatar Madhura Bhave

Add support for OIDC Configuration Provider

Closes gh-13210
parent 4fee54cf
...@@ -725,6 +725,11 @@ ...@@ -725,6 +725,11 @@
<artifactId>json-path</artifactId> <artifactId>json-path</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.sun.xml.messaging.saaj</groupId> <groupId>com.sun.xml.messaging.saaj</groupId>
<artifactId>saaj-impl</artifactId> <artifactId>saaj-impl</artifactId>
......
...@@ -208,6 +208,15 @@ public class OAuth2ClientProperties { ...@@ -208,6 +208,15 @@ public class OAuth2ClientProperties {
*/ */
private String jwkSetUri; 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() { public String getAuthorizationUri() {
return this.authorizationUri; return this.authorizationUri;
} }
...@@ -248,6 +257,14 @@ public class OAuth2ClientProperties { ...@@ -248,6 +257,14 @@ public class OAuth2ClientProperties {
this.jwkSetUri = jwkSetUri; this.jwkSetUri = jwkSetUri;
} }
public String getIssuerUri() {
return this.issuerUri;
}
public void setIssuerUri(String issuerUri) {
this.issuerUri = issuerUri;
}
} }
} }
...@@ -25,6 +25,7 @@ import org.springframework.boot.context.properties.PropertyMapper; ...@@ -25,6 +25,7 @@ import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionException;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; 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;
import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder; import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder;
import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.AuthorizationGrantType;
...@@ -37,6 +38,7 @@ import org.springframework.util.StringUtils; ...@@ -37,6 +38,7 @@ import org.springframework.util.StringUtils;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Thiago Hirata * @author Thiago Hirata
* @author Madhura Bhave
* @since 2.1.0 * @since 2.1.0
*/ */
public final class OAuth2ClientPropertiesRegistrationAdapter { public final class OAuth2ClientPropertiesRegistrationAdapter {
...@@ -54,6 +56,13 @@ public final class OAuth2ClientPropertiesRegistrationAdapter { ...@@ -54,6 +56,13 @@ public final class OAuth2ClientPropertiesRegistrationAdapter {
private static ClientRegistration getClientRegistration(String registrationId, private static ClientRegistration getClientRegistration(String registrationId,
Registration properties, Map<String, Provider> providers) { Registration properties, Map<String, Provider> 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); Builder builder = getBuilder(registrationId, properties.getProvider(), providers);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties::getClientId).to(builder::clientId); map.from(properties::getClientId).to(builder::clientId);
...@@ -70,6 +79,27 @@ public final class OAuth2ClientPropertiesRegistrationAdapter { ...@@ -70,6 +79,27 @@ public final class OAuth2ClientPropertiesRegistrationAdapter {
return builder.build(); return builder.build();
} }
private static String getIssuerIfPossible(String registrationId,
String configuredProviderId, Map<String, Provider> 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, private static Builder getBuilder(String registrationId, String configuredProviderId,
Map<String, Provider> providers) { Map<String, Provider> providers) {
String providerId = (configuredProviderId != null ? configuredProviderId String providerId = (configuredProviderId != null ? configuredProviderId
......
...@@ -17,16 +17,26 @@ ...@@ -17,16 +17,26 @@
package org.springframework.boot.autoconfigure.security.oauth2.client; package org.springframework.boot.autoconfigure.security.oauth2.client;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; 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.Provider;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; 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;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; 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 org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -40,6 +50,15 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -40,6 +50,15 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class OAuth2ClientPropertiesRegistrationAdapterTests { public class OAuth2ClientPropertiesRegistrationAdapterTests {
private MockWebServer server;
@After
public void cleanup() throws Exception {
if (this.server != null) {
this.server.shutdown();
}
}
@Rule @Rule
public ExpectedException thrown = ExpectedException.none(); public ExpectedException thrown = ExpectedException.none();
...@@ -217,4 +236,92 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { ...@@ -217,4 +236,92 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests {
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties); 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<String, ClientRegistration> 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<String, Object> getResponse(String issuer) {
Map<String, Object> 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;
}
} }
...@@ -163,7 +163,7 @@ ...@@ -163,7 +163,7 @@
<spring-plugin.version>1.2.0.RELEASE</spring-plugin.version> <spring-plugin.version>1.2.0.RELEASE</spring-plugin.version>
<spring-restdocs.version>2.0.2.BUILD-SNAPSHOT</spring-restdocs.version> <spring-restdocs.version>2.0.2.BUILD-SNAPSHOT</spring-restdocs.version>
<spring-retry.version>1.2.2.RELEASE</spring-retry.version> <spring-retry.version>1.2.2.RELEASE</spring-retry.version>
<spring-security.version>5.1.0.M1</spring-security.version> <spring-security.version>5.1.0.BUILD-SNAPSHOT</spring-security.version>
<spring-session-bom.version>Apple-SR3</spring-session-bom.version> <spring-session-bom.version>Apple-SR3</spring-session-bom.version>
<spring-ws.version>3.0.1.RELEASE</spring-ws.version> <spring-ws.version>3.0.1.RELEASE</spring-ws.version>
<sqlite-jdbc.version>3.23.1</sqlite-jdbc.version> <sqlite-jdbc.version>3.23.1</sqlite-jdbc.version>
......
...@@ -17,3 +17,9 @@ spring: ...@@ -17,3 +17,9 @@ spring:
provider: github provider: github
scope: user:email scope: user:email
redirect-uri-template: http://localhost:8080/login/oauth2/code/github 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
...@@ -33,7 +33,9 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -33,7 +33,9 @@ import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { @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 { public class SampleOAuth2ClientApplicationTests {
@LocalServerPort @LocalServerPort
...@@ -55,7 +57,8 @@ public class SampleOAuth2ClientApplicationTests { ...@@ -55,7 +57,8 @@ public class SampleOAuth2ClientApplicationTests {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/login", ResponseEntity<String> entity = this.restTemplate.getForEntity("/login",
String.class); String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); 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"); assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-2");
} }
......
...@@ -17,3 +17,9 @@ spring: ...@@ -17,3 +17,9 @@ spring:
provider: github provider: github
scope: user:email scope: user:email
redirect-uri-template: http://localhost:8080/login/oauth2/code/github 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
...@@ -28,7 +28,9 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -28,7 +28,9 @@ import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { @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 { public class SampleReactiveOAuth2ClientApplicationTests {
@Autowired @Autowired
...@@ -36,17 +38,16 @@ public class SampleReactiveOAuth2ClientApplicationTests { ...@@ -36,17 +38,16 @@ public class SampleReactiveOAuth2ClientApplicationTests {
@Test @Test
public void everythingShouldRedirectToLogin() { public void everythingShouldRedirectToLogin() {
this.webTestClient.get().uri("/").exchange() this.webTestClient.get().uri("/").exchange().expectStatus().isFound()
.expectStatus().isFound()
.expectHeader().valueEquals("Location", "/login"); .expectHeader().valueEquals("Location", "/login");
} }
@Test @Test
public void loginShouldHaveBothOAuthClientsToChooseFrom() { public void loginShouldHaveBothOAuthClientsToChooseFrom() {
byte[] body = this.webTestClient.get().uri("/login").exchange() byte[] body = this.webTestClient.get().uri("/login").exchange().expectStatus()
.expectStatus().isOk() .isOk().returnResult(String.class).getResponseBodyContent();
.returnResult(String.class).getResponseBodyContent();
String bodyString = new String(body); 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-1");
assertThat(bodyString).contains("/oauth2/authorization/github-client-2"); assertThat(bodyString).contains("/oauth2/authorization/github-client-2");
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment