Skip to main content
This walkthrough adds “Log in with Orgo” to a third-party application. It uses the OAuth 2.0 authorization code flow with PKCE.
Every URL here is on the tenant’s own host — acme.orgo.space in these examples, or the organization’s custom domain. The bare app.orgo.space domain resolves no tenant and returns 404. See Tenancy.
Who is this for: SaaS apps your members use that should not require a second login (member-only forums, member-only deals platforms, internal tools, partner services).

When to use OAuth vs Api-Token

  • Api-Token — your server acts as itself; the data is yours. Use for backend integrations, dashboards, batch jobs.
  • OAuth — your server acts on behalf of an Orgo user; you only see what that user can see. Use when each end-user logs into your app as themselves.
If you’re building a single integration that consumes “all members of Civic Collective”, that’s an Api-Token. If you’re building a forum where each Orgo member logs in individually, that’s OAuth.

Prerequisites

  • A callback URL on your app served over HTTPS (localhost is allowed for development).
  • For a confidential client: an admin in the Orgo tenant who can create the app.

Step 0 — Read the discovery document

Don’t hardcode endpoints. Every tenant publishes its own metadata, and issuer reflects the host you fetch it from:
Use authorization_endpoint, token_endpoint and userinfo_endpoint from the response. Fetch this once per tenant you integrate with — never reuse one tenant’s document for another.

Step 1 — Register your client

Best for SPAs, mobile apps, and anything that cannot keep a secret. No admin involvement and no authentication needed, but limited to 10 registrations per hour per IP:
Returns 201 with a client_id. No secret is issued — PKCE is your only client authentication. Optional metadata: client_uri, logo_uri, policy_uri, tos_uri.

Step 2 — Redirect the user to authorize

When a user clicks “Log in with Orgo” in your app, redirect them to:
PKCE is mandatory for every client, confidential ones included — this is stricter than baseline OAuth 2.0 and matches OAuth 2.1. The server rejects violations with invalid_request:
  • code_challenge_method must be S256; plain is not supported.
  • Both code_challenge and code_verifier must match [A-Za-z0-9-._~]{43,128}.
If the member isn’t signed in, Orgo handles login and returns them to the consent screen automatically — including via Google, Apple or Microsoft. You don’t handle that.

Step 3 — Receive the callback

Orgo sends the user back to your redirect_uri with two parameters:
Verify state matches the one you stored, then move on.

Step 4 — Exchange the code for an access token

Form-encoded body, not JSON. A confidential client does this server-side — the Client Secret must never reach the browser.
PKCE only. Sending a client_secret here is an error (invalid_client):
Response:
The code is single use. Redeeming it twice fails with invalid_grant — and a replay by an authenticated client revokes the tokens that code already produced. Never retry a code after a successful exchange. (A retry after a failed client authentication is safe: the code stays redeemable so you can fix credentials and try again.)

Step 5 — Use the access token

The access token works as a normal Bearer JWT.
Response:
sub is the user’s stable Orgo ID — use this as the primary key in your app’s users table. Email can change; sub cannot. You can also use the same access token to call other API endpoints scoped to the user’s permissions:

Step 6 — Refresh expired tokens

Access tokens last 1 hour (expires_in in the original response). Refresh tokens last 30 days. Use the refresh token to get a new pair without sending the user back through the consent screen:
Confidential clients authenticate here too, exactly as in step 4.
Refresh tokens rotate. Each refresh returns a new refresh_token and invalidates the one you just sent. Persist the new value every time — an app that keeps replaying its original refresh token will work once and then start failing with invalid_grant.
Do not call refresh on every API request — only when a request returns 401.

Step 7 — Know what your token cannot do

Two limits catch most integrations, and neither is adjustable per request. An app acts as a member, never as an administrator. The token is capped to member level (ROLE_USER) no matter who authorized it. If a tenant administrator connects your app, it still cannot perform administrator actions. Read-only apps are gated by HTTP method. If the app is registered read-only it may use GET, HEAD and OPTIONS only. Anything else returns 403:
The gate is by method, not intent — an endpoint that performs a read via POST is still blocked. If you need those, register the app with acting enabled.

Available scopes

Scopes describe what an application requests and what the member approves when they authorize it. What the resulting token can actually reach is bounded by the member-level cap and the application’s read-only / act-on-behalf setting in step 7.
Request only what you need — fewer scopes makes the consent screen shorter and clearer.

Common gotchas

The redirect_uri parameter must match a registered URI exactly. Watch for: trailing slashes, http vs https, port numbers (8080 vs 443), and URL encoding of query strings. Register every variant you use (dev, staging, prod) at creation.
Check that your callback handler is reachable from Orgo’s servers (not localhost-only behind a firewall) and serves HTTPS (except for localhost URIs which can use HTTP). Check browser network tab — Orgo issues a 302 to the callback; if your handler 500s, it shows there.
Wrong client_id, wrong client_secret, or a confidential client omitting its secret entirely (that one returns 401 with WWW-Authenticate: Basic realm="orgo-oauth"). A public client sending a client_secret also fails — it has none, and PKCE is its only authentication.Note that sending client_secret and code_verifier together is correct for a confidential client: PKCE applies to every client. What is not allowed is sending credentials by more than one method at once — Basic header and form field — which returns invalid_request.
Repeated failed client authentications for the same client_id are throttled: after 10 failures in 5 minutes you get 429 with a Retry-After header instead of 401. This means your credentials are wrong, not that you are calling too often — a request with the correct secret is never throttled, however many failures came before it. Fix the secret rather than backing off.
The groups scope returns slug-style group names, not display names. To resolve to a full Group resource, call GET /api/v1/units?slug=boston-local separately.
Use PKCE without a Client Secret. Generate a verifier (random 43-128 chars), hash it with SHA-256, base64url-encode the hash, send the hash as code_challenge on authorize, then send the verifier as code_verifier on token exchange. The Client Secret stays unused.
Access tokens are RS256 JWTs, but they are revocable server-side and revocation takes effect on the next request. A token whose signature verifies and whose exp is still in the future can still be rejected.Do not treat a local expiry check as proof a token works — handle 401 at any point in a token’s lifetime and fall back to refresh, then to re-authorization. Verify signatures against jwks_uri from the discovery document.

What to do next

  • Authentication — comparison of OAuth vs Api-Token vs JWT vs OTP, and the full discovery reference
  • Migrating from legacy SSO — if you already use the *-sso handshake
  • OAuth Server — admin-side documentation on managing OAuth applications
  • Tenancy — how OAuth tokens carry tenant scope