ext.oauth reference
ext.oauth + ctx.oauth_authorize_url — let the platform run the whole OAuth account-connect flow: declare a provider, build the authorize URL, and have accounts saved automatically with client creds from app-scope secrets.
ext.oauth(...) hands the entire OAuth "connect an account" flow to the platform. You declare which providers your extension connects (Google, Microsoft, Yahoo, …), and the platform runs the dance end to end: it builds the authorize URL, exchanges the code, fetches the account email, saves a standard account record, and renders the result window. You write no OAuth code — no webhook, no token-exchange, no cron, no profile call.
This replaces the old pattern of hand-rolling an @ext.webhook("callback") + a polling schedule. Don't do that anymore — it's error-prone (scope mistakes, raw-JSON windows, deferred account creation) and duplicates what the platform already does.
The two pieces
- Declare the provider (once, at module scope):
from imperal_sdk import Extension
ext = Extension("mail", version="1.0.0")
ext.oauth(
"google",
collection="gmail_accounts",
scopes=["https://www.googleapis.com/auth/gmail.modify"],
)- Return the authorize URL from your connect handler:
@chat.function("connect_account", action_type="write")
async def connect_account(ctx, params):
url = await ctx.oauth_authorize_url(params.provider) # "google" | "microsoft" | "yahoo"
return ActionResult.ok(data={"authorize_url": url})That's it. The user's browser opens url, authorizes, and the platform redirects to its own callback, saves the account, and bounces the user back to your panel.
What the platform does (the callback)
The platform owns one generic route — GET /v1/ext/{app_id}/oauth/{provider}/callback — for every extension. On the provider's redirect it:
- verifies the signed
state; - resolves your client credentials from this app's app-scope secrets
{provider}_client_id/{provider}_client_secret(never from env); - exchanges the
codefor tokens at the provider's token endpoint; - fetches the account email (Gmail
getProfile, Microsoft Graph/me, Yahoo userinfo); - saves a standard account record to your
collection; - renders a branded "connected" page that auto-redirects to
https://panel.imperal.io/ext/{app_id}?connected=1.
The account is saved synchronously — it appears the moment the user lands back on your panel. No cron, no "appears within a minute".
ext.oauth(provider, *, collection=None, scopes=None)
| arg | meaning |
|---|---|
provider | one of the built-in providers: "google", "microsoft", "yahoo". |
collection | the store collection the account record is written to. Defaults to f"{provider}_accounts". Use one shared collection (e.g. "gmail_accounts") if your extension lists all providers' accounts together. |
scopes | the OAuth scopes to request for this provider. |
The standard saved record is {email, provider, access_token, refresh_token, expires_at, is_active} — the first account for a user is set active; later ones are inactive until the user switches.
await ctx.oauth_authorize_url(provider, *, login_hint=None)
Builds the provider authorize URL. Reads the public client_id from your app-scope secret, the scopes from your ext.oauth(...) declaration, sets redirect_uri to the platform callback, and attaches a signed state. Return it from your connect() handler — never hardcode scopes, redirect_uri, or client_id.
Required setup (owner / operator, once)
- Save the client credentials as app-scope secrets in the Developer Portal → Secrets:
{provider}_client_idand{provider}_client_secret(e.g.google_client_id). Declare them with@ext.secret(scope="app")so one developer-owned key serves every user. - Register the redirect URI in the provider's developer console (Google Cloud, Azure AD, Yahoo):
https://panel.imperal.io/v1/ext/{app_id}/oauth/{provider}/callback. A mismatch yieldsredirect_uri_mismatch.
Token refresh
For ongoing API calls, refresh tokens with the same app-scope client creds you declared:
client_id = await ctx.secrets.get("google_client_id") # app-scope: shared, owner-set
client_secret = await ctx.secrets.get("google_client_secret")
# ...POST to the provider token endpoint with grant_type=refresh_token...Don't
- Don't request
userinfo-only scopes to read the email — use the provider's first-party profile call the platform already uses (GmailgetProfileworks undergmail.modify). - Don't put client creds in env or in code — they live in app-scope secrets.
- Don't build the callback yourself with
@ext.webhook— the platform owns/v1/ext/{app_id}/oauth/{provider}/callback.
@ext.secret reference
@ext.secret reference — declare and access per-user credentials safely: API keys, OAuth tokens, and webhook signing secrets, encrypted at rest and never logged.
Experimental decorators reference
Experimental decorators reference — @ext.tray, @ext.widget, and @ext.expose preview APIs, their stability caveats, and when it is actually safe to ship them.