Credential Linking
AuthProxy can let an already signed-in user prove control of an additional identity-provider login and associate it with their existing account — without ever replacing their current session. This is the proof-of-control building block behind an application’s “add a credential” feature.
GET /.cratis/link?token=<one-time-link-token> → embeddable provider-selection pageGET /.cratis/link/{scheme}?returnUrl=<relative-url>&token=<one-time-link-token> → challenge for one providerGET /.cratis/link/complete → completion pageIt is deliberately not the same as /.cratis/login/{scheme}: login signs the authenticated identity into
the primary session cookie (which, for a second identity, would swap who the user is — effectively logging
them out of the original account). The link flow authenticates the second provider, captures its subject,
and hands that subject to the application, all while leaving the primary session untouched.
Embedding the flow in the application
Section titled “Embedding the flow in the application”The flow is designed to run inside a modal iframe on the application’s own page. There is one hard
constraint it is built around: the provider leg cannot be framed. External identity providers
(login.microsoftonline.com, accounts.google.com, …) send X-Frame-Options: DENY /
frame-ancestors 'none' on their sign-in pages, so any challenge honored inside an iframe leaves a dead
frame. The flow is therefore split:
- The iframe shows AuthProxy pages only. The application embeds
/.cratis/link?token=…— the provider-selection page. It lists the configured providers and, on a click, opens/.cratis/link/{scheme}withwindow.open— a separate top-level window where the provider authenticates. AuthProxy enforces this shape server-side: a navigation to/.cratis/link/{scheme}whoseSec-Fetch-Destsays it is framed is answered with the selection page instead of a challenge. - The provider window reports back over a
BroadcastChannel. The completion page (/.cratis/link/complete, where the flow ends by default) and the failure page broadcast{ type: 'cratis:credential-link-complete' }/{ type: 'cratis:credential-link-failed' }on the same-origin channelcratis.credential-link— deliberately notwindow.opener, which an identity provider’sCross-Origin-Opener-Policycan sever mid-flow. The completion window then closes itself. - The framed page signals the parent with
postMessage. On either outcome the selection page forwards the message towindow.parent, targeted at the configured embed ancestor origins (never*), so the application can close its modal and refresh — or leave the modal open for a retry after a failure.
Embedding is off by default. The link pages send Content-Security-Policy: frame-ancestors 'none'
(plus X-Frame-Options: DENY) until the deployment names the origins allowed to frame them:
Cratis__AuthProxy__Link__EmbedAncestors__0=selfself means the proxy’s own origin — the common case, where the application is served through the proxy.
Additional entries may name other origins (e.g. https://app.example.com). This setting opens the link
pages only; sign-in and selection pages always refuse framing, and proxied application responses are
never touched.
Opening /.cratis/link/{scheme} directly in a popup or top-level redirect — the pre-embedding shape —
still works exactly as before.
How it works
Section titled “How it works”- The application mints a one-time link token. When the user starts “add a credential”, the application
issues a short-lived, single-use token bound to that signed-in user, and opens the popup at
/.cratis/link/{scheme}?returnUrl=…&token=…. Theschemeis a configured provider scheme (the same value used by/.cratis/login/{scheme}, e.g.github). - AuthProxy challenges the provider. The request must come from an authenticated session (an anonymous
request is rejected with
401; an unknown scheme with404; a missing token with400). AuthProxy starts an OAuth/OIDC challenge for the requested scheme, carrying a link-mode marker and the link token through the authentication properties. - On the provider callback the identity is captured, not signed in. Instead of writing the primary
authentication cookie, AuthProxy reads the freshly authenticated
subject(and identity provider) andPOSTs them to the configuredExchangeUrl, authenticated with the link token as the bearer credential — exactly mirroring the invite exchange. The user’s original session is preserved. - AuthProxy ends the flow — completion only when the exchange succeeded. On success the browser is
redirected to the supplied
returnUrl, or to the flow’s own completion page (/.cratis/link/complete) when none was supplied — which broadcasts the outcome and closes the window. If the exchange did not succeed — the endpoint is not configured, the link token or the provider subject could not be resolved, the endpoint was unreachable, or the application answered with a non-2xx status — the browser receives a generic link-failure page with HTTP403instead, and never the completion redirect. The page is the same for every cause, so it reveals nothing about which one occurred; the cause is logged for the operator. A failed provider round-trip (correlation failure, provider error, the person cancelling) ends on the same failure page — never on the sign-in selection page, whose full sign-ins would offer to replace the very session the link was preserving. In every case the user’s primary session is left exactly as it was.
The request body posted to ExchangeUrl depends on whether the selected provider opts into
canonical federated identity.
Legacy providers keep the existing body:
{ "subject": "<provider subject>", "identityProvider": "<issuer / provider>" }Canonical providers add the stable provider-aware identity fields:
{ "subject": "<configured canonical subject>", "providerKey": "<configured stable provider key>", "issuer": "<normalized validated or configured issuer>", "identityProvider": "<same value as providerKey>"}with Authorization: Bearer <one-time-link-token>.
Canonical identity is opt-in per provider. During migration, the application endpoint must accept both
bodies. For a canonical body, bind the account with the complete (providerKey, issuer, subject) tuple;
never treat subject alone or the compatibility identityProvider field as the stable account key.
The tuple records provider authentication metadata only. The application still decides whether that identity
may link a credential to the current account.
The returnUrl parameter
Section titled “The returnUrl parameter”returnUrl is echoed back to the browser after the link completes, so it is constrained to a same-site
relative path (a single leading /, but not //). Anything else — including an absolute URL to another
origin — falls back to the flow’s own completion page (/.cratis/link/complete), so the endpoint can never
be turned into an open redirect. Omitting returnUrl lands on the completion page too, which is the right
default for the embedded flow.
Configuration
Section titled “Configuration”Set the application endpoint that records the freshly authenticated subject under
Cratis:AuthProxy:Link:ExchangeUrl. It is the link counterpart of Cratis:AuthProxy:Invite:ExchangeUrl.
{ "Cratis": { "AuthProxy": { "Link": { "ExchangeUrl": "https://studio.example.com/api/internal/identity-providers/link", "EmbedAncestors": ["self"] } } }}Equivalent environment variables:
Cratis__AuthProxy__Link__ExchangeUrl=https://studio.example.com/api/internal/identity-providers/linkCratis__AuthProxy__Link__EmbedAncestors__0=selfEmbedAncestors names the origins allowed to embed the link pages in an iframe (see
Embedding the flow in the application); when empty — the default —
the link pages refuse framing entirely.
When ExchangeUrl is empty or the Link section is absent, there is nowhere to post the subject, so the link
callback cannot complete: the browser receives the generic link-failure page (HTTP 403) rather than the
completion redirect. The flow is effectively disabled, and it fails visibly rather than reporting success for
a link that was never recorded.
Security. The JSON callback body is not signed. The one-time bearer token binds the operation to the signed-in application user, but it does not by itself prove that AuthProxy sent the HTTP request. Keep
ExchangeUrlnetwork-isolated from public traffic or authenticate AuthProxy separately at the application endpoint. Keep the token short-lived and single-use, and never turn successful provider authentication into application authorization without applying the application’s own linking policy.