Skip to content

Project OAuth ​

Projects can register OAuth apps (ProjectApp) so a tenant product can send users through Grant’s hosted sign-in and consent pages, then receive a project-scoped JWT. Consent returns an authorization code (query string), not an access token in the URL fragment. Exchange the code at POST /api/auth/project/token with S256 PKCE. The security model — global user resolution, project membership, token claims, discovery — is in Project OAuth. This page covers the hosted UI, branding, and how social providers are configured.

Hosted pages ​

PageRoutePurpose
Sign-in/auth/projectChoose a provider (GitHub, Google, email) and see requested permissions
Email/auth/project/emailRequest a magic-link when email is enabled
Authorize/auth/project/consentConfirm the signed-in account and allow or deny access
Relay/auth/project/relayGrant-hosted callback for Test OAuth: exchanges ?code= and postMessages tokens to the same-origin opener. Tokens are never painted on this page.

Grant’s header stays visible. The client brand is the page subject: app mark, title, and theme. Use Test OAuth on the app to check the live pages. Hosted sign-in and Test OAuth attach S256 PKCE before calling authorize.

Tenant products should generate PKCE on their own origin, send the user through hosted sign-in (or directly to authorize), keep the code_verifier, and exchange at the token endpoint. Register the product callback on the ProjectApp redirectUris list. Do not use the Grant relay URL as a production client redirect.

Public metadata for those pages comes from unauthenticated REST:

  • GET /api/auth/project/app-info
  • GET /api/auth/project/consent-info

Both return resolved branding only. They never expose storage keys, inherit-from-project flags, organization logos, or OAuth client secrets.

Social connections (GitHub and Google) ​

Project apps can offer GitHub and Google sign-in. Unlike platform Grant login (which uses env vars — see Sign-in providers), social credentials for project apps are configured per Grant project as OAuth connections.

ConceptWhere it livesPurpose
OAuth connectionProject settings → OAuth connectionsCustomer’s GitHub or Google client id + secret for this project
ProjectAppApp → OAuth cardCustomer product redirect_uris, enabledProviders, scopes
Broker callbackGrant API env (GITHUB_PROJECT_CALLBACK_URL, GOOGLE_PROJECT_CALLBACK_URL)The only redirect URI registered on the customer’s GitHub/Google app

One connection per provider per project. All apps in that project share the same GitHub/Google client. Each app’s enabled providers list still controls which methods that app offers.

Email magic-link does not use a connection row; it is always available when enabled on the app.

Operator setup (BYO) ​

  1. Open the project OAuth connections page (…/projects/{projectId}/oauth-connections).
  2. For GitHub or Google, enter the client id and client secret from the customer’s OAuth app.
  3. Copy the Grant callback URL shown on that page and register it on the customer’s provider app:
    • Default: {APP_URL}/api/auth/project/callback (exact match for Google; GitHub accepts {APP_URL}/api/auth as a prefix — see Security).
  4. On the ProjectApp OAuth card, ensure the provider is in enabled providers.
  5. Set PROJECT_OAUTH_CONNECTION_ENCRYPTION_KEY on the API so BYO secrets can be stored (see Configuration).

Secrets are write-only: the API never returns them after save. Rotating a secret requires a new upsert.

Platform fallback (migration) ​

When a project has no BYO connection for a provider, project OAuth may fall back to the platform GITHUB_* / GOOGLE_* env credentials (same behavior as before this feature). Set PROJECT_OAUTH_REQUIRE_BYO_SOCIAL=true to disable fallback so project apps must have BYO connections for social sign-in. Default: false.

Hosted sign-in shows a social button only when the provider is configured for the project and enabled on the app:

visible = configuredProviders ∩ enabledProviders

configuredProviders comes from app-info (project connections + fallback policy). Hosted pages do not call GET /api/auth/providers (that endpoint is for platform login only).

The Password button (key icon) is different: it needs no connection, and it is shown only when the app's enabledProviders names password. An empty list shows Email and the configured social providers, never Password. It opens its own view with an email and password form; sign-in checks only the project's own credential for the app's project, and requires an existing membership.

Forgot password ​

The password view's Forgot password? link opens a view that asks for the email address and always answers "if an account exists, we've sent a link": it never says whether the address belongs to a member. The email (branded with the app's name and sent in the visitor's locale) is sent only to an existing member of the app's project that has an Email method. The link opens a view where the member chooses a new password; completing it sets the project password (a later CDM import never overwrites it), clears the failed-attempt lockout, marks the email address verified, and signs the member out of this project's apps everywhere (their refresh grants in the project are revoked). It then sends them back to the password sign-in. Their platform (console) password and their other projects are not affected. See Security § Project OAuth for the token, the limits and PROJECT_PASSWORD_RESET_*.

Branding ​

Project owners set a default logo and OAuth theme on the project Branding page (…/projects/{projectId}/branding). Each app can override those values on its Branding card.

FieldProjectProject appResolved default
PictureLogo / initialsOverride or inheritApp → project → initials
primaryColor#RRGGBBOverride or inheritApp → project → Grant blue
showHelpPanelOn / offApp sets the value used on hosted pagesApp → project → on
themeMode—light / dark / system / Grant defaultApp only; null keeps the visitor’s Grant theme

Organization logos are not in the fallback.

Pictures are upload-only. Update inputs do not accept pictureUrl. Anyone with Update on Project or ProjectApp may upload, clear, or change theme. Storage keys:

  • projects/{projectId}/picture.{ext}
  • project-apps/{projectAppId}/picture.{ext}

See File storage.

The authorize page shows who is consenting and which provider they used for this sign-in (GitHub, Google, or email). That value is stored on the consent token for this login; it is not inferred from every method on the account.

Long permission lists show the first five scopes, then Show N more.

Authorization code and tokens ​

After Allow, Grant redirects to redirect_uri?code=&state=. POST /api/auth/project/token supports:

  • grant_type=authorization_code — code, code_verifier, redirect_uri, client_id, plus client_secret when the app is confidential
  • grant_type=refresh_token — rotating refresh; membership and granted scopes are recomputed

The token endpoint does not accept client_credentials, does not issue id_token, and is not POST /api/auth/refresh. Refresh secrets live in project_oauth_grants (hashed). See Security § Token endpoint.

Discovery ​

RFC 8414 metadata is published with well-known before the issuer path. {iss}/.well-known/openid-configuration returns the same JSON. Grant is not an OpenID Provider. Shared authorize/token URLs; per-project iss and jwks_uri. See Security § Discovery.

  • Project OAuth security — authorize, consent ?code=, token endpoint, discovery, redirect allowlists, IdP callback setup
  • Sign-in providers — platform GitHub/Google env configuration (Grant account login)
  • ADR 0008 — why connections are per-project and BYO
  • File storage — upload adapters and paths

Released under the MIT License.