Skip to content

Commit 1cc7763

Browse files
authorization code module (in development - 2)
1 parent 523d335 commit 1cc7763

File tree

7 files changed

+130
-45
lines changed

7 files changed

+130
-45
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public class CommonDataSourceConfiguration {
193193
* Refer to ``client/src/docs/asciidoc/api-app.adoc``
194194

195195
## OAuth2 - Authorization Code
196-
* Open the web browser by connecting to ``http://localhost:8370/oauth2/authorization?code=32132&grant_type=authorization_code&response_type=code&client_id=client_customer&redirect_uri=http%3A%2F%2Flocalhost%3A8370%2Fcallback1&scope=message.read&state=random-state&prompt=consent&access_type=offline&code_challenge=YOUR_CODE_CHALLENGE&code_challenge_method=S256``
196+
* Open the web browser by connecting to ``http://localhost:8370/oauth2/authorization?code=32132&response_type=code&client_id=client_customer&redirect_uri=http%3A%2F%2Flocalhost%3A8370%2Fcallback1``
197197
* Login with ``[email protected] / 1234 ``
198198

199199
## Running this App with Docker

src/main/java/io/github/patternknife/securityhelper/oauth2/api/config/security/converter/auth/endpoint/KnifeAuthorizationCodeRequestConverterController.java

Lines changed: 118 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
import java.util.Map;
77
import java.util.Set;
88

9+
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.DefaultSecurityUserExceptionMessage;
10+
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.ISecurityUserExceptionMessageService;
911
import io.github.patternknife.securityhelper.oauth2.api.config.security.serivce.persistence.authorization.OAuth2AuthorizationServiceImpl;
12+
import org.apache.commons.logging.Log;
13+
import org.apache.commons.logging.LogFactory;
1014
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
1115
import org.springframework.security.oauth2.core.oidc.OidcScopes;
1216
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
@@ -19,79 +23,158 @@
1923
import org.springframework.ui.Model;
2024
import org.springframework.util.StringUtils;
2125
import org.springframework.web.bind.annotation.GetMapping;
26+
import org.springframework.web.bind.annotation.PostMapping;
2227
import org.springframework.web.bind.annotation.RequestParam;
2328

2429
/**
2530
* @author Daniel Garnier-Moiroux
2631
*/
2732
@Controller
2833
public class KnifeAuthorizationCodeRequestConverterController {
34+
35+
private final Log logger = LogFactory.getLog(this.getClass());
36+
2937
private final RegisteredClientRepository registeredClientRepository;
3038
private final OAuth2AuthorizationConsentService authorizationConsentService;
3139
private final OAuth2AuthorizationServiceImpl oAuth2AuthorizationService;
40+
private final ISecurityUserExceptionMessageService iSecurityUserExceptionMessageService;
3241

3342
public KnifeAuthorizationCodeRequestConverterController(RegisteredClientRepository registeredClientRepository,
3443
OAuth2AuthorizationConsentService authorizationConsentService,
35-
OAuth2AuthorizationServiceImpl oAuth2AuthorizationService) {
44+
OAuth2AuthorizationServiceImpl oAuth2AuthorizationService, ISecurityUserExceptionMessageService iSecurityUserExceptionMessageService) {
3645
this.registeredClientRepository = registeredClientRepository;
3746
this.authorizationConsentService = authorizationConsentService;
3847
this.oAuth2AuthorizationService = oAuth2AuthorizationService;
48+
this.iSecurityUserExceptionMessageService = iSecurityUserExceptionMessageService;
3949
}
4050

51+
52+
@PostMapping("/oauth2/authorization")
53+
public String authorize(@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
54+
@RequestParam(OAuth2ParameterNames.STATE) String state,
55+
@RequestParam(OAuth2ParameterNames.SCOPE) Set<String> scopes,
56+
@RequestParam(name = OAuth2ParameterNames.CODE, required = false) String authorizationCode,
57+
@RequestParam(name = "consent_action", required = false) String consentAction,
58+
Model model) {
59+
// 예시: 클라이언트의 등록된 콜백 URL 가져오기
60+
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
61+
String redirectUri = registeredClient.getRedirectUris().iterator().next();
62+
63+
// 승인된 스코프를 바탕으로 Authorization Code 생성 로직
64+
// Authorization Code를 생성하여 저장하고 해당 코드를 콜백 URL로 리다이렉트합니다.
65+
if ("approve".equals(consentAction)) {
66+
// 실제로는 이곳에서 OAuth2Authorization 객체를 생성하고 저장하는 로직 필요
67+
authorizationCode = "generated-authorization-code"; // 실제 생성된 코드로 교체
68+
69+
// 콜백 URL로 리다이렉트하며 Authorization Code를 전달
70+
return "redirect:" + redirectUri + "?code=" + authorizationCode + "&state=" + state;
71+
} else {
72+
// 거부한 경우 에러 페이지 혹은 다시 로그인 페이지로 리다이렉트
73+
return "redirect:/login?error=access_denied";
74+
}
75+
}
76+
77+
78+
/*
79+
* code, response_type, client_id, redirect_url
80+
* */
4181
@GetMapping(value = "/oauth2/authorization")
42-
public String consent( Model model,
82+
public String consent(Model model,
83+
@RequestParam(name = OAuth2ParameterNames.CODE) String authorizationCode,
84+
@RequestParam(name = OAuth2ParameterNames.RESPONSE_TYPE) String responseType,
4385
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
44-
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
45-
@RequestParam(OAuth2ParameterNames.STATE) String state,
46-
@RequestParam(name = OAuth2ParameterNames.CODE, required = false) String authorizationCode,
47-
@RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {
86+
@RequestParam(OAuth2ParameterNames.REDIRECT_URI) String redirectUri,
87+
@RequestParam(name = OAuth2ParameterNames.SCOPE, required = false) String scope) {
88+
4889

49-
String principalName;
5090
if(authorizationCode == null){
5191
return "login";
5292
}
5393

94+
if (!"code".equals(responseType)) {
95+
logger.error("message (Invalid Authorization Code): "
96+
+ "authorizationCode=" + authorizationCode + ", "
97+
+ "responseType=" + responseType + ", "
98+
+ "clientId=" + clientId + ", "
99+
+ "redirectUri=" + redirectUri + ", "
100+
+ "scope=" + scope);
101+
model.addAttribute("userMessage", iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_INVALID_RESPONSE_TYPE));
102+
return "error";
103+
}
104+
105+
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
106+
if(registeredClient == null){
107+
logger.error("message (Invalid Client ID): "
108+
+ "authorizationCode=" + authorizationCode + ", "
109+
+ "responseType=" + responseType + ", "
110+
+ "clientId=" + clientId + ", "
111+
+ "redirectUri=" + redirectUri + ", "
112+
+ "scope=" + scope + ", ");
113+
model.addAttribute("userMessage", iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_WRONG_CLIENT_ID_SECRET));
114+
return "error";
115+
}
116+
117+
if (!registeredClient.getRedirectUris().contains(redirectUri)) {
118+
logger.error("message (Invalid redirect URI): "
119+
+ "authorizationCode=" + authorizationCode + ", "
120+
+ "responseType=" + responseType + ", "
121+
+ "clientId=" + clientId + ", "
122+
+ "redirectUri=" + redirectUri + ", "
123+
+ "scope=" + scope + ", "
124+
+ "registeredRedirectUris=" + registeredClient.getRedirectUris().toString());
125+
model.addAttribute("userMessage", iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_INVALID_REDIRECT_URI));
126+
return "error";
127+
}
128+
129+
130+
54131
OAuth2Authorization oAuth2Authorization = oAuth2AuthorizationService.findByToken(authorizationCode, new OAuth2TokenType("authorization_code"));
55132
if(oAuth2Authorization == null){
56133
return "login";
57134
}
58-
principalName = oAuth2Authorization.getPrincipalName();
135+
String principalName = oAuth2Authorization.getPrincipalName();
59136

60137

61-
// Remove scopes that were already approved
62-
Set<String> scopesToApprove = new HashSet<>();
63-
Set<String> previouslyApprovedScopes = new HashSet<>();
64-
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
138+
Set<String> approvedScopes = new HashSet<>();
139+
65140
OAuth2AuthorizationConsent currentAuthorizationConsent =
66141
this.authorizationConsentService.findById(registeredClient.getId(), principalName);
67-
Set<String> authorizedScopes;
68-
if (currentAuthorizationConsent != null) {
69-
authorizedScopes = currentAuthorizationConsent.getScopes();
70-
} else {
71-
authorizedScopes = Collections.emptySet();
72-
}
73-
for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
74-
if (OidcScopes.OPENID.equals(requestedScope)) {
75-
continue;
142+
if(currentAuthorizationConsent != null){
143+
return "redirect:" + redirectUri + "?code=" + authorizationCode;
144+
}else{
145+
146+
Set<String> authorizedScopes = currentAuthorizationConsent.getScopes();
147+
148+
Set<String> requestedScopes = StringUtils.commaDelimitedListToSet(scope);
149+
150+
if (!authorizedScopes.containsAll(requestedScopes)) {
151+
logger.error("message (Scopes not approved): "
152+
+ "authorizationCode=" + authorizationCode + ", "
153+
+ "responseType=" + responseType + ", "
154+
+ "clientId=" + clientId + ", "
155+
+ "redirectUri=" + redirectUri + ", "
156+
+ "scope=" + scope + ", "
157+
+ "authorizedScopes=" + authorizedScopes);
158+
model.addAttribute("userMessage", iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_SCOPES_NOT_APPROVED));
159+
return "error";
76160
}
77-
if (authorizedScopes.contains(requestedScope)) {
78-
previouslyApprovedScopes.add(requestedScope);
79-
} else {
80-
scopesToApprove.add(requestedScope);
161+
162+
for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
163+
if (OidcScopes.OPENID.equals(requestedScope)) {
164+
continue;
165+
}
166+
if (authorizedScopes.contains(requestedScope)) {
167+
approvedScopes.add(requestedScope);
168+
}
81169
}
82170
}
83171

172+
173+
model.addAttribute("code", authorizationCode);
84174
model.addAttribute("clientId", clientId);
85-
model.addAttribute("state", state);
86-
model.addAttribute("scopes", withDescription(scopesToApprove));
87-
model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
175+
model.addAttribute("scopes", withDescription(approvedScopes));
88176
model.addAttribute("principalName", principalName);
89-
model.addAttribute("userCode", userCode);
90-
if (StringUtils.hasText(userCode)) {
91-
model.addAttribute("requestURI", "/oauth2/device_verification");
92-
} else {
93-
model.addAttribute("requestURI", "/oauth2/authorize");
94-
}
177+
model.addAttribute("requestURI", "/oauth2/authorization");
95178

96179
return "consent";
97180
}
@@ -140,4 +223,5 @@ public static class ScopeWithDescription {
140223
}
141224
}
142225

226+
143227
}

src/main/java/io/github/patternknife/securityhelper/oauth2/api/config/security/message/DefaultSecurityUserExceptionMessage.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ public enum DefaultSecurityUserExceptionMessage implements ExceptionMessageInter
1616
AUTHENTICATION_PASSWORD_FAILED_EXCEEDED("The number of password attempts has been exceeded."),
1717

1818
// Wrong Authorization Code
19-
AUTHORIZATION_CODE_NO_EXISTS("The specified Authorization code does not exist."),
20-
19+
AUTHENTICATION_INVALID_RESPONSE_TYPE("The specified Response Type is invalid."),
20+
AUTHENTICATION_INVALID_AUTHORIZATION_CODE("The specified Authorization Code is invalid."),
21+
AUTHENTICATION_INVALID_REDIRECT_URI("The specified Redirect URI is invalid."),
22+
AUTHENTICATION_SCOPES_NOT_APPROVED("The specified Scopes are not approved."),
2123
// CLIENT ID, SECRET
2224
AUTHENTICATION_WRONG_CLIENT_ID_SECRET("Client information is not verified."),
2325

src/main/java/io/github/patternknife/securityhelper/oauth2/api/config/security/provider/auth/endpoint/KnifeOauth2AuthenticationProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public Authentication authenticate(Authentication authentication)
5959
);
6060

6161
if (oAuth2Authorization == null) {
62-
throw new KnifeOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHORIZATION_CODE_NO_EXISTS));
62+
throw new KnifeOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_INVALID_AUTHORIZATION_CODE));
6363
}
6464
// UserDetails 로드
6565
userDetails = conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), clientId);

src/main/java/io/github/patternknife/securityhelper/oauth2/api/config/security/server/ServerConfig.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,16 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(
121121
.tokenGenerator(tokenGenerator)
122122
.oidc(Customizer.withDefaults())
123123
/*
124+
* https://sabarada.tistory.com/248
124125
*
125-
*
126-
* http://localhost:8370/oauth2/authorization?code=32132&grant_type=authorization_code&response_type=code&client_id=client_customer&redirect_uri=http%3A%2F%2Flocalhost%3A8370%2Fcallback1&scope=message.read&state=random-state&prompt=consent&access_type=offline&code_challenge=YOUR_CODE_CHALLENGE&code_challenge_method=S256
126+
* code, client_id, redirect_uri
127+
* http://localhost:8370/oauth2/authorization?code=32132&response_type=code&client_id=client_customer&redirect_uri=http%3A%2F%2Flocalhost%3A8370%2Fcallback1
127128
*
128129
* */
129130
// https://medium.com/@itsinil/oauth-2-1-pkce-%EB%B0%A9%EC%8B%9D-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-14500950cdbf
130131
.authorizationEndpoint(authorizationEndpoint ->
131132
authorizationEndpoint
132-
// [1] User goes to the 'consentPage' below ('http://localhost:8370/oauth2/authorization?code=32132&grant_type=authorization_code&response_type=code&client_id=client_customer&redirect_uri=http%3A%2F%2Flocalhost%3A8370%2Fcallback1&scope=message.read&state=random-state&prompt=consent&access_type=offline&code_challenge=YOUR_CODE_CHALLENGE&code_challenge_method=S256')
133+
// [1] User goes to the 'consentPage' below ('http://localhost:8370/oauth2/authorization?code=XXXXX&response_type=code&client_id=client_customer&redirect_uri=http%3A%2F%2Flocalhost%3A8370%2Fcallback1&scope=message.read&state=random-state&prompt=consent&access_type=offline&code_challenge=YOUR_CODE_CHALLENGE&code_challenge_method=S256')
133134
// [2] As you see 'KnifeAuthorizationCodeRequestConverterController', if the code parameter is NOT authenticated, it redirects you to the login page.
134135
// [3] If the login (/api/v1/traditional-oauth/authorization-code) in the 'src/main/resources/templates/login.html' is successful, it retries [1].
135136
// [4] Now you are on the consent page, check READ & WRITE and then press 'Submit'.

src/main/resources/templates/consent.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ <h1 class="text-center text-primary">App permissions</h1>
5050
<input type="hidden" name="client_id" th:value="${clientId}">
5151
<input type="hidden" name="state" th:value="${state}">
5252
<input type="hidden" name="code" th:value="${code}">
53-
<input th:if="${userCode}" type="hidden" name="user_code" th:value="${userCode}">
5453

5554
<div th:each="scope: ${scopes}" class="form-check py-1">
5655
<input class="form-check-input"

src/main/resources/templates/error.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
<head>
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1">
6-
<title>Spring Authorization Server sample</title>
6+
<title>Spring Authorization Server Sample</title>
77
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
88
</head>
99
<body>
1010
<div class="container">
1111
<div class="row py-5">
1212
<div class="col-md-6">
13-
<h2 class="text-danger" th:text="${errorTitle}"></h2>
14-
<p th:text="${errorMessage}"></p>
13+
<p th:text="${userMessage}"></p>
1514
</div>
1615
</div>
1716
</div>

0 commit comments

Comments
 (0)