Försvar på djupet: Del 2 Claimsbaserad behörighetskontroll

This page is also available in: 🇺🇸 English
19 September 2023

I förra artikeln diskuterade vi vilken information som behövs för en stark behörighetskontroll. I denna artikel går vi igenom hur vi överför informationen om vilka scopes och audiences som användaren godkänt, hennes identitet, samt detaljer kring inloggningstillfället, till rättigheter som vi använder för behörighetskontroll.

För att bygga en fungerande lösning så behöver vi introducera två nya begrepp: Access token och Identity Provider (IdP). Formatet för en access token kan variera. Vanligt för ett REST API är att access token är en JSON Web Token (JWT). Den här artikeln är skriven med JWT som exempel, men grundläggande koncept och resonemang gäller oavsett token format.

Översikt av hur en biljett skapas och sedan skickas från klient till API.

IdP, AS, STS etc, kärt barn har många namn och även om det finns skillnader så väljer vi att i den här artikeln använda IdP som begrepp för den tjänst som du autentiserar dig mot och får dina tokens från.

En JWT skapas av en IdP och består av kryptografiskt signerade påståenden, så kallade claims. Ett exempel kan vara:

{
    "sub": "8256-0346-3829",
    "name": "Eva Svensson"
}

Specifikationer för både JWT och OpenID Connect definierar ett antal olika claims. Centralt är “sub”, som är en unik identifikation av den användare som avses. Notera att identifieraren bör ge anonymitet i det avseendet att den kan användas utan att sprida persondata eller annan känslig information.

I vårt exempel kan man tänka sig att användaren autentiserar sig med BankID. Vår IdP behöver i så fall slå upp en unik och anonym identifierare för att undvika att personnummer exponeras mer än nödvändigt.

https://tools.ietf.org/html/rfc7519#section-4.1

https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims

Förutom identitet så innehåller en JWT från en IdP information från användarens autentisering, tex vilken typ av inloggning som använts. Detta kan vara användbart för att tex kräva extra stark autentisering vid särskilt känsliga operationer.

Notera att samma princip gäller även för integrationer (service till service), där ingen användare är involverad. I det fallet innehåller vår JWT inget användarid, utan vi utgår ifrån klientens id istället.

I praktiken representerar de claims som ingår i vår JWT ett kontrakt mellan IdP och tjänster. Om din IdP lägger till claims utöver de som är standard så behöver du tänka på bakåtkompabilitet och hur du ska hantera ändringar när systemet utvecklas. Vilka egna claims som din IdP ska inkludera i din JWT är en balans. Principen är att informationen ska handla om identitet, inte om behörighet.

Ta följande exempel på en JWT med custom claim “admin” :

{
    "sub": "8256-0346-3829",
    "name": "Eva Svensson",
    "admin": true
}

Problemet med “admin” i en lite större organisation kan vara att det finns många olika typer av administratörer och vi kommer behöva utöka med allt fler claims när systemet växer. Istället för “admin” så kanske ett urval av de Active Directory grupper Eva tillhör är ett bättre val? Då kan varje API själv bestämma vad en grupptillhörighet betyder och vi får en mer stabil lösning. Exempel på claims i vår JWT skulle i så fall istället vara:

{
    "sub": "8256-0346-3829",
    "name": "Eva Svensson",
    "groups": ["S-1-0", "S-1-1"] 
} 

Mängden grupptillhörigheter vi vill lägga till kan växa och bli opraktiskt många. Ett alternativ är att helt utelämna grupptillhörigheter och istället slå upp den informationen direkt från varje API. Man bör även tänka på att inte lägga till dynamisk information som ändras under en tokens livslängd.

Ju fler system som delar samma IdP, desto viktigare är balansen av claims som din IdP returnerar. Det finns ofta ett fåtal domänbegrepp som är generella för alla system inom ett företag, och som med fördel kan inkluderas av en IdP. Förutom grupper i Active Directory kan det vara kundnummer eller liknande.

Begrepp som roller och annan behörighet är ofta beroende på kontext och API. Det är en bättre utgångspunkt att låta varje API slå upp tex roller som en del av övergången till en lokal rättighetsmodell, baserat på användarens identitet.

Anledningen till att vi vill poängtera att din JWT bör vara liten och bara inkludera identitet handlar om systemprestanda och hur enkelt det blir att utöka systemet med nya delsystem som använder samma IdP.

Din JWT skickas med i en HTTP header i varje nätverksanrop. En mindre JWT och lokala, cachade uppslag av behörigheter i ditt API kan ge dig betydligt bättre systemprestanda, jämfört med en större JWT som innehåller många claims.

När du utökar ditt system med fler delsystem som använder sig av samma IdP så blir frågan om hur din JWT kan växa allt viktigare. Begränsningar i storlek för HTTP headers och cookies kan bli ett problem. En liten JWT är en bättre utgångspunkt och ger dig större handlingsfrihet i framtiden.

Vår erfarenhet är att denna balans mellan lokala och centrala rättighetsmodeller är en kärnpunkt i hur flexibel din lösning blir.

Eftersom en JWT är kryptografiskt signerad och följer med i varje anrop från klienten, kan ett API verifiera att de claims som följer med ett anrop kommer ifrån en källa som vi litar på. Uppgiften i vårt API blir att:

  1. Validera att JWT är korrekt
  2. Transformera JWT till rättighetsmodell
  3. Validera behörighet

Steg 1 sköter ramverket som vi bygger vårt API på. Steg 2 behöver du implementera själv, och steg 3 är en del av din applikation och domänlogik.

Vi går från en JWT till ett objekt som representerar vår rättighetsmodell, via ett uppslag mot den rättighetsmodell som konfigurerats.

I steg 2 transformerar vi den information som vi har fått i form av claims i vår JWT till ett objekt av vår rättighetsmodell. Objektet innehåller all den information som vi behöver i vår domänlogik för att reda ut om användaren har rätt till den funktion och det data som efterfrågas.

Från och med denna punkt arbetar vi enbart med vårt rättighetsobjekt, inte med information från vår JWT.

I transformationen slår du upp egenskaper påverkar användarens behörighet. Till exempel vilka roller hon tillhör, vilken typ av licens hon har köpt osv. Det kan betyda att du behöver göra ett anrop till en gemensam tjänst eller en slagning i en lokal databas.

function transform(JWT) 
    if JWT represents an authenticated request 
        scopes := scopes from JWT
        organization := organization from JWT /* Organization is a custom claim */ 
        roles := get roles from service by JWT/sub /* Network call or database access */ 

        permissions := intersection of scopes, roles and organization

        return new Permissions(permissions, JWT/sub)
    end if
    
    return empty Permissions 

För att få behörighetskontroll och spårbarhet på alla operationer i systemet behöver det resulterande objektet med rättighetsmodellen följa med ner i vår domänlogik. Det gör det möjligt för oss att fatta beslut kring behörighet utan beroenden till claims, protokoll och OAuth flöden.

Notera att detta mönster hanterar alla typer av requests, oavsett om de involverar en användare eller om de kommer från en integration (service till service).

Vår erfarenhet är att det är bra om man håller transformationen från claims till objekt i en klass. Testdriven utveckling är ett mycket kraftfullt verktyg för utveckling och underhåll av denna helt kritiska komponent.

Erica Edholm, Omegapoint

Jag ser ofta att vi missar att skriva tester som verifierar access till systemet. Det blir lättare att skriva tester när koden är väl strukturerad och transformeringen mellan identitet och rättighet finns på ett ställe.

Testfall som jag också gärna ser är should_return_403_when_not_admin och should_return_404_when_not_my_data.

I artikel 4, “Säkra API” kommer vi att diskutera vad vi behöver tänka på i steg 3, när vi verifierar att anropet har rätt att utföra den funktion som begärs och det data som efterfrågas. Men först, i artikel 3, behöver vi reda ut hur vi gör på klienten för att få tag på en token.

Se Defence in Depth för ytterligare material och kodexempel kopplade till den här artikelserien.


Fler artiklar i serien: