> For the complete documentation index, see [llms.txt](/docs/llms.txt)

# Spring Boot API | FusionAuth Docs

Quickstart integration of a Spring Boot API application action as a resource server with FusionAuth.

# Spring Boot API

In this quickstart, 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](https://www.youtube.com/watch?v=CXDxNCzUspM), 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-java-springboot-api](https://github.com/FusionAuth/fusionauth-quickstart-java-springboot-api).

## Prerequisites[#](#prerequisites)

*   [Java](https://www.oracle.com/java/technologies/downloads/): Java can be installed via a variety of methods. 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.
*   [Docker](https://www.docker.com): The quickest way to stand up FusionAuth. Ensure you also have [docker compose](https://docs.docker.com/compose/) installed.
*   (Alternatively, you can [install FusionAuth manually](/docs/get-started/download-and-install/fusionauth-app)).

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

*   [Spring Initializr](https://start.spring.io/): The Spring Boot template generator webpage.
*   [Maven Wrapper](https://maven.apache.org/wrapper/): A self-contained version of the Maven build tool which will download the application dependencies and compile the source code.

## General Architecture[#](#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.

```mermaid
    sequenceDiagram
        participant Client
        participant App as Resource Server
        participant FusionAuth

        Client ->> App : GET /resource
        App ->> Client : 401 Not Authorized
        Client ->> FusionAuth : POST /api/login
        FusionAuth ->> Client : 200 Ok<br/>(token)
        Client ->> App : GET /resource
        App ->> Client : 200 Ok<br/>(resource) 
```

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](/docs/lifecycle/authenticate-users/oauth/).

## Getting Started[#](#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[#](#clone-the-code)

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

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

### Run FusionAuth Via Docker[#](#run-fusionauth-via-docker)

You'll find a Docker Compose file (`docker-compose.yml`) and an environment variables configuration file (`.env`) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

```
docker compose up -d
```

Here you are using a bootstrapping feature of FusionAuth called [Kickstart](/docs/get-started/download-and-install/development/kickstart). When FusionAuth comes up for the first time, it will look at the `kickstart/kickstart.json` file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to 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:

*   Your client Id is `e9fdb985-9173-4e01-9d73-ac2d60d1dc8e`.
*   Your client secret is `super-secret-secret-that-should-be-regenerated-for-production`.
*   Your example teller username is `teller@example.com` and the password is `password`. They will have the role of `teller`.
*   Your example customer username is `customer@example.com` and the password is `password`. They will have the role of `customer`.
*   Your admin username is `admin@example.com` and the password is `password`.
*   The base URL of FusionAuth is `http://localhost:9011/`.

You can log in to the [FusionAuth admin UI](http://localhost:9011/admin) and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the [FusionAuth app](http://localhost:9011/admin). The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the `View` icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the `OAuth configuration` section.

The `.env` file contains passwords. In a real application, always add this file to your `.gitignore` file and never commit secrets to version control.

## Create Your Spring Boot Resource Server Application[#](#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:

*   `make-change`: This endpoint will allow you to call GET with a `total` amount and receive a response indicating how many nickels and pennies are needed to make change. Valid roles are `customer` and `teller`.
*   `panic`: Tellers may call this endpoint to call the police in case of an incident. The only valid role is `teller`.

Both endpoints will be protected such that a valid JSON web token (JWT) will be required in a cookie or 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[#](#get-the-template)

[Go to the Initializr site](https://start.spring.io/) and download your own starter package. You will rely on two dependencies for this project:

*   OAuth2 Resource Server
*   Spring Web

This example uses:

*   Maven
*   Spring Boot version `3.1.1`
*   Java 17
*   A `Group` of `io.fusionauth.quickstart`.
*   A `Name` of `FusionAuthQuickstart`.
*   An `Artifact` of `springapi`.

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[#](#install-the-dependencies)

```
./mvnw package
```

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

### Configure the Spring Application Properties[#](#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:

*   `spring.security.oauth2.resourceserver.jwt.issuer-uri` - This is set to the location of FusionAuth. Spring will call the `OpenID Connect Discovery` endpoint which can be found under the details view for the Example Application in the [Applications View](http://localhost:9011/admin/application/) in the FusionAuth admin app. This endpoint allows Spring to look up all the OAuth metadata that it needs to validate tokens created by FusionAuth.
*   `spring.security.oauth2.resourceserver.jwt.audiences` - This is a list of the application ids in FusionAuth allowed for this resource server. FusionAuth will populate this in the `aud` claim of the JWTs and you will use that claim to validate that the token is intended for this resource server.

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[#](#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[#](#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[#](#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:

*   `@RestController` tells Spring this is a `Controller` that returns a `ResponseBody`
*   `@RequestMapping` tells Spring which path this controller responds to
*   `@GetMapping` tells Spring this method handles GET requests

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[#](#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.http.HttpHeaders;
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.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.security.web.SecurityFilterChain;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Configuration
public class SecurityConfiguration {

    private final OAuth2ResourceServerProperties properties;

    public SecurityConfiguration(OAuth2ResourceServerProperties properties) {
        this.properties = properties;
    }
    
    @Bean
    BearerTokenResolver bearerTokenResolver() {
        // look in both app.at cookie and Authorization header
        BearerTokenResolver bearerTokenResolver = new BearerTokenResolver () {
            public String resolve(HttpServletRequest request) {
               Cookie[] cookies = request.getCookies();

               if (cookies != null) {
                   Optional<Cookie> cookie = Arrays.stream(cookies)
                       .filter(name -> name.getName().equals("app.at"))
                       .findFirst();
                   if (cookie.isPresent()) {
                       return cookie.get().getValue();
                   }
               }

               // handles authorization header
               DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
               return defaultBearerTokenResolver.resolve(request);
            }
        };
        return bearerTokenResolver;
    }

    @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:

*   `@Configuration` tells spring to autowire this configuration class and `@Bean` wires the `SecurityFilterChain` which will add the rules for handling the security on HTTP requests.
*   `OAuth2ResourceServerProperties` is injected into the configuration bean and holds the values you defined in the `application.properties` file.
*   The custom `BearerTokenResolver` looks in both the `app.at` cookie and the `Authorization` header for the access token
*   In the `SecurityFilterChain` you do the following:
    1.  Get `audiences` list from the `OAuth2ResourceServerProperties`
    2.  Create a new instance of `CustomJwtAuthenticationConverter` and pass it the `audiences` list
    3.  Tell the filter chain that any requests to `/make-change` must have the `customer` or `teller` role defined in the `GrantedAuthority` list you extract from the JWT.
    4.  Tell the filter chain that any requests to `/panic` must have the `teller` role defined in the `GrantedAuthority` list you extract from the JWT.
    5.  Set the `oauth2ResourceServer` configuration to use the `CustomJwtAuthenticationConverter` you defined above.
*   `JwtDecoder` this bean tells spring to decode the JWT in the request using the information from the `issuer` you defined in the properties file.

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

## Run the API[#](#run-the-api)

Start the API resource server by running:

```
./mvnw spring-boot:run
```

### Get a Token[#](#get-a-token)

There are [several ways to acquire a token](/docs/lifecycle/authenticate-users/oauth/) in FusionAuth, but for this example you will use the [Login API](/docs/apis/login) 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.com` 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"
    }'
    ```
    
2.  Copy the `token` from the response, which should look like this:
    
    ```
    {
        "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3M   y1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTMwNTksImlhdCI6MTY4OTM1Mjk5OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMTExMTExMTExMTExIiwianRpIjoiY2MzNWNiYjUtYzQzYy00OTRjLThmZjMtOGE4YWI1NTI0M2FjIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InRlbGxlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiZTlmZGI5ODUtOTE3My00ZTAxLTlkNzMtYWMyZDYwZDFkYzhlIiwicm9sZXMiOlsiY3VzdG9tZXIiLCJ0ZWxsZXIiXSwiYXV0aF90aW1lIjoxNjg5MzUyOTk5LCJ0aWQiOiJkN2QwOTUxMy1hM2Y1LTQwMWMtOTY4NS0zNGFiNmM1NTI0NTMifQ.WLzI9hSsCDn3ZoHKA9gaifkd6ASjT03JUmROGFZaezz9xfVbO3quJXEpUpI3poLozYxVcj2hrxKpNT9b7Sp16CUahev5tM0-4_FaYlmUEoMZBKo2JRSH8hg-qVDvnpeu8nL6FXxJII0IK4FNVwrQVFmAz99ZCf7m5xruQSziXPrfDYSU-3OZJ3SRuvD8bMopSiyRvZLx6YjWfBsvGSmMXeh_8vHG5fVkq5w1IkaDdugHnivtJIivHuCfl38kQBgw9rAqJLJoKRHHW0Ha7vHIcS6OCWWMDIIVspLyQNcLC16pL9Nss_5v9HMofow1OvQ9sUSMrbbkipjKq2peSjG7qA",
        "tokenExpirationInstant": 1689353059670,
        "user": {
            ...
        }
    }
    ```
    

### Make The Request[#](#make-the-request)

The code is set up to extract the token from either a cookie or the `Authorization` header so depending on your preference you can replace `--cookie 'app.at=<your_token>'` with `--header 'Authorization: Bearer <your_token>'` when making requests to the API.

If you use a cookie, make sure you store it in a secure, HttpOnly cookie to avoid exfiltration attacks. See [Storing OAuth Tokens](/articles/oauth/oauth-token-storage) for more information.

Make a request to `/make-change` with a query parameter `total=5.12`. Use the `token` as the `app.at` cookie.

```
curl --location 'http://localhost:8080/make-change?total=5.12' \
--cookie 'app.at=<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' \
--cookie 'app.at=<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' \
--cookie 'app.at=<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' \
--cookie 'app.at=<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!

## Made it this far? Want a free t-shirt? We got ya.

Thank you for spending some time getting familiar with FusionAuth.

\*Offer only valid in the United States and Canada, while supplies last.

## Next Steps[#](#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

*   Rather than call the Login API, you're probably going to want to use the [Authorization Code grant](/docs/lifecycle/authenticate-users/oauth/#example-authorization-code-grant), which keeps all sensitive credentials within the bounds of FusionAuth. You can customize the [hosted login pages](/docs/customize/look-and-feel/).
*   You may want to generate a token using the [Client Credentials grant](/docs/lifecycle/authenticate-users/oauth/#example-client-credentials-grant) if you are calling the API from another service.

### Security

*   Customize the [token expiration times and policies](/docs/lifecycle/authenticate-users/oauth/#configure-application-oauth-settings) in FusionAuth
*   [Make sure you know how to securely consume a token](/articles/tokens/building-a-secure-jwt#consuming-a-jwt)
*   Secure your API [using an API gateway](/docs/extend/examples/api-gateways/) rather than at the framework layer.

## Troubleshooting[#](#troubleshooting)

*   I get `This site can't be reached localhost refused to connect.` when I call the Login API.

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](http://localhost:9011/admin).

*   The `/panic` endpoint doesn't work when I call it.

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

*   I'm getting an error when running `./mvnw spring-boot:run` like this:

```
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.

*   I'm getting an error when running `./mvnw spring-boot:run` like this:

```
[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.

*   It still doesn't work

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

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