Skip to content

Commit 8b2b5c5

Browse files
feature : authorization consent module & fix : normalize scopes
1 parent 0adabed commit 8b2b5c5

File tree

14 files changed

+126
-53
lines changed

14 files changed

+126
-53
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ public class CommonDataSourceConfiguration {
228228
* Refer to ``client/src/docs/asciidoc/api-app.adoc``
229229

230230
## OAuth2 - Authorization Code
231-
- Beta
232231
- How to set it up
233232
1. Create your own login page with the /login route as indicated in the client project (In the future, this address will be customisable):
234233
```java

client/src/main/java/com/patternhelloworld/securityhelper/oauth2/client/config/securityimpl/response/CustomWebAuthenticationFailureHandlerImpl.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import java.io.IOException;
1919
import java.util.ArrayList;
20+
import java.util.HashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123

2224
@Primary
2325
@Qualifier("webAuthenticationFailureHandler")
@@ -47,6 +49,28 @@ public void onAuthenticationFailure(HttpServletRequest request, HttpServletRespo
4749
request.getRequestDispatcher("/login").forward(request, response);
4850
return;
4951
}
52+
if(oauth2Exception.getError().getErrorCode().equals(ErrorCodeConstants.REDIRECT_TO_CONSENT)){
53+
// Construct full URL
54+
String fullURL = request.getRequestURL().toString();
55+
if (request.getQueryString() != null) {
56+
fullURL += "?" + request.getQueryString();
57+
}
58+
Map<String, String> consentAttributes = new HashMap<>();
59+
consentAttributes.put("clientId", request.getParameter("client_id"));
60+
consentAttributes.put("redirectUri", request.getParameter("redirect_uri"));
61+
consentAttributes.put("code", request.getParameter("code"));
62+
consentAttributes.put("state", request.getParameter("state"));
63+
consentAttributes.put("scope", request.getParameter("scope"));
64+
if(request.getParameter("code_challenge") == null || request.getParameter("code_challenge_method") == null) {
65+
consentAttributes.put("codeChallenge", request.getParameter("code_challenge"));
66+
consentAttributes.put("codeChallengeMethod", request.getParameter("code_challenge_method"));
67+
}
68+
consentAttributes.put("consentRequestURI", fullURL);
69+
70+
request.setAttribute("consentAttributes", consentAttributes);
71+
request.getRequestDispatcher("/consent").forward(request, response);
72+
return;
73+
}
5074
}
5175
request.setAttribute("errorMessage", errorMessage);
5276
request.setAttribute("errorDetails", errorDetails);

client/src/main/java/com/patternhelloworld/securityhelper/oauth2/client/domain/common/web/LoginWeb.java renamed to client/src/main/java/com/patternhelloworld/securityhelper/oauth2/client/domain/common/web/AuthorizationCodeRequestWeb.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import org.springframework.web.bind.annotation.GetMapping;
77

88
@Controller
9-
public class LoginWeb {
9+
public class AuthorizationCodeRequestWeb {
1010
@GetMapping("/login")
1111
public String loginPage(HttpServletRequest request, Model model) {
1212
Object errorMessages = request.getAttribute("errorMessages");
@@ -15,4 +15,13 @@ public String loginPage(HttpServletRequest request, Model model) {
1515
}
1616
return "login";
1717
}
18+
19+
@GetMapping("/consent")
20+
public String consentPage(HttpServletRequest request, Model model) {
21+
Object errorMessages = request.getAttribute("errorMessages");
22+
if (errorMessages != null) {
23+
model.addAttribute("errorMessages", errorMessages);
24+
}
25+
return "consent";
26+
}
1827
}

client/src/main/resources/application.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,5 @@ patternhelloworld.securityhelper.oauth2.introspection.client-secret=12345
9696

9797
patternhelloworld.securityhelper.jwt.secret=5pAq6zRyX8bC3dV2wS7gN1mK9jF0hL4tUoP6iBvE3nG8xZaQrY7cW2fA
9898
patternhelloworld.securityhelper.jwt.algorithm=HmacSHA256
99+
100+
patternhelloworld.securityhelper.authorization-code.consent=Y

client/src/main/resources/templates/consent.html

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
<script>
99
function cancelConsent() {
10-
document.consent_form.reset();
11-
document.consent_form.submit();
10+
alert("Create your own Cancel logic.")
1211
}
1312
</script>
1413
</head>
@@ -21,17 +20,17 @@ <h1 class="text-center text-primary">App permissions</h1>
2120
<div class="col text-center">
2221
<p>
2322
The application
24-
<span class="fw-bold text-primary" th:text="${clientId}"></span>
23+
<span class="fw-bold text-primary" th:if="${clientId}" th:text="${clientId}">[client_id]</span>
2524
wants to access your account
26-
<span class="fw-bold" th:text="${principalName}"></span>
25+
<span class="fw-bold" th:if="${principalName}" th:text="${principalName}">[principal_name]</span>
2726
</p>
2827
</div>
2928
</div>
3029
<div th:if="${userCode}" class="row">
3130
<div class="col text-center">
3231
<p class="alert alert-warning">
3332
You have provided the code
34-
<span class="fw-bold" th:text="${userCode}"></span>.
33+
<span class="fw-bold" th:text="${userCode}">[user_code]</span>.
3534
Verify that this code matches what is shown on your device.
3635
</p>
3736
</div>
@@ -46,35 +45,15 @@ <h1 class="text-center text-primary">App permissions</h1>
4645
</div>
4746
<div class="row">
4847
<div class="col text-center">
49-
<form name="consent_form" method="post" th:action="${consentRequestURI}">
50-
<input type="hidden" name="client_id" th:value="${clientId}">
51-
<input type="hidden" name="redirect_uri" th:value="${redirectUri}">
52-
<input type="hidden" name="code" th:value="${code}">
53-
54-
<div th:each="scope: ${scopes}" class="form-check py-1">
55-
<input class="form-check-input"
56-
style="float: none"
57-
type="checkbox"
58-
name="scope"
59-
th:value="${scope.scope}"
60-
th:id="${scope.scope}">
61-
<label class="form-check-label fw-bold px-2" th:for="${scope.scope}" th:text="${scope.scope}"></label>
62-
<p class="text-primary" th:text="${scope.description}"></p>
63-
</div>
64-
65-
<p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">
66-
You have already granted the following permissions to the above app:
67-
</p>
68-
<div th:each="scope: ${previouslyApprovedScopes}" class="form-check py-1">
69-
<input class="form-check-input"
70-
style="float: none"
71-
type="checkbox"
72-
th:id="${scope.scope}"
73-
disabled
74-
checked>
75-
<label class="form-check-label fw-bold px-2" th:for="${scope.scope}" th:text="${scope.scope}"></label>
76-
<p class="text-primary" th:text="${scope.description}"></p>
77-
</div>
48+
<form name="consent_form" method="post" action="/oauth2/authorize">
49+
<input type="hidden" name="client_id" th:value="${consentAttributes.clientId}">
50+
<input type="hidden" name="redirect_uri" th:value="${consentAttributes.redirectUri}">
51+
<input type="hidden" name="code" th:value="${consentAttributes.code}">
52+
<input type="hidden" name="state" th:value="${consentAttributes.state}">
53+
<input type="hidden" name="scope" th:value="${consentAttributes.scope}">
54+
<input type="hidden" name="consentRequestURI" th:value="${consentAttributes.consentRequestURI}">
55+
<input type="hidden" name="code_challenge" th:if="${consentAttributes.codeChallenge}" th:value="${consentAttributes.codeChallenge}">
56+
<input type="hidden" name="code_challenge_method" th:if="${consentAttributes.codeChallengeMethod}" th:value="${consentAttributes.codeChallengeMethod}">
7857

7958
<div class="pt-3">
8059
<button class="btn btn-primary btn-lg" type="submit" id="submit-consent">

client/src/main/resources/templates/login.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
8383
'username': username,
8484
'password': password,
8585
'grant_type': "password",
86-
'response_type': "code"
86+
'response_type': "code",
87+
'scope': scope
8788
};
8889

8990
if (code_challenge && code_challenge_method) {

lib/src/main/java/io/github/patternhelloworld/securityhelper/oauth2/api/config/security/converter/auth/endpoint/AuthorizationCodeAuthorizationRequestConverter.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.patternhelloworld.securityhelper.oauth2.api.config.security.converter.auth.endpoint;
22

33
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.dao.EasyPlusAuthorizationConsentRepository;
4+
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.entity.EasyPlusAuthorizationConsent;
45
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.message.DefaultSecurityUserExceptionMessage;
56
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.message.ISecurityUserExceptionMessageService;
67
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.response.error.dto.EasyPlusErrorMessages;
@@ -10,13 +11,16 @@
1011
import io.github.patternhelloworld.securityhelper.oauth2.api.config.util.ErrorCodeConstants;
1112
import jakarta.servlet.http.HttpServletRequest;
1213
import lombok.RequiredArgsConstructor;
14+
import org.springframework.http.HttpMethod;
1315
import org.springframework.security.authentication.AnonymousAuthenticationToken;
1416
import org.springframework.security.core.Authentication;
1517
import org.springframework.security.core.authority.AuthorityUtils;
1618
import org.springframework.security.core.context.SecurityContextHolder;
1719
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
1820
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
1921
import org.springframework.security.oauth2.core.oidc.OidcScopes;
22+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
23+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
2024
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
2125
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
2226
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -38,6 +42,7 @@ public final class AuthorizationCodeAuthorizationRequestConverter implements Aut
3842

3943
private final ISecurityUserExceptionMessageService iSecurityUserExceptionMessageService;
4044

45+
private final String consentYN;
4146

4247
private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous",
4348
"anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
@@ -55,10 +60,6 @@ public final class AuthorizationCodeAuthorizationRequestConverter implements Aut
5560
@Override
5661
public Authentication convert(HttpServletRequest request) {
5762

58-
if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {
59-
throw new EasyPlusOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_AUTHORIZATION_CODE_REQUEST_WRONG_METHOD));
60-
}
61-
6263
MultiValueMap<String, String> parameters = EasyPlusOAuth2EndpointUtils.getWebParametersContainingEasyPlusHeaders(request);
6364

6465
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
@@ -103,7 +104,7 @@ public Authentication convert(HttpServletRequest request) {
103104
Set<String> registeredScopes = registeredClient.getScopes(); // Scopes from the RegisteredClient
104105

105106
if (!registeredScopes.containsAll(requestedScopes)) {
106-
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_INVALID_REDIRECT_URI))
107+
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_ERROR))
107108
.message("Invalid scopes: " + requestedScopes + ". Allowed scopes: " + registeredScopes).build());
108109
}
109110

@@ -113,6 +114,26 @@ public Authentication convert(HttpServletRequest request) {
113114
.errorCode(ErrorCodeConstants.REDIRECT_TO_LOGIN).build());
114115
}
115116

117+
// Check Consent
118+
if(consentYN.equals("Y")) {
119+
OAuth2Authorization oAuth2Authorization = oAuth2AuthorizationService.findByToken(code, new OAuth2TokenType(OAuth2ParameterNames.CODE));
120+
EasyPlusAuthorizationConsent easyPlusAuthorizationConsent = easyPlusAuthorizationConsentRepository.findByRegisteredClientIdAndPrincipalName(oAuth2Authorization.getRegisteredClientId(), oAuth2Authorization.getPrincipalName()).orElse(null);
121+
if (easyPlusAuthorizationConsent == null) {
122+
if (request.getMethod().equals(HttpMethod.POST.toString())) {
123+
// This means the user checks authorization consent OK
124+
easyPlusAuthorizationConsent = new EasyPlusAuthorizationConsent();
125+
easyPlusAuthorizationConsent.setPrincipalName(oAuth2Authorization.getPrincipalName());
126+
easyPlusAuthorizationConsent.setRegisteredClientId(oAuth2Authorization.getRegisteredClientId());
127+
easyPlusAuthorizationConsent.setAuthorities(oAuth2Authorization.getAuthorizedScopes().stream().reduce((scope1, scope2) -> scope1 + "," + scope2).orElse(""));
128+
easyPlusAuthorizationConsentRepository.save(easyPlusAuthorizationConsent);
129+
} else {
130+
// This means the user should check authorization consent OK
131+
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_AUTHORIZATION_CODE_MISSING))
132+
.errorCode(ErrorCodeConstants.REDIRECT_TO_CONSENT).build());
133+
}
134+
}
135+
}
136+
116137
return new OAuth2AuthorizationCodeAuthenticationToken(
117138
code,
118139
principal,

lib/src/main/java/io/github/patternhelloworld/securityhelper/oauth2/api/config/security/provider/auth/endpoint/OpaqueGrantTypeAuthenticationProvider.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
2828
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
2929

30+
import java.util.Arrays;
3031
import java.util.HashMap;
3132
import java.util.Map;
33+
import java.util.Set;
34+
import java.util.stream.Collectors;
3235

3336
/*
3437
* 1) ROPC (grant_type=password, grant_type=refresh_token)
@@ -102,6 +105,19 @@ public Authentication authenticate(Authentication authentication)
102105
if (registeredClient == null) {
103106
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().message("client_id NOT found in DB").userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_ERROR)).build());
104107
}
108+
Set<String> registeredScopes = registeredClient.getScopes();
109+
Set<String> requestedScopes = Arrays.stream(
110+
modifiableAdditionalParameters.getOrDefault(OAuth2ParameterNames.SCOPE, "")
111+
.toString()
112+
.split(",")
113+
)
114+
.map(String::trim)
115+
.filter(scope -> !scope.isEmpty())
116+
.collect(Collectors.toSet());
117+
if (!registeredScopes.containsAll(requestedScopes)) {
118+
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_INVALID_REDIRECT_URI))
119+
.message("Invalid scopes: " + requestedScopes + ". Allowed scopes: " + registeredScopes).build());
120+
}
105121

106122
if (responseType.equals(OAuth2ParameterNames.CODE)) {
107123
// [IMPORTANT] To return the "code" not "access_token". Check "AuthenticationSuccessHandler".

lib/src/main/java/io/github/patternhelloworld/securityhelper/oauth2/api/config/security/response/auth/authentication/DefaultWebAuthenticationFailureHandlerImpl.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public void onAuthenticationFailure(HttpServletRequest request, HttpServletRespo
4444
request.getRequestDispatcher("/login").forward(request, response);
4545
return;
4646
}
47+
if(oauth2Exception.getError().getErrorCode().equals(ErrorCodeConstants.REDIRECT_TO_CONSENT)){
48+
request.getRequestDispatcher("/consent").forward(request, response);
49+
return;
50+
}
4751
}
4852
request.setAttribute("errorMessage", errorMessage);
4953
request.setAttribute("errorDetails", errorDetails);

lib/src/main/java/io/github/patternhelloworld/securityhelper/oauth2/api/config/security/serivce/authentication/OAuth2AuthorizationBuildingServiceImpl.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.authentication;
22

3+
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.token.EasyPlusGrantAuthenticationToken;
34
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.token.generator.CustomAccessTokenCustomizer;
45
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.token.generator.CustomDelegatingOAuth2TokenGenerator;
5-
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.token.EasyPlusGrantAuthenticationToken;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.security.core.userdetails.UserDetails;
88
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -22,8 +22,8 @@
2222

2323
import java.time.Instant;
2424
import java.time.temporal.ChronoUnit;
25-
import java.util.Map;
26-
import java.util.UUID;
25+
import java.util.*;
26+
import java.util.stream.Collectors;
2727

2828
/*
2929
*
@@ -44,6 +44,12 @@ private OAuth2Authorization build(String clientId, UserDetails userDetails,
4444

4545
RegisteredClient registeredClient = registeredClientRepository.findByClientId(clientId);
4646

47+
Set<String> scopeSet = new HashSet<>();
48+
if(easyPlusGrantAuthenticationToken.getAdditionalParameters().get("scope") != null) {
49+
scopeSet = Arrays.stream(easyPlusGrantAuthenticationToken.getAdditionalParameters().get("scope").toString().split(","))
50+
.map(String::trim)
51+
.collect(Collectors.toSet());
52+
}
4753
if(AuthorizationServerContextHolder.getContext() == null){
4854

4955
// If you use "api/v1/traditional-oauth/token", "AuthorizationServerContextHolder.getContext()" is null,
@@ -82,25 +88,26 @@ public AuthorizationServerSettings getAuthorizationServerSettings() {
8288
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
8389
.authorizationGrantType(easyPlusGrantAuthenticationToken.getGrantType())
8490
.authorizationGrant(easyPlusGrantAuthenticationToken)
85-
.authorizedScopes(registeredClient.getScopes())
91+
.authorizedScopes(scopeSet)
8692
.build());
8793

88-
8994
OAuth2Token refreshToken = shouldBePreservedRefreshToken != null ? shouldBePreservedRefreshToken : customTokenGenerator.generate(DefaultOAuth2TokenContext.builder()
9095
.registeredClient(registeredClient)
9196
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
9297
.principal(easyPlusGrantAuthenticationToken)
9398
.tokenType(OAuth2TokenType.REFRESH_TOKEN)
9499
.authorizationGrantType(easyPlusGrantAuthenticationToken.getGrantType())
95100
.authorizationGrant(easyPlusGrantAuthenticationToken)
96-
.authorizedScopes(registeredClient.getScopes())
101+
.authorizedScopes(scopeSet)
97102
.build());
98103

99104

105+
100106
return OAuth2Authorization
101107
.withRegisteredClient(registeredClient)
102108
.principalName(userDetails.getUsername())
103109
.authorizationGrantType(easyPlusGrantAuthenticationToken.getGrantType())
110+
.authorizedScopes(scopeSet)
104111
.attribute("authorities", easyPlusGrantAuthenticationToken.getAuthorities())
105112
.attributes(attrs -> attrs.putAll(easyPlusGrantAuthenticationToken.getAdditionalParameters()))
106113
.token(authorizationCode)

0 commit comments

Comments
 (0)