Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Genom att använda denna starter så möjliggörs stöd för mer finkornig behörighetshantering av API-resurser när sådant behov uppstår. Funktionaliteten bygger på att klienten skapar ett signerat Java Web Token (JWT) innehållandes den användarinformation som krävs för att kontrollera användarens behörighet. Informationen skickas med i anropet till tjänsten, där den sedan används för att kontrollera behörighet till i första hand resurserna i klassen som specificerar API-resurserna.

För att få tillgång till modulens funktionalitet, lägg till följande i tjänstens pom.xml:

Code Block
<dependency>
	<groupId>se.sundsvall.dept44</groupId>
	<artifactId>dept44-starter-authorization</artifactId>
</dependency>

Lägg även till annoteringen @EnableJwtAuthorization i den klass som innehåller annoteringen @ServiceApplication.

Inställningar i tjänsten

När auktorisering via JWT är påslagen så behövs följande inställningar hanteras i tjänstens property-fil

  • jwt.authorization.secret är obligatorisk att sätta och innehåller det, mellan klient och tjänst, överenskomna värde som ska användas när signering och verifiering av signatur för JWT:n sker (observera att olika signerings-algoritmer har olika krav på hur långt värdet måste vara).

  • jwt.authorization.header-name är valbar att sätta och innehåller namnet på headern där modulen letar efter JWT:n. Default använder modulen header-namn x-authorization-info, men detta går att överrida om så önskas.

Struktur för JWT

Klienter som anropar tjänsten måste skicka med ett Java Web Token (JWT) med behörighetsinformation till tjänsten i de fall de anropar behörighetsskyddade resurser.

En JWT innehåller tre delar. En header med information kring den algoritm och token-typ som JWT:n använder. En payload innehållande behörighetsinformation för anropande användare. Slutligen en signatur för JWT:n som används för validering. Varje del Base-64-encodas. Observera att JWT:n inte är krypterad utan enbart signerad. Därför bör JWT:n inte innehålla känslig information.

Header-struktur

Code Block
languagejson
{
  "alg": "<information om algoritm för signering>",
  "typ": "<typ av token>"
}
  • alg (obligatorisk) innehåller den algoritm som använts för att signera JWT:n och som ska användas för att verifiera signaturen, tex HS512.

  • typ (obligatorisk) innehåller vilken typ token:et är av. Sätts till JWT.

Payload-struktur

Delen som innehåller användarinformation är uppbyggd enligt följande struktur:

Code Block
languagejson
{
  "sub": "<id for subject, for example user-id>",
  "exp": "<epoch timestamp när token blir för gammalt>",
  "roles": {
    "<role_name>": <godtycklig json-struktur>
  }
}
  • sub (obligatorisk) innehåller identifikation för det subjekt som JWT:n beskriver, i de flesta fall användarnamn på person som är kopplad till informationen

  • roles (obligatorisk) innehåller en map med de behörigheter som är kopplade till användaren, där rollnamnet är nyckel och värde innehåller en godtycklig json-struktur för att beskriva behörigheten. Json-strukturen kan sättas till null om man enbart behöver roll för att verifiera åtkomst.

  • exp innehåller epoch timestamp då JWT:n inte längre ska anses vara giltig. Attributet är valbart att skickas med och utelämnas det så tolkas JWT:n vara giltig till oändligheten.

Signatur-struktur

Har ingen struktur, men byggs upp utifrån den algoritm som definierats i huvudet, den information som finns i payloaden samt det värde som valts att användas mellan klient och tjänst. Exempel på hur signaturen byggs upp:

Code Block
HMACSHA512(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  <client-server-agreed-secret>
)

Exempel

...

Se tex https://jwt.io/ för mer information kring hur man skapar JWT, samt vilka olika algoritmer som finns att välja för signaturen.

Behörighetskontroll via annotering

För att styra åtkomst till en API-resurs (eller annan metod i tjänsten) så används de auktoriserings-annoteringar som ingår i Springs ramverk. Dessa är

  • @PreAuthorize används för att besluta ifall användare har behörighet att anropa metoden eller ej.

  • @PostFilter används för att filtrera resultatet ifrån metoden baserat på användarens behörighet.

  • @PreFilter möjliggör filtrering innan exekvering av metod (mindre vanligt usecase).

  • @PostAuthorize används för att avgöra behörighet till metoden efter att den anropats (mindre vanligt usecase).

Det går att använda inbyggda funktioner i Spring för att avgöra behörigheter, eller att skriva en egen implementation som gör kontrollen, där det senare kanske är det mest vanliga scenariot. Annoteringarna ovan använder sig av SPeL (Spring expression language) för att få åtkomst till i Spring security fördefinierade objekt, samt andra attribut (tex metodparametrar) som krävs för att avgöra access.

Generisk behörighetsmodell

Den JWT som lästs ut av Dept-44 ligger lagrad som ett objekt av klass GenericGrantedAuthority i Springs security-context. Klassen har ett antal hjälpmetoder för att läsa och verifiera behörighet för en användare. Följande metoder finns:

  • hasAuthority(String role)

  • hasAuthority(String role, String jsonPath)

I de fall mer komplex logik krävs för att avgöra behörighet så finns möjlighet att läsa ut json-strukturen kopplad till rollen som ett DocumentContext-objekt via metoden getAccesses().

Steg för steg guide för att skriva och använda en implementation för auktorisering

Skapa ny klass som annoteras med @Component och en metod som returneras boolean avseende ifall användare är behörig eller ej, inklusive de parametrar som behövs för att metoden ska kunna avgöra utfallet. Nedan följer exempel på implementation som verifierar att användaren har en roll med namn “write“, vilken har en lista som innehåller den den kategori som skickas in (se struktur på JWT-payload i stycket ovan):

Code Block
languagejava
@Component
public class AccessAuthorizer {
  public boolean authorize(Authentication authentication, Category category) {
    final AtomicBoolean hasAccess = new AtomicBoolean(false); 

    authentication.getAuthorities().forEach(auth -> {
      GenericGrantedAuthority generic = (GenericGrantedAuthority)auth;
      hasAccess.compareAndExchange(false, 
        generic.hasAuthority("write", 
        String.format("$.[?(@ ==\"%s\")]", category.name())));
    });

    return hasAccess.get();
  }
}

Lägg till någon av ovanstående annoteringar för den resurs som ska behörighetsskyddas. I detta exempel används @PreAuthorize.

Code Block
@GetMapping(path = "/{category}", produces = { APPLICATION_JSON_VALUE })
@Operation(summary = "Get agreements by category")
@ApiResponse(responseCode = "200", description = "Successful operation")
@ApiResponse(responseCode = "400", description = "Bad request")
@ApiResponse(responseCode = "401", description = "Unauthorized")
@ApiResponse(responseCode = "404", description = "Not found")
@ApiResponse(responseCode = "500", description = "Internal Server error")
@ApiResponse(responseCode = "502", description = "Bad Gateway")
@PreAuthorize("@accessAuthorizer.authorize(authentication, #category)")
public ResponseEntity<AgreementResponse> getAgreementsByCategory(
  @Parameter(name = "category") @PathVariable(name = "category") Category category) {
    ...
}

Med hjälp av SPeL så anropas implementationen för auktorisering, där de argument som krävs av metoden skickas med. För att använda en böna annoterad med @Component så används syntaxen @klassnamn (för att tala om vilken implementation som ska användas för att säkerställa access). För att skicka med argument från annoterad metod så används syntax #variabelnamn. Det finns även ett antal fördefinierade namn att använda, tex authentication som innehåller Spring securitys Authentication-objekt. Se Springs dokumentation kring säkerhet samt SPeL för ytterligare information kring vilka möjligheter som finns att tillgå.

Ifall användaren har access enligt implementationen så kommer exekveringen att fortsätta och metoden att anropas. Ifall användaren saknar access så kommer ramverket att returnera http-kod 401 med information om att behörighet saknas.

Scenarion då ramverket returnerar ‘401: Unauthorized’

  • Anropande användare saknar behörighet till efterfrågad resurs.

  • Token saknas när behörighetsskyddad resurs efterfrågas. Observera att token enbart behöver skickas när klienten efterfrågar en behörighetsskyddad resurs. För oskyddade resurser behövs token inte skickas med i anrop.

  • Token inte går att läsa, tex för att det inte är korrekt uppbyggt.

  • Token har manipulerats på vägen mellan klient och tjänst.

  • Token har blivit för gammalt.