Skip to content

Commit 5ef9c73

Browse files
committed
Remember user consent and make consent page configurable
1 parent a30a169 commit 5ef9c73

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;
@@ -96,6 +98,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
9698
this.tokenRevocationEndpointMatcher.matches(request) ||
9799
this.jwkSetEndpointMatcher.matches(request) ||
98100
this.oidcProviderConfigurationEndpointMatcher.matches(request);
101+
private String consentPage = null;
99102

100103
/**
101104
* Sets the repository of registered clients.
@@ -133,6 +136,43 @@ public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings
133136
return this;
134137
}
135138

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

243288
OAuth2TokenEndpointFilter tokenEndpointFilter =
@@ -310,6 +355,18 @@ private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService get
310355
return authorizationService;
311356
}
312357

358+
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationConsentService getAuthorizationConsentService(B builder) {
359+
OAuth2AuthorizationConsentService authorizationConsentService = builder.getSharedObject(OAuth2AuthorizationConsentService.class);
360+
if (authorizationConsentService == null) {
361+
authorizationConsentService = getOptionalBean(builder, OAuth2AuthorizationConsentService.class);
362+
if (authorizationConsentService == null) {
363+
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
364+
}
365+
builder.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
366+
}
367+
return authorizationConsentService;
368+
}
369+
313370
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
314371
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
315372
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)