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
<PackageReference Include="SvRooij.Testcontainers.IdentityProxy" Version="0.1.2" />
paket add SvRooij.Testcontainers.IdentityProxy --version 0.1.2
#r "nuget: SvRooij.Testcontainers.IdentityProxy, 0.1.2"
// 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 | Versions 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. |
-
net8.0
- Testcontainers (>= 3.9.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.