Naviger til forside. Bilde av Dfind Consulting logo
Systemutvikling
Sky
JWT

24.04.2024

3 min

Tilgangsstyring, med Duende IdentityServer

Bilde av artikkel forfatter

Matias Fehn Unsvåg

Systemutvikler

It sikkerhet har de siste årene vært et et tema med økt fokus og relevans. Når vi jobber med systemutvikling er det derfor viktig å håndtere tilgangsstyring på en sikker måte, med gode rammeverk og hjelpemidler. Autentisering (hvem er du) og Autorisering (hva har du lov til) er to aspekter som kan håndteres med Duende IdentityServer.


I 2020 gikk identityServer fra å hovedsakelig være et open source produkt, av Dominick Baier and Brock Allen, til å bli tilbudt som et mer komplett produkt fra Duende Software, og benyttes i dag av en rekke store og små it-selskaper.

IdentityServer gikk fra å være open source, til å bli tilbudt som komplett produkt fra Duende Software.

For tilgangsstyring skiller vi gjerne mellom:

  • bruker-til-system access
  • maskin-til-maskin access

For bruker-til-system access har IdentityServer god støtte for flere typer innlogging. Dette kan for eksempel være brukernavn og passord, eller ved å integrere med IdPorten så brukere kan benytte BankId.
For maskin-til-maskin access gjør api'er kall til IdentityServer for å autentisere seg.

Når det gjørs et vellykket innlogging/autentiseringsforsøk til IdentityServer, blir det sendt et JWT bearerToken i response.

Hvis man vil se innholdet i et Bearer token kan man f.eks benytte JWT.io som har et grensesnitt for å se innholdet i bearertokens:

Skjermbilde av JWT nettside hvor token blir dekodet.

For enkelhetsskyld går vi videre med et forenklet eksempel:

Encoded

1eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3VybC1mb3It
2ZGluLWlkZW50aXR5c2VydmVyLm5vIiwiaWF0IjoxNzEwODM0OTQ2LCJleHAiOjE3NDIzN
3zA5NDYsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6InRlc3RAbWFpbC5jb20iLC
4JOYW1lIjoiT2xlIE9sc2VuIiwiU2NvcGUiOiJbIGFwaUFjY2Vzcy5yZWFkIF0ifQ.KCSt
5ZhoT6DOIFsdELloz17SDx0JeL_Oeywe139rSe8k


Decoded:

1{
2  "iss": "https://url-for-din-identityserver.no",
3  "scope": [
4    "apiAccess.Read",
5  "name": "Ole Olsen",
6  "sid": "dadsas"
7}

Der token inneholder blant annet informasjon om:
iss = issuer, hvem har utstedt tokenet
name = hvem har fått tokenet
scope = hvilke rettigheter får den som bruker tokenet


Dette gjør det mulig for å sikre apier med bruk av bearer tokens:

1public class Startup
2{
3    public void ConfigureServices(IServiceCollection services)
4    {
5        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
6            .AddJwtBearer(options =>
7            {
8                ...
9                options.Authority = "https://url-for-din-identityserver.no";
10                ...
11            });
12    }
13}

Dette vil si at api’et godtar tokens utstedt fra “https://url-for-din-identityserver.no

Og for å opprette en policy, som sier hvilke scopes man krever:

1public void ConfigureServices(IServiceCollection services)
2{
3    services.AddAuthorization(options =>
4    {
5        options.AddPolicy("read_policy_navn", policy =>
6            policy.RequireClaim("scope", "apiAccess.read"));
7
8        options.AddPolicy("write_policy_navn", policy =>
9            policy.RequireClaim("scope", "apiAccess.write"));
10    });
11}

Disse policy’ene kan da benyttes for å sikre controller-endepunkter i API’ene med:

1public class RandomApiController : ControllerBase
2{
3    [Authorize("read_policy_navn")]
4    [HttpGet]
5    public async Task<IActionResult> GetDataFromApi()
6    {
7        return Ok(data);
8    }
9
10    [Authorize("write_policy_navn")]
11    [HttpPost]
12    public async Task<IActionResult> EditDataFromApi(weatherModel input)
13    {
14        UpdateData(input)
15        return Ok(input);
16    }
17}

Dette vil resultere i at kall til endepunktet uten token, automatisk gir en HttpStatusCode 401 Unauthorized, siden man ikke har sendt med noe token, og ikke er autentisert.

Kall med token, men som ikke har scope som behøves, vil gi en HttpStatusCode 403 Forbidden, siden man er autentisert, men ikke har rettigheter til å be om data fra endepunktet.

Tegning som viser kall mellom Weather API, API 1 og IdentityServer.

For å tilgjengeliggjøre at API’er kan hente ut sine egne tokens ved å kalle HTTP POST til connect/Token, må identityServeren konfigureres med tilhørende ClientId, ClientSecrets, og ApiScopes.
Dette kan enkelt gjøres i config, og IdentityServer har støtte for å benytte InMemory, eller for eksempel SqlServer med EF Core, for lagring av clients, scopes og secrets.

I IdentityServer:

1public static IEnumerable<ApiScope> ApiScopes =>
2    new ApiScope[]
3    {
4        new ApiScope(name: "weatherData.read", displayName: "fetch weather data"),
5        new ApiScope(name: "weatherData.write", displayName: "edit weather data")
6    };
7
8public static IEnumerable<Client> Clients =>
9    new Client
10    
11    {
12        new Client
13        {
14            ClientId = "Api1",
15            AllowedGrantTypes = GrantTypes.ClientCredentials,
16            ClientSecrets =
17            {
18                new Secret("secret".Sha256())
19            },
20            AllowedScopes = { "weatherData.read" } //Api1 får bare read-scope, ikke write
21        }
22    };
23
24public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
25{
26    builder.Services.AddIdentityServer()
27        .AddInMemoryApiScopes(Config.ApiScopes)
28        .AddInMemoryClients(Config.Clients);
29    return builder.Build();
30} 
31
32//se https://docs.duendesoftware.com/identityserver/v7/quickstarts/ for flere eksempler
33
34

Dette medfører at API1 kan gjør HTTP POST til Connect/Token, for å motta Bearer Token, som vil ha Scope “WeatherData.Read

For å gjøre det enda enklere kan stegene med å hente Token fra IdentityServer gjøres automatisk, med en nugetpakke Duende.AccessTokenManagement.

I Startup/Program.cs på API 1 kan man registrere en httpClient som automatisk henter og fornyer BearerToken for deg:

1builder.Services.AddClientCredentialsTokenManagement()
2    .AddClient("tokenHandlerClient", client =>
3    {
4        client.TokenEndpoint = "https://url-for-din-identityserver.no/connect/token";
5        client.ClientId = "Api1";
6        client.ClientSecret = "secret";
7        client.Scope = "weatherData.read";
8    });
9
10builder.Services.AddClientCredentialsHttpClient("ClientWithToken", "tokenHandlerClient", 
11client => {
12    client.BaseAddress = new Uri("my-secured-weather-api.com");
13});
14
15

Og deretter benytte Client for å hente data fra WeatherApi

1private readonly IHttpClientFactory _clientFactory;
2var client = _clientFactory.CreateClient("ClientWithToken");
3
4var response = await client.GetAsync("/Weather");

Denne typen flyt gjør tilgangsstyring veldig enkelt, sikkert og brukervennlig for utviklere, og vi kan sette opp kommunisering mellom en rekke systemer med tilgangsstyring.

Hvis dette virket interessant for deg, kan jeg på det sterkeste anbefale dokumentasjonen til Duende Software

Og lykke til med sikring av API’er.