Acting for a user at another service: OAuth grants
Signing in proves to your app who a person is. An OAuth grant goes the other way: your app acts at another service, such as a user's Google Calendar, an organisation's Xero or a Slack workspace, with that owner's consent. It isn't a way to sign in. Your app acting as an OAuth provider, for other apps to sign in with, is a third, separate thing.
The connection and its calls are in
Integrations. This
topic covers what touches identity. A connection's credential is an
OAuthGrant:
// app.jadpocalendar: { package: GoogleCalendar.v3 domains: [scheduling] credential: OAuthGrant { owner: User client_id: config.google_client_id client_secret: config.google_client_secret scopes: [...] return_to: "/settings/integrations" }}
- A grant is owned by one of the app's records:
owner:is the principal's user entity or any other entity (Organisation). The grant belongs to that record, not to a session, so it outlives every session and is used from subscribers and schedules, which run with no caller. - Connecting is endpoints, not pages.
POST /connections/<name>/startis a generated route that needs a signed-in caller (authentication: RouteAuthentication.noneis not available on it) and answers with the provider's consent address. It keeps thestateon the server against the caller's browser session or API token. The provider sends the browser back to the generatedGET /connections/<name>/callback, which finds thestate(a consent can complete only once, for the caller who started it), exchanges the code with PKCE, stores the grant and redirects to the frontend'sreturn_to, a path on the app's own site, with success or failure. The callback needs no cookie, so your own API-token clients connect exactly as browsers do. Both appear injadpo routes, OpenAPI and the API record, marked generated (Generated routes). - Who may connect for an owner is Policy's. Connecting
and disconnecting need the
updategrant on the owner's record. So a user connects their own account, and an organisation's grant needs whatever lets the caller update the organisation. There is no new kind of permission for it. The start route checks it before answering. - Grants are held by Jadpo, like sessions, in a
jadpo_grantssystem table in the owner entity's store, so grants need a database. Their tokens are encrypted withapplication { grants: { encryption_key: config.grant_encryption_key } }, aSigningKeysetting. It rotates as asigning_key:does: the current value encrypts, and its_PREVIOUSvalue is still read (Signing keys). Code can never read the tokens, and Jadpo refreshes them before they expire.encryption_key:is one of the places inapplication { }a secret may be read (Secrets). - Every call names whose grant it uses,
on_behalf_of: owner_id, an id of the owner's entity that the action has loaded or been given.grant_missingandgrant_revokedare failures your app maps to its own (Integrations). The caller of the route need not be the owner: a scheduled job acting for an organisation names the organisation. - Disconnecting is the generated
POST /connections/<name>/disconnectand its operation, revoking at the provider when the package declares how. Erasing a person or a record (Personal data) deletes and revokes its grants. A revoked grant gives theGrantRevokedevent, so your app can ask the owner to reconnect. - In development and tests,
jadpo dev grants add <connection> --owner <id>makes a fake grant, and tests declare grants as fixtures.
Not built yet: OAuth grants, their routes and on_behalf_of:. Acting as
an OAuth provider isn't built either.
Markdown for agents: /docs/acting-for-a-user-at-another-service-oauth-grants.md.Built from Jadpo main at a707efb.