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.
Övergripande lösningsbeskrivning
Utökad behörighetskontroll i tjänst
För att få tillgång till modulens funktionalitet, lägg till följande i tjänstens pom.xml
:
<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-namnx-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
{ "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:
{ "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 informationenroles
(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 tillnull
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:
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):
@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
.
@GetMapping(path = "/cases/{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<CasesResponse> getCasesByCategory( @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.