1. Docs
  2. Authentication
  3. Hosted Login

Hosted Login

Let Canopy host the login UI. Your app redirects to /oauth/authorize, the user signs in on Canopy's hosted page, and your callback receives an authorization code that you exchange for tokens.

Overview

Hosted Login is the standard OAuth 2.0 Authorization Code with PKCE flow plus OIDC identity claims — Canopy owns the sign-in UI, your app owns the callback. You send the user to /oauth/authorize, they authenticate against Canopy's hosted login page, and we redirect them back to your registered redirect_uri with a single-use authorization code. Your backend trades the code for an access_token, id_token, and refresh_token at /oauth/token. Choose this path when you don't want to render or maintain login UI yourself, can accept the redirect round-trip, and want OIDC discovery, JWKS verification, and refresh-token rotation out of the box.

Prerequisites

Before the redirect-and-receive-tokens flow will succeed end to end, you need these in place. Most are configured once per Environment.

The flow, end to end

Five steps from the user clicking your sign-in button to your backend holding their tokens. The diagram traces who talks to whom; the steps below give you the code for each leg.

Before redirecting, create a cryptographically random code_verifier and the SHA-256 hash of it (code_challenge). PKCE is mandatory — the token endpoint will reject any code exchange without a matching verifier. Store the verifier in your session, keyed by the state parameter you'll send next.

Build the authorize URL with your client metadata, the requested scopes, an opaque state, and the PKCE challenge. Send the user there with a 302. Canopy renders the hosted login page in their browser.

Canopy handles the form, the password check, and any error messaging. There's no consent screen — scopes are pre-approved at client registration — so on success the user is redirected immediately. No code from your side.

The browser hits your registered redirect_uri with ?code=…&state=…. The code is single-use and expires in 60 seconds. Validate state against the value you stored in step 1 — if it doesn't match, treat it as a CSRF attempt and abort.

Your backend POSTs to /oauth/token with the code, your client credentials, the original redirect URI, and the PKCE verifier from step 1. The body can be either application/x-www-form-urlencoded (the RFC 6749 standard used by every OIDC client library) or application/json — Canopy accepts both. The endpoint returns the access token, ID token, and refresh token. After this point the code is dead.

What's in the tokens

Three values come back from /oauth/token. The access token and ID token are RS256-signed JWTs verifiable against your JWKS endpoint. The refresh token is an opaque string Canopy uses for rotation — never inspect or parse it.

Sent on every API call as Authorization: Bearer …. 15-minute lifetime — short enough that revocation isn't necessary; let it expire and refresh.

Carries identity claims about the authenticated user. Same standard claims as the access token (sub, iat, exp, iss) plus identity claims gated by the scopes you requested: email and email_verified with the email scope, name with the profile scope. Pass to your frontend if it needs to display the user's name or email; never use it to authorize API calls (that's the access token's job).

An opaque string (not a JWT). Backend-only — store it next to the user's session and trade it for a new access token when the current one expires. Treat it like a long-lived credential. See Refresh tokens below for rotation rules.

Verifying tokens

Every JWT you accept must be verified locally — never trust the token without checking the signature, issuer, and expiration. Canopy publishes its public keys at the JWKS endpoint; cache them and refresh on a key-id miss.

Fetch the JWKS from https://auth.canopy-io.com/.well-known/jwks.json.Look at the JWT's kid header. Find the matching key in the JWKS by kid.Verify the RS256 signature locally with that public key.Reject if iss isn't https://auth.canopy-io.com, aud isn't your client_id, or exp is in the past.Cache the JWKS in memory. On a kid miss, refetch once — Canopy may have rotated keys.

Node — using the jose library

Refresh tokens

Refresh tokens last 180 days and are rotated on every use. Each call to /oauth/token with grant_type=refresh_token returns a new refresh token alongside the new access token. Save the new one immediately — the old one is dead the moment the response is sent.

Refresh request

Logout & revocation

Revoke a refresh token at any time by posting it to the revocation endpoint. The matching access token isn't revoked server-side — it expires on its own within 15 minutes, and there's no efficient way to invalidate JWTs without a per-request blocklist.

For a hard logout, revoke the refresh token and clear the user's session cookie. The short access-token TTL bounds the worst-case window for any in-flight requests.

Error responses

Token-endpoint errors follow RFC 6749 §5.2: { "error": "...", "error_description": "..." }. The error code is machine-readable; the description is for logging and developer-facing UI, not end users.

Sample error response

Gotchas

Things that will trip you up at least once. Bookmark this section.

Client secret is shown only once. Capture it at creation; there's no recovery flow. If you miss it, delete and re-register the application.PKCE S256 is mandatory. No plain method, no skipping the challenge — Canopy will reject the token exchange.Authorization codes expire in 60 seconds and are single-use. Don't store them, don't retry with them. Move straight to the token exchange.Redirect URI is exact-match. No wildcards, no trailing-slash forgiveness. Pre-register every callback URL your app can reach.Refresh-token reuse triggers chain revocation. Save the new refresh token atomically before completing the response. Never retry a failed refresh with the old token.ID-token claims are gated by scopes. If you don't request profile, you don't get name. If you don't request org, the ID token doesn't carry account_id, application_id, or environment_id — but the access token always carries them regardless of scopes.Validate state on every callback. Reject any callback whose state isn't one your app issued — that's how you catch replay and CSRF attacks.Don't request the permissions scope. It's not in OIDC discovery and adds no claims to issued tokens.