App-Token based easy OAuth2 implementation built to grow with Spring Boot
- Quick Start
- Features
- Dependencies
- Run the App
- API Guide
- OAuth2 - ROPC
- OAuth2 - Authorization Code
- Running this App with Docker
- Contribution Guide
<dependency>
    <groupId>io.github.patternhelloworld.securityhelper.oauth2.api</groupId>
    <artifactId>spring-oauth2-easyplus</artifactId>
    <version>4.4.1</version>
</dependency>- Complete separation of the library and the client
- Library : API
- Client : DOC, Integration tester
 
- Use JPA for various databases to gain full control over all tokens and permissions, unlike simple in-memory examples.
- Extensible: Supports multiple authorization servers and resource servers with this library.
- Hybrid Resource Servers Token Verification Methods: Support for multiple verification approaches, including API calls to the authorization server, direct database validation, and local JWT decoding.
- 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.
- Authentication management based on a combination of Username, client ID, and App-Token
- What is an App-Token?
- An App-Token is an additional token that serves as a unique identifier for each device. Unlike access tokens, it is not regenerated with each login. Instead, it uses a device-specific unique value, such as a GUID in Android, to control device-level authentication, even when the app is reinstalled. If the token values are the same, the same access token is shared.
 
 
- What is an App-Token?
| App-Token Status | Access Token Behavior | 
|---|---|
| same for the same user | Access-Token is shared | 
| different for the same user | Access-Token is NOT shared | 
- Set this in your application.properties.- App-Token Behavior Based on io.github.patternhelloworld.securityhelper.oauth2.no-app-token-same-access-token
 
- App-Token Behavior Based on 
| no-app-token-same-access-tokenValue | App-Token Status | Access Token Sharing Behavior | 
|---|---|---|
| true | App-Token is nullfor the same user | Same user with a nullApp-Token shares the same access token across multiple logins. | 
| false | App-Token is nullfor the same user | Even if the App-Token is null, the same user will receive a new access token for each login. | 
| - | App-Token is shared for the same user | Access tokens will not be shared. A new access token is generated for each unique App-Token, even for the same user. | 
| - | App-Token is NOT shared for the same user | Each unique App-Token generates a new access token for the same user. | 
- Separated UserDetails implementation for Admin and Customer roles as an example. (This can be extended such as Admin, Customer, Seller and Buyer... by implementing UserDetailsServiceFactory)
- Authorization Code Flow with Optional PKCE, Authorization Consent and Single Page Application (XMLHttpRequest)
- ROPC for scenarios where accessing a browser screen on the server is either unavailable or impractical
- Application of Spring Rest Docs, Postman payloads provided
- Set up the same access & refresh token APIs on both /oauth2/tokenand on our controller layer such as/api/v1/traditional-oauth/token, both of which function same and havethe same request & response payloads for success and errors. (However,/oauth2/tokenis the standard that "spring-authorization-server" provides.)- As you are aware, the API /oauth2/token(Recommended) is what "spring-authorization-server" provides.- 
/api/v1/traditional-oauth/token(Easily Customizable) is what this library implemented directly.- Success Payload
 { "access_token" : "Vd4x8D4lDg7VBFh...", "token_type" : "Bearer", "refresh_token" : "m3UgLrvPtXKdy7jiD...", "expires_in" : 3469, "scope" : "read write" }- Error Payload (Customizable)
 { "timestamp": 1719470948370, "message": "Couldn't find the client ID : client_admin", // Sensitive info such as being thrown from StackTraces "details": "uri=/oauth2/token", "userMessage": "Authentication failed. Please check your credentials.", "userValidationMessage": null }- In the following error payload, the 'message' shouldn't be exposed to clients; instead, the 'userMessage' should be.
- Definitely, you can customize the payload sent to the client by implementing AuthenticationFailureHandler.
 
 
- 
 
- As you are aware, the API 
- See the sample folder com.patternhelloworld.securityhelper.oauth2.client.config.securityimplto understand how to implement the library.
| Category | Dependencies | 
|---|---|
| Backend-Language | Java 17 | 
| Backend-Framework | Spring Boot 3.3.2 | 
| Main Libraries | Spring Security 6.3.1, Spring Security Authorization Server 1.3.1, JPA | 
| Package-Manager | Maven 3.6.3 (mvnw, Dockerfile) | 
| RDBMS | Mysql 8.0.17 | 
- Requires selected dependencies for Spring Boot. Check the pom.xmlor this link "https://libraries.io/maven/io.github.patternhelloworld.securityhelper.oauth2.api:spring-oauth2-easyplus/4.4.1/tree".
- If you don't have a MySQL instance readily available, you can clone https://github.com/patternhelloworld/docker-mysql-8 .
# Do NOT use your latest Maven version, but mvnw here or one with the same version.
cd lib
mvnw clean install
cd ..
cd client
mvnw clean install # Integration tests are done here, which creates docs by Spring-Rest-Doc.- Run the client module by running SpringSecurityOauth2PasswordJpaImplApplicationin the client.
- The API information is found on http://localhost:8370/docs/api-app.html, managed by Spring Rest Doc
- In case you use IntelliJ, I recommend creating an empty project and importing the API (root) module and client module separately.
- The client module definitely consumes the API module, but not vice versa.
- See the clientfolder.
- As the Api module consumes JPA, adding it to Beans is required.
// Add 'io.github.patternhelloworld.securityhelper.oauth2.api'
@SpringBootApplication(scanBasePackages =  {"com.patternhelloworld.securityhelper.oauth2.client", "io.github.patternhelloworld.securityhelper.oauth2.api"})
public class SpringSecurityOauth2PasswordJpaImplApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityOauth2PasswordJpaImplApplication.class, args);
    }
}@Configuration
// ADD 'io.github.patternhelloworld.securityhelper.oauth2.api.config.security'
@EnableJpaRepositories(
        basePackages = {"com.patternhelloworld.securityhelper.oauth2.client.domain",
                "com.patternhelloworld.securityhelper.oauth2.client.config.securityimpl",
                "io.github.patternhelloworld.securityhelper.oauth2.api.config.security"},
        entityManagerFactoryRef = "commonEntityManagerFactory",
        transactionManagerRef= "commonTransactionManager"
)
public class CommonDataSourceConfiguration {
    
   // ADD 'io.github.patternhelloworld.securityhelper.oauth2.api.config.security'
    @Primary
    @Bean(name = "commonEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean commonEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(commonDataSource())
                .packages("com.patternhelloworld.securityhelper.oauth2.client.domain",
                        "io.github.patternhelloworld.securityhelper.oauth2.api.config.security")
                .persistenceUnit("commonEntityManager")
                .build();
    }
}- As indicated, the clientfolder demonstrates how to use this library.
- The only mandatory setting is client.config.securityimpl.service.userdetail.CustomUserDetailsServiceFactory. The rest depend on your specific situation.
- 
Insert your code when events happen such as tokens created - SecurityPointCut
- See the source code in client.config.securityimpl.aop
 
- 
Register error user messages as desired - ISecurityUserExceptionMessageService
- See the source code in client.config.securityimpl.message
 
- 
Customize the whole error payload as desired for all cases - What is "all cases"?
- Authorization Server ("/oauth2/token", "/api/v1/traditional-oauth/token") and Resource Server (Bearer token authentication : 401, authorization (permission) : 403)
 
- Customize errors of the following cases
- Login (/oauth2/token) : client.config.securityimpl.response.CustomAuthenticationFailureHandlerImpl
- Login (/api/v1/traditional-oauth/token) : client.config.response.error.GlobalExceptionHandler.authenticationException("/api/v1/traditional-oauth/token", Resource Server (Bearer token inspection))
- Resource Server (Bearer token expired or with a wrong value, 401) :client.config.securityimpl.response.CustomAuthenticationEntryPointImpl
- Resource Server (Permission, 403, @PreAuthorized on your APIs) client.config.response.error.GlobalExceptionHandler.authorizationException
 
- Login (/oauth2/token) : 
 
- What is "all cases"?
- 
Customize the whole success payload as desired for the only "/oauth2/token" - client.config.securityimpl.response.CustomAuthenticationSuccessHandlerImpl
- The success response payload of "/api/v1/traditional-oauth/token" is in api.domain.traditionaloauth.dtoand is not yet customizable.
 
- 
Customize the verification logic for UsernamePassword and Client as desired - IOauth2AuthenticationHashCheckService
 
- 
Customize OpaqueTokenIntrospector as desired (!This is for Resource Servers) - client.config.securityimpl.introspector.CustomResourceServerTokenIntrospector
- 
# Introspection type configuration: # - api: The Resource Server sends introspection requests to the Authorization Server. # Benefits: High scalability and real-time authorization checks. # Drawbacks: Increased traffic due to frequent API calls. # # - database: The Resource Server and Authorization Server share the same database. # Benefits: Minimal traffic and real-time authorization checks. # Drawbacks: Limited scalability due to direct database dependency. # # - decode: The Resource Server decodes the Access Token locally using the JWT algorithm. # Benefits: No traffic and high scalability. # Drawbacks: Lacks real-time authorization updates. # # [WARNING] api: Certain test cases are currently failing due to issues with the specified introspection URI calls. patternhelloworld.securityhelper.oauth2.introspection.type=database patternhelloworld.securityhelper.oauth2.introspection.uri=http://localhost:8370/oauth2/introspect patternhelloworld.securityhelper.oauth2.introspection.client-id=client_customer patternhelloworld.securityhelper.oauth2.introspection.client-secret=12345 
 
- Refer to client/src/docs/asciidoc/api-app.adoc
- How to set it up
- Create your own login page with the /login route as indicated in the client project (In the future, this address will be customisable):
 @Controller public class LoginWeb { @GetMapping("/login") public String loginPage() { return "login"; } } spring.mvc.view.prefix=/templates/ spring.mvc.view.suffix=.html - Check the login page at the "resources/templates/login.hml"
- Ensure the callback URL (http://localhost:8081/callback1) is properly set in the oauth2_registered_clienttable in the database.
 
- How to use
- Open the web browser by connecting to http://localhost:8370/oauth2/authorize?client_id=client_customer&state=xxx&scope=read&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fcallback1&code_challenge=HVoKJYs8JruAxs7hKcG4oLpJXCP-z1jJQtXpQte6GyA&code_challenge_method=S256, using the values from theoauth2_registered_client- PKCE (code_challege, code_challege_METHOD) is optional.
- PKCE adds a Code Verifier and a Code Challenge to the flow, enhancing the Authorization Code Grant Flow by preventing the issuance of an Access Token if the Authorization Code is compromised.
 
- PKCE (
- Login with cicd@test.com / 1234
- You will be redirected to
https://localhost:8081/callback1?code=215e9539-1dcb-4843-b1ea-b2d7be0a3c44&state=xxx- However, if patternhelloworld.securityhelper.authorization-code.consent=Yis set in theapplication.properties, it will be redirected to the consent page.
 
- However, if 
- You can login with the API in the Postman
 
- Open the web browser by connecting to 
- Use the following module for Blue-Green deployment:
- The above module references this app's Dockerfile and the entrypoint script in the .docker folder.
- You can create a pull request directly to the main branch.
- Integration tests in the client folder are sufficient for now, but you may add more if necessary.
- There is a lack of unit tests, so contributions to unit test code are welcome, which will help improve the overall codebase.

