# Protect a Java API (Spring Security)

> Add OneiD to a Java API API with Spring Security, step by step.

Source: https://oltinid.com/docs/quickstarts/api-java/ · Section: Quickstarts · All OneiD documentation: https://oltinid.com/llms.txt

This quickstart adds OneiD to a api with **Spring Security**. Every file is complete and runs as it is.

> **Tip:** Using an AI coding agent? Give it this page as Markdown (add `.md` to the address) together with the [Connector specification](https://oltinid.com/docs/ai/connector-specification/). See [Build with AI coding agents](https://oltinid.com/docs/ai/build-with-ai/).

## Before you start

- A OneiD address, for example `https://YOUR_ONEID`. The code below uses the demonstration instance `https://auth.oltinid.com`; replace it with your own.
- An access token with the scope `orders.read` to test with.

## 1. Register the application

An API is not a client: it does not sign in. It needs an **API scope** that clients can request.

1. Ask your OneiD administrator to create the API scope `orders.read` (or a scope named after your API) in the admin console.
2. Ask for the scope to be allowed on the client that will call the API.
3. Get an access token with that scope: from a [server or browser application](https://oltinid.com/docs/quickstarts/), or with the [client credentials grant](https://oltinid.com/docs/quickstarts/curl/).

> **Note:** OneiD access tokens have no `aud` claim. The API accepts a token because it was issued by your OneiD, is unexpired and carries the scope the API requires. Give each API its own scopes. See [Protect an API](https://oltinid.com/docs/guides/protect-an-api/).

## 2. Install

Add the dependencies org.springframework.boot:spring-boot-starter-web and org.springframework.boot:spring-boot-starter-oauth2-resource-server.

## 3. Add the code

`application.yml`

```yaml
server:
  port: 4000
```

`SecurityConfig.java`

```java
package com.example.api;

import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(requests -> requests
                // Spring Security makes the authority SCOPE_<name> for each scope in the scope claim.
                .requestMatchers(HttpMethod.GET, "/orders").hasAuthority("SCOPE_orders.read")
                .anyRequest().denyAll())
            .oauth2ResourceServer(server -> server.jwt(Customizer.withDefaults()))
            .build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        NimbusJwtDecoder decoder = NimbusJwtDecoder
            .withJwkSetUri("https://auth.oltinid.com/.well-known/jwks") // OneiD's public signing keys
            // OneiD access tokens have the header typ "at+jwt". Without this line, Spring Security
            // accepts only "JWT" and refuses them.
            .jwtProcessorCustomizer(processor -> processor.setJWSTypeVerifier(
                new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("at+jwt"))))
            .build();

        // Checks the expiry and the issuer; the issuer name ends with a slash. There is no
        // audience check, because OneiD tokens have no aud claim.
        decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(),
            new JwtIssuerValidator("https://auth.oltinid.com/")));
        return decoder;
    }
}
```

`OrdersController.java`

```java
package com.example.api;

import java.util.Map;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrdersController {

    // The Jwt is the access token that Spring Security checked.
    @GetMapping("/orders")
    public Map<String, String> orders(@AuthenticationPrincipal Jwt token) {
        return Map.of("sub", token.getSubject());
    }
}
```

## 4. Run it

Create a Spring Boot project with these two starters, add these three files, and run it.

The API listens on http://localhost:4000. GET /orders answers 401 without a valid token, 403 without the scope orders.read, and otherwise returns the sub of the caller.

Test it with a token:

```bash
curl -i http://localhost:4000/orders
# HTTP/1.1 401 Unauthorized

curl -i http://localhost:4000/orders -H "Authorization: Bearer $ACCESS_TOKEN"
# HTTP/1.1 200 OK when the token carries orders.read, 403 when it does not
```

> **Checkpoint:** Without a token the API answers 401. With a valid token that carries `orders.read` it answers 200 and returns your `sub`; with a valid token without that scope it answers 403.

## Common issues

- **401 with a token that looks valid.** Check the issuer: OneiD’s issuer ends with a slash (`https://YOUR_ONEID/`).
- **The API checks `aud` and rejects every token.** OneiD access tokens carry no `aud`; remove the audience check and check the scope instead.
- More errors and fixes: [Errors and troubleshooting](https://oltinid.com/docs/reference/errors/).

## Learn more

- [Protect an API](https://oltinid.com/docs/guides/protect-an-api/)
- [Tokens](https://oltinid.com/docs/reference/tokens/)
- [Client credentials for services](https://oltinid.com/docs/guides/client-credentials/)
