Skip to content

Commit fe329ea

Browse files
committed
Remember user consent and make consent page configurable
1 parent 2712a7b commit fe329ea

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
@@ -35,9 +35,11 @@
3535
import org.springframework.security.oauth2.jwt.JwtEncoder;
3636
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
3737
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
38+
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
3839
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
3940
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
4041
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
42+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
4143
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
4244
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
4345
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
@@ -100,6 +102,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
100102
this.jwkSetEndpointMatcher.matches(request) ||
101103
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
102104
this.authorizationServerMetadataEndpointMatcher.matches(request);
105+
private String consentPage = null;
103106

104107
/**
105108
* Sets the repository of registered clients.
@@ -137,6 +140,43 @@ public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings
137140
return this;
138141
}
139142

143+
/**
144+
* Specify the URL to redirect {@code Resource Owners} to if consent is required during
145+
* the {@code authorization_code} flow. A default consent page will be generated when
146+
* this attribute is not specified.
147+
*
148+
* If a URL is specified, users are required to process the specified URL to generate
149+
* a consent page. The query string will contain the following parameters:
150+
*
151+
* <ul>
152+
* <li>{@code client_id} the client identifier</li>
153+
* <li>{@code scope} the space separated list of scopes present in the authorization request</li>
154+
* <li>{@code state} a CSRF protection token</li>
155+
* </ul>
156+
*
157+
* In general, the consent page should create a form that submits
158+
* a request with the following requirements:
159+
*
160+
* <ul>
161+
* <li>It must be an HTTP POST</li>
162+
* <li>It must be submitted to {@link ProviderSettings#authorizationEndpoint()}</li>
163+
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
164+
* <li>It must include the received {@code state} as an HTTP parameter</li>
165+
* <li>It must include the list of {@code scope}s the {@code Resource Owners}
166+
* consents to as an HTTP parameter</li>
167+
* <li>It must include the {@code consent_action} parameter, with value either
168+
* {@code approve} or {@code cancel} as an HTTP parameter</li>
169+
* </ul>
170+
*
171+
*
172+
* @param consentPage the consent page to redirect to if consent is required (e.g. "/consent")
173+
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
174+
*/
175+
public OAuth2AuthorizationServerConfigurer<B> consentPage(String consentPage) {
176+
this.consentPage = consentPage;
177+
return this;
178+
}
179+
140180
/**
141181
* Returns a {@link RequestMatcher} for the authorization server endpoints.
142182
*
@@ -245,7 +285,12 @@ public void configure(B builder) {
245285
new OAuth2AuthorizationEndpointFilter(
246286
getRegisteredClientRepository(builder),
247287
getAuthorizationService(builder),
248-
providerSettings.authorizationEndpoint());
288+
getAuthorizationConsentService(builder),
289+
providerSettings.authorizationEndpoint()
290+
);
291+
if (this.consentPage != null) {
292+
authorizationEndpointFilter.setCustomUserConsentUri(this.consentPage);
293+
}
249294
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
250295

251296
OAuth2TokenEndpointFilter tokenEndpointFilter =
@@ -320,6 +365,18 @@ private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService get
320365
return authorizationService;
321366
}
322367

368+
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationConsentService getAuthorizationConsentService(B builder) {
369+
OAuth2AuthorizationConsentService authorizationConsentService = builder.getSharedObject(OAuth2AuthorizationConsentService.class);
370+
if (authorizationConsentService == null) {
371+
authorizationConsentService = getOptionalBean(builder, OAuth2AuthorizationConsentService.class);
372+
if (authorizationConsentService == null) {
373+
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
374+
}
375+
builder.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
376+
}
377+
return authorizationConsentService;
378+
}
379+
323380
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
324381
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
325382
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.1
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.1
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)