Self-Service SSO Onboarding

Once you’ve federated a connection to a customer’s IdP, the next question is: what happens when someone who has never logged in before shows up? Authagonal gives you three postures for that unknown user, from strictest to most open, plus the controls to keep an external IdP from becoming a foot-gun. This guide is about picking and wiring the posture you want.

All of it is per-connection config (OidcProviderConfig / the OidcProviders seed, or the equivalent SAML fields). The relevant knobs:

Knob Effect
JitProvisioningEnabled May an unknown user be created at all?
ProvisioningAttributeParams Require invite context on the request before creating one.
AllowUninvitedJit Allow self-service creation with no invite (tagged with the connection).
IsExternalConnection Mark a third-party IdP so the first-party-only flags can’t apply.
InteractionPath Show a login-app page (name/terms) before federating.

Posture 1 — Invite-only (reject the uninvited)

The default. With JitProvisioningEnabled: false, an unknown SSO user is rejected outright (access_denied, “contact your administrator”) — good when every user must be pre-created by an admin or SCIM.

If you want JIT but only when an invite is present, enable JIT and declare ProvisioningAttributeParams. Those name the whitelisted /authorize query params that carry the invite context (e.g. acceptKind, acceptToken). An unknown user is provisioned only when that context actually arrived on the request; a bare SSO login with no invite is rejected, so a stray login can’t silently self-provision a new account/org.

{
  "OidcProviders": [
    {
      "ConnectionId": "acme-entra",
      "ConnectionName": "Acme (Entra)",
      "MetadataLocation": "https://login.microsoftonline.com/<tenant>/v2.0/.well-known/openid-configuration",
      "ClientId": "…", "ClientSecret": "…",
      "RedirectUrl": "https://auth.example.com/oidc/callback",
      "AllowedDomains": ["acme.com"],
      "JitProvisioningEnabled": true,
      "ProvisioningAttributeParams": ["acceptKind", "acceptToken"]
    }
  ]
}

The captured params land on the JIT user’s CustomAttributes and reach your provisioning Try handler, which is the real gate on the values (e.g. “does this invite token match this email?”). Authagonal captures whitelisted keys; your provisioner decides if they’re valid.

Posture 2 — Self-service (auto-provision an allowed-domain user)

For “any employee of a customer can just log in and get an account,” set AllowUninvitedJit: true. Now an unknown user from an allowed domain is provisioned even without invite context, and Authagonal tags them with the connection they came through so your provisioner can place them in the right tenant instead of spinning up a new one:

{
  "ConnectionId": "acme-entra",
  "AllowedDomains": ["acme.com"],
  "JitProvisioningEnabled": true,
  "ProvisioningAttributeParams": ["acceptKind", "acceptToken"],
  "AllowUninvitedJit": true
}

The tag arrives as a federated_connection custom attribute (its value is the connection name). Your Try handler branches on it:

app.post('/provisioning/try', async (req, res) => {
  const { userId, email, customAttributes } = req.body;

  if (customAttributes?.acceptToken) {
    // Invited: validate the invite and add them to that org.
    const org = await validateInvite(customAttributes.acceptToken, email);
    if (!org) return res.json({ approved: false, reason: 'Invalid invite' });
    stage(userId, { orgId: org.id, role: customAttributes.acceptKind ?? 'member' });
    return res.json({ approved: true, organizationId: org.id });
  }

  if (customAttributes?.federated_connection) {
    // Self-service: no invite, but they came through a known enterprise connection.
    const org = await orgForConnection(customAttributes.federated_connection);
    stage(userId, { orgId: org.id, role: 'member' });
    return res.json({ approved: true, organizationId: org.id });
  }

  return res.json({ approved: false, reason: 'No invite and no known connection' });
});

AllowUninvitedJit is opt-in per connection: a connection that declares ProvisioningAttributeParams but does not set it stays invite-only.

Keep external IdPs from becoming foot-guns

A few connection flags are safe on a connection you control but dangerous on an arbitrary third-party IdP:

Mark third-party connections external and those flags are neutralised even if set:

{
  "ConnectionId": "acme-entra",
  "IsExternalConnection": true,
  "UseUpstreamSubjectAsUserId": false,
  "AutoLinkExistingByEmail": false
}

IsExternalConnection defaults to false (first-party) so existing connections are unaffected. Set it on every connection that points at someone else’s IdP; a later misconfiguration then can’t hand that IdP control over local identities. (Attaching a federated identity to a pre-existing account still additionally requires the connection’s AllowedDomains to vouch for the email’s domain — see OIDC Federation → Security.)

Collect something before federating

Sometimes you need to show the user a page before bouncing them to the IdP — a guest’s display name, a terms checkbox, a plan picker. InteractionPath names a login-app route to render first:

{ "ConnectionId": "guest-link", "InteractionPath": "/guest" }

When an unauthenticated idp_hint={ConnectionId} request hits /connect/authorize, Authagonal redirects to {LoginAppUrl}{InteractionPath}?returnUrl=<authorize url>&connection={id} instead of straight to the IdP. Your page collects what it needs, appends the values to the returnUrl’s query (where PassthroughParams / ProvisioningAttributeParams read them from), and continues to /oidc/{id}/login itself. A page that decides no interaction is needed can continue immediately.