One proxy, several hosts
Say you run a console, an operations tool and a Chronicle workbench, each on its own host. Today that
means three AuthProxy instances, three OAuth apps (or three callback URLs on one app), and three sign-ins:
a person who signs in to the console is signed out on operations.example.com. This page puts all three
behind one AuthProxy with one sign-in.
It is a how-to for someone who already runs AuthProxy. For the individual settings, see Services, Authentication and Logout.
What you end up with
Section titled “What you end up with”| Host | Service | Who may use it |
|---|---|---|
console.example.com | console | Any signed-in person |
operations.example.com | operations | Members of Cratis/operations |
chronicle.example.com | chronicle | Any signed-in person |
auth.example.com | none — runs the sign-in handshake | Everyone (optional, see step 4) |
A request for a host reaches that host’s service, and that service’s own
authorization requirements apply to it. One session cookie, scoped to .example.com,
is honored on every host.
1. Give each service its hosts
Section titled “1. Give each service its hosts”{ "Cratis": { "AuthProxy": { "Services": { "console": { "Hosts": [ "console.example.com" ], "Frontend": { "BaseUrl": "http://console-web:3000/" }, "Backend": { "BaseUrl": "http://console-api:8080/" } }, "operations": { "Hosts": [ "operations.example.com" ], "Frontend": { "BaseUrl": "http://operations-web:3000/" }, "Backend": { "BaseUrl": "http://operations-api:8080/" }, "Authorization": { "RequiredClaims": [ { "Claim": "urn:github:team", "AnyOf": [ "Cratis/operations" ] } ] } }, "chronicle": { "Hosts": [ "chronicle.example.com" ], "Frontend": { "BaseUrl": "http://chronicle-web:3000/" }, "Backend": { "BaseUrl": "http://chronicle-api:8080/" } } } } }}AuthProxy refuses to start when two services claim the same host, or when a host is not a plain host name with an optional port. See Routing by host or path prefix for the precedence rules.
From Aspire:
authProxy .WithHosts("console", "console.example.com") .WithHosts("operations", "operations.example.com") .WithHosts("chronicle", "chronicle.example.com");What your ingress has to do
Section titled “What your ingress has to do”AuthProxy routes on the Host header it receives, so the ingress in front of it must forward every host
to AuthProxy and preserve the original Host header. An ingress that rewrites it to one internal name
sends every request to the same service. Set
AllowedHosts to the
host names you serve, as well: it is the setting that makes AuthProxy answer only for them. AuthProxy
warns at startup when a cookie domain is set and AllowedHosts accepts every host.
2. Share the session
Section titled “2. Share the session”{ "Cratis": { "AuthProxy": { "Session": { "CookieDomain": ".example.com" } } }}From Aspire: authProxy.WithSessionCookieDomain(".example.com").
The cookie now carries Domain=.example.com, so the browser sends it to every host under it. Choose the
narrowest domain that covers your hosts. A domain cookie also goes to hosts that are not behind
AuthProxy, and a broad domain such as .com widens who can replay it. AuthProxy refuses to start when:
- a service declares a host outside the domain;
- the domain is an IP address, a top-level domain, a wildcard, or has a port;
- tenants are resolved by
SubHost.
Nothing else about the session changes: the lifetime, the re-validation and the identity cookies behave as
described in Authentication. The services’ own
authorization still decides what each host lets a person do, so a session issued for console does not
open operations for someone without the Cratis/operations claim.
3. Register the provider callbacks
Section titled “3. Register the provider callbacks”Without a callback host, every host runs its own handshake, so the identity provider has to know the callback of
each one. For a GitHub OAuth app that provider is registered under the scheme github, and the callbacks are:
https://console.example.com/signin-githubhttps://operations.example.com/signin-githubhttps://chronicle.example.com/signin-githubRegister all of them, or use a provider that accepts a wildcard redirect address. The post-logout address
works the same way: each host’s /.cratis/logout/callback has to be registered with a provider that signs
out at the provider.
That is the whole setup when you only need one sign-in. If registering a callback per host is the part you want to get rid of, continue.
4. Run every handshake on one host
Section titled “4. Run every handshake on one host”{ "Cratis": { "AuthProxy": { "SignIn": { "CallbackHost": "auth.example.com" } } }}From Aspire: authProxy.WithSignInCallbackHost("auth.example.com").
A sign-in that starts on chronicle.example.com is sent to auth.example.com, runs the whole handshake
there, and returns to the page the person asked for on chronicle.example.com. The provider needs one
callback, https://auth.example.com/signin-github, and one post-logout address. The session cookie is the
one from step 2, so the person arrives signed in.
CallbackHost needs Session:CookieDomain, and the host has to lie inside it; AuthProxy refuses to start
otherwise. auth.example.com does not have to be declared as a service, but it has to reach AuthProxy.
Where a sign-in may return to
Section titled “Where a sign-in may return to”An absolute return address is an open redirect waiting to happen, so AuthProxy honors one only when a callback host is configured, and only when all of these hold:
- the origin belongs to a host a service declares, or to a configured frontend, lobby or
Logout:AllowedRedirectOriginsentry; - the host lies inside the cookie domain, so the session follows the person there;
- for a declared host, the scheme is the one the request arrived on and the port is the one the host declares, so an HTTPS sign-in is never returned to HTTP;
- there is no user information in the address.
Anything else is reduced to its path on the host the sign-in started on, or to /. Sign-out uses the same
list. Without a callback host, return addresses are relative paths exactly as before.
Invitations, links and registration stay on their host
Section titled “Invitations, links and registration stay on their host”Invitation, credential-link and registration flows start their own challenge on the host the person is on, with cookies that belong to that host alone. A callback host does not move them, and the provider has to accept the callback of every host where one can start. If you only register the callback host, send people into those flows from that host.
Migrating from several instances
Section titled “Migrating from several instances”The old instances each issued their own host-only cookie, usually under the default name. After the move, a browser can hold the old host-only cookie and the new shared one under the same name. AuthProxy deletes the stale copy for you:
- when a new session is issued, after the new cookie is written;
- when a person signs out;
- when the browser sends the session cookie twice;
- when a session cookie arrives that cannot be read.
It only ever deletes the host-only copy, never the shared one. If the request is for the cookie domain
itself (for example example.com with .example.com), the browser may not distinguish the two, so nothing
is deleted there.
Three more things help:
- Rename the cookie.
Session:CookieName(Aspire:WithSessionCookieName) gives the new session its own name, so it can never be confused with an old one, and lets two AuthProxy deployments share a parent domain without reading each other’s sessions. Changing it signs everyone out once, and the old default-named cookie is deleted on the next sign-in and sign-out. The__Host-prefix is refused with a cookie domain, because browsers do not accept a domain on such a cookie. - Delete cookies other deployments left.
Logout:AdditionalCookiesremoves cookies AuthProxy does not own, including ones scoped to the parent domain. - Watch header size. A cookie on the parent domain travels with every request to every host under it.
Sessions that carry many claims are chunked into several cookies, and an ingress with a small header
limit (nginx
large_client_header_buffers) can answer400for a browser that still holds the old chunks too. Raise the limit during the migration.
Why the cookie domain is refused with SubHost tenancy
Section titled “Why the cookie domain is refused with SubHost tenancy”With the SubHost strategy each tenant lives on its own subdomain,
and the host-only cookie is what keeps a session for one tenant off the next tenant’s host. A cookie shared
across the domain would carry it to all of them, and the tenant check only confirms that a tenant
exists, not that the person belongs to it. AuthProxy therefore refuses to start with both, and there is no
setting to override that. Resolve tenants some other way, or keep the host-only cookie.
Limits to know about
Section titled “Limits to know about”- A host is a default, not a boundary. A service-selection header or
?service=still wins over the host, as it always has, so a frontend on one host can call another service’s backend by naming it. The other service’s authorization requirements apply to that call, so it cannot do anything the person could not do there, but its responses are readable by scripts on the first host. If two applications must not read each other’s data, they must not share an AuthProxy. SameSite=Laxdoes not separate sibling subdomains. They are the same site, so a request fromconsole.example.comtooperations.example.comcarries the cookie. Treat every host under the cookie domain as inside the same trust boundary.- The
Hostheader is the ingress’s to protect. AuthProxy trusts the host it receives. SetAllowedHostsand keep the trusted-proxy boundary declared.
Check your work
Section titled “Check your work”- Sign in on
console.example.com, then openchronicle.example.comin the same browser. You should be signed in, without a provider redirect. - Open
operations.example.comas someone without the claim. You should get the forbidden page, not the application. - Sign out on
chronicle.example.com, then reloadconsole.example.com. You should be signed out there too. - With a callback host, start a sign-in on
chronicle.example.comand watch the address bar: it visitsauth.example.comand ends on the page you asked for. - In the browser’s cookie storage, look for the session cookie on each host. There should be one, with
Domain=.example.com, and no host-only copy.