Docs
Authentication

OAuth

Apps that connect on behalf of a person — AI assistants above all — sign in with OAuth 2.1 instead of an API key. The person signs in to NorReach, sees what the app asks for and chooses what to allow. The app then gets a token that acts exactly like an API key of that person: the same operations, scopes, limits, audit log and revocation, and never more than the person's role allows (read live on every request).

Who can connect apps

Owners and admins. An owner can let every member connect apps for themselves in Settings › API & webhooks › Connected apps; a member's app can then do only what that member's role allows. The API must be switched on for the workspace.

Endpoints

WhatWhere
Authorization server metadata (RFC 8414)https://api.norreach.app/.well-known/oauth-authorization-server
Protected resource metadata (RFC 9728) — MCPhttps://api.norreach.app/.well-known/oauth-protected-resource/mcp
Protected resource metadata (RFC 9728) — RESThttps://api.norreach.app/.well-known/oauth-protected-resource/v1
Authorize (the consent screen, in the browser)https://www.norreach.app/oauth/authorize
Tokenhttps://api.norreach.app/oauth/token
Dynamic client registration (RFC 7591)https://api.norreach.app/oauth/register
Revocation (RFC 7009)https://api.norreach.app/oauth/revoke

The issuer is https://api.norreach.app. The authorize page lives on the app's address because that is where people are signed in; everything else is on the API's address. The metadata and OAuth endpoints answer browsers too (CORS, never cookies); the API itself does not.

Registering a client

  • Public clients only: token_endpoint_auth_method "none", no client secret. PKCE protects the code instead.
  • Dynamic registration: POST the client metadata (JSON) to the registration endpoint and keep the client_id you get back. Redirect URIs must be https, or http on a loopback address for desktop apps (http://127.0.0.1:<port>/…, http://[::1]:<port>/… or http://localhost:<port>/… — any port at sign-in time). No fragments, no wildcards, up to 10 URIs. 20 registrations per hour per address.
  • Or a client ID metadata document: use an https URL that serves your client metadata as JSON as the client_id; the document must name that exact URL as its own client_id. NorReach fetches it from public addresses only (no redirects, 5 seconds, 16 KB) and again after 24 hours.
  • Every connected app is shown to people as unverified, with the address it returns to. No logo is ever shown.

The authorization request

  • response_type=code, your client_id and an exactly registered redirect_uri.
  • PKCE is required: code_challenge = BASE64URL(SHA-256(code_verifier)) with code_challenge_method=S256 (plain is refused).
  • state is required and comes back unchanged, with iss (RFC 9207).
  • resource is required (RFC 8707): https://api.norreach.app/mcp for the MCP server or https://api.norreach.app/v1 for the REST API. A token works only on the resource it was issued for.
  • scope: space-separated scopes from the scope list. Without one, the read-only set is asked for (18 scopes). People are offered only the scopes their role can give; scopes that reach people, spend, publish or change access start switched off and need a sign-in in the last 10 minutes. offline_access is accepted and ignored — refresh tokens are always issued.
  • An unknown client or an unregistered redirect URI is shown as an error page and never redirected. Other problems come back to your redirect URI as error (+ error_description and state): invalid_request, unsupported_response_type, invalid_scope, invalid_target or access_denied.

Tokens

Shell
curl -X POST https://api.norreach.app/oauth/token \
  -d grant_type=authorization_code \
  -d code=$CODE -d code_verifier=$VERIFIER \
  -d redirect_uri=http://127.0.0.1:33418/callback \
  -d client_id=$CLIENT_ID \
  -d resource=https://api.norreach.app/mcp

{ "access_token": "nr_oat_…", "token_type": "Bearer", "expires_in": 3600,
  "refresh_token": "nr_ort_…", "scope": "contacts:read campaigns:read" }
  • Codes last 60 seconds and work once. Using a code a second time revokes everything it gave access to.
  • Access tokens (nr_oat_…) last 60 minutes. Send them as Authorization: Bearer nr_oat_….
  • Refresh tokens (nr_ort_…) rotate: grant_type=refresh_token with refresh_token and client_id returns a new pair, and the old refresh token stops working. Presenting an old refresh token again revokes the whole connection. Unused for 60 days, a refresh token expires. A refresh token is never accepted as a bearer token.
  • Errors follow RFC 6749 §5.2 — { "error": "invalid_grant", "error_description": "…" } — and are never cached.
  • NorReach keeps only a fingerprint of codes and tokens, never the values.

Calling the API with a token

A token behaves like an API key of the person who allowed it: the same answers, scopes and limits. A 401 carries WWW-Authenticate with resource_metadata; an expired or revoked token answers 401 with error="invalid_token" — refresh, or connect again. The audit log records the person and the app.

Revoking

  • POST token (and your client_id) to the revocation endpoint: an access token stops; a refresh token disconnects the app. It always answers 200.
  • People disconnect their own apps in Settings › Security; owners and admins disconnect anyone's in Settings › API & webhooks › Connected apps. It takes effect on the app's next request. Removing or deactivating the person stops their apps too.