Skip to content
This repository was archived by the owner on Jan 27, 2023. It is now read-only.

Commit f4929aa

Browse files
committed
Remember user consent and make consent page configurable
Closes #spring-projectsgh-283
1 parent c37ecd7 commit f4929aa

File tree

18 files changed

+1796
-39
lines changed

18 files changed

+1796
-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,110 @@
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.NonNull;
19+
import org.springframework.lang.Nullable;
20+
import org.springframework.security.oauth2.core.Version;
21+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
22+
import org.springframework.util.Assert;
23+
24+
import java.io.Serializable;
25+
import java.util.Map;
26+
import java.util.Objects;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
29+
/**
30+
* An {@link OAuth2AuthorizationConsentService} that stores {@link OAuth2AuthorizationConsent}'s in-memory.
31+
*
32+
* <p>
33+
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
34+
*
35+
* @author Daniel Garnier-Moiroux
36+
* @since 0.1.2
37+
* @see OAuth2AuthorizationConsent
38+
*/
39+
public class InMemoryOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
40+
private final Map<Key, OAuth2AuthorizationConsent> consents = new ConcurrentHashMap<>();
41+
42+
@Override
43+
public void save(@NonNull OAuth2AuthorizationConsent consent) {
44+
Key key = Key.from(consent);
45+
this.consents.put(key, consent);
46+
}
47+
48+
@Override
49+
public void remove(@NonNull OAuth2AuthorizationConsent consent) {
50+
Key key = Key.from(consent);
51+
this.consents.remove(key, consent);
52+
}
53+
54+
@Override
55+
@Nullable
56+
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
57+
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
58+
Assert.hasText(principalName, "principalName cannot be empty");
59+
Key key = new Key(registeredClientId, principalName);
60+
return this.consents.get(key);
61+
}
62+
63+
/**
64+
* Identifier for {@link OAuth2AuthorizationConsent}.
65+
*
66+
* @author Daniel Garnier-Moiroux
67+
* @see OAuth2AuthorizationConsent
68+
* @since 0.1.2
69+
*/
70+
private final static class Key implements Serializable {
71+
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
72+
73+
private final String registeredClientId;
74+
private final String principalName;
75+
76+
/**
77+
* Constructs a {@code Key} using the provided parameters.
78+
*
79+
* @param registeredClientId the identifier for the {@link RegisteredClient registeredClient}
80+
* @param principalName the name of the {@code Principal}, Resource Owner or Client
81+
*/
82+
private Key(@NonNull String registeredClientId, @NonNull String principalName) {
83+
this.registeredClientId = registeredClientId;
84+
this.principalName = principalName;
85+
}
86+
87+
/**
88+
* Constructs a {@code Key} with the values from the provided {@link OAuth2AuthorizationConsent}.
89+
*
90+
* @param consent the {@link OAuth2AuthorizationConsent}
91+
*/
92+
private static Key from(OAuth2AuthorizationConsent consent) {
93+
Assert.notNull(consent, "consent cannot be null");
94+
return new Key(consent.getRegisteredClientId(), consent.getPrincipalName());
95+
}
96+
97+
@Override
98+
public boolean equals(Object o) {
99+
if (this == o) return true;
100+
if (o == null || getClass() != o.getClass()) return false;
101+
Key key = (Key) o;
102+
return this.registeredClientId.equals(key.registeredClientId) && this.principalName.equals(key.principalName);
103+
}
104+
105+
@Override
106+
public int hashCode() {
107+
return Objects.hash(this.registeredClientId, this.principalName);
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)