15
15
*/
16
16
package org .springframework .security .oauth2 .server .authorization .web ;
17
17
18
- import java .io .IOException ;
19
- import java .nio .charset .StandardCharsets ;
20
- import java .security .Principal ;
21
- import java .time .Instant ;
22
- import java .time .temporal .ChronoUnit ;
23
- import java .util .Arrays ;
24
- import java .util .Base64 ;
25
- import java .util .Collections ;
26
- import java .util .HashSet ;
27
- import java .util .List ;
28
- import java .util .Set ;
29
-
30
- import javax .servlet .FilterChain ;
31
- import javax .servlet .ServletException ;
32
- import javax .servlet .http .HttpServletRequest ;
33
- import javax .servlet .http .HttpServletResponse ;
34
-
35
18
import org .springframework .http .HttpMethod ;
36
19
import org .springframework .http .HttpStatus ;
37
20
import org .springframework .http .MediaType ;
50
33
import org .springframework .security .oauth2 .core .endpoint .PkceParameterNames ;
51
34
import org .springframework .security .oauth2 .core .oidc .OidcScopes ;
52
35
import org .springframework .security .oauth2 .server .authorization .OAuth2Authorization ;
36
+ import org .springframework .security .oauth2 .server .authorization .OAuth2AuthorizationCode ;
53
37
import org .springframework .security .oauth2 .server .authorization .OAuth2AuthorizationService ;
54
38
import org .springframework .security .oauth2 .server .authorization .client .RegisteredClient ;
55
39
import org .springframework .security .oauth2 .server .authorization .client .RegisteredClientRepository ;
56
- import org .springframework .security .oauth2 .server .authorization .OAuth2AuthorizationCode ;
40
+ import org .springframework .security .oauth2 .server .authorization .consent .InMemoryUserConsentRepository ;
41
+ import org .springframework .security .oauth2 .server .authorization .consent .UserConsentRecord ;
42
+ import org .springframework .security .oauth2 .server .authorization .consent .UserConsentRepository ;
57
43
import org .springframework .security .web .DefaultRedirectStrategy ;
58
44
import org .springframework .security .web .RedirectStrategy ;
59
45
import org .springframework .security .web .util .matcher .AndRequestMatcher ;
68
54
import org .springframework .web .filter .OncePerRequestFilter ;
69
55
import org .springframework .web .util .UriComponentsBuilder ;
70
56
57
+ import javax .servlet .FilterChain ;
58
+ import javax .servlet .ServletException ;
59
+ import javax .servlet .http .HttpServletRequest ;
60
+ import javax .servlet .http .HttpServletResponse ;
61
+ import java .io .IOException ;
62
+ import java .nio .charset .StandardCharsets ;
63
+ import java .security .Principal ;
64
+ import java .time .Instant ;
65
+ import java .time .temporal .ChronoUnit ;
66
+ import java .util .Arrays ;
67
+ import java .util .Base64 ;
68
+ import java .util .Collections ;
69
+ import java .util .HashSet ;
70
+ import java .util .List ;
71
+ import java .util .Set ;
72
+ import java .util .function .Function ;
73
+ import java .util .stream .Collectors ;
74
+
71
75
/**
72
76
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
73
77
* which handles the processing of the OAuth 2.0 Authorization Request.
@@ -99,6 +103,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
99
103
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator (Base64 .getUrlEncoder ().withoutPadding (), 96 );
100
104
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator (Base64 .getUrlEncoder ());
101
105
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy ();
106
+ private final UserConsentRepository userConsentRepository = new InMemoryUserConsentRepository ();
102
107
103
108
/**
104
109
* Constructs an {@code OAuth2AuthorizationEndpointFilter} using the provided parameters.
@@ -198,7 +203,17 @@ private void processAuthorizationRequest(HttpServletRequest request, HttpServlet
198
203
.attribute (Principal .class .getName (), principal )
199
204
.attribute (OAuth2AuthorizationRequest .class .getName (), authorizationRequest );
200
205
201
- if (requireUserConsent (registeredClient , authorizationRequest )) {
206
+ final Set <String > alreadyAuthorizedScopes = new HashSet <>(this .userConsentRepository .findBySubjectAndClientId (
207
+ principal .getName (),
208
+ registeredClient .getClientId ()))
209
+ .stream ()
210
+ .map (UserConsentRecord ::getAuthorizedScope )
211
+ .collect (Collectors .toSet ());
212
+ Set <String > scopesRequiringConsent = new HashSet <>(authorizationRequest .getScopes ());
213
+ scopesRequiringConsent .removeAll (alreadyAuthorizedScopes );
214
+ scopesRequiringConsent .remove (OidcScopes .OPENID ); // openid scope does not require consent
215
+
216
+ if (requireUserConsent (registeredClient , authorizationRequest ) && !scopesRequiringConsent .isEmpty ()) {
202
217
String state = this .stateGenerator .generateKey ();
203
218
OAuth2Authorization authorization = builder
204
219
.attribute (OAuth2ParameterNames .STATE , state )
@@ -207,7 +222,8 @@ private void processAuthorizationRequest(HttpServletRequest request, HttpServlet
207
222
208
223
// TODO Need to remove 'in-flight' authorization if consent step is not completed (e.g. approved or cancelled)
209
224
210
- UserConsentPage .displayConsent (request , response , registeredClient , authorization );
225
+ UserConsentPage .displayConsent (request , response , registeredClient , authorization ,
226
+ scopesRequiringConsent , alreadyAuthorizedScopes );
211
227
} else {
212
228
Instant issuedAt = Instant .now ();
213
229
Instant expiresAt = issuedAt .plus (5 , ChronoUnit .MINUTES ); // TODO Allow configuration for authorization code time-to-live
@@ -269,15 +285,30 @@ private void processUserConsent(HttpServletRequest request, HttpServletResponse
269
285
return ;
270
286
}
271
287
272
- Instant issuedAt = Instant .now ();
273
- Instant expiresAt = issuedAt .plus (5 , ChronoUnit .MINUTES ); // TODO Allow configuration for authorization code time-to-live
274
- OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode (
275
- this .codeGenerator .generateKey (), issuedAt , expiresAt );
276
- Set <String > authorizedScopes = userConsentRequestContext .getScopes ();
288
+ Set <String > authorizedScopes = new HashSet <>(userConsentRequestContext .getScopes ());
277
289
if (userConsentRequestContext .getAuthorizationRequest ().getScopes ().contains (OidcScopes .OPENID )) {
278
290
// openid scope is auto-approved as it does not require consent
279
291
authorizedScopes .add (OidcScopes .OPENID );
280
292
}
293
+
294
+ this .userConsentRepository .saveAll (
295
+ userConsentRequestContext .getAuthorization ().getPrincipalName (),
296
+ userConsentRequestContext .getClientId (),
297
+ authorizedScopes );
298
+
299
+ Set <String > deniedScopes = new HashSet <>(userConsentRequestContext .getAuthorizationRequest ().getScopes ());
300
+ deniedScopes .removeAll (authorizedScopes );
301
+ deniedScopes .remove (OidcScopes .OPENID );
302
+
303
+ this .userConsentRepository .revokeAll (
304
+ userConsentRequestContext .getAuthorization ().getPrincipalName (),
305
+ userConsentRequestContext .getClientId (),
306
+ deniedScopes );
307
+
308
+ Instant issuedAt = Instant .now ();
309
+ Instant expiresAt = issuedAt .plus (5 , ChronoUnit .MINUTES ); // TODO Allow configuration for authorization code time-to-live
310
+ OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode (
311
+ this .codeGenerator .generateKey (), issuedAt , expiresAt );
281
312
OAuth2Authorization authorization = OAuth2Authorization .from (userConsentRequestContext .getAuthorization ())
282
313
.token (authorizationCode )
283
314
.attributes (attrs -> {
@@ -654,9 +685,11 @@ private static class UserConsentPage {
654
685
private static final String CONSENT_ACTION_CANCEL = "cancel" ;
655
686
656
687
private static void displayConsent (HttpServletRequest request , HttpServletResponse response ,
657
- RegisteredClient registeredClient , OAuth2Authorization authorization ) throws IOException {
688
+ RegisteredClient registeredClient , OAuth2Authorization authorization ,
689
+ Set <String > scopesRequiringConsent , Set <String > alreadyAuthorizedScopes ) throws IOException {
658
690
659
- String consentPage = generateConsentPage (request , registeredClient , authorization );
691
+ String consentPage = generateConsentPage (request , registeredClient , authorization ,
692
+ scopesRequiringConsent , alreadyAuthorizedScopes );
660
693
response .setContentType (TEXT_HTML_UTF8 .toString ());
661
694
response .setContentLength (consentPage .getBytes (StandardCharsets .UTF_8 ).length );
662
695
response .getWriter ().write (consentPage );
@@ -671,12 +704,9 @@ private static boolean isConsentCancelled(HttpServletRequest request) {
671
704
}
672
705
673
706
private static String generateConsentPage (HttpServletRequest request ,
674
- RegisteredClient registeredClient , OAuth2Authorization authorization ) {
707
+ RegisteredClient registeredClient , OAuth2Authorization authorization ,
708
+ Set <String > scopesRequiringConsent , Set <String > alreadyAuthorizedScopes ) {
675
709
676
- OAuth2AuthorizationRequest authorizationRequest = authorization .getAttribute (
677
- OAuth2AuthorizationRequest .class .getName ());
678
- Set <String > scopes = new HashSet <>(authorizationRequest .getScopes ());
679
- scopes .remove (OidcScopes .OPENID ); // openid scope does not require consent
680
710
String state = authorization .getAttribute (
681
711
OAuth2ParameterNames .STATE );
682
712
@@ -711,7 +741,14 @@ private static String generateConsentPage(HttpServletRequest request,
711
741
builder .append (" <input type=\" hidden\" name=\" client_id\" value=\" " + registeredClient .getClientId () + "\" >" );
712
742
builder .append (" <input type=\" hidden\" name=\" state\" value=\" " + state + "\" >" );
713
743
714
- for (String scope : scopes ) {
744
+ for (String scope : scopesRequiringConsent ) {
745
+ builder .append (" <div class=\" form-group form-check py-1\" >" );
746
+ builder .append (" <input class=\" form-check-input\" type=\" checkbox\" name=\" scope\" value=\" " + scope + "\" id=\" " + scope + "\" >" );
747
+ builder .append (" <label class=\" form-check-label\" for=\" " + scope + "\" >" + scope + "</label>" );
748
+ builder .append (" </div>" );
749
+ }
750
+
751
+ for (String scope : alreadyAuthorizedScopes ) {
715
752
builder .append (" <div class=\" form-group form-check py-1\" >" );
716
753
builder .append (" <input class=\" form-check-input\" type=\" checkbox\" name=\" scope\" value=\" " + scope + "\" id=\" " + scope + "\" checked>" );
717
754
builder .append (" <label class=\" form-check-label\" for=\" " + scope + "\" >" + scope + "</label>" );
0 commit comments