Skip to content

Commit ffea88e

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

File tree

18 files changed

+1836
-36
lines changed

18 files changed

+1836
-36
lines changed

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

+70-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;
110113

111114
/**
112115
* Sets the repository of registered clients.
@@ -132,6 +135,18 @@ public OAuth2AuthorizationServerConfigurer<B> authorizationService(OAuth2Authori
132135
return this;
133136
}
134137

138+
/**
139+
* Sets the authorization consent service.
140+
*
141+
* @param authorizationConsentService the authorization service
142+
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
143+
*/
144+
public OAuth2AuthorizationServerConfigurer<B> authorizationConsentService(OAuth2AuthorizationConsentService authorizationConsentService) {
145+
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
146+
this.getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
147+
return this;
148+
}
149+
135150
/**
136151
* Sets the provider settings.
137152
*
@@ -144,6 +159,43 @@ public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings
144159
return this;
145160
}
146161

162+
/**
163+
* Specify the URL to redirect Resource Owners to if consent is required during
164+
* the {@code authorization_code} flow. A default consent page will be generated when
165+
* this attribute is not specified.
166+
*
167+
* If a URL is specified, users are required to process the specified URL to generate
168+
* a consent page. The query string will contain the following parameters:
169+
*
170+
* <ul>
171+
* <li>{@code client_id} the client identifier</li>
172+
* <li>{@code scope} the space separated list of scopes present in the authorization request</li>
173+
* <li>{@code state} a CSRF protection token</li>
174+
* </ul>
175+
*
176+
* In general, the consent page should create a form that submits
177+
* a request with the following requirements:
178+
*
179+
* <ul>
180+
* <li>It must be an HTTP POST</li>
181+
* <li>It must be submitted to {@link ProviderSettings#authorizationEndpoint()}</li>
182+
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
183+
* <li>It must include the received {@code state} as an HTTP parameter</li>
184+
* <li>It must include the list of {@code scope}s the {@code Resource Owners}
185+
* consents to as an HTTP parameter</li>
186+
* <li>It must include the {@code consent_action} parameter, with value either
187+
* {@code approve} or {@code cancel} as an HTTP parameter</li>
188+
* </ul>
189+
*
190+
*
191+
* @param consentPage the consent page to redirect to if consent is required (e.g. "/consent")
192+
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
193+
*/
194+
public OAuth2AuthorizationServerConfigurer<B> consentPage(String consentPage) {
195+
this.consentPage = consentPage;
196+
return this;
197+
}
198+
147199
/**
148200
* Returns a {@link RequestMatcher} for the authorization server endpoints.
149201
*
@@ -263,7 +315,12 @@ public void configure(B builder) {
263315
new OAuth2AuthorizationEndpointFilter(
264316
getRegisteredClientRepository(builder),
265317
getAuthorizationService(builder),
266-
providerSettings.authorizationEndpoint());
318+
getAuthorizationConsentService(builder),
319+
providerSettings.authorizationEndpoint()
320+
);
321+
if (this.consentPage != null) {
322+
authorizationEndpointFilter.setUserConsentUri(this.consentPage);
323+
}
267324
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
268325

269326
OAuth2TokenEndpointFilter tokenEndpointFilter =
@@ -347,6 +404,18 @@ private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService get
347404
return authorizationService;
348405
}
349406

407+
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationConsentService getAuthorizationConsentService(B builder) {
408+
OAuth2AuthorizationConsentService authorizationConsentService = builder.getSharedObject(OAuth2AuthorizationConsentService.class);
409+
if (authorizationConsentService == null) {
410+
authorizationConsentService = getOptionalBean(builder, OAuth2AuthorizationConsentService.class);
411+
if (authorizationConsentService == null) {
412+
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
413+
}
414+
builder.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
415+
}
416+
return authorizationConsentService;
417+
}
418+
350419
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
351420
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
352421
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)