SvRooij.Testcontainers.IdentityProxy 0.1.2

dotnet add package SvRooij.Testcontainers.IdentityProxy --version 0.1.2                
NuGet\Install-Package SvRooij.Testcontainers.IdentityProxy -Version 0.1.2                
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="SvRooij.Testcontainers.IdentityProxy" Version="0.1.2" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add SvRooij.Testcontainers.IdentityProxy --version 0.1.2                
#r "nuget: SvRooij.Testcontainers.IdentityProxy, 0.1.2"                
#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.
// Install SvRooij.Testcontainers.IdentityProxy as a Cake Addin
#addin nuget:?package=SvRooij.Testcontainers.IdentityProxy&version=0.1.2

// Install SvRooij.Testcontainers.IdentityProxy as a Cake Tool
#tool nuget:?package=SvRooij.Testcontainers.IdentityProxy&version=0.1.2                

IdentityProxy

IdentityProxy is a proxy that sits between your API (protected with tokens from an IdP) and your Identity Provider (IdP) to provide a way to mock tokens during integration tests. More details here.

Usage

The best way to use IdentityProxy is the run it as a TestContainer. This way you can start the proxy in your test setup, and it will be automatically stopped when the tests are done.

TestContainer .NET

This is a work in progress, and the API might change. And the TestContainer library is not yet released.

using Testcontainers.IdentityProxy;

var identityProxy = new IdentityProxyBuilder()
    .WithAuthority("https://login.microsoftonline.com/svrooij.io/v2.0/")
    .Build();

await identityProxy.StartAsync();

Console.WriteLine($"Well known config at: {identityProxy.GetAuthority()}.well-known/openid-configuration");
// At this point you should configure your api to use the value from identityProxy.GetAuthority() as the authority in the JWT middleware.

Console.WriteLine($"You can request a token by posting to {identityProxy.GetAuthority()}api/identity/token");
// Or by using identityProxy.GetTokenAsync(...)
var tokenResult = await identityProxy.GetTokenAsync(new TokenRequest
{
    Audience = "https://api.svrooij.io",
    Subject = "test",
    AdditionalClaims = new Dictionary<string, object>
    {
        { "scope", "openid profile email" }
    }
});

Console.WriteLine($"Token: {tokenResult?.AccessToken}");

Console.ReadLine();

await identityProxy.DisposeAsync();

Docker

The IdentityProxy is just a Docker container ghcr.io/svrooij/identityproxy:latest. You can run it with the following command:

docker run -p 8080:8080 -e EXTERNAL_URL='http://localhost:8080/' -e IDENTITY_AUTHORITY='https://login.microsoftonline.com/svrooij.io/v2.0/' ghcr.io/svrooij/identityproxy:latest

The EXTERNAL_URL is the URL where the proxy is reachable from the outside. The IDENTITY_AUTHORITY is the base URL of the IdP to mock. The proxy will then listen on port 8080 and forward requests to the IdP.

Get a mocked token

If you want a token you can request it from the /api/identity/token endpoint. The token will be signed with a certificate that is generated on startup. This certificate (the public key) is also injected in the JWKS response (so the server will accept the tokens as if they were real).

POST http://localhost:8080/api/identity/token
Accept: application/json
Content-Type: application/json

{
  "aud": "62eb2412-f410-4e23-95e7-6a91146bc32c",
  "sub": "99f0cbaa-b3bb-4a77-81a5-e8d17b2232ec",
  "expires_in": 3600,
  "additional_claim_1": "value1",
  "additional_claim_2": "value2"
}

The sub (Subject) claim is required, as well as the aud (Audience) claim. Any additional claims you provide will be added to the token. The nbf (Not Before) and exp (Expiration) claims are automatically added to the token, you can however control the lifetime of the token by providing the expires_in claim, with the number of seconds you want to token to be valid.

And you'll get a response like this:

{
  "access_token": "::token::",
  "expires_in": 3600,
}

How does it work?

API authentication these days is mostly done with Json Web Tokens, since they are stateless and don't require a database lookup for each request. This means that the API needs to have the public keys of the IdP to validate the tokens. The IdP provides these keys in a JWKS (Json Web Key Set) endpoint, which the API can use to validate the tokens. Most backends can be configured by just specifying the Authority (the base URL of the IdP) and the Audience (the client ID of the API). The backend will then fetch the JWKS from the IdP and use it to validate the tokens. First it loads the OpenID Configuration from a well-known endpoint (/.well-known/openid-configuration), which contains the URL of the JWKS endpoint. Then it fetches the Json Web Key Set from the JWKS endpoint, which contains the public keys.

JWT Authentication flow

During normal operation the client requests a token from the IdP, then uses that token to make requests to the API. The API validates the token using the IdP's public keys. Check this image if the flow won't show up.

  sequenceDiagram
    participant Client
    participant API
    participant IdP
    Client->>IdP: Give me a token
    activate IdP
    IdP->>Client: Here is a token
    deactivate IdP
    Client->>API: Request with token
    API-->>IdP: Give me the openid config (once)
    activate IdP
    IdP-->>API: OpenID Configuration
    API-->>IdP: Give me the signing keys (once)
    IdP-->>API: JWKS result
    deactivate IdP
    API->>API: Validate token using signing keys
    API->>Client: Response

JWT Authentication flow with IdentityProxy

During integration testing you will need to test multiple user roles and scenarios. This can be difficult or cumbersome with a real IdP, where you would have to manage all the different credentials. IdentityProxy allows you to mock the .well-known/openid-configuration endpoint, to change the jwks_uri to point to the proxy. The proxy will then return the real public keys from the IdP, and inject an additional certificate to be able to generate any tokens you need for testing. Check this image if the flow won't show up.

sequenceDiagram
    participant Client
    participant API
    participant Proxy
    participant IdP
    Client->>Proxy: Give me a token
    activate Proxy
    Proxy-->>IdP: Give me the OpenID config (once)
    IdP-->>Proxy: OpenID Configuration
    Proxy-->>Proxy: Generate signing certificate
    Proxy->>Proxy: Sign token with cert
    Proxy->>Client: Here is a token
    deactivate Proxy
    Client->>API: Request with token
    API-->>Proxy: Give me the openid config (once)
    activate Proxy
    Proxy-->>IdP: Give me the OpenID config (once)
    IdP-->>Proxy: OpenID Configuration
    Proxy-->>API: OpenID Configuration (with JWKS_uri modified)
    API-->>Proxy: Give me the signing keys (once)
    Proxy-->>IdP: Give me the real signing keys (once)
    IdP-->>Proxy: Real JWKS result
    Proxy-->>API: JWKS result (+ 1 extra cert)
    deactivate Proxy
    API->>API: Validate token using signing keys
    API->>Client: Response

Developer notes

I've tried my best to make the proxy as fast as possible, by enabling Native AOT and packaging it in a chiseled docker image.

Having an OpenAPI spec would be nice, but is seems there is an issue with Swashbuckle and AOT.

And the root url should return a fancy page with some info about the proxy, if someone would actually open it in their browser.

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. 
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
0.1.2 110 7/13/2024
0.1.1 83 7/11/2024