Skip to content

Commit f5deebf

Browse files
committed
Support authorization_code grant for OAuth2 client
This commit also refactors OAuth2 client properties. With the added support for authorization_code clients, client registrations are now divided into `login` and `authorization_code`. An environment post processor is used for backward compatibility with old Open ID Connect login clients. Closes gh-13812
1 parent 5af7835 commit f5deebf

File tree

18 files changed

+723
-191
lines changed

18 files changed

+723
-191
lines changed

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

+25-9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.boot.autoconfigure.security.oauth2.client;
1717

1818
import java.util.Collections;
19+
import java.util.HashMap;
1920
import java.util.Map;
2021
import java.util.stream.Collectors;
2122

@@ -37,29 +38,44 @@
3738
*/
3839
public class ClientsConfiguredCondition extends SpringBootCondition {
3940

40-
private static final Bindable<Map<String, OAuth2ClientProperties.Registration>> STRING_REGISTRATION_MAP = Bindable
41-
.mapOf(String.class, OAuth2ClientProperties.Registration.class);
41+
private static final Bindable<Map<String, OAuth2ClientProperties.LoginClientRegistration>> STRING_LOGIN_REGISTRATION_MAP = Bindable
42+
.mapOf(String.class, OAuth2ClientProperties.LoginClientRegistration.class);
43+
44+
private static final Bindable<Map<String, OAuth2ClientProperties.AuthorizationCodeClientRegistration>> STRING_AUTHORIZATIONCODE_REGISTRATION_MAP = Bindable
45+
.mapOf(String.class,
46+
OAuth2ClientProperties.AuthorizationCodeClientRegistration.class);
4247

4348
@Override
4449
public ConditionOutcome getMatchOutcome(ConditionContext context,
4550
AnnotatedTypeMetadata metadata) {
4651
ConditionMessage.Builder message = ConditionMessage
4752
.forCondition("OAuth2 Clients Configured Condition");
48-
Map<String, OAuth2ClientProperties.Registration> registrations = getRegistrations(
53+
Map<String, OAuth2ClientProperties.BaseClientRegistration> registrations = getRegistrations(
4954
context.getEnvironment());
5055
if (!registrations.isEmpty()) {
51-
return ConditionOutcome.match(message
52-
.foundExactly("registered clients " + registrations.values().stream()
53-
.map(OAuth2ClientProperties.Registration::getClientId)
56+
return ConditionOutcome.match(message.foundExactly(
57+
"registered clients " + registrations.values().stream().map(
58+
OAuth2ClientProperties.BaseClientRegistration::getClientId)
5459
.collect(Collectors.joining(", "))));
5560
}
5661
return ConditionOutcome.noMatch(message.notAvailable("registered clients"));
5762
}
5863

59-
private Map<String, OAuth2ClientProperties.Registration> getRegistrations(
64+
private Map<String, OAuth2ClientProperties.BaseClientRegistration> getRegistrations(
6065
Environment environment) {
61-
return Binder.get(environment).bind("spring.security.oauth2.client.registration",
62-
STRING_REGISTRATION_MAP).orElse(Collections.emptyMap());
66+
Map<String, OAuth2ClientProperties.BaseClientRegistration> registrations = new HashMap();
67+
Map<String, OAuth2ClientProperties.LoginClientRegistration> loginClientRegistrations = Binder
68+
.get(environment).bind("spring.security.oauth2.client.registration.login",
69+
STRING_LOGIN_REGISTRATION_MAP)
70+
.orElse(Collections.emptyMap());
71+
Map<String, OAuth2ClientProperties.AuthorizationCodeClientRegistration> authCodeClientRegistrations = Binder
72+
.get(environment)
73+
.bind("spring.security.oauth2.client.registration.authorizationcode",
74+
STRING_AUTHORIZATIONCODE_REGISTRATION_MAP)
75+
.orElse(Collections.emptyMap());
76+
registrations.putAll(loginClientRegistrations);
77+
registrations.putAll(authCodeClientRegistrations);
78+
return registrations;
6379
}
6480

6581
}

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

+80-19
Original file line numberDiff line numberDiff line change
@@ -44,31 +44,105 @@ public class OAuth2ClientProperties {
4444
/**
4545
* OAuth client registrations.
4646
*/
47-
private final Map<String, Registration> registration = new HashMap<>();
47+
private final Registration registration = new Registration();
4848

4949
public Map<String, Provider> getProvider() {
5050
return this.provider;
5151
}
5252

53-
public Map<String, Registration> getRegistration() {
53+
public Registration getRegistration() {
5454
return this.registration;
5555
}
5656

5757
@PostConstruct
5858
public void validate() {
59-
this.getRegistration().values().forEach(this::validateRegistration);
59+
this.getRegistration().getLogin().values().forEach(this::validateRegistration);
60+
this.getRegistration().getAuthorizationCode().values()
61+
.forEach(this::validateRegistration);
6062
}
6163

62-
private void validateRegistration(Registration registration) {
64+
private void validateRegistration(BaseClientRegistration registration) {
6365
if (!StringUtils.hasText(registration.getClientId())) {
6466
throw new IllegalStateException("Client id must not be empty.");
6567
}
6668
}
6769

70+
public static class Registration {
71+
72+
/**
73+
* OpenID Connect client registrations.
74+
*/
75+
private Map<String, LoginClientRegistration> login = new HashMap<>();
76+
77+
/**
78+
* OAuth2 authorization_code client registrations.
79+
*/
80+
private Map<String, AuthorizationCodeClientRegistration> authorizationCode = new HashMap<>();
81+
82+
public Map<String, LoginClientRegistration> getLogin() {
83+
return this.login;
84+
}
85+
86+
public void setLogin(Map<String, LoginClientRegistration> login) {
87+
this.login = login;
88+
}
89+
90+
public Map<String, AuthorizationCodeClientRegistration> getAuthorizationCode() {
91+
return this.authorizationCode;
92+
}
93+
94+
public void setAuthorizationCode(
95+
Map<String, AuthorizationCodeClientRegistration> authorizationCode) {
96+
this.authorizationCode = authorizationCode;
97+
}
98+
99+
}
100+
68101
/**
69-
* A single client registration.
102+
* A single client registration for OpenID Connect login.
70103
*/
71-
public static class Registration {
104+
public static class LoginClientRegistration extends BaseClientRegistration {
105+
106+
/**
107+
* Redirect URI. May be left blank when using a pre-defined provider.
108+
*/
109+
private String redirectUriTemplate;
110+
111+
public String getRedirectUriTemplate() {
112+
return this.redirectUriTemplate;
113+
}
114+
115+
public void setRedirectUriTemplate(String redirectUriTemplate) {
116+
this.redirectUriTemplate = redirectUriTemplate;
117+
}
118+
119+
}
120+
121+
/**
122+
* A single client registration for OAuth2 authorization_code flow.
123+
*/
124+
public static class AuthorizationCodeClientRegistration
125+
extends BaseClientRegistration {
126+
127+
/**
128+
* Redirect URI for the registration.
129+
*/
130+
private String redirectUri;
131+
132+
public String getRedirectUri() {
133+
return this.redirectUri;
134+
}
135+
136+
public void setRedirectUri(String redirectUri) {
137+
this.redirectUri = redirectUri;
138+
}
139+
140+
}
141+
142+
/**
143+
* Base class for a single client registration.
144+
*/
145+
public static class BaseClientRegistration {
72146

73147
/**
74148
* Reference to the OAuth 2.0 provider to use. May reference an element from the
@@ -98,11 +172,6 @@ public static class Registration {
98172
*/
99173
private String authorizationGrantType;
100174

101-
/**
102-
* Redirect URI. May be left blank when using a pre-defined provider.
103-
*/
104-
private String redirectUriTemplate;
105-
106175
/**
107176
* Authorization scopes. May be left blank when using a pre-defined provider.
108177
*/
@@ -153,14 +222,6 @@ public void setAuthorizationGrantType(String authorizationGrantType) {
153222
this.authorizationGrantType = authorizationGrantType;
154223
}
155224

156-
public String getRedirectUriTemplate() {
157-
return this.redirectUriTemplate;
158-
}
159-
160-
public void setRedirectUriTemplate(String redirectUriTemplate) {
161-
this.redirectUriTemplate = redirectUriTemplate;
162-
}
163-
164225
public Set<String> getScope() {
165226
return this.scope;
166227
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.autoconfigure.security.oauth2.client;
17+
18+
import java.util.Collections;
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
import java.util.function.Supplier;
22+
23+
import org.springframework.boot.SpringApplication;
24+
import org.springframework.boot.context.config.ConfigFileApplicationListener;
25+
import org.springframework.boot.context.properties.bind.Bindable;
26+
import org.springframework.boot.context.properties.bind.Binder;
27+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
28+
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
29+
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
30+
import org.springframework.boot.env.EnvironmentPostProcessor;
31+
import org.springframework.core.Ordered;
32+
import org.springframework.core.env.ConfigurableEnvironment;
33+
import org.springframework.core.env.MapPropertySource;
34+
35+
/**
36+
* {@link EnvironmentPostProcessor} that migrates legacy OAuth2 login client properties
37+
* under the `spring.security.oauth2.client.login` prefix.
38+
*
39+
* @author Madhura Bhave
40+
* @since 2.1.0
41+
*/
42+
public class OAuth2ClientPropertiesEnvironmentPostProcessor
43+
implements EnvironmentPostProcessor, Ordered {
44+
45+
private static final Bindable<Map<String, OAuth2ClientProperties.LoginClientRegistration>> STRING_LEGACY_REGISTRATION_MAP = Bindable
46+
.mapOf(String.class, OAuth2ClientProperties.LoginClientRegistration.class);
47+
48+
private static final String PREFIX = "spring.security.oauth2.client.registration";
49+
50+
private static final String LOGIN_REGISTRATION_PREFIX = PREFIX + ".login.";
51+
52+
private static final String UPDATED_PROPERTY_SOURCE_SUFFIX = "-updated-oauth-client";
53+
54+
private int order = ConfigFileApplicationListener.DEFAULT_ORDER + 1;
55+
56+
@Override
57+
public void postProcessEnvironment(ConfigurableEnvironment environment,
58+
SpringApplication application) {
59+
environment.getPropertySources().forEach((propertySource) -> {
60+
String name = propertySource.getName();
61+
Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources
62+
.from(propertySource);
63+
ConfigurationPropertySource source = sources.iterator().next();
64+
Binder binder = new Binder(sources);
65+
Map<String, Object> map = new LinkedHashMap<>();
66+
MapPropertySource updatedPropertySource = new MapPropertySource(
67+
name + UPDATED_PROPERTY_SOURCE_SUFFIX, map);
68+
Map<String, OAuth2ClientProperties.LoginClientRegistration> registrations = binder
69+
.bind(PREFIX, STRING_LEGACY_REGISTRATION_MAP)
70+
.orElse(Collections.emptyMap());
71+
registrations.entrySet()
72+
.forEach((entry) -> addProperties(entry, source, map));
73+
if (!map.isEmpty()) {
74+
environment.getPropertySources().addBefore(name, updatedPropertySource);
75+
}
76+
});
77+
}
78+
79+
private void addProperties(
80+
Map.Entry<String, OAuth2ClientProperties.LoginClientRegistration> entry,
81+
ConfigurationPropertySource source, Map<String, Object> map) {
82+
OAuth2ClientProperties.LoginClientRegistration registration = entry.getValue();
83+
String registrationId = entry.getKey();
84+
addProperty(registrationId, "client-id", registration::getClientId, map, source);
85+
addProperty(registrationId, "client-secret", registration::getClientSecret, map,
86+
source);
87+
addProperty(registrationId, "client-name", registration::getClientName, map,
88+
source);
89+
addProperty(registrationId, "redirect-uri-template",
90+
registration::getRedirectUriTemplate, map, source);
91+
addProperty(registrationId, "authorization-grant-type",
92+
registration::getAuthorizationGrantType, map, source);
93+
addProperty(registrationId, "client-authentication-method",
94+
registration::getClientAuthenticationMethod, map, source);
95+
addProperty(registrationId, "provider", registration::getProvider, map, source);
96+
addProperty(registrationId, "scope", registration::getScope, map, source);
97+
}
98+
99+
private void addProperty(String registrationId, String property,
100+
Supplier<Object> valueSupplier, Map<String, Object> map,
101+
ConfigurationPropertySource source) {
102+
String registrationKey = PREFIX + "." + registrationId + ".";
103+
String loginRegistrationKey = LOGIN_REGISTRATION_PREFIX + registrationId + ".";
104+
if (source.getConfigurationProperty(
105+
ConfigurationPropertyName.of(registrationKey + property)) != null) {
106+
map.put(loginRegistrationKey + property, valueSupplier.get());
107+
}
108+
}
109+
110+
@Override
111+
public int getOrder() {
112+
return this.order;
113+
}
114+
115+
}

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

+30-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.Map;
2121

2222
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider;
23-
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration;
2423
import org.springframework.boot.context.properties.PropertyMapper;
2524
import org.springframework.boot.convert.ApplicationConversionService;
2625
import org.springframework.core.convert.ConversionException;
@@ -51,30 +50,54 @@ private OAuth2ClientPropertiesRegistrationAdapter() {
5150
public static Map<String, ClientRegistration> getClientRegistrations(
5251
OAuth2ClientProperties properties) {
5352
Map<String, ClientRegistration> clientRegistrations = new HashMap<>();
54-
properties.getRegistration().forEach((key, value) -> clientRegistrations.put(key,
55-
getClientRegistration(key, value, properties.getProvider())));
53+
properties.getRegistration().getLogin()
54+
.forEach((key, value) -> clientRegistrations.put(key,
55+
getLoginClientRegistration(key, value,
56+
properties.getProvider())));
57+
properties.getRegistration().getAuthorizationCode()
58+
.forEach((key, value) -> clientRegistrations.put(key,
59+
getAuthorizationCodeClientRegistration(key, value,
60+
properties.getProvider())));
5661
return clientRegistrations;
5762
}
5863

59-
private static ClientRegistration getClientRegistration(String registrationId,
60-
Registration properties, Map<String, Provider> providers) {
64+
private static ClientRegistration getAuthorizationCodeClientRegistration(
65+
String registrationId,
66+
OAuth2ClientProperties.AuthorizationCodeClientRegistration properties,
67+
Map<String, Provider> providers) {
68+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
69+
Builder builder = getBuilder(map, registrationId, properties, providers);
70+
map.from(properties::getRedirectUri).to(builder::redirectUriTemplate);
71+
return builder.build();
72+
}
73+
74+
private static Builder getBuilder(PropertyMapper map, String registrationId,
75+
OAuth2ClientProperties.BaseClientRegistration properties,
76+
Map<String, Provider> providers) {
6177
Builder builder = getBuilderFromIssuerIfPossible(registrationId,
6278
properties.getProvider(), providers);
6379
if (builder == null) {
6480
builder = getBuilder(registrationId, properties.getProvider(), providers);
6581
}
66-
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
6782
map.from(properties::getClientId).to(builder::clientId);
6883
map.from(properties::getClientSecret).to(builder::clientSecret);
6984
map.from(properties::getClientAuthenticationMethod)
7085
.as(ClientAuthenticationMethod::new)
7186
.to(builder::clientAuthenticationMethod);
7287
map.from(properties::getAuthorizationGrantType).as(AuthorizationGrantType::new)
7388
.to(builder::authorizationGrantType);
74-
map.from(properties::getRedirectUriTemplate).to(builder::redirectUriTemplate);
7589
map.from(properties::getScope).as((scope) -> StringUtils.toStringArray(scope))
7690
.to(builder::scope);
7791
map.from(properties::getClientName).to(builder::clientName);
92+
return builder;
93+
}
94+
95+
private static ClientRegistration getLoginClientRegistration(String registrationId,
96+
OAuth2ClientProperties.LoginClientRegistration properties,
97+
Map<String, Provider> providers) {
98+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
99+
Builder builder = getBuilder(map, registrationId, properties, providers);
100+
map.from(properties::getRedirectUriTemplate).to(builder::redirectUriTemplate);
78101
return builder.build();
79102
}
80103

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAda
6060

6161
@Override
6262
protected void configure(HttpSecurity http) throws Exception {
63-
http.authorizeRequests().anyRequest().authenticated().and().oauth2Login();
63+
http.authorizeRequests().anyRequest().authenticated().and().oauth2Login()
64+
.and().oauth2Client().authorizationCodeGrant();
6465
}
6566

6667
}

0 commit comments

Comments
 (0)