REST API with OIDC, Spring, and FusionAuth.
Spring Security 5 OAuth support makes it relatively painless.
REST API with OIDC, Spring, and FusionAuth.
Objective
This article will create a very simple example of a Spring application that provides a basic REST API secured with OIDC using FusionAuth as the identity provider. Users will be able to have three roles, “basic”, “editor”, and “admin” with “admin” providing access to everything. There will be an API endpoint that can be called by anyone, an endpoint for authenticated users with the “basic” role, an endpoint for users with the “editor” role, and an endpoint for “admin” users. We will also use OpenAPI 3 with Swagger to document the API and provide a Swagger UI to test our calls with.
This is meant for someone who has knowledge of working with Java and Spring. If you are just starting off with either, you might want to look for some resources for those starting out before going on here. Also, this example is primarily about securing a REST API with FusionAuth, not a detailed examination of Spring support for REST.
Prerequisites
Java (I’m using Java 17 in the example)
FusionAuth instance (I’m using an installation on my local network)
curl (or some way to access the endpoints)
Some basic knowledge of Spring Boot, Java, and FusionAuth
Source Code
The source for this example can be found at Gitlab at https://gitlab.com/welarson/spring-rest-fusionauth-example
There is a branch called step1 that has the source code as it stands after completing step1. There is also a branch called step2 that has the source code as it stands after completing step2. Finally, there is a branch called step4 that has the source code as it stands at the end of step4 which is the final step. The main branch is the most current.
Step 1 — Create the Spring Project
We’ll start with a very simple server that provides a REST API. To speed things up, let’s use the Spring Initializr at https://start.spring.io

For dependencies, we’ll just use Spring Web, Spring Security, and OAuth2 Resource Server. That’s all that’s needed for this basic example.
As for the other choices, here’s what I’m going with for this example.
Project: Maven Project (I think Maven is more common)
Language: Java
Spring Boot: 2.6.2 (Latest release version at this time)
Group: net.example
Artifact: farest
Name: farest
Description: Simple REST API secured with FusionAuth
Package Name: net.example.farest
Packaging: Jar
Java: 17
This will create a zip file called “farest.zip” that contains the generated project. Once extracted, the project will look like this.

We have a project template now, but so far it doesn’t do anything. Time to move to step two.
Step 2 — Creating a REST API
In this step, we’re going to create a REST API, create a completely insecure security configuration so we can access that API, and add a Swagger UI interface to let us test that API with a nice GUI.
First, let’s add a property to set the port the server will use. The project generator put a couple of empty directories and an empty properties file in the {root}/src/main/resources directory. Since Spring also supports defining properties in a YAML file, let’s do that instead. So delete everything in {root}/src/main/resources and create {root}/src/main/resources/application.yml instead.
The project tree will look like this now:

Since the project is in git now, I’m not showing hidden files with all that git-related cruft. Also, you’ll see I added a license file, but that’s immaterial for this example.
Right now, the only thing we need for the application properties is to set the server port. I’m using 9080 for this example, but of course you can use whatever you want, just remember to be consistent with whatever port you use. So, let’s edit {root}/src/main/resources/application.yml
application.yml
server:
port: 9080
That’s it for now.
Now to define the API. All the endpoints in the API will be simple GET methods, and will just return which users are allowed to use the API endpoint, the authentication status of the user making the call, and the authorities that the user has.
Let’s create a POJO for the data that will be returned from our API. All the methods in our API will return data in the same format so we’ll just need a single class which we’ll call… SomeData.
Let’s create a new package net.example.farest.model and create the class SomeData in the new package. We’ll use a record class to keep things clean.
SomeData.java
package net.example.farest.model;
public record SomeData(String allowed,
boolean authenticated,
String authorities) {
}
Now let’s create the REST calls for our API. To do that let’s create a controller class BasicController in a new package named net.example.farest.controller.
BasicController.java
package net.example.farest.controller;
import net.example.farest.model.SomeData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1")
public class BasicController {
@GetMapping("/anyone")
public SomeData allowAnyone() {
return new SomeData("Anyone",
isAuthenticated(),
getAuthorities());
}
@GetMapping("/basic")
public SomeData allowBasicUser() {
return new SomeData("Basic User",
isAuthenticated(),
getAuthorities());
}
@GetMapping("/editor")
public SomeData allowEditorUser() {
return new SomeData("Editor User",
isAuthenticated(),
getAuthorities());
}
@GetMapping("/admin")
public SomeData allowAdminUser() {
return new SomeData("Admin User",
isAuthenticated(),
getAuthorities());
}
private boolean isAuthenticated() {
return false;
}
private String getAuthorities() {
return "";
}
}
As you can see, the BasicController is, indeed, very basic. Since we haven’t implemented any security yet, we just return placeholders for authenticated status and authorities.
At this point, we have a REST API, but with no security configuration. Calling the endpoints will just give 401 errors since we are using the defaults for Spring Security and by default it uses basic auth. So let’s create another new package and add an insecure security configuration so we can use the endpoints.
We’ll create the class SecurityConfig in the new package net.example.farest.config.
SecurityConfig.java
package net.example.farest.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest()
.permitAll();
}
}
Let’s take a quick look at this. SecurityConfig extends WebSecurityConfigurerAdapter so we inherit a lot of our configuration instead of having to implement the whole WebSecurityConfigurer interface. Also, we need to annotate SecurityConfig with @Configuration to let Spring know this class needs to be loaded as configuration and the @EnableWebSecurity annotation to indicate the type of security we want activated.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
Now let’s look at the configuration itself. While our very simple service isn’t going to do anything with CORS configuration (don’t worry, we’ll still have some CORS issues to deal with later), generally you will need to activate CORS support.
http.cors()
Next, we specify that we won’t be using sessions because this REST API service is stateless.
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
Since we aren’t using sessions, we also don’t need CSRF protection either so we’ll disable it. Again, since this service is so simple, this doesn’t really matter, but, in general, you’ll want to disable CSRF protection if your API is stateless.
.csrf().disable()
Finally, we will configure what is required to access our endpoints. At this point we are going to just allow all requests from anyone authenticated or not.
.authorizeRequests()
.anyRequest()
.permitAll();
Now let’s try it out.
From the root of the project directory run the Maven command to build and package the project.
./mvnw package
Then execute the Spring application.
java -jar target/farest-0.0.1-SNAPSHOT.jar
Now use curl to exercise the endpoints of the application.
curl http://localhost:9080/api/v1/anyone
curl http://localhost:9080/api/v1/basic
curl http://localhost:9080/api/v1/editor
curl http://localhost:9080/api/v1/admin
You should see JSON showing us who (in theory at this point) is allowed to use the endpoint with authentication always being false and no authorities present.

Using curl to exercise the endpoints isn’t especially convenient and will be a lot less so once we’ve secured the API, so now lets add Swagger’s UI to the mix. Since Swagger’s UI is built from OpenAPI 3 documentation, this means that we’ll also be adding API documentation as well.
We’ll be using the Springdoc implementation of OpenAPI 3 which isn’t provided by any of our current dependencies, so let’s add those dependencies to our Maven pom.xml file. First, let’s add a property with the version of the Springdoc implementation we’ll be using. At this time, the latest is 1.6.3, so we’ll use that.
In pom.xml…
...
<properties>
<java.version>17</java.version>
<springdoc.version>1.6.3</springdoc.version>
</properties>
...
Now we’ll add the actual dependencies, using the new property for the version.
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>${springdoc.version}</version>
</dependency>
...
Now let’s add some annotations to our rest controller, BasicController, to better document things.
In BasicController.java…
...
@Operation(summary = "Get some data for anyone")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/anyone")
public SomeData allowAnyone() {
return new SomeData("Anyone",
isAuthenticated(),
getAuthorities());
}
@Operation(summary = "Get some data for basic users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/basic")
public SomeData allowBasicUser() {
return new SomeData("Basic User",
isAuthenticated(),
getAuthorities());
}
@Operation(summary = "Get some data for editor users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/editor")
public SomeData allowEditorUser() {
return new SomeData("Editor User",
isAuthenticated(),
getAuthorities());
}
@Operation(summary = "Get some data for admin users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/admin")
public SomeData allowAdminUser() {
return new SomeData("Admin User",
isAuthenticated(),
getAuthorities());
}
...
The @Operation annotation lets us provide a summary of what the endpoint does, and the @ApiResponses annotation provides a description of possible responses. For the sake of simplicity, we’ll only document successful results along with the type and shape of the response data.
If you build and fire up the application again, you can now access the OpenAPI documentation at [http://localhost:9080/v3/api-docs](http://localhost:9080/v3/api-docs)

That’s nice, but we want an actual UI for our REST service. That can be accessed at [http://localhost:9080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config](http://localhost:9040/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config)

Nice. But you’ll notice that the title is “OpenAPI definition” with version 0. We can improve that with a couple more changes. First we’ll make another addition to the Maven pom.xml file to provide our service build information. We do that by adding an execution to the spring-boot-maven-plugin declaration.
...
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
...
Now we’ll add another configuration to the net.example.farest.config package with the OpenApi3Config class.
OpenApi3Config.java
package net.example.farest.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApi3Config {
private final BuildProperties buildProperties;
@Autowired
public OpenApi3Config(BuildProperties buildProperties) {
this.buildProperties = buildProperties;
}
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("Simple Example REST Service")
.description("""
Simple REST Service used to demonstrate
securing a Spring REST service with
FusionAuth.
""")
.version(buildProperties.getVersion())
);
}
}
All we are doing is just providing a title, description, and the current version for OpenAPI. The project structure will now look like the following.

Now rebuild and run the server again, and then load the Swagger UI again (IntelliJ IDE Tip, use the Maven compile task or rebuild project before running again from the IDE or the BuildProperties object won’t be created.) [http://localhost:9080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config](http://localhost:9040/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config)

That’s better. Now if we try out an endpoint (Use the “Try It Out” and then the “Execute” button) we get the expected result.

We can use the /api/v1/admin endpoint since we’re allowing everything and we get our placeholder values.
This simple example is obvisouly not for a production environment so we aren’t going to worry about disabling the OpenAPI 3 documentation and Swagger in production, but that’s easily done with the a couple of springdoc properties.
springdoc.api-docs.enabled=false
springdoc.swagger-ui.enabled=false
Okay, we now have a simple REST API with documentation and a testing UI. Now let’s secure it with FusionAuth.
Step 3 — Create a Tenant and Application in FusionAuth
I have FusionAuth installed on my local network at yorktown.net.lan. You’ll need to substitute the address where your FusionAuth instance is hosted (or just localhost if it’s installed on the same machine you’re using for building the example). I’m running FusionAuth on port 9011 which is the default port when installing FusionAuth. This article isn’t going to go into setting up a FusionAuth installation, but you can find all the info you need at FusionAuth’s website https://fusionauth.io.
We’ll start with a new tenant in order to avoid conflicting with settings for any other applications you may have set up on your FusionAuth instance.
Before creating the tenant, let’s create a key to use for it. In my experience, using the default key can cause problems with the Spring OAuth 2 library, but generating a new RSA key will work just fine. To do this, click on “Settings” in the FusionAuth sidebar and then select “Key Master”. From the droplist that says “Generate Elliptic”, select the option to “Generate RSA” and generate a RSA key pair named “Example Key”.

Now we’re ready to create a Tenant. Click on “Tenants” in the sidebar and then click the add button (the one with a plus). Give the new tenant a name of “Example” and we’ll use “example.net” for the Issuer.
As fair warning, I mention clicking the blue button with the disk icon to save a number of times since I have a terrible tendancy to forget it. Sorry if it gets repetitive.

Now switch to the “JWT” tab and set the values for “Access Token signing key” and “Id Token signing key” both to “Example Key (RS256)”. (The screenshot is is during editing, not adding, but the UI is the same.)

Save the new tenant by clicking on the blue button with the disk icon.
Now, we need to add a new application. Click on “Applications” on the sidebar and add a new one. We’ll give the application the name “SimpleREST”. Select “Example” for the tenant. Then add our three roles, basic, editor, and admin. “basic” should be a default role and and “admin” is a super role.

Once again, click on the blue disk button to save. Now click to edit our new application and set the OAuth settings. The only change we need to make is to add an authorized redirect URL: http://localhost:9080/swagger-ui/oauth2-redirect.html. While we are here, copy the values for the the “Client Id” and “Client Secret” as we’ll be needing those later.

Click on the blue disk button to save the application and now add a new User. Set the tenant to “Example”, then enter an Email and Username. Since we didn’t set up email for the Tenant or Application, toggle off the option to send email to set up a password and create a password manually (might want to make a note of that password as you’ll need it later).

Save the user. That should bring you to a details page for that new user. From here, register the user with the SimpleREST application. I’m assigning the user just the basic role for right now.

Almost there. I promised a CORS issue and now we are going to resolve it. We’ll need to add some CORS settings to let our Swagger-UI do logins for FusionAuth. Select “Settings” on the FusionAuth sidebar, then select “System” and edit the CORS filter.

Remember to save with that blue disk button again.
Okay, FusionAuth is set up for our application. Time to return to the java code and secure it.
Step 4 — Secure the REST service
We need to add some properties to our application.yml for all this OAuth stuff. The configuration values for your FusionAuth instance should be available at {FusionAuth Instance Address}/.well-known/openid-configuration. You may notice that the issuer isn’t “example.net” as we set earlier. That’s because this is the default tenant, not the tenant we created. We can use the tenantId query parameter to specify the tenant, but all the important stuff will be the same. We’ll use another property to set the proper issuer.

Spring will be able to configure most of the values from issuer-uri by using that openid-configuration info. However, we also need to set the jwk-set-uri value for our implementation of the JWT decoder. So we’ll set the properties spring.security.oauth2.resourceserver.jwt.issuer-uri and spring.security.oauth2.resourceserver.jwt.jwk-set-uri.
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://yorktown.net.lan:9011/.well-known/jwks.json
issuer-uri: http://yorktown.net.lan:9011/
Then we’ll add some of our own properties that we’ll use to provide the proper issuer and to provide some values for our OpenAPI3 configuration. The issuer property will be oidc.issuer and set to “example.net” to match our tenant. The authorization endpoint will be set with oidc.auth-url and the token_endpoint will be set with oidc.token-url.
oidc:
issuer: example.net
auth-url: http://yorktown.net.lan:9011/oauth2/authorize
token-url: http://yorktown.net.lan:9011/oauth2/token
The Swagger UI has to actually do a login so it will need the client-id and client-secret values we copied down when creating the SimpleREST application in FusionAuth. That will allow Swagger to perform a authentication-code flow for loging in a user. In a real application, you wouldn’t want to put the raw values in the yml file, but for this simple example it will do.
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://yorktown.net.lan:9011/.well-known/jwks.json
issuer-uri: http://yorktown.net.lan:9011/
The end application.yml should look like this:
server:
port: 9080
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://yorktown.net.lan:9011/.well-known/jwks.json
issuer-uri: http://yorktown.net.lan:9011/
oidc:
issuer: example.net
auth-url: http://yorktown.net.lan:9011/oauth2/authorize
token-url: http://yorktown.net.lan:9011/oauth2/token
springdoc:
swagger-ui:
oauth:
client-id: <YOUR CLIENT ID>
client-secret: <YOUR CLIENT SECRET>
We’ll need to make some big changes to our security configuration, but part of that will be telling Spring how to figure out what a user’s authorities are. Left to its own devices, Spring will add a role called “ROLE_USER” and add any OAuth2 scopes to a user’s authorities. However, what we want are the roles that we have defined for the user in FusionAuth. To accomplish this, we will create our own implmentation for a converter from JWT to an authentication token.
package net.example.farest.security;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
public class OidcJwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String EMAIL_CLAIM = "email";
private static final String ROLES_CLIAM = "roles";
@Override
public AbstractAuthenticationToken convert(final Jwt jwt) {
return new UsernamePasswordAuthenticationToken(
getUserName(jwt), "n/a", getAuthorities(jwt));
}
private String getUserName(final Jwt jwt) {
return jwt.getClaimAsString(EMAIL_CLAIM);
}
private Collection<GrantedAuthority> getAuthorities(final Jwt jwt) {
return this.getRoles(jwt).stream()
.map(role -> new SimpleGrantedAuthority(role.toLowerCase()))
.collect(Collectors.toSet());
}
private Collection<String> getRoles(final Jwt jwt) {
final var claim = jwt.getClaims().get(ROLES_CLIAM);
if (claim instanceof String roles && StringUtils.hasText(roles)) {
return Arrays.asList(roles.split(" "));
}
if (claim instanceof Collection<?> roles) {
return roles.stream()
.map(Object::toString)
.collect(Collectors.toSet());
}
return Collections.emptyList();
}
}
This is also an excellent place to load any additional user information you may need into the UsernamePasswordAuthenticationToken. The first argument (the Principal) is an Object so while we are just setting it to the user’s email address, you could put in whatever object you want to represent the user.
As for the authorities, we are going to get them from the “roles” claim. FusionAuth will provide the roles in that claim as a collection.
Now let’s modify our security configuration. The new security configuration will look like the below.
package net.example.farest.config;
import net.example.farest.security.OidcJwtAuthConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final OAuth2ResourceServerProperties oauth2Properties;
private final String issuer;
@Autowired
public SecurityConfig(OAuth2ResourceServerProperties oauth2Properties,
@Value("${oidc.issuer}") String issuer) {
this.oauth2Properties = oauth2Properties;
this.issuer = issuer;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/v1/anyone").permitAll()
.antMatchers("/swagger-ui/**").permitAll()
.antMatchers("/v3/api-docs/**").permitAll()
.anyRequest()
.fullyAuthenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new OidcJwtAuthConverter());
}
@Bean
public JwtDecoder jwtDecoder() {
final var jwtDecoder =
NimbusJwtDecoder.withJwkSetUri(oauth2Properties.getJwt().getJwkSetUri())
.build();
final var withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
jwtDecoder.setJwtValidator(withIssuer);
return jwtDecoder;
}
}
We’ve added an @EnableGlobalMethodSecurity annotation with the value prePostEnabled=true. This will let us use annotations to specify the authorities required for an endpoint to be accessed.
...
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
We didn’t need to inject any beans or values before, but with our more complex configuration, we’ll need the bean with Spring’s OAuth2 properties. In addition to that, we’ll also need the issuer that we defined for our tenant (“example.net”) which was placed in the oidc.issuer property.
...
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final OAuth2ResourceServerProperties oauth2Properties;
private final String issuer;
@Autowired
public SecurityConfig(OAuth2ResourceServerProperties oauth2Properties,
@Value("${oidc.issuer}") String issuer) {
this.oauth2Properties = oauth2Properties;
this.issuer = issuer;
}
...
Our HTTPSecurity configuration starts out the same, but instead of allowing any request for anyone, we’ll only allow a select few for anyone. One endpoint in our API is available for anybody, authenticated or not, so that one needs to be allowed for all. We also need to allow unauthenticated users to access the api-documentation and the Swagger UI so we can test our API. All other requests will require that the user is fully authenticated.
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/v1/anyone").permitAll()
.antMatchers("/swagger-ui/**").permitAll()
.antMatchers("/v3/api-docs/**").permitAll()
.anyRequest()
.fullyAuthenticated()
...
Next, we need to specify that our authentication will be provided by OAuth2. We do that by using the oauth2ResourceServer() configuration and telling it that we want to use our customer converter OidcJwtAuthConverter to convert the JWT token to an Authentication instance.
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/v1/anyone").permitAll()
.antMatchers("/swagger-ui/**").permitAll()
.antMatchers("/v3/api-docs/**").permitAll()
.anyRequest()
.fullyAuthenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new OidcJwtAuthConverter());
}
...
We also need to provide a JWT decoder bean. Spring’s OAuth library provides us with the NimbusJwtDecoder so we’ll use that to construct our JwtDecoder, giving it the jwt-set-uri that we set in Spring’s OAuth properties. Then we create our own validator for the issuer since Spring won’t be able to figure out the correct value for our tenant. We tell the decoder to use that issuer validation and return the decoder.
...
@Bean
public JwtDecoder jwtDecoder() {
final var jwtDecoder =
NimbusJwtDecoder.withJwkSetUri(oauth2Properties.getJwt().getJwkSetUri())
.build();
final var withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
jwtDecoder.setJwtValidator(withIssuer);
return jwtDecoder;
}
...
Now let’s modify our REST controller to limit access to the endpoints based on the authorities of the user and to actually figure out if the user is authenticated and list authorities.
package net.example.farest.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import net.example.farest.model.SomeData;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1")
public class BasicController {
@Operation(summary = "Get some data for anyone")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/anyone")
@PreAuthorize("permitAll()")
public SomeData allowAnyone(Authentication authentication) {
return new SomeData("Anyone",
isAuthenticated(authentication),
getAuthorities(authentication));
}
@Operation(summary = "Get some data for basic users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/basic")
@PreAuthorize("hasAuthority('basic') or hasAuthority('admin')")
public SomeData allowBasicUser(Authentication authentication) {
return new SomeData("Basic User",
isAuthenticated(authentication),
getAuthorities(authentication));
}
@Operation(summary = "Get some data for editor users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/editor")
@PreAuthorize("hasAuthority('editor') or hasAuthority('admin')")
public SomeData allowEditorUser(Authentication authentication) {
return new SomeData("Editor User",
isAuthenticated(authentication),
getAuthorities(authentication));
}
@Operation(summary = "Get some data for admin users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/admin")
@PreAuthorize("hasAuthority('admin')")
public SomeData allowAdminUser(Authentication authentication) {
return new SomeData("Admin User",
isAuthenticated(authentication),
getAuthorities(authentication));
}
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && authentication.isAuthenticated();
}
private String getAuthorities(Authentication authentication) {
return isAuthenticated(authentication)
? authentication.getAuthorities().toString()
: "";
}
}
With our updated security configuration we can use the @PreAuthorize annotation to set security on our endpoint methods. The endpoint /api/v1/anyone is allowed for, well, anyone so we’ll permit everyone to access it.
@Operation(summary = "Get some data for anyone")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/anyone")
@PreAuthorize("permitAll()")
public SomeData allowAnyone(Authentication authentication) {
return new SomeData("Anyone",
isAuthenticated(authentication),
getAuthorities(authentication));
}
The endpoint /api/v1/basic is available for users with the “basic” or “admin” roles so we’ll use an expression that allows a “basic” authority or an “admin” authority.
@Operation(summary = "Get some data for basic users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/basic")
@PreAuthorize("hasAuthority('basic') or hasAuthority('admin')")
public SomeData allowBasicUser(Authentication authentication) {
return new SomeData("Basic User",
isAuthenticated(authentication),
getAuthorities(authentication));
}
The endpoint /api/v1/editor is available for users with the “editor” or “admin” roles so we’ll use an expression that allows a “editor” authority or an “admin” authority.
@Operation(summary = "Get some data for editor users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/editor")
@PreAuthorize("hasAuthority('editor') or hasAuthority('admin')")
public SomeData allowEditorUser(Authentication authentication) {
return new SomeData("Editor User",
isAuthenticated(authentication),
getAuthorities(authentication));
}
The endpoint /api/v1/admin is available for only admin users so the expression will only allow users with the “admin” authority.
@Operation(summary = "Get some data for admin users")
@ApiResponses({
@ApiResponse(responseCode = "200",
description = "Success",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SomeData.class))})
})
@GetMapping("/admin")
@PreAuthorize("hasAuthority('admin')")
public SomeData allowAdminUser(Authentication authentication) {
return new SomeData("Admin User",
isAuthenticated(authentication),
getAuthorities(authentication));
}
We also need to replace our isAuthenticated() and getAuthorities() methods with real implementations. All the endpoints now have a Authentication parameter which Spring will automatically populate for us. We can then pass that Authentication instance to our updated methods to get the authentication and authorization information.
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && authentication.isAuthenticated();
}
private String getAuthorities(Authentication authentication) {
return isAuthenticated(authentication)
? authentication.getAuthorities().toString()
: "";
}
Our REST API is now secured, but we still need to configure our Swagger UI so we have a handy test tool. Let’s add some security settings to our OpenAPI3Config class.
package net.example.farest.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class OpenApi3Config {
private final BuildProperties buildProperties;
private final String authUrl;
private final String tokenUrl;
@Autowired
public OpenApi3Config(BuildProperties buildProperties,
@Value("${oidc.auth-url}") String authUrl,
@Value("${oidc.token-url}") String tokenUrl) {
this.buildProperties = buildProperties;
this.authUrl = authUrl;
this.tokenUrl = tokenUrl;
}
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("oauth2", new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.description("OAuth2 Flow")
.flows(new OAuthFlows()
.authorizationCode(new OAuthFlow()
.authorizationUrl(authUrl)
.tokenUrl(tokenUrl)
.scopes(new Scopes())
)
)
)
)
.security(List.of(new SecurityRequirement()
.addList("oauth2")))
.info(new Info()
.title("Simple Example REST Service")
.description("""
Simple REST Service used to demonstrate
securing a Spring REST service with
FusionAuth.
""")
.version(buildProperties.getVersion())
);
}
}
So let’s take a quick look at what is happening here. First of all, we need to inject a couple of values from our application properties so we can configure the authorization url and token url in the OAuth flow.
@Configuration
public class OpenApi3Config {
private final BuildProperties buildProperties;
private final String authUrl;
private final String tokenUrl;
@Autowired
public OpenApi3Config(BuildProperties buildProperties,
@Value("${oidc.auth-url}") String authUrl,
@Value("${oidc.token-url}") String tokenUrl) {
this.buildProperties = buildProperties;
this.authUrl = authUrl;
this.tokenUrl = tokenUrl;
}
Next, we add the actual security scheme to our OpenAPI configuration. We are using OAUTH2 type security and adding an authorization code flow for authenticating a user. We need to configure the authorization url and token url so Swagger can perform the login operation. We’re not concerned with the scopes, so we’ll just leave that empty. Because we also added springdoc properties for the client id and secret, the Swagger user won’t have to know those values to perform the authentication flow.
...
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("oauth2", new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.description("OAuth2 Flow")
.flows(new OAuthFlows()
.authorizationCode(new OAuthFlow()
.authorizationUrl(authUrl)
.tokenUrl(tokenUrl)
.scopes(new Scopes())
)
)
)
)
...
Finally, we add the security scheme to the list of configured security requirements for the API.
...
.security(List.of(new SecurityRequirement()
.addList("oauth2")))
.info(new Info()
...
Time to build and execute the service again.
./mvnw package
java -jar target/farest-0.0.1-SNAPSHOT.jar
I suggest opening up a private or incognito window to test it out in order to avoid conflicts with being logged into the FusionAuth instance at the same time. We are probably okay since it is a different tenant, but it’s one less thing to worry about. Now use the Swagger url to bring up Swagger. http://localhost:9080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config

You’ll see that there is now an “Authorize” button. Clicking that button will allow us to authenticate with FusionAuth. However, before authenticating, let’s try a couple of endpoints. First, we’ll try the /api/v1/anyone endpoint that we configured to accessable to everyone.

It works, and we are informed that the user is unauthenticated and has no authorities. Of course, that’s exactly how it worked before.
Next, let’s try /api/v1/basic which requires an authenticated user with the “basic” or “admin” role.

This time around we get a 401 error since the user is unauthenticated. So far, so good.
Now, click on the “Authorize” button, and we’ll get a dialog with the available authorizations. The only one available is for OAuth2 authorization code. The client_id and client_secret fields are pre-filled so we don’t have to worry about them.

Clicking “Authorize” takes us to FusionAuth for authentication. Go ahead and log in as the basic user that we created in FusionAuth.

This takes us back to Swagger. Close the authorization dialog and let’s try that /api/v1/anyone endpoint again.

Once again, the call is successful, but now we can see that the user is authenticated and has the “basic” authority.
Now let’s try the /api/v1/basic endpoint again.

Unlike last time, this call is successful, telling us again that the user is authenticated and has the “basic” authority.
Finally, let’s try the /api/v1/admin endpoint.

Now we get a 403 Forbidden error since the user is authenticated, but doesn’t have the required authority to use the endpoint. Feel free to try users with other authorities, but you’re probably ready for this article to wrap up.
As you can see, there isn’t really a lot of code needed to implement the FusionAuth support and, for the most part, the same code would work for other identity providers as well. Spring takes care of most of the work for you. Even adding in support for Swagger UI doesn’t take much more effort, though it isn’t always obvious how to do it from the documentation.
I hope you’ve found this helpful.
As mentioned earlier, you can find the source code for this example on Gitlab at https://gitlab.com/welarson/spring-rest-fusionauth-example.
Happy coding!
메타데이터
- post_id
- f8a7915e4d06
- slug
- rest-api-with-oidc-spring-and-fusionauth-f8a7915e4d06
- url
- https://blog.devgenius.io/rest-api-with-oidc-spring-and-fusionauth-f8a7915e4d06
- canonical_url
- https://blog.devgenius.io/rest-api-with-oidc-spring-and-fusionauth-f8a7915e4d06
- author_url
- https://medium.com/@w.e.larson
- status
- ok
- fetched_at
- 2026-08-04 07:36:05