Skip to content

Commit 20dbae4

Browse files
committed
Remember user consent and make consent page configurable
Closes #gh-283
1 parent c37ecd7 commit 20dbae4

File tree

18 files changed

+1821
-39
lines changed

18 files changed

+1821
-39
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java

+58-1
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
import org.springframework.security.oauth2.jwt.JwtEncoder;
3737
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
3838
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
39+
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
3940
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
4041
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
4142
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
43+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
4244
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
4345
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
4446
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
@@ -107,6 +109,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
107109
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
108110
this.authorizationServerMetadataEndpointMatcher.matches(request) ||
109111
this.oidcClientRegistrationEndpointMatcher.matches(request);
112+
private String consentPage = null;
110113

111114
/**
112115
* Sets the repository of registered clients.
@@ -144,6 +147,43 @@ public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings
144147
return this;
145148
}
146149

150+
/**
151+
* Specify the URL to redirect {@code Resource Owners} to if consent is required during
152+
* the {@code authorization_code} flow. A default consent page will be generated when
153+
* this attribute is not specified.
154+
*
155+
* If a URL is specified, users are required to process the specified URL to generate
156+
* a consent page. The query string will contain the following parameters:
157+
*
158+
* <ul>
159+
* <li>{@code client_id} the client identifier</li>
160+
* <li>{@code scope} the space separated list of scopes present in the authorization request</li>
161+
* <li>{@code state} a CSRF protection token</li>
162+
* </ul>
163+
*
164+
* In general, the consent page should create a form that submits
165+
* a request with the following requirements:
166+
*
167+
* <ul>
168+
* <li>It must be an HTTP POST</li>
169+
* <li>It must be submitted to {@link ProviderSettings#authorizationEndpoint()}</li>
170+
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
171+
* <li>It must include the received {@code state} as an HTTP parameter</li>
172+
* <li>It must include the list of {@code scope}s the {@code Resource Owners}
173+
* consents to as an HTTP parameter</li>
174+
* <li>It must include the {@code consent_action} parameter, with value either
175+
* {@code approve} or {@code cancel} as an HTTP parameter</li>
176+
* </ul>
177+
*
178+
*
179+
* @param consentPage the consent page to redirect to if consent is required (e.g. "/consent")
180+
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
181+
*/
182+
public OAuth2AuthorizationServerConfigurer<B> consentPage(String consentPage) {
183+
this.consentPage = consentPage;
184+
return this;
185+
}
186+
147187
/**
148188
* Returns a {@link RequestMatcher} for the authorization server endpoints.
149189
*
@@ -263,7 +303,12 @@ public void configure(B builder) {
263303
new OAuth2AuthorizationEndpointFilter(
264304
getRegisteredClientRepository(builder),
265305
getAuthorizationService(builder),
266-
providerSettings.authorizationEndpoint());
306+
getAuthorizationConsentService(builder),
307+
providerSettings.authorizationEndpoint()
308+
);
309+
if (this.consentPage != null) {
310+
authorizationEndpointFilter.setCustomUserConsentUri(this.consentPage);
311+
}
267312
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
268313

269314
OAuth2TokenEndpointFilter tokenEndpointFilter =
@@ -347,6 +392,18 @@ private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService get
347392
return authorizationService;
348393
}
349394

395+
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationConsentService getAuthorizationConsentService(B builder) {
396+
OAuth2AuthorizationConsentService authorizationConsentService = builder.getSharedObject(OAuth2AuthorizationConsentService.class);
397+
if (authorizationConsentService == null) {
398+
authorizationConsentService = getOptionalBean(builder, OAuth2AuthorizationConsentService.class);
399+
if (authorizationConsentService == null) {
400+
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
401+
}
402+
builder.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
403+
}
404+
return authorizationConsentService;
405+
}
406+
350407
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
351408
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
352409
if (jwtEncoder == null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2020-2021 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+
* https://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.security.oauth2.server.authorization;
17+
18+
import org.springframework.lang.Nullable;
19+
import org.springframework.util.Assert;
20+
21+
import java.util.Arrays;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Objects;
26+
import java.util.concurrent.ConcurrentHashMap;
27+
28+
/**
29+
* An {@link OAuth2AuthorizationConsentService} that stores {@link OAuth2AuthorizationConsent}'s in-memory.
30+
*
31+
* <p>
32+
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
33+
*
34+
* @author Daniel Garnier-Moiroux
35+
* @since 0.1.2
36+
* @see OAuth2AuthorizationConsentService
37+
*/
38+
public final class InMemoryOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
39+
private final Map<Integer, OAuth2AuthorizationConsent> authorizationConsents = new ConcurrentHashMap<>();
40+
41+
/**
42+
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService}.
43+
*/
44+
public InMemoryOAuth2AuthorizationConsentService() {
45+
this(Collections.emptyList());
46+
}
47+
48+
/**
49+
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
50+
*
51+
* @param authorizationConsents the authorization consent(s)
52+
*/
53+
public InMemoryOAuth2AuthorizationConsentService(OAuth2AuthorizationConsent... authorizationConsents) {
54+
this(Arrays.asList(authorizationConsents));
55+
}
56+
57+
/**
58+
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
59+
*
60+
* @param authorizationConsents the authorization consent(s)
61+
*/
62+
public InMemoryOAuth2AuthorizationConsentService(List<OAuth2AuthorizationConsent> authorizationConsents) {
63+
Assert.notNull(authorizationConsents, "authorizationConsents cannot be null");
64+
authorizationConsents.forEach(authorizationConsent -> {
65+
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
66+
int id = getId(authorizationConsent);
67+
Assert.isTrue(!this.authorizationConsents.containsKey(id),
68+
"The authorizationConsent must be unique. Found duplicate, with registered client id: ["
69+
+ authorizationConsent.getRegisteredClientId()
70+
+ "] and principal name: [" + authorizationConsent.getPrincipalName() + "]");
71+
this.authorizationConsents.put(id, authorizationConsent);
72+
});
73+
}
74+
75+
@Override
76+
public void save(OAuth2AuthorizationConsent authorizationConsent) {
77+
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
78+
int id = getId(authorizationConsent);
79+
this.authorizationConsents.put(id, authorizationConsent);
80+
}
81+
82+
@Override
83+
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
84+
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
85+
int id = getId(authorizationConsent);
86+
this.authorizationConsents.remove(id, authorizationConsent);
87+
}
88+
89+
@Override
90+
@Nullable
91+
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
92+
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
93+
Assert.hasText(principalName, "principalName cannot be empty");
94+
int id = getId(registeredClientId, principalName);
95+
return this.authorizationConsents.get(id);
96+
}
97+
98+
private static int getId(String registeredClientId, String principalName) {
99+
return Objects.hash(registeredClientId, principalName);
100+
}
101+
102+
private static int getId(OAuth2AuthorizationConsent authorizationConsent) {
103+
return getId(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
104+
}
105+
}

0 commit comments

Comments
 (0)