Sign-in providers
Grant users authenticate with one or more authentication methods. Email/password is always available. GitHub and Google are optional social OAuth providers: enable each independently with a client ID and secret. Unconfigured providers are omitted from platform login, register, and Settings → Login & Security.
Project-app hosted sign-in uses a separate model. Social providers for tenant products are configured as per-project OAuth connections (customer BYO apps), not the platform env vars below. See Project OAuth. Platform env credentials may still be used as a fallback when a project has no BYO connection and PROJECT_OAUTH_REQUIRE_BYO_SOCIAL is false.
This page covers platform operator configuration, how Grant links identities, and how IdP profile photos become the user’s avatar.
Platform configuration
Set credentials in the Config app (GitHub OAuth / Google OAuth categories) or in the root env file. Leave a provider’s client ID and secret empty to hide it on platform surfaces.
| Variable | Purpose |
|---|---|
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET | GitHub OAuth App credentials |
GITHUB_CALLBACK_URL | Platform callback (default {APP_URL}/api/auth/github/callback) |
GITHUB_PROJECT_CALLBACK_URL | Grant broker callback for project OAuth (default {APP_URL}/api/auth/project/callback) — register on customer GitHub apps, not only platform |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Google Cloud Web client credentials |
GOOGLE_CALLBACK_URL | Platform callback (default {APP_URL}/api/auth/google/callback) |
GOOGLE_PROJECT_CALLBACK_URL | Grant broker callback for project OAuth (default {APP_URL}/api/auth/project/callback) — register on customer Google clients when using BYO |
PROJECT_OAUTH_CONNECTION_ENCRYPTION_KEY | Encrypts per-project BYO client secrets at rest (required to save connections) |
PROJECT_OAUTH_REQUIRE_BYO_SOCIAL | When true, project social sign-in requires BYO connections (no platform fallback; default false) |
If either ID or secret is set, both are required; the API refuses to start with a half-configured provider.
The web app loads GET /api/auth/providers for platform login/register only and renders providers with configured: true. Project hosted sign-in uses configuredProviders from GET /api/auth/project/app-info instead. Restart the API after changing credentials.
Callback URL rules differ by IdP:
- GitHub (platform) — one Authorization callback URL, prefix-matched. Register
{API}/api/authso the platform callback works. See Configuring platform GitHub OAuth. - Google (platform) — exact Authorized redirect URI for
{API}/api/auth/google/callback. See Configuring platform Google OAuth. - Project BYO — customer GitHub/Google apps register Grant’s broker project callback only (
GITHUB_PROJECT_CALLBACK_URL/GOOGLE_PROJECT_CALLBACK_URL). Never add tenant SPA URLs to the platform Google client. See Configuring social OAuth for project apps.
Requested scopes:
| Provider | Scopes |
|---|---|
| GitHub | user:email, read:user |
openid, email, profile |
Authentication methods
Each method is stored independently (user_authentication_methods). Constraints:
- A provider identity (for example one GitHub user id) can belong to only one Grant user.
- A Grant user can have at most one method per provider.
- Every user has exactly one primary method. OAuth-first signup keeps the social method primary.
Email is the mailbox, not the password. provider = email is the verified contact address (providerId). A password is optional and is stored as a separate platform credential, not in the Email method. Settings shows Set password when there is no platform credential, and Change password when there is.
Account linking
OAuth callbacks resolve the Grant user in this order:
| Path | When | Result |
|---|---|---|
| Login | The GitHub/Google identity is already linked | Issue a session for that user |
| Connect | The user started OAuth from Settings (or another authenticated flow) with userId in OAuth state | Create the social method on that account |
| Auto-link | No social method yet, but a verified Email method exists for the same address and the IdP marked the mailbox verified | Create the social method on that existing user |
| Register | None of the above | Create a user, the social method, and a personal or organization account |
Auto-link requires both sides verified. If the IdP email is unverified, or Grant’s Email method for that address is unverified, the callback fails (Email is not verified) instead of creating a second account or linking silently.
Passwordless Email bind
After signup, connect, auto-link, later social login, and project OAuth, Grant also tries to attach a passwordless Email method when the IdP reports a verified mailbox:
provider = email,providerId= normalized address,isVerified = true- no platform credential (password) is created
- Existing Email methods are not overwritten
- If another user already owns that Email address, OAuth still succeeds and the bind is skipped
Users can set a password later from Login & Security. Email/password login then uses that hash; social login still uses the GitHub/Google method.
Profile picture inheritance
Grant stores a single users.pictureUrl (max 500 characters). Social OAuth copies the IdP photo URL into that field; it does not download the file into Grant storage.
| Event | Behavior |
|---|---|
| New user from OAuth | Persist the IdP avatar URL when it is an http(s) URL within the length limit |
| Link / auto-link / connect | Copy the IdP URL only if pictureUrl is empty |
| Later social login | Does not replace an existing photo (uploaded avatar or a previous IdP URL) |
| User upload | Settings / profile upload writes a Grant storage URL and becomes the lasting picture |
The IdP URL is also kept on the social method’s providerData.avatarUrl for consent and display fallbacks. Access tokens in providerData are never sent to the client.
Displaying Google photos
Googleusercontent URLs (lh3.googleusercontent.com) reject hotlinks that send a Referer from the Grant origin (including localhost). Chrome then blocks the response as Opaque Response Blocking. Avatar <img> elements use referrerPolicy="no-referrer" so the photo loads. GitHub avatars (avatars.githubusercontent.com) do not need this, and the same policy is harmless for Grant-hosted /storage/ URLs.
Project apps
Project OAuth is documented in Project OAuth. Summary:
- Credentials — per-project OAuth connections (BYO GitHub/Google client id + secret), with optional fallback to platform env vars.
- Per app —
ProjectApp.enabledProviderssubsets which methods each app offers (email,github,google,password);redirectUrishold customer product callbacks only.password(email and password, checked against the project's own credential, never the platform password) is opt-in: an empty list does not enable it. - Linking — same global user resolution as platform OAuth, then project membership is checked.
- Tokens and callbacks — authorization-code + PKCE; Security § Project OAuth.
For multi-tenant SaaS, plan BYO connections per customer project rather than relying on one platform Google/GitHub app for all tenant products.
See also
- Security & session management — JWT, sessions, GitHub/Google callback URLs
- Configuration — Config app and env files
- Environment setup — Docker env for OAuth and email
References
- Handlers:
OAuthHandler.listProviders/handleCallback,AuthHandler.createUserFromOAuth,linkOAuthAuthToExistingUser,resolveUserIdFromOAuthForProject - Services:
UserAuthenticationMethodService.ensureVerifiedContactEmail - Libs:
oauth-picture.lib.ts,oauth-contact-email.lib.ts - Web:
OAuthProviderButtons,AvatarImage(referrerPolicy="no-referrer")