This page is for a product whose users each connect their own Stripe, Linear,
Notion or Google account — the way they would connect one to any SaaS app.
Connecting your own account to a script needs none of it:
your own account covers that.
SubjectProvider that builds one
user’s credential from what you stored when they connected. Then name the user
around each run:
many_users.py
Every pack
The connect column is what your user does. Store is what you keep afterwards. Provider is whatfor_user returns.
Every row takes the same
OAuth2Flow, with a
different server constant. Getting the grant has the full
shape: the two routes, state, PKCE, and what to store.
A pasted key needs no flow. Show a field, check the key with one call, and
store it the way you store any other secret.
Every OAuth row needs an app registered with the provider first: a redirect URL,
the scopes, and the client ID and secret the flow is built from. Each of these
has a screenshot for every screen:
Google, Slack,
GitHub, Linear,
Notion, Stripe,
Shopify and Microsoft.
Each provider
Every OAuth provider has a page with its server constant, the flow and the per-user client, what a refresh does, and how a user is disconnected.Linear
Linear: refresh tokens that rotate with a 30-minute grace, and a pasted personal key beside OAuth in one configuration.Stripe
Stripe: a Stripe App, its test link before you publish, and a pasted restricted key as the other path.Notion
Notion: a link that asks for no scopes, and what a refresh does that Notion’s docs leave out.Shopify
Shopify: a server per store,shop= per call, and
the expiring offline token Shopify now requires.
Microsoft
Microsoft: a tenant segment that decides who can connect, theoffline_access a refresh token needs, and no revocation endpoint.
The original three are Google,
Slack and GitHub.
Disconnecting a user
A “Disconnect” button revokes the grant at the provider, then deletes your row. Deleting the row alone leaves the grant live, with every scope it was given, and your app still listed on the user’s account. TheSubjectProvider
the pack is configured with does both lookups for you:
disconnect.py
revoke(user_id) returns False
when the grant was already gone (the user removed the app on the provider’s
side first) and raises only when the provider refused for some other reason.
Delete the row after it, not before: the client reads the grant from your
store to know what to revoke, and under refresh_lock it is the grant the
store holds, not one another process has already rotated.
What each server is sent is on its declaration, so the call is the same for
every pack. What each one does with it, measured live, is on its page:
- Google: the refresh token, which ends every grant your app holds for that account
- Slack:
apps.uninstall, which removes the installation - GitHub:
DELETE /applications/{client_id}/grant, with an access token - Linear: the refresh token, with the
token_type_hintLinear needs - Notion: the access token; Notion answers
200to any token - Shopify: uninstalls the app, and with it every grant on that store
- Stripe: uninstalls the Stripe App, through the App Installs API
What stays yours
The same line as everywhere in Charter’s auth. The consent routes, the session that holdsstate and the PKCE verifier, the database the grants live in, and
any encryption at rest are your application’s. Charter builds the link, exchanges
the code, refreshes the grant, revokes it when the user disconnects, and puts
the right user’s credential on each call.
Security invariants lists the rules the
callback has to keep.