Elvia.Elvid.TokenClient 2.3.0

dotnet add package Elvia.Elvid.TokenClient --version 2.3.0
                    
NuGet\Install-Package Elvia.Elvid.TokenClient -Version 2.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Elvia.Elvid.TokenClient" Version="2.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Elvia.Elvid.TokenClient" Version="2.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Elvia.Elvid.TokenClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Elvia.Elvid.TokenClient --version 2.3.0
                    
#r "nuget: Elvia.Elvid.TokenClient, 2.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Elvia.Elvid.TokenClient@2.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Elvia.Elvid.TokenClient&version=2.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Elvia.Elvid.TokenClient&version=2.3.0
                    
Install as a Cake Tool

Elvia.Elvid.TokenClient

OAuth2 Client Credentials bibliotek for Elvid med automatisk token-håndtering og caching.

Erstatter IdentityModel avhengigheter og duplikat ElvidAccessTokenService kode.

Kom i gang

1. Installer

dotnet add package Elvia.Elvid.TokenClient

2. Konfigurer

// Forutsetter eksisterende Vault-konfigurasjon (AddHashiVaultSecrets)
builder.Services.AddElvidTokenProvider(options =>
{
    options.TokenEndpoint = builder.Configuration.EnsureHasValue("system:kv:elvid:tokenendpoint");
    options.ClientId = builder.Configuration.EnsureHasValue("system:kv:elvid:clientid");
    options.ClientSecret = builder.Configuration.EnsureHasValue("system:kv:elvid:clientsecret");
});

3. Bruk

HttpClient (anbefalt):

// Default provider
builder.Services.AddHttpClient("ExternalApi", client =>
{
    client.BaseAddress = new Uri("https://api.elvia.no/");
})
.AddElvidAuthHandler(); // Automatisk Bearer token fra default provider

// Named provider (for multiple clients)
builder.Services.AddHttpClient("KumaApi", client =>
{
    client.BaseAddress = new Uri("https://kuma.elvia.io/");
})
.AddElvidAuthHandler("kuma"); // Bruker navngitt "kuma" provider

// I service:
var client = _httpClientFactory.CreateClient("ExternalApi");
var response = await client.GetAsync("endpoint"); // Token legges til automatisk

Direkte (Client Credentials):

public class MyService
{
    private readonly IElvidTokenProvider _tokenProvider;
    
    public async Task<string> GetTokenAsync()
    {
        return await _tokenProvider.GetAccessTokenAsync([]);
    }
}

Password Grant (for testing/smoketests):

public class MySmokeTestService
{
    private readonly IElvidTokenProvider _tokenProvider;
    
    public async Task<string> GetUserTokenAsync()
    {
        return await _tokenProvider.GetAccessTokenAsync("testuser", "testpass", []);
    }
}

Standalone bruk (uten Dependency Injection):

// Perfect for smoketests og enkle skript
var tokenProvider = ElvidTokenClientFactory.Create(
    "https://elvid.test-elvia.io/connect/token",
    "your-client-id",
    "your-client-secret");

// Client credentials
var token = await tokenProvider.GetAccessTokenAsync([]);

// Password grant
var userToken = await tokenProvider.GetAccessTokenAsync("testuser", "testpass", []);

Multiple API clients (anbefalt for større systemer):

// Registrer flere navngitte clients
builder.Services.AddElvidTokenProvider("convey", options =>
{
    options.TokenEndpoint = builder.Configuration.EnsureHasValue("elvid:kv:elvid:elvid-convey:tokenendpoint");
    options.ClientId = builder.Configuration.EnsureHasValue("elvid:kv:elvid:elvid-convey:clientid");
    options.ClientSecret = builder.Configuration.EnsureHasValue("elvid:kv:elvid:elvid-convey:clientsecret");
});

builder.Services.AddElvidTokenProvider("kuma", options =>
{
    options.TokenEndpoint = builder.Configuration.EnsureHasValue("elvid:kv:elvid:elvid-kuma:tokenendpoint");
    options.ClientId = builder.Configuration.EnsureHasValue("elvid:kv:elvid:elvid-kuma:clientid");
    options.ClientSecret = builder.Configuration.EnsureHasValue("elvid:kv:elvid:elvid-kuma:clientsecret");
});

// I service - bruk factory til å få riktig client
public class MyService
{
    private readonly IElvidTokenProviderFactory _tokenProviderFactory;

    public async Task CallConveyAsync()
    {
        var conveyProvider = _tokenProviderFactory.GetProvider("convey");
        var token = await conveyProvider.GetAccessTokenAsync([]);
        // Use token for Convey API calls
    }

    public async Task CallKumaAsync()
    {
        var kumaProvider = _tokenProviderFactory.GetProvider("kuma");
        var token = await kumaProvider.GetAccessTokenAsync([]);
        // Use token for Kuma API calls
    }
}

JWT Bearer Authentication

For applikasjoner som skal validere ElvID tokens (ikke bare hente dem), tilbyr TokenClient en standardisert JWT authentication setup:

Enkel oppsett:

using Elvia.Elvid.TokenClient.DependencyInjection;

// I Program.cs
var authority = builder.Configuration.EnsureHasValue("elvid:kv:shared:elvidauthority");
builder.Services.AddElvidJwtAuthentication(authority);

// Det er alt! Standardoppsett for ElvID tokens er konfigurert:
// ✅ MapInboundClaims = false (bevarer originale claim types)
// ✅ ValidateAudience = false (ElvID tokens krever ikke audience validering)
// ✅ ValidateIssuer = false (stoler på authority vi konfigurerte)

Før (manuelt oppsett):

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = authority;
        options.MapInboundClaims = false;
        options.TokenValidationParameters.ValidateAudience = false;
        options.TokenValidationParameters.ValidateIssuer = false;
    });

Etter (TokenClient helper):

builder.Services.AddElvidJwtAuthentication(authority);

Multiple authentication schemes:

// Legg til flere schemes ved behov
builder.Services
    .AddElvidJwtAuthentication("https://elvid.elvia.io") // Default scheme
    .AddElvidJwtBearer("https://other-authority.io", "OtherScheme");

Bruk i controllers:

[Authorize] // Krever gyldig ElvID token
public class MyController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        var userId = User.FindFirst("sub")?.Value;
        var email = User.FindFirst("email")?.Value;
        return Ok(new { userId, email });
    }
}

Microsoft Graph API Integration

TokenClient inkluderer innebygd støtte for Microsoft Graph API med automatisk token-håndtering og caching. Perfekt for AD-gruppeoppslag, brukeroppslag, og authorization handlers.

Erstatter: ElvID sin GraphApiService og manuelle Graph API kode i andre systemer.

Konfigurer

using Elvia.Elvid.TokenClient.DependencyInjection;

// I Program.cs
builder.Services.AddElvidGraphClient(options =>
{
    options.TenantId = builder.Configuration.EnsureHasValue("elviaad:tenantid");
    options.ClientId = builder.Configuration.EnsureHasValue("elviaad:clientid");
    options.ClientSecret = builder.Configuration.EnsureHasValue("elviaad:clientsecret");
});

Brukeroppslag

public class UserService
{
    private readonly IElvidGraphClient _graphClient;

    public async Task<GraphUser?> GetUserInfoAsync(string userPrincipalName)
    {
        var user = await _graphClient.GetUserAsync(userPrincipalName);

        if (user != null)
        {
            // ElvID Graph API returnerer kun essensielle felt
            Console.WriteLine($"ID: {user.Id}");
            Console.WriteLine($"Name: {user.DisplayName}");
            Console.WriteLine($"Email: {user.Mail}");
            Console.WriteLine($"UPN: {user.UserPrincipalName}");

            // MERK: Sensitive felt er filtrert bort (telefon, jobbtittel, kontor)
        }

        return user;
    }
}

AD Gruppeoppslag

public class GroupService
{
    private readonly IElvidGraphClient _graphClient;

    // Hent alle gruppe-IDer (rask, cached i 5 min)
    public async Task<string[]> GetUserGroupsAsync(string userPrincipalName)
    {
        return await _graphClient.GetUserGroupIdsAsync(userPrincipalName);
    }

    // Hent alle grupper med detaljer (navn, beskrivelse, etc.)
    public async Task<GraphGroup[]> GetUserGroupDetailsAsync(string userPrincipalName)
    {
        return await _graphClient.GetUserGroupsAsync(userPrincipalName);
    }

    // Sjekk om bruker er medlem av spesifikk gruppe
    public async Task<bool> IsAdminAsync(string userPrincipalName)
    {
        return await _graphClient.IsUserMemberOfGroupAsync(
            userPrincipalName,
            "admin-group-id");
    }

    // Sjekk om bruker er medlem av noen av gruppene
    public async Task<bool> HasAccessAsync(string userPrincipalName)
    {
        var allowedGroups = new[] { "group-1-id", "group-2-id", "group-3-id" };
        return await _graphClient.IsUserMemberOfAnyGroupAsync(
            userPrincipalName,
            allowedGroups);
    }
}

Authorization Handler (ASP.NET Core)

public class AdGroupRequirement : IAuthorizationRequirement
{
    public string[] AllowedGroupIds { get; }

    public AdGroupRequirement(params string[] allowedGroupIds)
    {
        AllowedGroupIds = allowedGroupIds;
    }
}

public class AdGroupHandler : AuthorizationHandler<AdGroupRequirement>
{
    private readonly IElvidGraphClient _graphClient;

    public AdGroupHandler(IElvidGraphClient graphClient)
    {
        _graphClient = graphClient;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AdGroupRequirement requirement)
    {
        var userPrincipalName = context.User.FindFirst("upn")?.Value
                             ?? context.User.FindFirst("preferred_username")?.Value;

        if (userPrincipalName == null) return;

        // IsUserMemberOfAnyGroupAsync bruker cached data (5 min)
        var isMember = await _graphClient.IsUserMemberOfAnyGroupAsync(
            userPrincipalName,
            requirement.AllowedGroupIds);

        if (isMember)
        {
            context.Succeed(requirement);
        }
    }
}

// I Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.Requirements.Add(new AdGroupRequirement("admin-group-id")));
});

builder.Services.AddSingleton<IAuthorizationHandler, AdGroupHandler>();

Gruppedetaljer og medlemmer

public class GroupManagementService
{
    private readonly IElvidGraphClient _graphClient;

    // Hent info om en gruppe
    public async Task<GraphGroup?> GetGroupInfoAsync(string groupId)
    {
        return await _graphClient.GetGroupAsync(groupId);
    }

    // Hent alle medlemmer i en gruppe (kun IDs - rask)
    public async Task<string[]> GetGroupMemberIdsAsync(string groupId)
    {
        return await _graphClient.GetGroupMemberIdsAsync(groupId);
    }

    // Hent alle medlemmer med grunnleggende brukerinfo (for visning i UI)
    public async Task<GraphUser[]> GetGroupMembersAsync(string groupId)
    {
        var members = await _graphClient.GetGroupMembersAsync(groupId);

        // Hver bruker har kun essensielle felt (personvern):
        // - Id, UserPrincipalName, DisplayName, Mail
        //
        // MERK: Sensitive felt er filtrert bort av ElvID Graph API:
        // - Telefonnummer (MobilePhone, BusinessPhones)
        // - Jobbtittel (JobTitle)
        // - Kontorplassering (OfficeLocation)
        // - Navn-komponenter (GivenName, Surname)

        return members;
    }

    // Hent alle eiere av en gruppe (administratorer)
    public async Task<GraphUser[]> GetGroupOwnersAsync(string groupId)
    {
        // Returnerer samme filtrerte brukerinfo som GetGroupMembersAsync()
        return await _graphClient.GetGroupOwnersAsync(groupId);
    }

    // Hent grunnleggende brukerinfo
    public async Task<GraphUser?> GetUserAsync(string userIdentifier)
    {
        // Kan bruke UPN (user@elvia.no) eller bruker-ID
        // Returnerer samme filtrerte brukerinfo som gruppemedlemmene
        return await _graphClient.GetUserAsync(userIdentifier);
    }
}

Use Case: Meldingsportalen

Viser hvordan Meldingsportalen (eller lignende systemer) kan bruke Graph API:

public class MessagePortalService
{
    private readonly IElvidGraphClient _graphClient;

    // Vise hvem som har tilgang til en melding (basert på gruppe)
    public async Task<List<UserInfo>> GetMessageRecipientsAsync(string groupId)
    {
        // Hent alle medlemmer i gruppen med full info
        var members = await _graphClient.GetGroupMembersAsync(groupId);

        return members.Select(m => new UserInfo
        {
            EntraId = m.Id,
            UserName = m.DisplayName ?? "Unknown",
            Email = m.Mail ?? m.UserPrincipalName
        }).ToList();
    }

    // Identifisere administratorer/ansvarlige
    public async Task<List<UserInfo>> GetGroupAdministratorsAsync(string groupId)
    {
        var owners = await _graphClient.GetGroupOwnersAsync(groupId);

        return owners.Select(o => new UserInfo
        {
            EntraId = o.Id,
            UserName = o.DisplayName ?? "Unknown",
            Email = o.Mail ?? o.UserPrincipalName
        }).ToList();
    }

    // Display navn i UI/logger (optimalisert)
    public async Task<string> GetDisplayNameForLogAsync(string userId)
    {
        // Kun display navn - raskere enn full user lookup
        var displayName = await _graphClient.GetUserDisplayNameAsync(userId);
        return displayName ?? "Unknown User";
    }
}

Fordeler med Graph Integration

  • Automatisk token-håndtering - Ingen manuell håndtering av Graph API tokens
  • Innebygd caching - Gruppeoppslag caches i 5 minutter (konfigurerbart)
  • Thread-safe - Sikker for concurrent requests
  • Retry-logikk - Automatisk retry ved midlertidige feil
  • Personvern innebygd - ElvID Graph API filtrerer sensitive felt automatisk
  • Erstatter GraphApiService - Drop-in replacement for ElvID sin GraphApiService
  • Ingen IdentityModel - Ingen avhengighet til deprecated IdentityModel pakker
  • Type-safe - Sterkt typede modeller for alle operasjoner

Migration fra IdentityModel

Problem: KubernetesClient vulnerability krever oppgradering av Elvia.Configuration, som fjerner IdentityModel avhengigheter.

Løsning: Erstatt IdentityModel med TokenClient.

SetBearerToken Extension Method

TokenClient inkluderer SetBearerToken extension method for enkel migrering:

using Elvia.Elvid.TokenClient.Http;

// Fungerer akkurat som IdentityModel sin SetBearerToken
var token = await _tokenProvider.GetAccessTokenAsync([]);
httpClient.SetBearerToken(token);

Før (IdentityModel - fungerer ikke lenger):

// Krever IdentityModel pakke som ikke lenger er inkludert
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
    Address = "https://elvid.test-elvia.io/connect/token",
    ClientId = "client-id",
    ClientSecret = "secret"
});
var token = tokenResponse.AccessToken;

Etter (TokenClient - fungerer uten eksterne avhengigheter):

Med Dependency Injection:

// I Program.cs - kun én gang
builder.Services.AddElvidTokenProvider(options =>
{
    options.TokenEndpoint = builder.Configuration.EnsureHasValue("system:kv:elvid:tokenendpoint");
    options.ClientId = builder.Configuration.EnsureHasValue("system:kv:elvid:clientid");
    options.ClientSecret = builder.Configuration.EnsureHasValue("system:kv:elvid:clientsecret");
});

// Client credentials - caches automatisk
var token = await _tokenProvider.GetAccessTokenAsync([]);

// Password grant (for smoketests)
var userToken = await _tokenProvider.GetAccessTokenAsync("testuser", "testpass", []);

Uten Dependency Injection (smoketests):

// Direkte opprettelse - perfect for smoketests
var tokenProvider = ElvidTokenClientFactory.Create(
    "https://elvid.test-elvia.io/connect/token",
    "client-id",
    "client-secret");

var token = await tokenProvider.GetAccessTokenAsync("testuser", "testpass", []);

Fordeler

  • Løser IdentityModel-problem - Ingen avhengighet til IdentityModel eller Duende pakker
  • Client Credentials + Password Grant - Støtter både machine-to-machine og user authentication
  • JWT Bearer Authentication - Standardisert oppsett for validering av ElvID tokens
  • Microsoft Graph API - Innebygd støtte for AD-gruppeoppslag og brukerinfo
  • Automatisk caching - Thread-safe med per-scope, per-user og Graph API caching
  • Multiple named clients - Factory pattern for separate API konfigurasjoner (Convey, Kuma etc.)
  • HttpClient-integrasjon - Automatisk Bearer token påføring (også uten scopes)
  • Retry-logikk - Automatisk retry ved 5xx feil
  • Ingen scopes påkrevd - Client credentials flow uten scopes
  • Fjerner duplikat kode - Erstatter ElvidAccessTokenService, GraphApiService og standard authentication setup
  • Perfect for smoketests - Password grant støtte og standalone factory
  • Standalone support - ElvidTokenClientFactory.Create() for bruk uten DI

Publisering

Tag en versjon for å publisere til NuGet:

git tag tokenclient-v1.0.0
git push origin tokenclient-v1.0.0
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.3.0 10,386 1/8/2026
2.2.0 315 1/6/2026
2.1.0 3,120 12/1/2025
2.0.0 524 12/1/2025
1.6.0 4,892 10/28/2025
1.5.1 1,611 10/10/2025
1.5.0 730 10/8/2025
1.4.0 624 10/7/2025
1.3.0 1,907 9/30/2025
1.2.0 383 9/29/2025
1.1.0 328 9/26/2025
1.0.1 189 9/26/2025

v2.3.0:
- FIX: Default scope for ElvidGraphClient changed from "elvid.api" to "elvid.graph.read"
- Graph API now works out of the box without explicitly setting Scopes
- Updated documentation to reflect correct scope requirements

v2.2.0:
- FIX: IHttpClientFactory is now automatically registered by AddElvidTokenProvider extensions
- Fixes "Unable to resolve service for type 'System.Net.Http.IHttpClientFactory'" error
- No code changes required for consumers - just update the package

v2.1.0:
- Multi-target support: Now compatible with both .NET 8 and .NET 10
- .NET 8 consumers can use this package again (was .NET 10 only in v2.0.0)
- .NET 10 consumers continue to get optimized .NET 10 assemblies
- No API changes from v2.0.0

v2.0.0:
- BREAKING: Upgraded to .NET 10 LTS
- Updated all Microsoft.Extensions.* packages to 10.0.0
- Updated Microsoft.AspNetCore.Authentication.JwtBearer to 10.0.0

v1.6.0:
- NEW: Microsoft Graph API integration with IElvidGraphClient
- NEW: AD group membership queries with automatic caching (GetUserGroupIdsAsync, GetUserGroupsAsync)
- NEW: User lookup operations (GetUserAsync) - privacy-filtered, essential fields only
- NEW: Group membership checks (IsUserMemberOfGroupAsync, IsUserMemberOfAnyGroupAsync)
- NEW: Group details and member queries (GetGroupAsync, GetGroupMemberIdsAsync)
- NEW: GetGroupMembersAsync() - Get group members with privacy-filtered user info
- NEW: GetGroupOwnersAsync() - Get group owners with privacy-filtered user info
- NEW: AddElvidGraphClient() extension method for easy setup
- PRIVACY: ElvID Graph API filters sensitive fields (phone, job title, office location) automatically
- PRIVACY: All user endpoints return only essential fields (Id, UPN, DisplayName, Mail)
- Enhanced documentation for JWT Bearer authentication setup
- Replaces ElvID's GraphApiService and removes IdentityModel dependency for Graph API
- All Graph API results cached for 5 minutes by default
- Thread-safe Graph API operations with automatic token management
- Comprehensive examples for authorization handlers using AD groups
- Real-world examples: Meldingsportalen integration patterns
- Updated package description and tags to reflect Graph API support

v1.5.1:
- CRITICAL FIX: Domain detection now works correctly with cached discovery endpoints
- Fixed bug where cached token endpoint was not adjusted for current request domain
- Ensures domain translation (.no vs .io) applies on every request, not just first discovery call

v1.5.0:
- Added automatic domain detection and translation (.no vs .io) for ElvID token endpoints
- TokenClient now detects if application runs on elvia.no or elvia.io domain and adjusts token endpoint accordingly
- Similar to AtlasUrl logic, but domain-aware based on incoming HTTP requests
- Fixes issues where applications on .no domain tried to call ElvID on .io (and vice versa)
- Uses IHttpContextAccessor to detect current host domain
- Changed default Create() method to use TokenEndpoint (recommended approach)
- Added CreateWithAuthority() for backwards compatibility with Authority-based discovery
- Updated README to recommend TokenEndpoint as primary configuration method
- TokenEndpoint is simpler, faster, and more robust than Authority-based discovery
- Breaking change: Existing ElvidTokenClientFactory.Create(authority, ...) calls will now interpret first parameter as TokenEndpoint. Use CreateWithAuthority() for Authority.
- Fully backward compatible - works seamlessly for both single-domain and multi-domain deployments

v1.4.0:
- Added TokenEndpoint option to bypass discovery (CRITICAL FIX for internal ElvID calls)
- Added AddElvidJwtAuthentication() extension method for standard JWT Bearer setup
- Added AddElvidJwtBearer() for multiple authentication schemes
- Fixes deployment issues when discovery endpoint not accessible from internal services
- Simplifies common JWT authentication configuration across applications consuming ElvID tokens
- Fully backward compatible

v1.3.0:
- Enhanced AddElvidAuthHandler to support named token providers
- Smart parameter detection: single string = provider name, multiple = scopes
- Enables proper HttpClient integration with multiple named providers
- Fixes integration with IElvidTokenProviderFactory

v1.2.0:
- Added SetBearerToken extension method for easier migration from IdentityModel
- Improved documentation for HttpClient usage patterns

v1.1.0:
- Added Password Grant support for smoketests and user authentication
- Added standalone factory (ElvidTokenClientFactory.Create) for non-DI scenarios
- Added multiple named client support via IElvidTokenProviderFactory
- Removed scope requirement from HttpClient extensions
- Thread-safe per-user token caching for Password Grant
- Enhanced error handling and logging

v1.0.x: Initial OAuth2 Client Credentials support