Skip to content

AuthProxy

The Cratis Stack

A gateway for the edges of your app

Every application eventually grows the same crop of edge concerns: who is this user, which tenant are they in, where does this request route, and how do you onboard someone who has been invited but doesn’t have an account yet. AuthProxy is a small .NET gateway that owns those edges — so each of your services can assume the request is already authenticated, already scoped to a tenant, and already enriched with identity.

Without a gateway, every service re-implements the same boilerplate: an OpenID Connect handshake, tenant resolution from the host or a claim, a call to fetch the user’s profile, the invite-acceptance flow. It’s repetitive, it drifts between services, and it’s exactly the kind of code you don’t want copy-pasted across a fleet.

AuthProxy is a reverse proxy (built on YARP) that you put in front of your backend and frontend services. It authenticates the request, resolves the tenant, enriches the identity, and then forwards the request to your service — with the tenant and identity attached as headers. Your services trust the proxy and read those headers.

/api/** + Tenant-ID + identity

/**

AuthProxy

Authenticate

(OIDC / JWT Bearer)

Resolve tenant

Enrich identity

(/.cratis/me)

Browser / client

Backend service

Frontend SPA

AuthProxy is configured entirely through the Cratis:AuthProxy section of appsettings.json (or the equivalent Cratis__AuthProxy__ environment variables). There is no code to write — you describe the providers, tenants, and services, and the proxy wires up the pipeline.

{
"Cratis": {
"AuthProxy": {
"Authentication": { },
"TenantResolutions": [ ],
"TenantVerification": { },
"Tenants": { },
"Services": { },
"Invite": { },
"PagesPath": ""
}
}
}

Configure one or more OpenID Connect providers. With a single provider, unauthenticated users are challenged directly; when more than one provider is configured, they land on a built-in provider-selection page. A provider’s Type selects the brand and logo shown there — Microsoft, Google, Apple, or Custom.

{
"Cratis": {
"AuthProxy": {
"Authentication": {
"OidcProviders": [
{
"Name": "Microsoft",
"Type": "Microsoft",
"Authority": "https://login.microsoftonline.com/<tenant-id>/v2.0",
"ClientId": "<client-id>",
"ClientSecret": "<client-secret>"
}
]
}
}
}
}

For providers that don’t publish an OIDC discovery document — GitHub is the common one — use OAuthProviders instead and give the endpoints explicitly. ClaimMappings maps fields from the provider’s user-info response onto claims:

{
"Cratis": {
"AuthProxy": {
"Authentication": {
"OAuthProviders": [
{
"Name": "GitHub",
"Type": "GitHub",
"AuthorizationEndpoint": "https://github.com/login/oauth/authorize",
"TokenEndpoint": "https://github.com/login/oauth/access_token",
"UserInformationEndpoint": "https://api.github.com/user",
"ClientId": "<client-id>",
"ClientSecret": "<client-secret>",
"ClaimMappings": { "name": "login" }
}
]
}
}
}
}

For machine-to-machine and API calls, configure JWT Bearer instead of (or alongside) the browser providers:

{
"Cratis": {
"AuthProxy": {
"Authentication": {
"JwtBearer": {
"Authority": "https://login.microsoftonline.com/<tenant-id>/v2.0",
"Audience": "<api-audience>"
}
}
}
}
}

TenantResolutions is an ordered list of strategies — AuthProxy tries each until one resolves a tenant. The resolved tenant id is forwarded to your services as a Tenant-ID header.

StrategyResolves the tenant from…
Hostthe request host, matched against configured Domains / SourceIdentifiers
SubHosta subdomain convention (e.g. acme.example.comacme) — handy for SaaS provisioning
Claima claim on the authenticated user
Routea regex over the request path
Specifieda fixed tenant id
Defaulta fallback tenant id
Selectiona .cratis-tenant cookie set by a tenant-selection page
{
"Cratis": {
"AuthProxy": {
"TenantResolutions": [
{ "Strategy": "Host" }
],
"Tenants": {
"acme": { "Domains": ["acme.example.com"] },
"contoso": { "Domains": ["contoso.example.com"] }
}
}
}
}

Optionally verify that a resolved tenant actually exists by pointing AuthProxy at a verification endpoint — a non-200 sends the user to the tenant-not-found page:

{
"Cratis": {
"AuthProxy": {
"TenantVerification": {
"UrlTemplate": "https://platform.example.com/api/tenants/{tenantId}"
}
}
}
}

Services is where you declare what AuthProxy sits in front of. Each service can have a backend, a frontend, or both. Set ResolveIdentityDetails to have AuthProxy call the backend’s /.cratis/me endpoint and attach the enriched identity to forwarded requests.

{
"Cratis": {
"AuthProxy": {
"Services": {
"portal": {
"Backend": { "BaseUrl": "http://portal-api:8080/" },
"Frontend": { "BaseUrl": "http://portal-web:3000/" },
"ResolveIdentityDetails": true
}
}
}
}
}

With a single service, AuthProxy uses catch-all routes (/api/** → backend, /** → frontend). With multiple services, route by a Service-ID header or a ?service= query parameter.

For invite-based onboarding, AuthProxy validates a signed JWT invite token (against a configured public key), runs the user through login, then exchanges the token for tenant membership. Users without a tenant yet can be routed to an optional lobby service.

{
"Cratis": {
"AuthProxy": {
"Invite": {
"PublicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"Issuer": "https://studio.example.com",
"Audience": "authproxy",
"ExchangeUrl": "https://studio.example.com/internal/invites/exchange",
"Lobby": {
"Frontend": { "BaseUrl": "http://lobby-service:3000/" },
"Backend": { "BaseUrl": "http://lobby-service:8080/" }
}
}
}
}
}

AuthProxy ships friendly built-in HTML for the edge cases — 404, 403, tenant-not-found, expired/invalid invitations, provider selection. Override any of them by mounting a directory of your own pages and pointing PagesPath at it.

{
"Cratis": {
"AuthProxy": {
"PagesPath": "/mnt/pages"
}
}
}

AuthProxy is published as a container image — cratis/authproxy — that you run with your configuration mounted or supplied as environment variables.

services:
authproxy:
image: cratis/authproxy:latest
ports:
- "8080:8080"
volumes:
- ./appsettings.json:/app/appsettings.json:ro
- ./pages:/mnt/pages:ro
environment:
Cratis__AuthProxy__PagesPath: /mnt/pages