Java Spring Boot Security Demo (with MongoDB)
A couple of weeks back I wanted to check out the Java Spring framework out of curiosity. My interest was especially in the OAuth2 authentication aspect of it and how the framework could reduce the required overhead for handling the authentication of frontend users and the OAuth2 Access Tokens etc.
There are plenty of tutorials out there on how to set up a basic authorization server as well as a basic resource server with Spring. They typically use an In-Memory-Database for the users, OAuth2 clients and tokens.
But unfortunately I had a simple set of constraints on how I wanted to implement this little project:
- use only the most recent stable version of Spring
- use Kotlin on top of Java
- use MongoDB as database backend for everything - including the data that the clients (via Web Frontend and REST API) could read and write
And that is where my rather lengthy journey began :)
One issue was that - as far as I can tell - the Spring Security Authorization Server package comes "only" with support for databases like MariaDB or MySQL that are supported through JDBC. But MongoDB doesn't use SQL and cannot really be used through JDBC with that package.
Another issue was that the people at Spring heavily changed the API for the Security framework which made it difficult to find examples on how to do a couple of the things I wanted to do with it.
And understanding the Spring DSL also took me quite a while.
But once I got past these obstacles I eventually found Spring to be a very powerful framework that provides solid security and requires a relatively small amount of code in order to get things rolling.
What the Demo encompasses
- Authorization Server:
- Users
- Username/Password
- Group
- User Information (first/last name, birthdate, address, etc.)
- Resource Access Control configuration
- OAuth2 Clients
- Client ID/Secret
- allowed Authorization Grant types
- allowed Scopes
- Resource Access Control configuration
- OAuth2 Access Tokens
- OpenID Connect
- storage of configuration values and issued OAuth2 Access Tokens in MongoDB collections
- scheduled task for pruning expired OAuth2 Access Tokens from the DB
- securing REST API endpoints (with Sessions, OAuth2 and Basic Authentication)
- custom application properties with more or less complex data structures
- reading/writing data to/from MongoDB - with automagically generated and custom queries
- Users
- Resource Server:
- verification of OAuth2 Access Tokens via JSON Web Keys (JWK)
- enforcement of Resource Access Control via roles that were granted an OAuth2 Client
- a couple of REST API endpoints for retrieving user information and user data
- passing data via models to and from REST API endpoints
- reading/writing data to/from MongoDB
- custom application properties
- Web Client:
- securing the web app by only allowing authenticated users to access the frontend
- custom application properties
- HTML rendering with the Thymeleaf template engine
- HTML Form validation
- automatic and manual retrieval of OAuth2 Access Tokens
- accessing remote OAuth2 secured REST API endpoints
Let's get started
First of all here's the GitHub repository that contains all the good stuff:
You can simply clone the repository to your PC by running
$ git clone https://github.com/tsitle/spring_demo-authsrv_rscsrv_webclient.git
Within the repositories' README you'll find all the commands necessary to run MongoDB and the apps.
Here's how to quickly get everything up and running:
$ echo "127.0.0.1 spring-demo-authsrv" | sudo tee -a /etc/hosts
$ ./docker-mongodb/dc-db-mongo.sh up
$ ./docker-run_apps/dc-run_apps.sh up
This will
- create a fresh MongoDB instance with no data in it
- create the MongoDB root user
- create the MongoDB user for the Authorization Server
- create the MongoDB user for the Resource Server
- start a Mongo Express instance which can then be used to look inside the DB
- start the Authorization Server
- start the Resource Server
- start the Web Client
But it might take a couple of minutes until everything is actually up and running.
The easiest way of monitoring the progress is to invoke
$ docker stats
and to wait until all Docker containers have a CPU load of about 0%.
Once that is the case you can head to your browser and open
http://127.0.0.1:8080
This will lead you to the Web Client's (pretty ugly) interface.
Default Users
(can be found in mod_auth_server/src/main/resources/application.properties):
Username | Password |
---|---|
admin@springdemo.org |
pw |
regular@springdemo.org |
pw |
nobody@springdemo.org |
pw |
To stop all the Docker containers you can simply run
$ ./docker-run_apps/dc-run_apps.sh down
$ ./docker-mongodb/dc-db-mongo.sh down
The Gradle Modules
The code is divided into four Gradle modules:
- mod_auth_server
- mod_resource_server
- mod_webclient
- mod_z_common
The latter only contains some classes that are shared by the other modules.
The other three each contain a configuration file
src/main/resources/application.properties
where essentially all of the configuration values are stored in.
That means that the code contains almost zero hard-coded configuration.
If you have OpenJDK 18 installed on your host PC you can also run the individual modules directly - preferably one module per Terminal - given that MongoDB is already running:
Terminal 1: $ ./gradlew :mod_auth_server:run
Terminal 2: $ ./gradlew :mod_resource_server:run
Terminal 3: $ ./gradlew :mod_webclient:run
And you can build JAR files for each module and then execute those as well:
$ ./gradlew :mod_auth_server:bootJar
$ ./gradlew :mod_resource_server:bootJar
$ ./gradlew :mod_webclient:bootJar
Terminal 1: $ java -jar mod_auth_server/build/libs/mod_auth_server-0.0.1-SNAPSHOT.jar
Terminal 2: $ java -jar mod_resource_server/build/libs/mod_resource_server-0.0.1-SNAPSHOT.jar
Terminal 3: $ java -jar mod_webclient/build/libs/mod_webclient-0.0.1-SNAPSHOT.jar
But keep in mind that the application.properties files will become part of those JAR files and cannot be edited afterwards.
Using MongoDB with the Authorization Server
Besides from the obvious configuration properties in the application.properties file there are two classes that need to be configured to be used by the Auth Server:
package com.ts.springdemo.authserver.config
[...]
import com.ts.springdemo.authserver.mongoshim.MongoOAuth2AuthorizationService
import com.ts.springdemo.authserver.mongoshim.MongoRegisteredClientRepository
[...]
@Configuration(proxyBeanMethods = false)
class AuthorizationServerConfig([...]) {
[...]
@Bean
fun registeredClientRepository(mongoTemplate: MongoTemplate): RegisteredClientRepository {
return MongoRegisteredClientRepository(mongoTemplate)
}
@Bean
fun authorizationService(mongoTemplate: MongoTemplate, registeredClientRepository: RegisteredClientRepository): OAuth2AuthorizationService {
return MongoOAuth2AuthorizationService(mongoTemplate, registeredClientRepository, customOAuth2AuthorizationRepository)
}
[...]
}
This will allow the Auth Server to store and retrieve information about OAuth2 Client authorizations that have been granted and about the OAuth2 Clients that are allowed to request Access Tokens in/from the database.
What the above code essentially does is to create an instance of MongoRegisteredClientRepository
i.e. MongoOAuth2AuthorizationService
that uses the already configured MongoTemplate (which tells Spring how to access the MongoDB).
Resource Access Control through Roles
In order to be able to configure who is allowed to do what (aka Resource Access Control) I wrote a couple of classes that make this possible.
Spring Security then enables us to secure individual URIs with respect to the HTTP method being used to access those URIs (e.g. GET, PUT, etc.).
First of all we need to define which resources exist that need Access Control.
This is done in mod_auth_server/src/main/resources/application.properties:
Each line needs to be prefixed with
"custom-app.resource-id-to-url-paths."
# Resource ID to URL Paths mappings
## for Auth Server
as_custom_userinfo_basicauth_api.srv=auth_srv
as_custom_userinfo_basicauth_api.paths[0]=/api/v1/userinfo/basicauth
as_custom_userinfo_oauth_api.srv=auth_srv
as_custom_userinfo_oauth_api.paths[0]=/api/v1/userinfo/oauth
as_custom_userinfo_web.srv=auth_srv
as_custom_userinfo_web.paths[0]=/ui/userinfo/in_browser
## for Resource Server
rs_bogus_api.srv=rsc_srv
rs_bogus_api.paths[0]=/api/v1/bogus
rs_bogus_api.paths[1]=/api/v2/bogus
rs_articles_api.srv=rsc_srv
rs_articles_api.paths[0]=/api/v1/articles
rs_products_api.srv=rsc_srv
rs_products_api.paths[0]=/api/v1/products
rs_custom_userinfo_api.srv=rsc_srv
rs_custom_userinfo_api.paths[0]=/api/v1/userinfo
So for every resource we have what I call a "Resource ID" Â (e.g. as_custom_userinfo_basicauth_api
) that then contains:
- the server the resource is located on (either
auth_srv
orrsc_srv
) - the URI path(s) that the resource uses (e.g.
/api/v1/userinfo/basicauth
)
Note that you can define multiple paths here. E.g.paths[0]=/api/v1/bogus
andpaths[1]=/api/v2/bogus
And then we need to define which users have access to which resources (aka Resource Access Rules):
Each line needs to be prefixed with
"custom-app.auth-server.users.<USERNAME>.resource-access."
as_custom_userinfo_basicauth_api.methods[0]=get
as_custom_userinfo_oauth_api.methods[0]=post
as_custom_userinfo_web.methods[0]=get
rs_bogus_api.methods[0]=put
rs_bogus_api.methods[1]=delete
rs_bogus_api.methods[2]=patch
rs_custom_userinfo_api.methods[0]=post
rs_articles_api.methods[0]=get
rs_articles_api.methods[1]=post
rs_products_api.methods[0]=get
rs_products_api.methods[1]=post
And we can do the same for OAuth2 Clients, too. All that changes is the prefix:custom-app.auth-server.oauth2-clients.<CLIENT_ID>.resource-access.
The above config properties then cause the users and OAuth2 Clients to obtain additional "roles".
Here are two examples for how the resource access rules translate to "roles":
Rule=as_custom_userinfo_basicauth_api.methods[0]=get
--> Role=RESOURCE.URI.AUTHSRV.AS_CUSTOM_USERINFO_BASICAUTH_API.GET
Rule=rs_bogus_api.methods[0]=put
--> Role=RESOURCE.URI.RSCSRV.RS_BOGUS_API.PUT
In the Auth Server the "roles" that a given user or OAuth2 Client possesses are then added to the Access Token. This is done within the classcom.ts.springdemo.authserver.service.AccessTokenEnhancingService
In the Resource Server these "roles" are then extracted from the Access Token before being used to ensure that the user or OAuth2 Client is actually allowed to access the resource.
This is done within the classcom.ts.springdemo.rscserver.config.oidc.CustomJwtAuthenticationConverter
The Web Client also extracts these "roles" within the classcom.ts.springdemo.oauthwebclient.service.CustomOidcUserService
But the Web Client should never use them for security purposes. They should rather only be used for modifying the navigation menu or displaying/hiding buttons and things along that line.
It is important to note that "roles" cannot be added to an existing Access Token (or JSON Web Token to be more precise) since Access Tokens are digitally signed by the Auth Server. Whenever the Resource Server receives an Access Token it then checks whether the Access Token has been altered in any way by using the Auth Server's JWK RSA Public Key.
And here's an example of how an REST API endpoint can be protected by using "roles":
package com.ts.springdemo.rscserver.config
[...]
import com.ts.springdemo.common.constants.AuthRole
import com.ts.springdemo.common.constants.AuthRscAcc
[...]
@EnableWebSecurity
class ResourceServerConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
[...]
val roAdmin = AuthRole.buildAuthRole(AuthRole.EnRoles.GRP_ADMIN)
val roRscAccArticlesGet = AuthRscAcc.buildAuthRole(
AuthRscAcc.EnSrv.RSC_SRV, "rs_articles_api", AuthRscAcc.EnMeth.GET
)
val roRscAccArticlesPost = AuthRscAcc.buildAuthRole(
AuthRscAcc.EnSrv.RSC_SRV, "rs_articles_api", AuthRscAcc.EnMeth.POST
)
[...]
val hasAuthForArtGet = "hasRole('${roAdmin}') or hasRole('${roRscAccArticlesGet}')"
val hasAuthForArtPost = "hasRole('${roAdmin}') or hasRole('${roRscAccArticlesPost}')"
[...]
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/v1/articles/**").access(hasAuthForArtGet)
.antMatchers(HttpMethod.POST, "/api/v1/articles/**").access(hasAuthForArtPost)
[...]
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
[...]
return http.build()
}
}
Available REST API Endpoints
Note that when using OAuth2 secured requests from the collection you'll have to acquire an Access Token in the "Authorization" tab before being able to make the request.
Auth | HTTP Method | URL |
---|---|---|
AC | GET | {{authServerBaseUrl}}/userinfo |
Basic | POST | {{authServerBaseUrl}}/api/v1/userinfo/basicauth |
CC | POST | {{authServerBaseUrl}}/api/v1/userinfo/oauth |
AC/CC | POST | {{rscServerBaseUrl}}/api/v1/userinfo |
AC/CC | GET | {{rscServerBaseUrl}}/api/v1/articles |
AC | POST | {{rscServerBaseUrl}}/api/v1/articles |
AC | GET | {{rscServerBaseUrl}}/api/v1/products |
AC | POST | {{rscServerBaseUrl}}/api/v1/products |
AC: OAuth2 Authorization Code Grant
CC: OAuth2 Client Credentials Grant
Basic: Basic Authentication
{{authServerBaseUrl}}
: http://spring-demo-authsrv:9000
{{rscServerBaseUrl}}
: http://127.0.0.1:8090
The individual endpoints may require different OAuth2 scopes and/or request bodies. Please see the Postman collection for more details.
That's all for now
I hope you'll find the demo useful and that this article helps understanding the key components.
Have fun and enjoy!