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.
The friction it removes
Section titled “The friction it removes”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.
What it handles
Section titled “What it handles”OpenID Connect and OAuth 2.0 (single or multi-provider), plus JWT Bearer. Unauthenticated requests are challenged or sent to a provider-selection page.
Multi-tenancyResolve the current tenant per request — from the host, a subdomain, a claim, the route, or a fixed value — with optional remote verification.
Identity enrichmentCalls a /.cratis/me endpoint on your service and attaches the enriched identity to forwarded requests.
Invite-based onboarding with signed JWT tokens, plus an optional lobby service for users not yet assigned to a tenant.
Configuration at a glance
Section titled “Configuration at a glance”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": "" } }}Authentication
Section titled “Authentication”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>" } } } }}Tenancy
Section titled “Tenancy”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.
| Strategy | Resolves the tenant from… |
|---|---|
Host | the request host, matched against configured Domains / SourceIdentifiers |
SubHost | a subdomain convention (e.g. acme.example.com → acme) — handy for SaaS provisioning |
Claim | a claim on the authenticated user |
Route | a regex over the request path |
Specified | a fixed tenant id |
Default | a fallback tenant id |
Selection | a .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 and routing
Section titled “Services and routing”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.
Invites and lobby
Section titled “Invites and lobby”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/" } } } } }}Custom error pages
Section titled “Custom error pages”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" } }}Running it
Section titled “Running it”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{ "AllowedHosts": "*", "Cratis": { "AuthProxy": { "Authentication": { "OidcProviders": [ { "Name": "Microsoft", "Type": "Microsoft", "Authority": "https://login.microsoftonline.com/<tenant-id>/v2.0", "ClientId": "<client-id>", "ClientSecret": "<client-secret>" } ] }, "TenantResolutions": [ { "Strategy": "Host" } ], "Tenants": { "acme": { "Domains": ["acme.example.com"] } }, "Services": { "portal": { "Backend": { "BaseUrl": "http://portal-api:8080/" }, "Frontend": { "BaseUrl": "http://portal-web:3000/" } } } } }}