Resource Server OAuth 2.0 con Spring Security 5

1. Panoramica

In questo tutorial impareremo come configurare un server di risorse OAuth 2.0 utilizzando Spring Security 5 .

Lo faremo utilizzando JWT e token opachi, i due tipi di token al portatore supportati da Spring Security.

Prima di passare all'implementazione e agli esempi di codice, stabiliremo alcune informazioni di base.

2. Un po 'di storia

2.1. Cosa sono i JWT e i token opachi?

JWT, o JSON Web Token, è un modo per trasferire informazioni sensibili in modo sicuro nel formato JSON ampiamente accettato. Le informazioni contenute potrebbero riguardare l'utente o il token stesso, come la sua scadenza e l'emittente.

D'altra parte, un token opaco, come suggerisce il nome, è opaco in termini di informazioni che trasporta. Il token è solo un identificatore che punta alle informazioni archiviate nel server di autorizzazione: viene convalidato tramite introspezione alla fine del server.

2.2. Che cos'è un server di risorse?

Nel contesto di OAuth 2.0, un server di risorse è un'applicazione che protegge le risorse tramite token OAuth . Questi token vengono emessi da un server di autorizzazione, in genere a un'applicazione client. Il compito del server di risorse è convalidare il token prima di servire una risorsa al client.

La validità di un token è determinata da diversi fattori:

  • Questo token proviene dal server di autorizzazione configurato?
  • Non è scaduto?
  • Questo server di risorse è il suo pubblico previsto?
  • Il token ha l'autorità richiesta per accedere alla risorsa richiesta?

Per visualizzare, diamo un'occhiata a un diagramma di sequenza per il flusso del codice di autorizzazione e vediamo tutti gli attori in azione:

Come possiamo vedere nel passaggio 8, quando l'applicazione client chiama l'API del server di risorse per accedere a una risorsa protetta, va prima al server di autorizzazione per convalidare il token contenuto nell'intestazione Authorization: Bearer della richiesta , quindi risponde al client.

Il passaggio 9 è ciò su cui ci stiamo concentrando in questo tutorial.

Va bene, ora passiamo alla parte del codice. Configureremo un server di autorizzazione utilizzando Keycloak, un server di risorse che convalida i token JWT, un altro server di risorse che convalida i token opachi e un paio di test JUnit per simulare le app client e verificare le risposte.

3. Server di autorizzazione

Per prima cosa, configureremo un server di autorizzazione o la cosa che emette i token.

Per questo, utilizzeremo Keycloak incorporato in un'applicazione Spring Boot . Keycloak è una soluzione open source per la gestione delle identità e degli accessi. Dato che in questo tutorial ci stiamo concentrando sul server delle risorse, non lo approfondiremo.

Il nostro Keycloak Server incorporato ha due client definiti - fooClient e barClient - corrispondenti alle nostre due applicazioni del server di risorse.

4. Resource Server - Utilizzo di JWT

Il nostro server di risorse avrà quattro componenti principali:

  • Modello : la risorsa da proteggere
  • API : un controller REST per esporre la risorsa
  • Configurazione della sicurezza : una classe per definire il controllo dell'accesso per la risorsa protetta che l'API espone
  • application.yml - un file di configurazione per dichiarare le proprietà, incluse le informazioni sul server di autorizzazione

Vediamoli uno per uno per il nostro server di risorse che gestisce i token JWT, dopo aver dato un'occhiata alle dipendenze.

4.1. Dipendenze di Maven

Principalmente, avremo bisogno di spring-boot-starter-oauth2-resource-server , lo starter di Spring Boot per il supporto del server di risorse. Questo starter include Spring Security per impostazione predefinita, quindi non è necessario aggiungerlo esplicitamente:

 org.springframework.boot spring-boot-starter-web 2.2.6.RELEASE   org.springframework.boot spring-boot-starter-oauth2-resource-server 2.2.6.RELEASE   org.apache.commons commons-lang3 3.9 

Oltre a questo, abbiamo anche aggiunto il supporto web.

Per i nostri scopi dimostrativi, genereremo risorse in modo casuale invece di prenderle da un database, con un po 'di aiuto dalla libreria commons-lang3 di Apache .

4.2. Modello

Per mantenerlo semplice, useremo Foo , un POJO, come nostra risorsa protetta:

public class Foo { private long id; private String name; // constructor, getters and setters } 

4.3. API

Ecco il nostro controller di riposo, per rendere Foo disponibile per la manipolazione:

@RestController @RequestMapping(value = "/foos") public class FooController { @GetMapping(value = "/{id}") public Foo findOne(@PathVariable Long id) { return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); } @GetMapping public List findAll() { List fooList = new ArrayList(); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); return fooList; } @ResponseStatus(HttpStatus.CREATED) @PostMapping public void create(@RequestBody Foo newFoo) { logger.info("Foo created"); } }

Come è evidente, abbiamo la possibilità di GET all Foo s, GET a Foo by id e POST a Foo .

4.4. Configurazione della protezione

In questa classe di configurazione, definiamo i livelli di accesso per la nostra risorsa:

@Configuration public class JWTSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/foos/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/foos").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt()); } } 

Chiunque abbia un token di accesso con ambito di lettura può ottenere Foo s. Per POST un nuovo Foo , il loro token dovrebbe avere un ambito di scrittura .

Inoltre, abbiamo aggiunto una chiamata a jwt () utilizzando il DSL oauth2ResourceServer () per indicare il tipo di token supportato dal nostro server qui .

4.5. application.yml

Nelle proprietà dell'applicazione, oltre al solito numero di porta e percorso di contesto, dobbiamo definire il percorso dell'URI dell'emittente del nostro server di autorizzazione in modo che il server di risorse possa scoprire la configurazione del provider :

server: port: 8081 servlet: context-path: /resource-server-jwt spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung

Il server di risorse utilizza queste informazioni per convalidare i token JWT in arrivo dall'applicazione client, come indicato al passaggio 9 del nostro diagramma di sequenza.

Affinché questa convalida funzioni utilizzando la proprietà issuer-uri , il server delle autorizzazioni deve essere attivo e in esecuzione. Altrimenti, il server delle risorse non si avvia.

Se dobbiamo avviarlo in modo indipendente, possiamo fornire la proprietà jwk-set-uri invece di puntare all'endpoint del server di autorizzazione che espone le chiavi pubbliche:

jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

E questo è tutto ciò di cui abbiamo bisogno per far sì che il nostro server convalidi i token JWT.

4.6. Test

Per i test, configureremo una JUnit. Per eseguire questo test, abbiamo bisogno del server di autorizzazione e del server di risorse attivo e funzionante.

Let's verify that we can get Foos from resource-server-jwt with a read scoped token in our test:

@Test public void givenUserWithReadScope_whenGetFooResource_thenSuccess() { String accessToken = obtainAccessToken("read"); Response response = RestAssured.given() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .get("//localhost:8081/resource-server-jwt/foos"); assertThat(response.as(List.class)).hasSizeGreaterThan(0); }

In the above code, at Line #3 we obtain an access token with read scope from the authorization server, covering Steps from 1 through 7 of our sequence diagram.

Step 8 is performed by RestAssured‘s get() call. Step 9 is performed by the resource server with the configurations we saw and is transparent to us as users.

5. Resource Server – Using Opaque Tokens

Next, let's see the same components for our resource server handling opaque tokens.

5.1. Maven Dependencies

To support opaque tokens, we'll additionally need the oauth2-oidc-sdk dependency:

 com.nimbusds oauth2-oidc-sdk 8.19 runtime 

5.2. Model and Controller

For this one, we'll add a Bar resource:

public class Bar { private long id; private String name; // constructor, getters and setters } 

We'll also have a BarController with endpoints similar to our FooController before, to dish out Bars.

5.3. application.yml

In the application.yml here, we'll need to add an introspection-uri corresponding to our authorization server's introspection endpoint. As mentioned before, this is how an opaque token gets validated:

server: port: 8082 servlet: context-path: /resource-server-opaque spring: security: oauth2: resourceserver: opaque: introspection-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect introspection-client-id: barClient introspection-client-secret: barClientSecret

5.4. Security Configuration

Keeping access levels similar to that of Foo for the Bar resource as well, this configuration class also makes a call to opaqueToken() using the oauth2ResourceServer() DSL to indicate the use of the opaque token type:

@Configuration public class OpaqueSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}") String introspectionUri; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}") String clientId; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}") String clientSecret; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/bars/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/bars").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .opaqueToken(token -> token.introspectionUri(this.introspectionUri) .introspectionClientCredentials(this.clientId, this.clientSecret))); } } 

Here we're also specifying the client credentials corresponding to the authorization server's client we'll be using. We defined these earlier in our application.yml.

5.5. Testing

We'll set up a JUnit for our opaque token-based resource server, similar to how we did it for the JWT one.

In this case, let's check if a write scoped access token can POST a Bar to resource-server-opaque:

@Test public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() { String accessToken = obtainAccessToken("read write"); Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); Response response = RestAssured.given() .contentType(ContentType.JSON) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(newBar) .log() .all() .post("//localhost:8082/resource-server-opaque/bars"); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value()); }

If we get a status of CREATED back, it means the resource server successfully validated the opaque token and created the Bar for us.

6. Conclusion

In this tutorial, we saw how to configure a Spring Security based resource server application for validating JWT as well as opaque tokens.

As we saw, with minimal setup, Spring made it possible to seamlessly validate the tokens with an issuer and send resources to the requesting party – in our case, a JUnit test.

As always, source code is available over on GitHub.