Skip to content

Commit 5751118

Browse files
feature : change the access token be self-contained (JWT supported)
1 parent 94afc41 commit 5751118

File tree

15 files changed

+509
-426
lines changed

15 files changed

+509
-426
lines changed

client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/response/error/GlobalExceptionHandler.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package com.patternknife.securityhelper.oauth2.client.config.response.error;
22

33

4-
import com.patternknife.securityhelper.oauth2.client.config.response.error.message.GeneralErrorMessage;
54
import io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.util.ExceptionKnifeUtils;
65
import io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.dto.SecurityKnifeErrorResponsePayload;
76
import io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.exception.KnifeOauth2AuthenticationException;
87
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.DefaultSecurityUserExceptionMessage;
98
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.ISecurityUserExceptionMessageService;
10-
import io.github.patternknife.securityhelper.oauth2.api.config.util.OrderConstants;
9+
import io.github.patternknife.securityhelper.oauth2.api.config.util.KnifeOrderConstants;
1110
import lombok.RequiredArgsConstructor;;
1211
import org.springframework.core.annotation.Order;
1312
import org.springframework.http.HttpStatus;
@@ -28,7 +27,7 @@
2827
* Once you create 'GlobalExceptionHandler', you should insert the following two (authenticationException, authorizationException) as default. Otherwise, 'unhandledExceptionHandler' is prior to 'io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.handler.SecurityKnifeExceptionHandler'.
2928
* "OrderConstants.SECURITY_KNIFE_EXCEPTION_HANDLER_ORDER - 1" means this is prior to "SecurityKnifeExceptionHandler"
3029
* */
31-
@Order(OrderConstants.SECURITY_KNIFE_EXCEPTION_HANDLER_ORDER - 1)
30+
@Order(KnifeOrderConstants.SECURITY_KNIFE_EXCEPTION_HANDLER_ORDER - 1)
3231
@ControllerAdvice
3332
@RequiredArgsConstructor
3433
public class GlobalExceptionHandler {

client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/securityimpl/introspector/CustomResourceServerTokenIntrospector.java

+48-20
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,41 @@
22

33
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.DefaultSecurityUserExceptionMessage;
44
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.ISecurityUserExceptionMessageService;
5+
import io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.dto.KnifeErrorMessages;
56
import io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.exception.KnifeOauth2AuthenticationException;
67
import io.github.patternknife.securityhelper.oauth2.api.config.security.serivce.persistence.authorization.OAuth2AuthorizationServiceImpl;
78
import io.github.patternknife.securityhelper.oauth2.api.config.security.serivce.userdetail.ConditionalDetailsService;
89
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
11+
import org.springframework.security.oauth2.jwt.Jwt;
12+
import org.springframework.security.oauth2.jwt.JwtDecoder;
913
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
1014
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
15+
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
1116
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
1217
import org.springframework.stereotype.Component;
13-
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
14-
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
18+
19+
import java.util.Arrays;
20+
import java.util.Map;
1521

1622
/*
17-
* Set this to your resource servers
23+
* Set this to your resource servers.
1824
* */
1925
@Component
2026
public class CustomResourceServerTokenIntrospector implements OpaqueTokenIntrospector {
2127

2228
private final OpaqueTokenIntrospector delegate;
29+
private final JwtDecoder jwtDecoder;
2330

2431
/*
25-
* api : resource servers call the authorization server
26-
* database : the database is shared with the authorization server and resource servers
27-
* */
32+
* Specifies the type of introspection:
33+
* - "api": Resource servers call the authorization server.
34+
* - "database": The database is shared between the authorization server and resource servers.
35+
* - "decode": Token is decoded locally using JWT.
36+
*
37+
* You can customize logic by combining "api" and "decode" methods.
38+
* For example, if a token is created within the last 10 minutes, the API can be used for validation.
39+
*/
2840
String introspectionType;
2941

3042
private final OAuth2AuthorizationServiceImpl authorizationService;
@@ -39,7 +51,7 @@ public CustomResourceServerTokenIntrospector(
3951
@Value("${patternknife.securityhelper.oauth2.introspection.type:database}") String introspectionType,
4052
@Value("${patternknife.securityhelper.oauth2.introspection.uri:default-introspect-uri}") String introspectionUri,
4153
@Value("${patternknife.securityhelper.oauth2.introspection.client-id:default-client-id}") String clientId,
42-
@Value("${patternknife.securityhelper.oauth2.introspection.client-secret:default-client-secret}") String clientSecret) {
54+
@Value("${patternknife.securityhelper.oauth2.introspection.client-secret:default-client-secret}") String clientSecret, JwtDecoder jwtDecoder) {
4355

4456
this.authorizationService = authorizationService;
4557
this.conditionalDetailsService = conditionalDetailsService;
@@ -48,26 +60,42 @@ public CustomResourceServerTokenIntrospector(
4860
this.introspectionType = introspectionType;
4961

5062
this.delegate = new SpringOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
63+
this.jwtDecoder = jwtDecoder;
5164
}
5265

5366
@Override
5467
public OAuth2AuthenticatedPrincipal introspect(String token) {
55-
if(introspectionType.equals("api")) {
56-
try {
57-
return delegate.introspect(token);
58-
} catch (Exception e) {
59-
throw new KnifeOauth2AuthenticationException(e.getMessage());
68+
switch (introspectionType) {
69+
case "api" -> {
70+
try {
71+
return delegate.introspect(token);
72+
} catch (Exception e) {
73+
throw new KnifeOauth2AuthenticationException(KnifeErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE)).message(e.getMessage() + Arrays.toString(e.getStackTrace())).build());
74+
}
75+
}
76+
case "database" -> {
77+
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
78+
79+
if (oAuth2Authorization == null || oAuth2Authorization.getAccessToken() == null || oAuth2Authorization.getAccessToken().isExpired()
80+
|| oAuth2Authorization.getRefreshToken() == null || oAuth2Authorization.getRefreshToken().isExpired()) {
81+
throw new KnifeOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE));
82+
}
83+
return (OAuth2AuthenticatedPrincipal) conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), (String) oAuth2Authorization.getAttributes().get("client_id"));
6084
}
61-
} else if (introspectionType.equals("database")) {
62-
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
85+
case "decode" -> {
86+
try {
87+
Jwt jwt = jwtDecoder.decode(token);
88+
89+
Map<String, Object> claims = jwt.getClaims();
90+
String username = (String) claims.get("username");
91+
String clientId = (String) claims.get("client_id");
6392

64-
if(oAuth2Authorization == null || oAuth2Authorization.getAccessToken() == null || oAuth2Authorization.getAccessToken().isExpired()
65-
|| oAuth2Authorization.getRefreshToken() == null || oAuth2Authorization.getRefreshToken().isExpired()){
66-
throw new KnifeOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE));
93+
return (OAuth2AuthenticatedPrincipal) conditionalDetailsService.loadUserByUsername(username, clientId);
94+
}catch (Exception e) {
95+
throw new KnifeOauth2AuthenticationException(KnifeErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE)).message(e.getMessage() + Arrays.toString(e.getStackTrace())).build());
96+
}
6797
}
68-
return (OAuth2AuthenticatedPrincipal) conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), (String) oAuth2Authorization.getAttributes().get("client_id"));
69-
}else{
70-
throw new KnifeOauth2AuthenticationException("Wrong introspection type : " + introspectionType);
98+
default -> throw new KnifeOauth2AuthenticationException("Wrong introspection type : " + introspectionType);
7199
}
72100
}
73101
}

client/src/main/resources/application.properties

+7-3
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ spring.mvc.view.suffix=.html
7777

7878

7979
# Introspection type configuration:
80-
# - api: The Resource Server sends introspection requests to the Authorization Server (better scalability)
81-
# - database: The Resource Server and Authorization Server share the same database (faster performance)
80+
# - api: The Resource Server sends introspection requests to the Authorization Server (high traffic, better scalability, instant authorization check)
81+
# - database: The Resource Server and Authorization Server share the same database (low traffic, low scalability, instant authorization check)
82+
# - decode: The Resource Server decodes the Access Token according to the JWT algorithm. (no traffic, better scalability, no instant authorization check)
8283
# [WARNING] api: Some test codes are currently NOT working due to the following introspection URI calls
8384
patternknife.securityhelper.oauth2.introspection.type=database
8485
patternknife.securityhelper.oauth2.introspection.uri=http://localhost:8370/oauth2/introspect
8586
patternknife.securityhelper.oauth2.introspection.client-id=client_customer
86-
patternknife.securityhelper.oauth2.introspection.client-secret=12345
87+
patternknife.securityhelper.oauth2.introspection.client-secret=12345
88+
89+
patternknife.securityhelper.jwt.secret=5pAq6zRyX8bC3dV2wS7gN1mK9jF0hL4tUoP6iBvE3nG8xZaQrY7cW2fA
90+
patternknife.securityhelper.jwt.algorithm=HmacSHA256

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public Authentication convert(HttpServletRequest request) {
3030

3131
KnifeGrantAuthenticationToken knifeGrantAuthenticationToken = new KnifeGrantAuthenticationToken(new AuthorizationGrantType((String) additionalParameters.get("grant_type")),
3232
oAuth2ClientAuthenticationToken, additionalParameters);
33-
additionalParameters.put(Principal.class.getName(), knifeGrantAuthenticationToken);
33+
// additionalParameters.put(Principal.class.getName(), knifeGrantAuthenticationToken);
3434

3535
return knifeGrantAuthenticationToken;
3636
}
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
package io.github.patternknife.securityhelper.oauth2.api.config.security.introspector;
22

3-
43
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.DefaultSecurityUserExceptionMessage;
54
import io.github.patternknife.securityhelper.oauth2.api.config.security.message.ISecurityUserExceptionMessageService;
5+
import io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.dto.KnifeErrorMessages;
66
import io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.exception.KnifeOauth2AuthenticationException;
77
import io.github.patternknife.securityhelper.oauth2.api.config.security.serivce.persistence.authorization.OAuth2AuthorizationServiceImpl;
88
import io.github.patternknife.securityhelper.oauth2.api.config.security.serivce.userdetail.ConditionalDetailsService;
99
import org.springframework.beans.factory.annotation.Value;
1010
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
11+
import org.springframework.security.oauth2.jwt.Jwt;
12+
import org.springframework.security.oauth2.jwt.JwtDecoder;
1113
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
1214
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
1315
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
1416
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
15-
import org.springframework.stereotype.Component;
17+
18+
import java.util.Arrays;
19+
import java.util.Map;
1620

1721

1822
public class DefaultResourceServerTokenIntrospector implements OpaqueTokenIntrospector {
1923

2024
private final OpaqueTokenIntrospector delegate;
25+
private final JwtDecoder jwtDecoder;
2126

2227
/*
2328
* api : resource servers call the authorization server
2429
* database : the database is shared with the authorization server and resource servers
30+
* decode : JWT
2531
* */
2632
String introspectionType;
2733

@@ -34,10 +40,10 @@ public DefaultResourceServerTokenIntrospector(
3440
OAuth2AuthorizationServiceImpl authorizationService,
3541
ConditionalDetailsService conditionalDetailsService,
3642
ISecurityUserExceptionMessageService iSecurityUserExceptionMessageService,
37-
String introspectionType,
38-
String introspectionUri,
39-
String clientId,
40-
String clientSecret) {
43+
@Value("${patternknife.securityhelper.oauth2.introspection.type:database}") String introspectionType,
44+
@Value("${patternknife.securityhelper.oauth2.introspection.uri:default-introspect-uri}") String introspectionUri,
45+
@Value("${patternknife.securityhelper.oauth2.introspection.client-id:default-client-id}") String clientId,
46+
@Value("${patternknife.securityhelper.oauth2.introspection.client-secret:default-client-secret}") String clientSecret, JwtDecoder jwtDecoder) {
4147

4248
this.authorizationService = authorizationService;
4349
this.conditionalDetailsService = conditionalDetailsService;
@@ -46,26 +52,42 @@ public DefaultResourceServerTokenIntrospector(
4652
this.introspectionType = introspectionType;
4753

4854
this.delegate = new SpringOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
55+
this.jwtDecoder = jwtDecoder;
4956
}
5057

5158
@Override
5259
public OAuth2AuthenticatedPrincipal introspect(String token) {
53-
if(introspectionType.equals("api")) {
54-
try {
55-
return delegate.introspect(token);
56-
} catch (Exception e) {
57-
throw new KnifeOauth2AuthenticationException(e.getMessage());
60+
switch (introspectionType) {
61+
case "api" -> {
62+
try {
63+
return delegate.introspect(token);
64+
} catch (Exception e) {
65+
throw new KnifeOauth2AuthenticationException(KnifeErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_ERROR)).message(e.getMessage() + Arrays.toString(e.getStackTrace())).build());
66+
}
5867
}
59-
} else if (introspectionType.equals("database")) {
60-
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
68+
case "database" -> {
69+
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
70+
71+
if (oAuth2Authorization == null || oAuth2Authorization.getAccessToken() == null || oAuth2Authorization.getAccessToken().isExpired()
72+
|| oAuth2Authorization.getRefreshToken() == null || oAuth2Authorization.getRefreshToken().isExpired()) {
73+
throw new KnifeOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE));
74+
}
75+
return (OAuth2AuthenticatedPrincipal) conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), (String) oAuth2Authorization.getAttributes().get("client_id"));
76+
}
77+
case "decode" -> {
78+
try {
79+
Jwt jwt = jwtDecoder.decode(token);
80+
81+
Map<String, Object> claims = jwt.getClaims();
82+
String username = (String) claims.get("username");
83+
String clientId = (String) claims.get("client_id");
6184

62-
if(oAuth2Authorization == null || oAuth2Authorization.getAccessToken() == null || oAuth2Authorization.getAccessToken().isExpired()
63-
|| oAuth2Authorization.getRefreshToken() == null || oAuth2Authorization.getRefreshToken().isExpired()){
64-
throw new KnifeOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE));
85+
return (OAuth2AuthenticatedPrincipal) conditionalDetailsService.loadUserByUsername(username, clientId);
86+
}catch (Exception e) {
87+
throw new KnifeOauth2AuthenticationException(KnifeErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_ERROR)).message(e.getMessage() + Arrays.toString(e.getStackTrace())).build());
88+
}
6589
}
66-
return (OAuth2AuthenticatedPrincipal) conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), (String) oAuth2Authorization.getAttributes().get("client_id"));
67-
}else{
68-
throw new KnifeOauth2AuthenticationException("Wrong introspection type : " + introspectionType);
90+
default -> throw new KnifeOauth2AuthenticationException("Wrong introspection type : " + introspectionType);
6991
}
7092
}
7193
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ else if (((String) knifeGrantAuthenticationToken.getAdditionalParameters().get("
131131
getAuthenticatedClientElseThrowInvalidClient(authentication),
132132
oAuth2Authorization.getAccessToken().getToken(),
133133
oAuth2Authorization.getRefreshToken() != null ? oAuth2Authorization.getRefreshToken().getToken() : null,
134-
((KnifeGrantAuthenticationToken) authentication).getAdditionalParameters()
134+
knifeGrantAuthenticationToken.getAdditionalParameters()
135135
);
136136
} else {
137137
throw new KnifeOauth2AuthenticationException();
@@ -141,7 +141,7 @@ else if (((String) knifeGrantAuthenticationToken.getAdditionalParameters().get("
141141
}catch (KnifeOauth2AuthenticationException e){
142142
throw e;
143143
} catch (Exception e){
144-
throw new KnifeOauth2AuthenticationException(KnifeErrorMessages.builder().message(e.getMessage()).userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_ERROR)).build());
144+
throw e;
145145
}
146146

147147
}

0 commit comments

Comments
 (0)