SvRooij.Testcontainers.IdentityProxy 0.3.3

dotnet add package SvRooij.Testcontainers.IdentityProxy --version 0.3.3
                    
NuGet\Install-Package SvRooij.Testcontainers.IdentityProxy -Version 0.3.3
                    
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.3.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SvRooij.Testcontainers.IdentityProxy" Version="0.3.3" />
                    
Directory.Packages.props
<PackageReference Include="SvRooij.Testcontainers.IdentityProxy" />
                    
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 SvRooij.Testcontainers.IdentityProxy --version 0.3.3
                    
#r "nuget: SvRooij.Testcontainers.IdentityProxy, 0.3.3"
                    
#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 SvRooij.Testcontainers.IdentityProxy@0.3.3
                    
#: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=SvRooij.Testcontainers.IdentityProxy&version=0.3.3
                    
Install as a Cake Addin
#tool nuget:?package=SvRooij.Testcontainers.IdentityProxy&version=0.3.3
                    
Install as a Cake Tool

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.

Check out this example project on how to test a protected API using IdentityProxy as TestContainer and the WebApplicationFactory.

TestContainer .NET

Nuget package

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.

You can also check out the interactive documentation at http://localhost:8080/scalar/ or the openapi spec at http://localhost:8080/openapi/v1.json

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, everything else is optional. Microsoft Entra also uses 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 parameter, with the number of seconds you want the token to be valid.

And you'll get a response like this:

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

Duplicate a real token

If you have a real token, and you want a not expired version of it, you can post it to the /api/identity/duplicate-token endpoint. The proxy will decode the token, and create a new token with the same claims, but with a new expiration time.

For security reasons, you should make sure to strip of part of the signature, or to replace the signature with a dummy value!

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

{
  "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImZqQjMzS1E2OGhoQk5OSUZUMERuS3Roa3g1dURtaV9RWEpENGhnTGVUMEUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiI2MmViMjQxMi1mNDEwLTRlMjMtOTVlNy02YTkxMTQ2YmMzMmMiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vZGY2OGFhMDMtNDhlYi00YjA5LTlmM2UtOGFlY2M1OGUyMDdjL3YyLjAiLCJleHAiOjE3NjA0Njg2MzAsImlhdCI6MTc2MDQ2NTAzMCwibmJmIjoxNzYwNDY1MDIwLCJzdWIiOiI5OWYwY2JhYS1iM2JiLTRhNzctODFhNS1lOGQxN2IyMjMyZWMiLCJzdXBlcl9zcGVjaWFsX2NsYWltIjoiYmxhYmxhIn0",
}

This will give you the same answer as above.

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.

As of version v0.3.3 there is an interactive documentation page running at the /scalar endpoint, which uses the OpenAPI specifications available at /openapi/v1.json.

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
0.3.3 37 12/1/2025
0.3.1 63 12/1/2025
0.3.0 165 10/16/2025
0.2.0 199 8/11/2025
0.1.4 211 4/9/2025
0.1.2 277 7/13/2024
0.1.1 166 7/11/2024