Skip to content

Commit 0f6d844

Browse files
:Merge branch 'self-contained-feature'
2 parents 15aa351 + 5751118 commit 0f6d844

File tree

18 files changed

+536
-409
lines changed

18 files changed

+536
-409
lines changed

README.md

+22-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
# Spring Security Oauth2 JPA Implementation
22

3-
> App-Token based OAuth2 POC built to grow with Spring Boot and ORM
3+
> App-Token based OAuth2 implementation built to grow with Spring Boot and JPA
4+
5+
## Table of Contents
6+
7+
- [Quick Start](#quick-start)
8+
- [Features](#features)
9+
- [Dependencies](#dependencies)
10+
- [Run the App](#run-the-app)
11+
- [API Guide](#api-guide)
12+
- [Registration](#registration)
13+
- [Implementations](#implementations)
14+
- [Mandatory Settings](#mandatory-settings)
15+
- [Customizable Settings](#customizable-settings)
16+
- [OAuth2 - ROPC](#oauth2---ropc)
17+
- [OAuth2 - Authorization Code](#oauth2---authorization-code)
18+
- [Running this App with Docker](#running-this-app-with-docker)
19+
- [Contribution Guide](#contribution-guide)
420

5-
## Supporting Oauth2 Type
6-
| ROPC | Authorization Code |
7-
|------------------|-------------------------------------------------|
8-
| production-level | Beta (expected to reach production-level in v3) |
921

1022
## Quick Start
1123
```xml
@@ -15,21 +27,15 @@
1527
<version>3.5.0</version>
1628
</dependency>
1729
```
18-
For v2, using the database tables from Spring Security 5 (only the database tables; follow the dependencies as above):
19-
```xml
20-
<dependency>
21-
<groupId>io.github.patternknife.securityhelper.oauth2.api</groupId>
22-
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
23-
<version>2.8.2</version>
24-
</dependency>
25-
```
2630

27-
## Overview
2831

29-
* Complete separation of the library (API) and the client for testing it
32+
## Features
33+
34+
* Complete separation of the library and the client
35+
* Library : API
36+
* Client : DOC, Integration tester
3037
* Immediate Permission (Authority) Check: Not limited to verifying the token itself, but also ensuring real-time validation of any updates to permissions in the database.
3138
* Token Introspector: Enable the ``/oauth2/introspect`` endpoint to allow multiple resource servers to verify the token's validity and permissions with the authorization server.
32-
3339
* Set up the same access & refresh token APIs on both ``/oauth2/token`` and on our controller layer such as ``/api/v1/traditional-oauth/token``, both of which function same and have `the same request & response payloads for success and errors`. (However, ``/oauth2/token`` is the standard that "spring-authorization-server" provides.)
3440
* As you are aware, the API ``/oauth2/token`` is what "spring-authorization-server" provides.
3541
* ``/api/v1/traditional-oauth/token`` is what this library implemented directly.

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/java/com/patternknife/securityhelper/oauth2/client/domain/customer/api/CustomerApi.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public Map<String, Boolean> logoutCustomer(HttpServletRequest request) {
103103

104104
@PreAuthorize("@resourceServerAuthorityChecker.hasRole('CUSTOMER_ADMIN')")
105105
@GetMapping("/customers/{id}")
106-
public CustomerResDTO.Id getCustomerForAuthorizationTest(@PathVariable("id") final long id, @CustomAuthenticationPrincipal KnifeUserInfo knifeUserInfo)
106+
public CustomerResDTO.Id getCustomerForAuthorizationTest(@PathVariable("id") final long id, @CustomAuthenticationPrincipal KnifeUserInfo<CustomizedUserInfo> knifeUserInfo)
107107
throws ResourceNotFoundException {
108108
return new CustomerResDTO.Id(id);
109109
}

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/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
1010
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
1111
<version>3.5.0</version>
1212
<name>spring-security-oauth2-password-jpa-implementation</name>
13-
<description>The implementation of Spring Security 6 Spring Authorization Server for stateful OAuth2 Password Grant</description>
13+
<description>App-Token based OAuth2 implementation built to grow with Spring Boot and JPA</description>
1414
<packaging>jar</packaging>
1515

1616
<url>https://github.com/patternknife/spring-security-oauth2-password-jpa-implementation</url>

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
}

0 commit comments

Comments
 (0)