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 .stream .Collectors ;
73
+
71
74
/**
72
75
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
73
76
* which handles the processing of the OAuth 2.0 Authorization Request.
@@ -99,6 +102,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
99
102
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator (Base64 .getUrlEncoder ().withoutPadding (), 96 );
100
103
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator (Base64 .getUrlEncoder ());
101
104
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy ();
105
+ private final UserConsentRepository userConsentRepository = new InMemoryUserConsentRepository ();
102
106
103
107
/**
104
108
* Constructs an {@code OAuth2AuthorizationEndpointFilter} using the provided parameters.
@@ -198,7 +202,17 @@ private void processAuthorizationRequest(HttpServletRequest request, HttpServlet
198
202
.attribute (Principal .class .getName (), principal )
199
203
.attribute (OAuth2AuthorizationRequest .class .getName (), authorizationRequest );
200
204
201
- if (requireUserConsent (registeredClient , authorizationRequest )) {
205
+ final Set <String > alreadyAuthorizedScopes = new HashSet <>(this .userConsentRepository .findBySubjectAndClientId (
206
+ principal .getName (),
207
+ registeredClient .getClientId ()))
208
+ .stream ()
209
+ .map (UserConsentRecord ::getAuthorizedScope )
210
+ .collect (Collectors .toSet ());
211
+ Set <String > scopesRequiringConsent = new HashSet <>(authorizationRequest .getScopes ());
212
+ scopesRequiringConsent .removeAll (alreadyAuthorizedScopes );
213
+ scopesRequiringConsent .remove (OidcScopes .OPENID ); // openid scope does not require consent
214
+
215
+ if (requireUserConsent (registeredClient , authorizationRequest ) && !scopesRequiringConsent .isEmpty ()) {
202
216
String state = this .stateGenerator .generateKey ();
203
217
OAuth2Authorization authorization = builder
204
218
.attribute (OAuth2ParameterNames .STATE , state )
@@ -207,7 +221,8 @@ private void processAuthorizationRequest(HttpServletRequest request, HttpServlet
207
221
208
222
// TODO Need to remove 'in-flight' authorization if consent step is not completed (e.g. approved or cancelled)
209
223
210
- UserConsentPage .displayConsent (request , response , registeredClient , authorization );
224
+ UserConsentPage .displayConsent (request , response , registeredClient , authorization ,
225
+ scopesRequiringConsent , alreadyAuthorizedScopes );
211
226
} else {
212
227
Instant issuedAt = Instant .now ();
213
228
Instant expiresAt = issuedAt .plus (5 , ChronoUnit .MINUTES ); // TODO Allow configuration for authorization code time-to-live
@@ -269,15 +284,30 @@ private void processUserConsent(HttpServletRequest request, HttpServletResponse
269
284
return ;
270
285
}
271
286
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 ();
287
+ Set <String > authorizedScopes = new HashSet <>(userConsentRequestContext .getScopes ());
277
288
if (userConsentRequestContext .getAuthorizationRequest ().getScopes ().contains (OidcScopes .OPENID )) {
278
289
// openid scope is auto-approved as it does not require consent
279
290
authorizedScopes .add (OidcScopes .OPENID );
280
291
}
292
+
293
+ this .userConsentRepository .saveAll (
294
+ userConsentRequestContext .getAuthorization ().getPrincipalName (),
295
+ userConsentRequestContext .getClientId (),
296
+ authorizedScopes );
297
+
298
+ Set <String > deniedScopes = new HashSet <>(userConsentRequestContext .getAuthorizationRequest ().getScopes ());
299
+ deniedScopes .removeAll (authorizedScopes );
300
+ deniedScopes .remove (OidcScopes .OPENID );
301
+
302
+ this .userConsentRepository .revokeAll (
303
+ userConsentRequestContext .getAuthorization ().getPrincipalName (),
304
+ userConsentRequestContext .getClientId (),
305
+ deniedScopes );
306
+
307
+ Instant issuedAt = Instant .now ();
308
+ Instant expiresAt = issuedAt .plus (5 , ChronoUnit .MINUTES ); // TODO Allow configuration for authorization code time-to-live
309
+ OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode (
310
+ this .codeGenerator .generateKey (), issuedAt , expiresAt );
281
311
OAuth2Authorization authorization = OAuth2Authorization .from (userConsentRequestContext .getAuthorization ())
282
312
.token (authorizationCode )
283
313
.attributes (attrs -> {
@@ -654,9 +684,11 @@ private static class UserConsentPage {
654
684
private static final String CONSENT_ACTION_CANCEL = "cancel" ;
655
685
656
686
private static void displayConsent (HttpServletRequest request , HttpServletResponse response ,
657
- RegisteredClient registeredClient , OAuth2Authorization authorization ) throws IOException {
687
+ RegisteredClient registeredClient , OAuth2Authorization authorization ,
688
+ Set <String > scopesRequiringConsent , Set <String > alreadyAuthorizedScopes ) throws IOException {
658
689
659
- String consentPage = generateConsentPage (request , registeredClient , authorization );
690
+ String consentPage = generateConsentPage (request , registeredClient , authorization ,
691
+ scopesRequiringConsent , alreadyAuthorizedScopes );
660
692
response .setContentType (TEXT_HTML_UTF8 .toString ());
661
693
response .setContentLength (consentPage .getBytes (StandardCharsets .UTF_8 ).length );
662
694
response .getWriter ().write (consentPage );
@@ -671,12 +703,9 @@ private static boolean isConsentCancelled(HttpServletRequest request) {
671
703
}
672
704
673
705
private static String generateConsentPage (HttpServletRequest request ,
674
- RegisteredClient registeredClient , OAuth2Authorization authorization ) {
706
+ RegisteredClient registeredClient , OAuth2Authorization authorization ,
707
+ Set <String > scopesRequiringConsent , Set <String > alreadyAuthorizedScopes ) {
675
708
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
709
String state = authorization .getAttribute (
681
710
OAuth2ParameterNames .STATE );
682
711
@@ -711,7 +740,14 @@ private static String generateConsentPage(HttpServletRequest request,
711
740
builder .append (" <input type=\" hidden\" name=\" client_id\" value=\" " + registeredClient .getClientId () + "\" >" );
712
741
builder .append (" <input type=\" hidden\" name=\" state\" value=\" " + state + "\" >" );
713
742
714
- for (String scope : scopes ) {
743
+ for (String scope : scopesRequiringConsent ) {
744
+ builder .append (" <div class=\" form-group form-check py-1\" >" );
745
+ builder .append (" <input class=\" form-check-input\" type=\" checkbox\" name=\" scope\" value=\" " + scope + "\" id=\" " + scope + "\" >" );
746
+ builder .append (" <label class=\" form-check-label\" for=\" " + scope + "\" >" + scope + "</label>" );
747
+ builder .append (" </div>" );
748
+ }
749
+
750
+ for (String scope : alreadyAuthorizedScopes ) {
715
751
builder .append (" <div class=\" form-group form-check py-1\" >" );
716
752
builder .append (" <input class=\" form-check-input\" type=\" checkbox\" name=\" scope\" value=\" " + scope + "\" id=\" " + scope + "\" checked>" );
717
753
builder .append (" <label class=\" form-check-label\" for=\" " + scope + "\" >" + scope + "</label>" );
0 commit comments