Skip to main content
Pylon ships native OAuth for 25 providers, plus OpenID Connect discovery for any compliant IdP (Auth0, Okta, Keycloak, Cognito, Logto, Authentik, Zitadel…). A user clicks sign in, goes to the provider, and returns with a session token. Single-use state tokens protect against CSRF and survive a server restart mid-handshake. Pylon uses PKCE for providers that require it (Twitter/X, Kick) and response_mode=form_post for Apple. Pylon handles the provider-specific details; you set two env vars per provider.

Built-in providers

For any other OIDC-compliant IdP, use the generic OIDC adapter — see Generic OIDC.

Set up credentials

Register your app with the provider, then set:
Replace <PROVIDER> with GOOGLE, GITHUB, APPLE, etc. (the uppercase form of the table id above). If a provider’s CLIENT_ID or CLIENT_SECRET is unset, Pylon disables that provider. /api/auth/providers does not list it. On Pylon Cloud, set these in Settings → Environment per workspace.

Apple

Apple is different. Its client_secret is a JWT you sign with your developer ES256 key, not a static string. Pylon creates a fresh JWT on every token exchange, so you do not manage rotation.
Apple POSTs the callback (response_mode=form_post) instead of the usual GET. Pylon’s callback handler accepts both. The user’s identity comes from the id_token JWT in the token response, not a userinfo endpoint.
Note: Apple sends the user’s name only on the first sign-in for an app. Later logins return sub and email only. Pylon stores what it can and does not overwrite an existing displayName.

Microsoft

Set PYLON_OAUTH_MICROSOFT_TENANT for single-tenant apps:
Default is common (any account — work, school, personal).

Twitter / X

Twitter requires PKCE. Pylon generates the verifier and challenge automatically and stores them in the OAuth state record. No extra config is needed. Twitter’s userinfo does not include email by default, so Pylon synthesizes <username>@x.invalid to give account rows a value. Apps that require a real email should reject these accounts.

Generic OIDC

For any OpenID Connect provider — Auth0, Okta, Keycloak, Cognito, Logto, Authentik, Zitadel, your homegrown IdP — set:
The provider id is the lowercase of the env-var prefix (auth0 here). Pylon fetches <issuer>/.well-known/openid-configuration on first use and caches the discovered endpoints. The discovery doc’s token_endpoint_auth_methods_supported decides whether pylon uses Basic auth (the OIDC default) or a client_secret-in-body POST.

How it works

Profile pictures

Pylon captures the provider’s profile picture (OIDC picture claim, GitHub avatar_url) on every OAuth login and stores it on the account link. It refreshes each sign-in, so rotated provider CDN URLs stay current. It appears as avatar_url on:
  • GET /api/auth/me and GET /api/auth/session — for the signed-in user’s own account chip (useSession() exposes it as session.avatarUrl);
  • GET /api/auth/orgs/:id/members — so team rosters can render real photos.
The value is null for password and magic-link users (no provider picture), so render an initials fallback. Pylon stores only https:// URLs. When a user has several linked providers, the most recently used one wins.

Two ways to invoke

Browser flow (302 redirect)

GET /api/auth/login/<provider>?callback=<url>&redirect=1 returns a 302 directly to the provider. The callback handler sets a cookie and 302s to the callback URL you supplied:
callback (and the optional error_callback) MUST have an origin listed in manifest.auth.trustedOrigins (or PYLON_TRUSTED_ORIGINS). Loopback (http://localhost, 127.0.0.1, [::1], any port) is always auto-trusted, so pylon dev works without config. See Security plugins → Unified trustedOrigins.

JSON flow (manual)

For SPAs or native apps that want to control the redirect themselves:
You navigate the browser to redirect, the user comes back to your app’s ?code=...&state=xyz URL, and you POST it to the callback:
Response:

CSRF protection

Every OAuth start mints a random state token (256 bits, prefixed pylon_) that expires after 10 minutes and is single-use. The callback rejects the request if:
  • The state is missing
  • The state has expired
  • The state was already used (replay)
  • The state was minted for a different provider (e.g. Google state on a GitHub callback)
State storage defaults to in-memory; the runtime swaps in a SQLite or Postgres backend so a server restart mid-handshake doesn’t break in-flight sign-ins. PKCE verifiers ride along in the same record for providers that need them.

What gets created

On first OAuth sign-in for a given email, Pylon creates a User row:
On later sign-ins, Pylon looks up the user by (provider, provider_account_id), not by email, so a user who renamed their email keeps their account. Email verification is implicit. The OAuth provider already verified the email, so Pylon stamps emailVerified immediately. Your User entity should have:

Discovering configured providers

Use this in your sign-in UI to render only the buttons you’ve configured.

Error responses

Pylon sanitizes provider error messages. It redacts client_secret, code_verifier, refresh_token, id_token, and access_token values before they reach oauth_error_message redirect URLs or server logs.