Logo

api

Spring Boot API Resource Server

Spring Boot API Resource Server

In this tutorial, you are going to learn how to integrate a Java Spring Boot resource server with FusionAuth. You will protect an API resource from unauthorized usage. You’ll be building it for ChangeBank, a global leader in converting dollars into coins.

The docker compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-springboot-api.

Prerequisites

The example repository has already set up a Spring Boot template application that includes a Maven wrapper. You can find out more about those by viewing:

This app was built using Java 17. You can change which version to use by editing the project’s pom.xml file. The code in this example should be compatible down to Java 8, though minor differences may occur.

General Architecture

A client wants access to an API resource at /resource. However, it is denied this resource until it acquires an access token from FusionAuth.

ClientResource ServerFusionAuthGET /resource404 Not AuthorizedPOST /api/login200 Ok(token)GET /resource200 Ok(resource)ClientResource ServerFusionAuth

Resource Server Authentication with FusionAuth

While the access token is acquired via the Login API above, this is for simplicity of illustration. The token can be, and typically is, acquired through one of the OAuth grants.

Getting Started

In this section, you’ll get FusionAuth up and running and create a resource server which will serve the API.

Clone The Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-springboot-api.git
cd fusionauth-quickstart-springboot-api

Run FusionAuth Via Docker

In the root directory of the repo you’ll find a Docker compose file (docker-compose.yml) and an environment variables configuration file (.env). Assuming you have Docker installed on your machine, you can stand up FusionAuth up on your machine with:

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth, called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to a certain initial state.

If you ever want to reset the FusionAuth system, delete the volumes created by docker-compose by executing docker-compose down -v, then re-run docker-compose up -d.

FusionAuth will be initially configured with these settings:

You can log into the FusionAuth admin UI and look around if you want, but with Docker/Kickstart you don’t need to.

Create Your Spring Boot Resource Server Application

Now you are going to create a Spring Boot API application. While this section builds a simple API, you can use the same configuration to integrate an existing API with FusionAuth.

We are going to be building an API backend for a banking application called ChangeBank. This API will have two endpoints:

Both endpoints will be protected such that a valid JSON web token (JWT) will be required in the Authorization header in order to be accessed. Additionally, the JWT must have a roles claim containing the appropriate role to use the endpoint.

Make a directory for this API.

mkdir spring-api && cd spring-api

Get The Template

Go to the Initializr site and download your own starter package. You will rely on two dependencies for this project:

This example uses:

If you choose different options, the configuration and code may be different.

Download and unzip the package into the spring-api directory.

mv /path/to/downloads/springapi.zip .
unzip springapi.zip
mv springapi/* .
mv springapi/.* .
rm springapi.zip
rmdir springapi

Install the Dependencies

./mvnw package
If you are on Windows substitute ./mvnw with .\mvnw.cmd

Configure the Spring Application Properties

Open src/main/resources/application.properties.

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9011
spring.security.oauth2.resourceserver.jwt.audiences=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e

We need two properties here:

If you are using the kickstart the OAuth metadata URL will be http://localhost:9011/.well-known/openid-configuration/d7d09513-a3f5-401c-9685-34ab6c552453.

Write Java Code

Now you are going to write some Java code. You are going to write two API controllers, their corresponding model objects and some Spring configuration.

This tutorial puts all these classes in the same package for simplicity, but for a production application, you’d separate these out.

Add Model Objects

We will need response objects for the API to return. One is the PanicResponse which returns a message when a successful POST call is made to /panic. In the base package create a new class PanicResponse.java.

package io.fusionauth.quickstart.springapi;

public class PanicResponse {

    public PanicResponse(String message) {
        this.message = message;
    }

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Next you need an object to show the breakdown from making change. This object will hold a BigDecimal total and Integers nickels and pennies. In the base package create a new class Change.java.

package io.fusionauth.quickstart.springapi;

import java.math.BigDecimal;

public class Change {
    private BigDecimal total;

    private Integer nickels;

    private Integer pennies;

    public BigDecimal getTotal() {
        return total;
    }

    public void setTotal(BigDecimal total) {
        this.total = total;
    }

    public Integer getNickels() {
        return nickels;
    }

    public void setNickels(Integer nickels) {
        this.nickels = nickels;
    }

    public Integer getPennies() {
        return pennies;
    }

    public void setPennies(Integer pennies) {
        this.pennies = pennies;
    }
}

Add Controllers

Next you need to add controllers to handle API calls. In the base package add a new class MakeChangeController.java.

package io.fusionauth.quickstart.springapi;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.math.RoundingMode;

@RestController
@RequestMapping("make-change")
public class MakeChangeController {

    @GetMapping
    public Change get(@RequestParam(required = false) BigDecimal total) {
        var change = new Change();
        change.setTotal(total);
        change.setNickels(total.divide(new BigDecimal("0.05"), RoundingMode.HALF_DOWN).intValue());
        change.setPennies(total.subtract(new BigDecimal("0.05")
                        .multiply(new BigDecimal(change.getNickels())))
                .multiply(new BigDecimal(100))
                .intValue());
        return change;
    }
}

In this class a total is a http request query parameter that is converted to a BigDecimal, and the get method divides the total into nickels and pennies and returns the response object.

There are three annotations that are important for the controller to work:

Now, add a controller for tellers to call in an emergency. In the base package add a class PanicController.java.

package io.fusionauth.quickstart.springapi;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("panic")
public class PanicController {

    @PostMapping
    public PanicResponse postPanic() {
        return new PanicResponse("We've called the police!");
    }
}

This class listens on /panic for a POST request and returns a response indicating that the police were called. The annotations are the same as MakeChangeController.java except for @PostMapping indicates a POST request handler.

Add Security

So far you have not done anything with auth, and the controllers are unaware of authentication at all. Now, you’ll protect your endpoints based on the roles encoded in the JWT you receive from FusionAuth. The decoded payload of a JWT for a teller might look like this:

{
  "aud": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "exp": 1689289585,
  "iat": 1689285985,
  "iss": "http://localhost:9011",
  "sub": "00000000-0000-0000-0000-111111111111",
  "jti": "ebaa4184-2320-47dd-925b-2e18756c635f",
  "authenticationType": "PASSWORD",
  "email": "teller@example.com",
  "email_verified": true,
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "roles": [
    "teller"
  ],
  "auth_time": 1689285985,
  "tid": "d7d09513-a3f5-401c-9685-34ab6c552453"
}

You need to tell Spring how to parse the roles out of the claim in the JWT, and for that you need a Converter. In the base package add a class CustomJwtAuthenticationConverter.java:

package io.fusionauth.quickstart.springapi;

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 java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    private static final String ROLES_CLAIM = "roles";

    private static final String EMAIL_CLAIM = "email";

    private static final String AUD_CLAIM = "aud";

    private final List<String> audiences;

    public CustomJwtAuthenticationConverter(List<String> audiences) {
        this.audiences = audiences;
    }

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        String email = jwt.getClaimAsString(EMAIL_CLAIM);
        if (!hasAudience(jwt)) {
            return new UsernamePasswordAuthenticationToken(email, "n/a");
        } else {
            Collection<GrantedAuthority> authorities = extractRoles(jwt);
            return new UsernamePasswordAuthenticationToken(email, "n/a", authorities);
        }
    }

    private boolean hasAudience(Jwt jwt) {
        return jwt.hasClaim(AUD_CLAIM)
                && jwt.getClaimAsStringList(AUD_CLAIM)
                .stream()
                .anyMatch(audiences::contains);
    }

    private List<GrantedAuthority> extractRoles(Jwt jwt) {
        return jwt.hasClaim(ROLES_CLAIM)
                ? jwt.getClaimAsStringList(ROLES_CLAIM)
                    .stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList())
                : List.of();
    }
}

This class implements the Converter interface and takes the roles claim and maps it to a Collection<GrantedAuthority> Spring will use to authorize the user. We set these as the authorities on the AuthenticationToken.

Because you are using a custom converter you also need to check the audience. Pull out the aud claim and validate that at least one of them is in the list of configured audiences for this application.

Now you can add a security configuration. In the base package create a new class SecurityConfiguration.java.

package io.fusionauth.quickstart.springapi;

import org.springframework.beans.factory.annotation.Autowired;
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.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

import java.util.List;

@Configuration
public class SecurityConfiguration {

    private final OAuth2ResourceServerProperties properties;

    public SecurityConfiguration(OAuth2ResourceServerProperties properties) {
        this.properties = properties;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        List<String> audiences = properties.getJwt().getAudiences();
        CustomJwtAuthenticationConverter converter = new CustomJwtAuthenticationConverter(audiences);

        return http.authorizeHttpRequests(authz -> authz
                        .requestMatchers("make-change")
                            .hasAnyAuthority("customer", "teller")
                        .requestMatchers("panic")
                            .hasAuthority("teller"))
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt.jwtAuthenticationConverter(converter)))
                .build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withIssuerLocation(properties.getJwt().getIssuerUri()).build();
    }
}

This class does a lot in a few lines of code. Let’s break it down:

The NimbusJwtDecoder will automatically check that the token is not expired based on the exp claim.

Run the API

Start the API resource server by running:

./mvnw spring-boot:run

Get a Token

There are several ways to acquire a token in FusionAuth, but for this example you will use the Login API to keep things simple.

First let’s try the requests as the teller@example.com user. Based on the configuration this user has the teller role and should be able to use both /make-change and /panic.

  1. Acquire an access token for teller@example by making the following request
curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "teller@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Copy the token from the response, which should look like this:

{
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTMwNTksImlhdCI6MTY4OTM1Mjk5OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMTExMTExMTExMTExIiwianRpIjoiY2MzNWNiYjUtYzQzYy00OTRjLThmZjMtOGE4YWI1NTI0M2FjIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InRlbGxlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiZTlmZGI5ODUtOTE3My00ZTAxLTlkNzMtYWMyZDYwZDFkYzhlIiwicm9sZXMiOlsiY3VzdG9tZXIiLCJ0ZWxsZXIiXSwiYXV0aF90aW1lIjoxNjg5MzUyOTk5LCJ0aWQiOiJkN2QwOTUxMy1hM2Y1LTQwMWMtOTY4NS0zNGFiNmM1NTI0NTMifQ.WLzI9hSsCDn3ZoHKA9gaifkd6ASjT03JUmROGFZaezz9xfVbO3quJXEpUpI3poLozYxVcj2hrxKpNT9b7Sp16CUahev5tM0-4_FaYlmUEoMZBKo2JRSH8hg-qVDvnpeu8nL6FXxJII0IK4FNVwrQVFmAz99ZCf7m5xruQSziXPrfDYSU-3OZJ3SRuvD8bMopSiyRvZLx6YjWfBsvGSmMXeh_8vHG5fVkq5w1IkaDdugHnivtJIivHuCfl38kQBgw9rAqJLJoKRHHW0Ha7vHIcS6OCWWMDIIVspLyQNcLC16pL9Nss_5v9HMofow1OvQ9sUSMrbbkipjKq2peSjG7qA",
    "tokenExpirationInstant": 1689353059670,
    "user": {
        ...
    }
}

Make the Request

Use the token as a Bearer token in the Authorization header. Make a request to /make-change with a query parameter total=5.12.

curl --location 'http://localhost:8080/make-change?total=5.12' \
--header 'Authorization: Bearer <your_token>'

Your response should look like this:

{
    "total": 5.12,
    "nickels": 102,
    "pennies": 2
}

You were authorized, success! You can try making the request without the Authorization header or with a different string rather than a valid token, and see that you are denied access.

Next call the /panic endpoint because you are in trouble!

curl --location --request POST 'http://localhost:8080/panic' \
--header 'Authorization: Bearer <your_token>'

This is a POST not a get because you want all your emergency calls to be non-idempotent.

Your response should look like this:

{
    "message": "We've called the police!"
}

Nice, help is on the way!

Now let’s try as customer@example.com who has the role customer. Acquire a token for customer@example.com.

curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "customer@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Your response should look like this:

{
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTQxMjMsImlhdCI6MTY4OTM1MzUyMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMjIyMjIyMjIyMjIyIiwianRpIjoiYjc2YWMwMGMtMDdmNi00NzkzLTgzMjgtODM4M2M3MGU4MWUzIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6ImN1c3RvbWVyQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImFwcGxpY2F0aW9uSWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJyb2xlcyI6WyJjdXN0b21lciJdLCJhdXRoX3RpbWUiOjE2ODkzNTM1MjMsInRpZCI6ImQ3ZDA5NTEzLWEzZjUtNDAxYy05Njg1LTM0YWI2YzU1MjQ1MyJ9.T1bELQ6a_ItOS0_YYpvqhIVknVMNeamcoC7BWnPjg2lgA9XpCmFA2mVnycoeuz-mSOHbp2cCoauP5opxehBR2lCn4Sz0If6PqgJgXKEpxh5pAxCPt91UyfjH8hGDqE3rDh7E4Hqn7mb-dFFwdfX7CMdKvC3dppMbXAGCZTl0LizApw5KIG9Wmt670339pSf5lzD38P9WAG5Wr7fAmVrIJPVu6yv2FoR-pMYD2lnAF63HWKknrWB-khmhr9ZKRLXKhP1UK-ThY1FSnmpp8eNblsBqCxf6WaYxYkdp5_F2e56M4sQwHzrg4P9tZGVCmMri4dShF3Ck7OGa7hel-iIPew",
    "tokenExpirationInstant": 1689354123118,
    "user": {
        ...
    }
}

Now use that token to call /make-change with a query parameter total=3.24

curl --location 'http://localhost:8080/make-change?total=3.24' \
--header 'Authorization: Bearer <your_token>'

Your response should look like this:

{
    "total": 3.24,
    "nickels": 64,
    "pennies": 4
}

So far so good. Now let’s try to call the /panic endpoint. (We’re adding the -i flag to see the headers of the response)

curl -i --request POST 'http://localhost:8080/panic' \
--header 'Authorization: Bearer <your_token>'

Your response should look like:

HTTP/1.1 403
WWW-Authenticate: Bearer
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Fri, 14 Jul 2023 16:59:28 GMT

Uh oh, I guess you are not allowed to do that.

Enjoy your secured resource server!

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your API in production, there are some things you’re going to want to do.

FusionAuth Integration

Security

Troubleshooting

Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, admin@example.com with a password of password at http://localhost:9011/admin.

Make sure you are making a POST call and using a token with the teller role.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'filterChain' defined in class path resource [io/fusionauth/quickstart/springapi/SecurityConfiguration.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'filterChain' threw exception with message: Error creating bean with name 'jwtDecoder' defined in class path resource [io/fusionauth/quickstart/springapi/SecurityConfiguration.class]: Failed to instantiate [org.springframework.security.oauth2.jwt.JwtDecoder]: Factory method 'jwtDecoder' threw exception with message: Cannot invoke "String.length()" because "this.input" is null

Make sure you set up the application.properties file correctly.

[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR : 
[INFO] -------------------------------------------------------------
[ERROR] /path/fusionauth-quickstart-springboot-api/spring-api/src/main/java/io/fusionauth/quickstart/springapi/CustomJwtAuthenticationConverter.java:[4,51] package org.springframework.security.authentication does not exist
[ERROR] /path/fusionauth-quickstart-springboot-api/spring-api/src/main/java/io/fusionauth/quickstart/springapi/CustomJwtAuthenticationConverter.java:[5,51] package org.springframework.security.authentication does not exist

Make sure you requested the two required dependencies when you used the Initializr.

You can always pull down a complete running application and compare what’s different.

git clone https://github.com/FusionAuth/fusionauth-quickstart-spring-api.git
cd fusionauth-quickstart-spring-api
docker-compose up -d
cd complete-app
./mvnw package spring-boot:run