Sichern einer Spring Boot WebApp mit OAuth 2.0

Authentifizierung ist der erste Schritt in jedem Sicherheitssystem. Es ist der Prozess mit dem die Identität eines Clients verifiziert wird. Ob der Client Zugriff auf bestimmte Ressourcen erhält, wird allerdings durch den Autorisierungsprozess festgestellt. In diesem Artikel werden wir eine API mit Spring Boot erstellen und das OAuth 2.0 Authentifizierungsprotokoll verwenden, um diese API abzusichern.

Table of Contents

Eingehende Erläuterungen und Definitionen

OAuth 2.0

OAuth 2.0 (Open Authentication) ist ein von der IETF entwickeltes Standardprotokoll, das die Authentifizierung und Autorisierung von Desktop- und Mobile-Anwendungen gegen eine API ermöglicht. Ein Benutzer kann mittels dieses Protokolls einer Client Anwendung den Zugriff auf seine Daten geben, ohne der Anwendung seine Credentials preiszugeben. Der Resource Server, der die Nutzerdaten bereitstellt entscheidet anhand des sogenannten JWT Token, ob die Anwendung autorisierten Zugriff auf die Daten hat. 

JSON Web Tokens (JWT)

Ein JSON Web Token ist ein Base64 verschlüsseltes JSON Objekt mit einem bestimmten Format, das zum Austauschen von Claims über einen Benutzer zwischen einem Identity Provider und einem Service Provider verwendet wird. Claims sind in diesem Context authentifizierungsrelevante Informationen, die den Benutzer und seinen Zugriff definieren. 

Authorization Server

Ein Server, der den Resource Owner authentifiziert und einen zeitlich begrenzten Access-Token für einen von ihm definierten Scope ausstellt.

Resource Server

Im OAuth 2.0 Protokoll fungiert der Resource Server als der API Server, der authentifizierte Anfragen verarbeitet, wenn ein gültiges Access Token vorliegt. Bei autorisierten Anfragen werden die Daten bereitgestellt und zurückgegeben.

Das folgende Diagramm veranschaulicht den Ablauf des Authorization Code Flow und die Rollen der beteiligten Komponenten:

OAuth 2.0: Authorization Code Flow

Einrichten des Authorization Servers

Wir werden Azure B2C als Authorization Server verwenden. Dafür brauchen wir einen Mandant (Tenant). Der Tenant ist eine Gruppe von Identitäten, die eine Organisation repräsentieren.  Jede Identität hat bestimmte Attribute und Berechtigungen, die ihre Zugriffe bestimmen. Damit eine Client Anwendung Access Tokens für unsere API von Azure AD B2C ausstellen kann, müssen wir sowohl die Client App als auch die API im Tenant registrieren.

  1. Im Azure AD B2C (in dem neu angelegten Tenant) unter App Registrations auf New Registration klicken
  2. Das Formular ausfüllen (name, supported account types) und auf Register klicken, um die Anwendung zu erstellen.
  3. Die Anwendung muss als API definiert werden und muss außerdem eine eindeutige URI haben. Nach der Definition der API, selektieren wir die Anwendung aus der Liste der registrierten Apps und beenden den Vorgang mit einem Klick auf Set in Expose an API. Es wird daraufhin eine neue URI generiert, die wir mit einem Klick auf Save speichern können.
  4. APIs müssen auf jeden Fall einen Scope publizieren, damit Client Apps einen gültigen Access Token erhalten können.

Anwendungsbeispiel für JAVA Spring Boot

Notwendige Dependencies definieren

Wir brauchen spring-boot-starter-oauth2-resource-server, mit dem wir unsere Sicherheitskonfiguration definieren. 

 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
 </dependency>
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

Modell

Definition des POJO Person.

public class Person {
    public String name;
    public String lastName;
    
   // ... constructor, getters and setters
}

Controller

Hier definieren wir den PeopleController, der die API beschreibt. Der Controller soll die zwei folgende Operationen implementieren:

  • Abfrage der Liste der Personen: GET
  • Hinzufügen einer Person in der List: POST

Anhand der Spring Annotationen @GetMapping und @PostMapping (usw…) können wir die einzelnen Endpunkte der API definieren.

@RestController
@RequestMapping("/people")
public class PeopleController {
    private List<Person> people = Arrays.asList(Person("foo", "bar"), Person("baz", "bar"));

    @GetMapping("/")
    public ResponseEntity<List<Person>> getAllPeople() {
        return new ResponseEntity(people, HttpStatus.OK);
    } 
    
    @PostMapping
    public ResponseEntity addPerson(@RequestBody person) {
        people.add(person);
        return new ResponseEntity(HttpStatus.OK);
    }
}

CORS

Wir konfigurieren CORS, um Ressourcen außerhalb der Domain der Resource Servers abfragen zu können. 

@Configuration
public class AppConf implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}

Security Configuration

Zunächst müssen wir den Endpunkt des Authorization Servers setzen, gegen den unsere API die Access Tokens validieren wird. Dies können wir in der application.yml Datei definieren.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://example-authorization-server.com/keys

Bisher ist unsere API noch nicht gesichert, da alle Anfragen die den „/people“ Endpunkt treffen durchgelassen werden.

Spring Security bietet uns die Möglichkeit, verschiedene Access Levels anhand der Eigenschaften (URL, Headers,…) der Anfragen zu konfigurieren.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // allows CORS preflight checks to succeed
        http.cors()

            .and()
                .authorizeRequests()
                .antMatchers("/admin/*")
                .hasRole("ADMIN")
            .and()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
            .and()               
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                .oauth2ResourceServer()
                .jwt();
    }
}

In dieser Konfiguration werden:

  1. Anfragen zum „/admin“ Endpunkt nur erlauben, wenn in den Claims die Rolle „Admin“ vorhanden ist.
  2. Sonst alle authentifizierte Anfragen

Die Authentifizierung in unserer Anwendung ist zustandslos, weshalb wir keinen Session Kontext benötigen.

Die zwei letzen Zeilen der Konfiguration stellen sicher, dass die Resource Server Funktionalitäten bereitgestellt werden (Abfangen der HTTP Anfragen, Extraktion des Tokens aus dem „Authorization“ Header und die Authentifizierung an sich…) . jwt() spezifiziert den Typen der Tokens, die die API erwarten soll.

Tests

Um unsere Konfiguration programmatisch zu testen, erstellen wir zwei Unit Tests – ein Test für den positiven Fall der Authentifizierung und ein Test für den negativen Fall.

   @Test
    void userIsNotAuthenticated_whenGetPeople_thenFail() {
        String token = getToken();

        Response response = RestAssured.given()
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .get("http://localhost:8081/people/");

        assertThat(response.statusCode()).isEqualTo(HttpStatus.SC_UNAUTHORIZED);
    }

    @Test
    void userIsAuthenticated_whenGetPeople_thenSucceed() {
        String token = getToken();

        Response response = RestAssured.given()
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .get("http://localhost:8081/people/");

        assertThat(response.as(List.class).isEmpty()).isFalse();
    }

Method Security

Spring Boot unterstützt das Konfigurieren der Sicherheit der API auf Methodenebene mittels drei Annotationen:

  • @Secured: definiert eine Liste von Rollen. Die annotierte Methode wird nur ausgeführt, für Clients die mindestens eine dieser Rollen besitzt.
  • @PreAuthorize & @PostAuthorize : definieren Zugriffskontrolle anhand Predicates, die in SpEL geschrieben sind.

Bevor wir die Annotationen benutzen können, müssen wir die MethodSecurity einstellen.

@Configuration
@EnableGlobalMethodSecurity(
        prePostEnabled = true,
        securedEnabled = true,
        jsr250Enabled = true)
public class MethodSecurityConfig
        extends GlobalMethodSecurityConfiguration {
}

In unserem Controller, können wir nun die Methoden wie folgend annotieren

@Secured({ "ROLE_ADMIN", "ROLE_MEMBER" })
public String getMembershipId(String userId) {
    return userRepository.getMembershipId(userId);
}
@PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_MEMBER')")
public String getMembershipId(String userId) {
     //...
}

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.