Invitation to Organization
Use this flow when you invite a user into an organization that already exists. AuthProxy still uses
the standard /invite/<token> bootstrap, but the invite token carries tenant information so the
user can continue directly into the application instead of being sent to the lobby.
- The user opens
https://your-authproxy/invite/<token>. - AuthProxy validates the token, creates an independent 256-bit transaction and challenge, and calls
Invite.StageUrl. The signedinvite-stageattestation binds those values to the exact capability hash, invitation ID, and tenant before provider authentication starts. - AuthProxy protects the pending state in an HTTP-only cookie and binds it into the provider’s protected challenge state. A browser can carry the values but cannot author or substitute them.
- After login, AuthProxy re-validates the token (signature, issuer, audience, and lifetime), the protected pending state, and the provider challenge binding.
- AuthProxy requires one canonical provider identity, one provider-derived email with an exact verified value
of
true, one provider-derived assurance value, and the authentication-ticket issue time. - AuthProxy calls
Invite.ExchangeUrlwith a signedinvite-completeattestation. The JSON body contains only the opaque transaction ID; the browser and request body never supply identity authority. - AuthProxy compares the configured
Invite.TenantClaimfrom the token with the resolved tenant for the request. - If the tenant IDs match, AuthProxy skips the lobby redirect and continues to the target service.
If the tenant IDs do not match, or AuthProxy cannot resolve a tenant for the request, the invite is treated like lobby onboarding and falls back to the configured lobby behavior.
Configuration
Section titled “Configuration”{ "Cratis": { "AuthProxy": { "Invite": { "StageUrl": "https://lobby.example.com/_invite/stage", "ExchangeUrl": "https://lobby.example.com/_invite/exchange", "TenantClaim": "tenant_id", "EmailClaim": "email", "Attestation": { "Issuer": "https://auth.example.com", "Audience": "ada-lobby", "ActiveKeyId": "invite-2026-08", "Lifetime": "00:01:00", "SigningKeys": [ { "KeyId": "invite-2026-08", "PrivateKeyPem": "<load-from-secret-provider>" } ] }, "Lobby": { "Frontend": { "BaseUrl": "http://lobby-service:3000/" } } } } }}| Property | Type | Description |
|---|---|---|
StageUrl | string | Absolute URL of the Lobby invitation authority’s pre-authentication staging endpoint. Required when Attestation is configured. |
ExchangeUrl | string | Absolute URL of the same Lobby invitation authority’s completion endpoint. |
TenantClaim | string | Claim in the invite token that contains the tenant ID. |
EmailClaim | string | Claim in the invite token that contains the invited email. Required by the signed protocol. |
Attestation.Issuer | string | Exact issuer the invitation authority validates. |
Attestation.Audience | string | Exact invitation-authority audience. |
Attestation.ActiveKeyId | string | Key ID used for newly signed attestations. |
Attestation.SigningKeys | array | RSA private signing keys. Load private PEM values from a secret provider; pin only the matching public keys downstream. |
Attestation.Lifetime | TimeSpan | Short-lived token lifetime from 10 through 60 seconds. Defaults to 60 seconds. |
Lobby.Frontend.BaseUrl | string | Fallback redirect if the invite cannot continue directly into the organization. |
Signed protocol
Section titled “Signed protocol”The staging call uses Authorization: Bearer <stage-attestation> and this bounded body:
{ "invitationTransaction": "<opaque transaction>", "invitationToken": "<exact signed invitation capability>", "invitationChallenge": "<independent opaque challenge>"}The completion call uses Authorization: Bearer <complete-attestation> and a body with no identity fields:
{ "invitationTransaction": "<opaque transaction>"}Both RS256 JWTs require kid, iss, aud, jti, iat, nbf, and exp. They bind purpose, tenant_id,
invitation_id, invitation_transaction, invitation_challenge, and capability_hash. Every completion
attestation additionally carries provider_key, provider_issuer, provider_subject, assurance, and
authenticated_at. Email-targeted completion also carries email and email_verified=true.
The same Lobby invitation authority owns both endpoints. It must independently validate the raw invitation during staging, compare its exact SHA-256
hash to capability_hash, persist no raw capability, and atomically consume the transaction and complete-attestation
jti exactly once. It must reject a wrong purpose, signature, key ID, issuer, audience, lifetime, tenant,
invitation, transaction, challenge, or capability hash.
Configure canonical federated identity for every provider.
The selected email, verification, and assurance claim types must name provider-derived claims. Missing, duplicate,
empty, unverified, or ambiguous evidence fails closed before the completion endpoint is called.
Set CanonicalIdentity.InvitationCompletionEnabled=true only for providers that can supply that evidence. Signed
invitation provider selection hides every other provider while leaving it available for ordinary sign-in. Microsoft
Entra commonly omits email_verified; do not enable it unless the tenant maps an equivalent trustworthy custom
claim and configures its exact claim type. AuthProxy never promotes email or preferred_username to verified
evidence implicitly.
Every signed invitation must select exactly one recipient-authority mode: either one nonempty invited-email claim, or the exact immutable provider-binding pair below. Missing, duplicate, partial, or mixed recipient claims are rejected before staging and again before completion.
For a recipient already known by immutable provider identity, the signed invitation may carry both
recipient_provider_key and recipient_identity_binding. The binding is a canonical 43-character base64url
HMAC-SHA-256 value over the provider key, tenant-specific validated issuer, and immutable provider subject using
the invitation authority’s documented length-delimited input format. AuthProxy validates the claim shape, restricts
the chooser and callback to the exact provider key, and emits the provider key, validated issuer, subject,
assurance, and authentication time without inventing email evidence. The invitation authority remains responsible
for independently recomputing the opaque binding and comparing it to the staged capability before atomic
consumption. Enable this route with
CanonicalIdentity.InvitationIdentityBindingCompletionEnabled=true; email-targeted invitations continue to require
InvitationCompletionEnabled=true and exact verified-email evidence.
For Microsoft Entra identity-bound invitations, configure a tenant-specific authority and
CanonicalIdentity.SubjectClaimType=oid. The framework-validated tenant issuer plus immutable object ID is the
identity tuple; email and preferred_username are not substitutes.
Compatibility. Omitting
Invite.Attestationretains the released unsigned JSON exchange for existing deployments. That legacy mode is not sufficient authority for creating or linking an account. Enable the signed protocol before an application treats invitation completion as identity proof.
Rotate signing keys
Section titled “Rotate signing keys”- Add the new public key to the invitation authority’s pinned verification set.
- Add the new private key and key ID to
SigningKeyswithout changingActiveKeyId. - Deploy both sides, then switch
ActiveKeyId. - Keep the previous public key until every token it signed has expired, then remove the old key from both sides.
AuthProxy selects exactly one active key and always writes its kid. It never logs private key material or
attestation payloads.
Requirements
Section titled “Requirements”- The invite token must include the claim configured in
Invite.TenantClaim. - The request must resolve to the same tenant value after authentication.
- The invitation link should use the same host and route shape that the organization’s normal traffic uses so tenant resolution produces the expected value.
When to use another flow
Section titled “When to use another flow”- If the invited user should create a new organization, use Invitation for Creating Organization.
- If the user should self-register and create an organization without an invite, use Registration.