Skip to content

Commit f88ebc0

Browse files
committed
Add support for OIDC Configuration Provider
Closes gh-13210
1 parent 4fee54c commit f88ebc0

File tree

9 files changed

+187
-12
lines changed

9 files changed

+187
-12
lines changed

spring-boot-project/spring-boot-autoconfigure/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,11 @@
725725
<artifactId>json-path</artifactId>
726726
<scope>test</scope>
727727
</dependency>
728+
<dependency>
729+
<groupId>com.squareup.okhttp3</groupId>
730+
<artifactId>mockwebserver</artifactId>
731+
<scope>test</scope>
732+
</dependency>
728733
<dependency>
729734
<groupId>com.sun.xml.messaging.saaj</groupId>
730735
<artifactId>saaj-impl</artifactId>

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java

+17
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,15 @@ public static class Provider {
208208
*/
209209
private String jwkSetUri;
210210

211+
/**
212+
* URI that an OpenID Connect Provider asserts as its Issuer Identifier. If the
213+
* issuer provided is "https://example.com", then an "OpenID Provider
214+
* Configuration Request" will be made to
215+
* "https://example.com/.well-known/openid-configuration". The result is expected
216+
* to be an "OpenID Provider Configuration Response".
217+
*/
218+
private String issuerUri;
219+
211220
public String getAuthorizationUri() {
212221
return this.authorizationUri;
213222
}
@@ -248,6 +257,14 @@ public void setJwkSetUri(String jwkSetUri) {
248257
this.jwkSetUri = jwkSetUri;
249258
}
250259

260+
public String getIssuerUri() {
261+
return this.issuerUri;
262+
}
263+
264+
public void setIssuerUri(String issuerUri) {
265+
this.issuerUri = issuerUri;
266+
}
267+
251268
}
252269

253270
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java

+30
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.boot.convert.ApplicationConversionService;
2626
import org.springframework.core.convert.ConversionException;
2727
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
28+
import org.springframework.security.config.oauth2.client.oidc.OidcConfigurationProvider;
2829
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2930
import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder;
3031
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -37,6 +38,7 @@
3738
*
3839
* @author Phillip Webb
3940
* @author Thiago Hirata
41+
* @author Madhura Bhave
4042
* @since 2.1.0
4143
*/
4244
public final class OAuth2ClientPropertiesRegistrationAdapter {
@@ -54,6 +56,13 @@ public static Map<String, ClientRegistration> getClientRegistrations(
5456

5557
private static ClientRegistration getClientRegistration(String registrationId,
5658
Registration properties, Map<String, Provider> providers) {
59+
String issuer = getIssuerIfPossible(registrationId, properties.getProvider(),
60+
providers);
61+
if (issuer != null) {
62+
return OidcConfigurationProvider.issuer(issuer).registrationId(registrationId)
63+
.clientId(properties.getClientId())
64+
.clientSecret(properties.getClientSecret()).build();
65+
}
5766
Builder builder = getBuilder(registrationId, properties.getProvider(), providers);
5867
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
5968
map.from(properties::getClientId).to(builder::clientId);
@@ -70,6 +79,27 @@ private static ClientRegistration getClientRegistration(String registrationId,
7079
return builder.build();
7180
}
7281

82+
private static String getIssuerIfPossible(String registrationId,
83+
String configuredProviderId, Map<String, Provider> providers) {
84+
String providerId = (configuredProviderId != null ? configuredProviderId
85+
: registrationId);
86+
if (providers.containsKey(providerId)) {
87+
Provider provider = providers.get(providerId);
88+
String issuer = provider.getIssuerUri();
89+
if (issuer != null) {
90+
return cleanIssuerPath(issuer);
91+
}
92+
}
93+
return null;
94+
}
95+
96+
private static String cleanIssuerPath(String issuer) {
97+
if (issuer.endsWith("/")) {
98+
return issuer.substring(0, issuer.length() - 1);
99+
}
100+
return issuer;
101+
}
102+
73103
private static Builder getBuilder(String registrationId, String configuredProviderId,
74104
Map<String, Provider> providers) {
75105
String providerId = (configuredProviderId != null ? configuredProviderId

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java

+107
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,26 @@
1717
package org.springframework.boot.autoconfigure.security.oauth2.client;
1818

1919
import java.util.Collections;
20+
import java.util.HashMap;
2021
import java.util.Map;
2122

23+
import okhttp3.mockwebserver.MockResponse;
24+
import okhttp3.mockwebserver.MockWebServer;
25+
import org.junit.After;
2226
import org.junit.Rule;
2327
import org.junit.Test;
2428
import org.junit.rules.ExpectedException;
29+
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
2530

2631
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider;
2732
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration;
33+
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.HttpStatus;
35+
import org.springframework.http.MediaType;
2836
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2937
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
38+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
39+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
3040
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
3141

3242
import static org.assertj.core.api.Assertions.assertThat;
@@ -40,6 +50,15 @@
4050
*/
4151
public class OAuth2ClientPropertiesRegistrationAdapterTests {
4252

53+
private MockWebServer server;
54+
55+
@After
56+
public void cleanup() throws Exception {
57+
if (this.server != null) {
58+
this.server.shutdown();
59+
}
60+
}
61+
4362
@Rule
4463
public ExpectedException thrown = ExpectedException.none();
4564

@@ -217,4 +236,92 @@ public void getClientRegistrationsWhenProviderNotSpecifiedAndUnknownProviderShou
217236
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties);
218237
}
219238

239+
@Test
240+
public void oidcProviderConfigurationWhenProviderNotSpecifiedOnRegistration()
241+
throws Exception {
242+
Registration registration = new Registration();
243+
registration.setClientId("clientId");
244+
registration.setClientSecret("clientSecret");
245+
testOidcConfiguration(registration, "okta");
246+
}
247+
248+
@Test
249+
public void oidcProviderConfigurationWhenProviderSpecifiedOnRegistration()
250+
throws Exception {
251+
Registration registration = new Registration();
252+
registration.setProvider("okta-oidc");
253+
registration.setClientId("clientId");
254+
registration.setClientSecret("clientSecret");
255+
testOidcConfiguration(registration, "okta-oidc");
256+
}
257+
258+
private void testOidcConfiguration(Registration registration, String providerId)
259+
throws Exception {
260+
this.server = new MockWebServer();
261+
this.server.start();
262+
String issuer = this.server.url("").toString();
263+
String cleanIssuerPath = cleanIssuerPath(issuer);
264+
setupMockResponse(cleanIssuerPath);
265+
OAuth2ClientProperties properties = new OAuth2ClientProperties();
266+
Provider provider = new Provider();
267+
provider.setIssuerUri(issuer);
268+
properties.getProvider().put(providerId, provider);
269+
properties.getRegistration().put("okta", registration);
270+
Map<String, ClientRegistration> registrations = OAuth2ClientPropertiesRegistrationAdapter
271+
.getClientRegistrations(properties);
272+
ClientRegistration adapted = registrations.get("okta");
273+
ProviderDetails providerDetails = adapted.getProviderDetails();
274+
assertThat(adapted.getClientAuthenticationMethod())
275+
.isEqualTo(ClientAuthenticationMethod.BASIC);
276+
assertThat(adapted.getAuthorizationGrantType())
277+
.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
278+
assertThat(adapted.getRegistrationId()).isEqualTo("okta");
279+
assertThat(adapted.getClientName()).isEqualTo(cleanIssuerPath);
280+
assertThat(adapted.getScopes()).containsOnly("openid");
281+
assertThat(providerDetails.getAuthorizationUri())
282+
.isEqualTo("https://example.com/o/oauth2/v2/auth");
283+
assertThat(providerDetails.getTokenUri())
284+
.isEqualTo("https://example.com/oauth2/v4/token");
285+
assertThat(providerDetails.getJwkSetUri())
286+
.isEqualTo("https://example.com/oauth2/v3/certs");
287+
assertThat(providerDetails.getUserInfoEndpoint().getUri())
288+
.isEqualTo("https://example.com/oauth2/v3/userinfo");
289+
}
290+
291+
private String cleanIssuerPath(String issuer) {
292+
if (issuer.endsWith("/")) {
293+
return issuer.substring(0, issuer.length() - 1);
294+
}
295+
return issuer;
296+
}
297+
298+
private void setupMockResponse(String issuer) throws Exception {
299+
MockResponse mockResponse = new MockResponse()
300+
.setResponseCode(HttpStatus.OK.value())
301+
.setBody(new ObjectMapper().writeValueAsString(getResponse(issuer)))
302+
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
303+
this.server.enqueue(mockResponse);
304+
}
305+
306+
private Map<String, Object> getResponse(String issuer) {
307+
Map<String, Object> response = new HashMap<>();
308+
response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth");
309+
response.put("claims_supported", Collections.emptyList());
310+
response.put("code_challenge_methods_supported", Collections.emptyList());
311+
response.put("id_token_signing_alg_values_supported", Collections.emptyList());
312+
response.put("issuer", issuer);
313+
response.put("jwks_uri", "https://example.com/oauth2/v3/certs");
314+
response.put("response_types_supported", Collections.emptyList());
315+
response.put("revocation_endpoint", "https://example.com/o/oauth2/revoke");
316+
response.put("scopes_supported", Collections.singletonList("openid"));
317+
response.put("subject_types_supported", Collections.singletonList("public"));
318+
response.put("grant_types_supported",
319+
Collections.singletonList("authorization_code"));
320+
response.put("token_endpoint", "https://example.com/oauth2/v4/token");
321+
response.put("token_endpoint_auth_methods_supported",
322+
Collections.singletonList("client_secret_basic"));
323+
response.put("userinfo_endpoint", "https://example.com/oauth2/v3/userinfo");
324+
return response;
325+
}
326+
220327
}

spring-boot-project/spring-boot-dependencies/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
<spring-plugin.version>1.2.0.RELEASE</spring-plugin.version>
164164
<spring-restdocs.version>2.0.2.BUILD-SNAPSHOT</spring-restdocs.version>
165165
<spring-retry.version>1.2.2.RELEASE</spring-retry.version>
166-
<spring-security.version>5.1.0.M1</spring-security.version>
166+
<spring-security.version>5.1.0.BUILD-SNAPSHOT</spring-security.version>
167167
<spring-session-bom.version>Apple-SR3</spring-session-bom.version>
168168
<spring-ws.version>3.0.1.RELEASE</spring-ws.version>
169169
<sqlite-jdbc.version>3.23.1</sqlite-jdbc.version>

spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ spring:
1616
client-name: Github email
1717
provider: github
1818
scope: user:email
19-
redirect-uri-template: http://localhost:8080/login/oauth2/code/github
19+
redirect-uri-template: http://localhost:8080/login/oauth2/code/github
20+
google-oidc:
21+
client-id: ${GOOGLE-CLIENT-ID}
22+
client-secret: ${GOOGLE-CLIENT-SECRET}
23+
provider:
24+
google-oidc:
25+
issuer-uri: https://accounts.google.com

spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333

3434
@RunWith(SpringRunner.class)
3535
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
36-
"APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret" })
36+
"APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret",
37+
"GOOGLE-CLIENT-ID=my-google-client-id",
38+
"GOOGLE-CLIENT-SECRET=my-google-client-secret" })
3739
public class SampleOAuth2ClientApplicationTests {
3840

3941
@LocalServerPort
@@ -55,7 +57,8 @@ public void loginShouldHaveBothOAuthClientsToChooseFrom() {
5557
ResponseEntity<String> entity = this.restTemplate.getForEntity("/login",
5658
String.class);
5759
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
58-
assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-1");
60+
assertThat(entity.getBody()).contains("/oauth2/authorization/google");
61+
assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-2");
5962
assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-2");
6063
}
6164

spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ spring:
1616
client-name: Github email
1717
provider: github
1818
scope: user:email
19-
redirect-uri-template: http://localhost:8080/login/oauth2/code/github
19+
redirect-uri-template: http://localhost:8080/login/oauth2/code/github
20+
google-oidc:
21+
client-id: ${GOOGLE-CLIENT-ID}
22+
client-secret: ${GOOGLE-CLIENT-SECRET}
23+
provider:
24+
google-oidc:
25+
issuer-uri: https://accounts.google.com

spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/test/java/sample/oauth2/client/SampleReactiveOAuth2ClientApplicationTests.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,26 @@
2828

2929
@RunWith(SpringRunner.class)
3030
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
31-
"APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret" })
31+
"APP-CLIENT-ID=my-client-id", "APP-CLIENT-SECRET=my-client-secret",
32+
"GOOGLE-CLIENT-ID=my-google-client-id",
33+
"GOOGLE-CLIENT-SECRET=my-google-client-secret" })
3234
public class SampleReactiveOAuth2ClientApplicationTests {
3335

3436
@Autowired
3537
private WebTestClient webTestClient;
3638

3739
@Test
3840
public void everythingShouldRedirectToLogin() {
39-
this.webTestClient.get().uri("/").exchange()
40-
.expectStatus().isFound()
41-
.expectHeader().valueEquals("Location", "/login");
41+
this.webTestClient.get().uri("/").exchange().expectStatus().isFound()
42+
.expectHeader().valueEquals("Location", "/login");
4243
}
4344

4445
@Test
4546
public void loginShouldHaveBothOAuthClientsToChooseFrom() {
46-
byte[] body = this.webTestClient.get().uri("/login").exchange()
47-
.expectStatus().isOk()
48-
.returnResult(String.class).getResponseBodyContent();
47+
byte[] body = this.webTestClient.get().uri("/login").exchange().expectStatus()
48+
.isOk().returnResult(String.class).getResponseBodyContent();
4949
String bodyString = new String(body);
50+
assertThat(bodyString).contains("/oauth2/authorization/google");
5051
assertThat(bodyString).contains("/oauth2/authorization/github-client-1");
5152
assertThat(bodyString).contains("/oauth2/authorization/github-client-2");
5253
}

0 commit comments

Comments
 (0)