close icon
Spring Boot

Add Security and Authorization to a Java Spring Boot API

Learn how to use Spring Boot, Java, and Auth0 to secure a feature-complete API by implementing authorization in Spring Boot with Auth0.

March 20, 2024

Learn how to use Spring Boot, Java, and Auth0 to secure a feature-complete API by implementing authorization in Spring Boot with Auth0.

This tutorial was created with the following tools and services:

Authentication and Authorization on the Web

Authentication is the process of proving a user's identity, proving they are who they claim to be. Authorization is the process of giving someone the ability to access a resource.

In computer systems, authentication and authorization are part of a discipline called Identity and Access Management (IAM). OAuth 2.0 is a standard designed to authorize a website or application to access resources hosted by third-party services on behalf of a user. For web and mobile applications, an identity protocol was born in 2014, OpenID Connect 1.0 (OIDC), a simple identity layer on top of OAuth 2.0, now widely adopted as part of the IAM strategy of many identity providers and identity clients on the internet.

To authorize access to a protected resource, OAuth 2.0 uses Access Tokens. An access token is a piece of data representing an authorization issued to the client. It may also include scopes and durations of the access. The token is validated at the resource server. Briefly, the standard defines the roles of the resource owner, resource server, client, and authorization server, as well as a protocol flow for the client requesting access through an authorization server to resources controlled by the resource owner and hosted by the resource server.

OpenID Connect provides authentication built on top of OAuth 2.0, and information about the authentication performed is returned in an ID Token with JWT format (JSON Web Token). In the abstract, the protocol defines a client role or Relying Party that sends a request to the OpenID Provider, which in turn authenticates the end-user and obtains authorization, returning an ID Token and Access Token to the Relying Party.

Authorization in a Spring Boot API

After the year 2020, in Buenos Aires, many bars and restaurants implemented a digital menu with a QR code that translates to a public document for having an updated price list. Prices change so often due to inflation, and updating a physical menu seems tedious and costly. Still, restaurant tables are managed with a software application, which includes a menu management module.

For learning purposes, let's assume you have built a Spring Boot menu API that must be secured so only authorized users can make requests to its endpoints. Now, you are going to implement authorization for the API with OAuth 2.0 and Auth0. Start by doing a checkout of the API repository, which already implements basic request handling:

git clone https://github.com/oktadev/spring-menu-api.git

The repository contains two project folders, start and demo. The bare bones menu API is a Gradle project in the start folder, open it with your favorite IDE. If you would rather skip the step-by-step security configuration and just run the final demo project, follow the instructions in the README.

Sign up at Auth0 and install the Auth0 CLI. Then, in the command line, run:

auth0 login

The command output will display a device confirmation code and open a browser session to activate the device.

You need to create a client application for your API if you are using opaque tokens. Since this tutorial does not use opaque tokens, you only need to register the API within your tenant. You can do it using Auth0 CLI:

auth0 apis create \
  --name "Menu API" \
  --identifier https://menu-api.okta.com \
  --scopes "create:items,update:items,delete:items" \
  --offline-access=false

The scopes create:items, update:items, delete:items will be required ahead in the tutorial.

Next, add the okta-spring-boot-starter dependency:

// build.gradle
dependencies {
    ...
    implementation 'com.okta.spring:okta-spring-boot-starter:3.0.6'
    ...
}

As the menu-api must be configured as an OAuth2 resource server, add the following properties to the application.properties file:

# application.properties
okta.oauth2.issuer=https://<your-auth0-domain>/
okta.oauth2.audience=https://menu-api.okta.com

You can find your Auth0 domain with the following Auth0 CLI command:

auth0 tenants list

Run the API with:

./gradlew bootRun

Test the API authorization with curl:

curl -i localhost:8080/api/menu/items

You will get HTTP response code 401 because the request requires bearer authentication. Using Auth0 CLI, get an access token:

auth0 test token -a https://<your-auth0-domain>/api/v2/ -s openid

Select any available client when prompted. You also will be prompted to open a browser window and log in with a user credential. You can sign up as a new user using an email and password or using the Google social login.

With curl, send a request to the API server using a bearer access token:

ACCESS_TOKEN=<auth0-access-token>
curl -i --header "Authorization: Bearer $ACCESS_TOKEN" localhost:8080/api/menu/items

The request will not be authorized yet; you will see the error message This aud claim is not equal to the configured audience. The error means that the audience contained in the aud claim of the access token does not match the expected audience configured in the server properties. When requesting a token with theauth0 test token command, the default audience value is https://<your-auth0-domain>/api/v2, which is the Auth0 Provider management API audience.

NOTE: The Okta Spring Boot Starter autoconfigures the issuer and audience validation from the resource server properties for JWT authorization.

Request a test token again, this time with the expected audience:

auth0 test token -a https://<your-auth0-domain>/api/v2/ -s openid -a https://menu-api.okta.com

Try the API request again, and you should get a JSON response listing the menu items:

[
    {
        "description": "Tasty",
        "id": 1,
        "imageUrl": "https://cdn.auth0.com/blog/whatabyte/burger-sm.png",
        "name": "Burger",
        "price": 599.0
    },
    {
        "description": "Cheesy",
        "id": 2,
        "imageUrl": "https://cdn.auth0.com/blog/whatabyte/pizza-sm.png",
        "name": "Pizza",
        "price": 299.0
    },
    {
        "description": "Informative",
        "id": 3,
        "imageUrl": "https://cdn.auth0.com/blog/whatabyte/tea-sm.png",
        "name": "Tea",
        "price": 199.0
    }
]

Authentication from a Single-Page Application (SPA)

It would be so much fun testing the API with a client application. Good fortune is on your side because you can use the WHATABYTE client, a live demo application where you can configure Auth0 authentication and send requests to your local API server.

For the Auth0 authentication, you need to register the live client as a Single-Page Application to Auth0. You can do it with the Auth0 CLI:

auth0 apps create \
  --name "WHATABYTE client" \
  --description "Single-Page Application for menu items CRUD" \
  --type spa \
  --callbacks https://dashboard.whatabyte.app/home \
  --logout-urls https://dashboard.whatabyte.app/home \
  --origins https://dashboard.whatabyte.app \
  --web-origins https://dashboard.whatabyte.app

Go to the WHATABYTE client, and set API Server Base URL to http://localhost:8080. Toggle on Enable Authentication Features and set the following values:

Click Save.

For the API server to accept requests from the WHATABYTE client, you must tweak the CORS configuration, enabling the WHATABYTE client URL as an allowed origin for requests.

In the API project under the start folder, add a SecurityConfig class at the root package:

// src/main/java/com/example/menu/SecurityConfig.java
package com.example.menu;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        return http
                .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(withDefaults()))
                .build();
    }

}

Modify ItemController by adding the @CrossOrigin annotation:

// src/main/java/com/example/menu/web/ItemController.java
...

@RestController
@RequestMapping("/api/menu/items")
@CrossOrigin(origins = "https://dashboard.whatabyte.app")
public class ItemController {

...

Restart the API server. Before the sign in, you can create some test users with the Auth0 CLI. This step is optional; you can sign up later from the Auth0 Universal Login form or choose Google social login.

auth0 users create

Complete the required information; you will see an output like the lines below:

Connection Name: Username-Password-Authentication
 Name: Thomas
 Email: thomas@email.com
 Password: ********

=== dev-avup2laz.us.auth0.com user created

  ID          auth0|643ec0e1e671c7c9c5916ed6
  EMAIL       thomas@email.com
  CONNECTION  Username-Password-Authentication

Save the ID for later.

In the WHATABYTE client, click the Sign in button. The client will redirect you to the Auth0 Universal Login page. Sign in with the user you just created. In the left menu, choose Menu, and the menu items will display.

WHATABYTE client

On the top right corner, click Add Item, and a pre-populated form will display. Click on Save and verify that the user request is authorized in the server.

Role-Based Access Control (RBAC)

If you disable the authentication features in the WHATABYTE client, it will allow you to attempt CRUD operations from the UI, but your API server will reject item modifications as an unauthenticated user.

WHATABYTE client Unauthenticated

Besides requiring a user to be authenticated, it is a standard practice to create roles for authorizing write operations.

Create and assign roles

In the WHATABYTE client settings, re-enable the authentication features and also enable RBAC. Set menu-admin in the User Role text box. Click on Save.

Now, if you sign in with the user you created, the UI will not display the links to perform write operations, as the role has not yet been assigned.

First, the menu-admin role must be defined in the Auth0 tenant as well. You can use the following Auth0 CLI command:

auth0 roles create

Assign the role to the user you created:

auth0 users roles assign

NOTE: You can find the user id with auth0 users search --query email:<email>

Follow the steps, you will see the output below:

=== dev-avup2laz.us.auth0.com user roles (1)

  ID                    NAME           DESCRIPTION                                
  rol_Nf2xmon4GzTbLU9T  menu-admin     Admin role for menu item write operations

Mapping the roles to token claims

The role menu-admin and its permissions must be mapped to a claim in the access token, to make them available in the API for authorization. With Auth0 Actions you can customize the Login flow to map the user roles to a custom claim.

First configure your preferred editor to use with the Auth0 CLI:

# for example
export EDITOR=nano

Then, create the Login Action:

auth0 actions create

Set the name Add Roles, and select post-login for the Trigger. When the editor opens, set the following implementation for the onExecutePostLogin function.

exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://menu-api.okta.com';
  if (event.authorization) {
    api.idToken.setCustomClaim('preferred_username', event.user.email);
    api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
    api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
  }
}

Save the file. You will see a detailed output once the action is created.

You can list the available actions with the following command:

auth0 actions list

The output will show the deployment status of each action:

=== dev-avup2laz.us.auth0.com actions

  ID                                    NAME       TYPE        STATUS  DEPLOYED  
  da49ae42-b5e4-496a-8305-4fff437f813b  Add Roles  post-login  built   ✗

Note the DEPLOYED status is x. Go ahead and deploy it using the action ID:

auth0 actions deploy <ACTION_ID>

Once the action is deployed, you must attach it to the login flow. You can do this with Auth0 Management API for Actions:

auth0 api patch "actions/triggers/post-login/bindings" \
  --data '{"bindings":[{"ref":{"type":"action_id","value":"<ACTION_ID>"},"display_name":"Add Roles"}]}'

You can visualize the flow in the Auth0 dashboard. Sign in, and on the left menu, you choose Actions, then in the Flows screen, choose Login.

Custom Auth0 Login Action

Now, the role has been assigned and mapped to the ID and access tokens, and the UI will display the links to perform write operations. Still, the API server is not yet enforcing RBAC.

Enable RBAC in Auth0

The next step is to enable RBAC for the API in Auth0. You can do it with the following Auth0 CLI commands:

auth0 apis list

Copy the Menu API ID and use it for the next command:

auth0 api patch "resource-servers/<API_ID>" \
  --data '{ "enforce_policies": true, "token_dialect": "access_token_authz" }'

The token_dialect value access_token_authz enables the permissions to be included in the access token in a custom permissions claim.

Assign the Menu API permissions to the menu-admin role:

auth0 roles permissions add

Follow the instructions, and make sure to select all the API permissions: - create:items - delete:items - update:items

Okta FGA LogoWant to take your Authorization to the next level? → fga.dev

Implement RBAC in the Spring Boot API

The Okta Starter provides a simple way to specify the claim from which authorities must be extracted. In the application.properties file, add the following property:

okta.oauth2.groupsClaim=permissions

With the @PreAuthorize annotation, you define the required permission to perform the endpoint operation. The final controller implementation should look like this:

// src/main/java/com/example/menu/web/ItemController.java
package com.example.menu.web;

@RestController
@RequestMapping("/api/menu/items")
@CrossOrigin(origins = "https://dashboard.whatabyte.app")
public class ItemController {

    private ItemRepository itemRepository;

    public ItemController(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    @GetMapping
    public Collection<Item> items(){
        List<Item> list = new ArrayList<>();
        this.itemRepository.findAll().forEach(list::add);
        return list;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Item> item(@PathVariable Long id){
        return this.itemRepository.findById(id)
                .map(item -> ResponseEntity.ok().body(item))
                .orElse(ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    @PreAuthorize("hasAuthority('update:items')")
    public ResponseEntity<Item> updateItem(@Valid @RequestBody Item items, @PathVariable Long id){
        return this.itemRepository.findById(id)
                .map(item -> {
                    item.setName(items.getName());
                    item.setPrice(items.getPrice());
                    item.setDescription(items.getDescription());
                    item.setImage(items.getImage());
                    Item result = this.itemRepository.save(item);
                    return ResponseEntity.ok().body(result);
                }).orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @PreAuthorize("hasAuthority('create:items')")
    public ResponseEntity<Item> createItem(@Valid @RequestBody Item item) throws URISyntaxException {
        Item result = this.itemRepository.save(item);
        return ResponseEntity.created(new URI("/api/menu/items/" + result.getId())).body(result);

    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasAuthority('delete:items')")
    public ResponseEntity<?> deleteItem(@PathVariable Long id){
        this.itemRepository.deleteById(id);
        return ResponseEntity.ok().build();
    }


}

For the @PreAuthorize rules to take effect, you must add the annotation @EnableGlobalMethodSecurity to the security configuration, the final code must look like this:

// src/main/java/com/example/menu/SecurityConfig.java
package com.example.menu;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        return http.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(withDefaults())).build();
    }

}

Restart the API and your user should now have the required permissions to request write operations to the Menu API.

If you remove the permissions from the menu-admin role, the UI will display links for item modifications, but the API server will reject the operations as the required permissions will not be present in the access token.

Learn More about Spring Boot Authentication and Authorization

I hope you enjoyed this tutorial on how to secure a Spring Boot API with OAuth2 and Auth0. You learned how to configure a resource server with okta-spring-boot-starter and how to implement RBAC.

You can find all the code shown in this tutorial on GitHub in the spring-menu-api repository.

If you liked this post, you might enjoy these related posts:

Check out the Spring Boot Security labs in our Developer Center:

Please follow us on Twitter @oktadev and subscribe to our YouTube channel for more Spring Boot and microservices knowledge.

You can also sign up for our developer newsletter to stay updated on everything about identity and security.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon