Skip to content

Commit 85ec40d

Browse files
Steve Riesenbergjgrandja
Steve Riesenberg
authored andcommitted
Customize OAuth2AuthorizationConsent prior to saving
Closes spring-projectsgh-436
1 parent ae8cbb8 commit 85ec40d

File tree

5 files changed

+569
-12
lines changed

5 files changed

+569
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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 java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.function.Consumer;
21+
22+
import org.springframework.lang.Nullable;
23+
import org.springframework.security.core.Authentication;
24+
import org.springframework.security.oauth2.core.context.Context;
25+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
26+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
27+
import org.springframework.util.Assert;
28+
import org.springframework.util.CollectionUtils;
29+
30+
/**
31+
* A context that holds an {@link OAuth2AuthorizationConsent.Builder} and (optionally) additional information
32+
* and is used when customizing the building of {@link OAuth2AuthorizationConsent}.
33+
*
34+
* @author Steve Riesenberg
35+
* @since 0.2.1
36+
* @see Context
37+
*/
38+
public final class OAuth2AuthorizationConsentContext implements Context {
39+
private final Map<Object, Object> context;
40+
41+
/**
42+
* Constructs an {@code OAuth2AuthorizationConsentContext} using the provided parameters.
43+
*
44+
* @param context a {@code Map} of additional context information
45+
*/
46+
private OAuth2AuthorizationConsentContext(@Nullable Map<Object, Object> context) {
47+
this.context = new HashMap<>();
48+
if (!CollectionUtils.isEmpty(context)) {
49+
this.context.putAll(context);
50+
}
51+
}
52+
53+
/**
54+
* Returns the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
55+
*
56+
* @return the {@link OAuth2AuthorizationConsent.Builder}
57+
*/
58+
public OAuth2AuthorizationConsent.Builder getAuthorizationConsentBuilder() {
59+
return get(OAuth2AuthorizationConsent.Builder.class);
60+
}
61+
62+
/**
63+
* Returns the {@link Authentication} representing the {@code Principal} resource owner (or client).
64+
*
65+
* @param <T> the type of the {@code Authentication}
66+
* @return the {@link Authentication} representing the {@code Principal} resource owner (or client)
67+
*/
68+
@Nullable
69+
public <T extends Authentication> T getPrincipal() {
70+
return get(Builder.PRINCIPAL_AUTHENTICATION_KEY);
71+
}
72+
73+
/**
74+
* Returns the {@link RegisteredClient registered client}.
75+
*
76+
* @return the {@link RegisteredClient}, or {@code null} if not available
77+
*/
78+
@Nullable
79+
public RegisteredClient getRegisteredClient() {
80+
return get(RegisteredClient.class);
81+
}
82+
83+
/**
84+
* Returns the {@link OAuth2Authorization authorization}.
85+
*
86+
* @return the {@link OAuth2Authorization}, or {@code null} if not available
87+
*/
88+
@Nullable
89+
public OAuth2Authorization getAuthorization() {
90+
return get(OAuth2Authorization.class);
91+
}
92+
93+
/**
94+
* Returns the {@link OAuth2AuthorizationRequest authorization request}.
95+
*
96+
* @return the {@link OAuth2AuthorizationRequest}, or {@code null} if not available
97+
*/
98+
@Nullable
99+
public OAuth2AuthorizationRequest getAuthorizationRequest() {
100+
return get(OAuth2AuthorizationRequest.class);
101+
}
102+
103+
@SuppressWarnings("unchecked")
104+
@Override
105+
public <V> V get(Object key) {
106+
return (V) this.context.get(key);
107+
}
108+
109+
@Override
110+
public boolean hasKey(Object key) {
111+
return this.context.containsKey(key);
112+
}
113+
114+
/**
115+
* Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationConsent.Builder}.
116+
*
117+
* @param authorizationConsentBuilder the {@link OAuth2AuthorizationConsent.Builder} to initialize the builder
118+
* @return the {@link Builder}
119+
*/
120+
public static OAuth2AuthorizationConsentContext.Builder with(OAuth2AuthorizationConsent.Builder authorizationConsentBuilder) {
121+
return new Builder(authorizationConsentBuilder);
122+
}
123+
124+
/**
125+
* A builder for {@link OAuth2AuthorizationConsentContext}.
126+
*/
127+
public static final class Builder {
128+
private static final String PRINCIPAL_AUTHENTICATION_KEY =
129+
Authentication.class.getName().concat(".PRINCIPAL");
130+
private final Map<Object, Object> context = new HashMap<>();
131+
132+
private Builder(OAuth2AuthorizationConsent.Builder authorizationConsentBuilder) {
133+
Assert.notNull(authorizationConsentBuilder, "authorizationConsentBuilder cannot be null");
134+
put(OAuth2AuthorizationConsent.Builder.class, authorizationConsentBuilder);
135+
}
136+
137+
/**
138+
* Sets the {@link Authentication} representing the {@code Principal} resource owner (or client).
139+
*
140+
* @param principal the {@link Authentication} representing the {@code Principal} resource owner (or client)
141+
* @return the {@link Builder} for further configuration
142+
*/
143+
public Builder principal(Authentication principal) {
144+
return put(PRINCIPAL_AUTHENTICATION_KEY, principal);
145+
}
146+
147+
/**
148+
* Sets the {@link RegisteredClient registered client}.
149+
*
150+
* @param registeredClient the {@link RegisteredClient}
151+
* @return the {@link Builder} for further configuration
152+
*/
153+
public Builder registeredClient(RegisteredClient registeredClient) {
154+
return put(RegisteredClient.class, registeredClient);
155+
}
156+
157+
/**
158+
* Sets the {@link OAuth2Authorization authorization}.
159+
*
160+
* @param authorization the {@link OAuth2Authorization}
161+
* @return the {@link Builder} for further configuration
162+
*/
163+
public Builder authorization(OAuth2Authorization authorization) {
164+
return put(OAuth2Authorization.class, authorization);
165+
}
166+
167+
/**
168+
* Sets the {@link OAuth2AuthorizationRequest authorization request}.
169+
*
170+
* @param authorizationRequest the {@link OAuth2AuthorizationRequest}
171+
* @return the {@link Builder} for further configuration
172+
*/
173+
public Builder authorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {
174+
return put(OAuth2AuthorizationRequest.class, authorizationRequest);
175+
}
176+
177+
/**
178+
* Associates an attribute.
179+
*
180+
* @param key the key for the attribute
181+
* @param value the value of the attribute
182+
* @return the {@link OAuth2TokenContext.AbstractBuilder} for further configuration
183+
*/
184+
public Builder put(Object key, Object value) {
185+
Assert.notNull(key, "key cannot be null");
186+
Assert.notNull(value, "value cannot be null");
187+
this.context.put(key, value);
188+
return this;
189+
}
190+
191+
/**
192+
* A {@code Consumer} of the attributes {@code Map}
193+
* allowing the ability to add, replace, or remove.
194+
*
195+
* @param contextConsumer a {@link Consumer} of the attributes {@code Map}
196+
* @return the {@link Builder} for further configuration
197+
*/
198+
public Builder context(Consumer<Map<Object, Object>> contextConsumer) {
199+
contextConsumer.accept(this.context);
200+
return this;
201+
}
202+
203+
/**
204+
* Builds a new {@link OAuth2AuthorizationConsentContext}.
205+
*
206+
* @return the {@link OAuth2AuthorizationConsentContext}
207+
*/
208+
public OAuth2AuthorizationConsentContext build() {
209+
return new OAuth2AuthorizationConsentContext(this.context);
210+
}
211+
}
212+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import org.springframework.security.authentication.AnonymousAuthenticationToken;
3131
import org.springframework.security.authentication.AuthenticationProvider;
32+
import org.springframework.security.config.Customizer;
3233
import org.springframework.security.core.Authentication;
3334
import org.springframework.security.core.AuthenticationException;
3435
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
@@ -46,6 +47,7 @@
4647
import org.springframework.security.oauth2.core.oidc.OidcScopes;
4748
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
4849
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
50+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentContext;
4951
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
5052
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
5153
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -82,6 +84,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
8284
private final OAuth2AuthorizationConsentService authorizationConsentService;
8385
private Supplier<String> authorizationCodeGenerator = DEFAULT_AUTHORIZATION_CODE_GENERATOR::generateKey;
8486
private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
87+
private Customizer<OAuth2AuthorizationConsentContext> authorizationConsentCustomizer;
8588

8689
/**
8790
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
@@ -145,6 +148,30 @@ public void setAuthenticationValidatorResolver(Function<String, OAuth2Authentica
145148
this.authenticationValidatorResolver = authenticationValidatorResolver;
146149
}
147150

151+
/**
152+
* Sets the {@link Customizer} providing access to the {@link OAuth2AuthorizationConsentContext} containing an
153+
* {@link OAuth2AuthorizationConsent.Builder}.
154+
*
155+
* <p>
156+
* The following context attributes are available:
157+
* <ul>
158+
* <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
159+
* prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}</li>
160+
* <li>The {@link Authentication authentication principal} of type
161+
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken}</li>
162+
* <li>The {@link OAuth2Authorization} associated with the state token presented in the
163+
* authorization consent request.</li>
164+
* <li>The {@link OAuth2AuthorizationRequest} requiring the resource owner's consent.</li>
165+
* </ul>
166+
*
167+
* @param authorizationConsentCustomizer the {@link Customizer} providing access to the
168+
* {@link OAuth2AuthorizationConsentContext} containing an {@link OAuth2AuthorizationConsent.Builder}
169+
*/
170+
public void setAuthorizationConsentCustomizer(Customizer<OAuth2AuthorizationConsentContext> authorizationConsentCustomizer) {
171+
Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
172+
this.authorizationConsentCustomizer = authorizationConsentCustomizer;
173+
}
174+
148175
private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
149176
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
150177
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
@@ -301,7 +328,8 @@ private Authentication authenticateAuthorizationConsent(Authentication authentic
301328
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
302329
currentAuthorizationConsent.getScopes() : Collections.emptySet();
303330

304-
if (authorizedScopes.isEmpty() && currentAuthorizedScopes.isEmpty()) {
331+
if (authorizedScopes.isEmpty() && currentAuthorizedScopes.isEmpty()
332+
&& authorizationCodeRequestAuthentication.getAdditionalParameters().isEmpty()) {
305333
// Authorization consent denied
306334
this.authorizationService.remove(authorization);
307335
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
@@ -321,16 +349,30 @@ private Authentication authenticateAuthorizationConsent(Authentication authentic
321349
}
322350
}
323351

324-
if (!authorizedScopes.isEmpty() && !authorizedScopes.equals(currentAuthorizedScopes)) {
325-
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
326-
if (currentAuthorizationConsent != null) {
327-
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
328-
} else {
329-
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
330-
authorization.getRegisteredClientId(), authorization.getPrincipalName());
331-
}
332-
authorizedScopes.forEach(authorizationConsentBuilder::scope);
333-
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
352+
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
353+
if (currentAuthorizationConsent != null) {
354+
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
355+
} else {
356+
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
357+
authorization.getRegisteredClientId(), authorization.getPrincipalName());
358+
}
359+
authorizedScopes.forEach(authorizationConsentBuilder::scope);
360+
361+
if (this.authorizationConsentCustomizer != null) {
362+
// @formatter:off
363+
OAuth2AuthorizationConsentContext authorizationConsentContext =
364+
OAuth2AuthorizationConsentContext.with(authorizationConsentBuilder)
365+
.principal(authorizationCodeRequestAuthentication)
366+
.registeredClient(registeredClient)
367+
.authorization(authorization)
368+
.authorizationRequest(authorizationRequest)
369+
.build();
370+
// @formatter:on
371+
this.authorizationConsentCustomizer.customize(authorizationConsentContext);
372+
}
373+
374+
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
375+
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
334376
this.authorizationConsentService.save(authorizationConsent);
335377
}
336378

0 commit comments

Comments
 (0)