Developer

OAuth API

Run the OAuth 2.0 Authorization Code flow with optional PKCE: the host captures the redirect, exchanges the code, and stores the access token for you.

ctx.sdk.oauth runs the OAuth 2.0 Authorization Code flow with optional PKCE. The host opens the user’s browser, binds a localhost port for the redirect, captures the code, performs the token exchange, and stores the resulting access token in the credential store. Bundles declare the flow as data: authorize URL template, exchange URL and body shape, scopes, and the credential storage key.

Scope required: credentials:store

Usage

Inside a method run, reach the surface via ctx.sdk.oauth.

import { defineMethod } from "@rightplace/applet-sdk/v2";

export const connect = defineMethod({
  name: "@acme/store.connect",
  async run({ shop, resourceId }, ctx) {
    const result = await ctx.sdk.oauth.completeAuthCodeFlow({
      // Authorize URL with placeholders the host substitutes:
      //   {callbackUrl}    full URL the host bound (URL-encoded)
      //   {state}          CSRF token the host generates and verifies
      //   {pkceChallenge}  only if `pkce` is set; the SHA-256 challenge
      authorizeUrlTemplate:
        `https://${shop}.myshopify.com/admin/oauth/authorize` +
        `?client_id=${CLIENT_ID}&scope=${SCOPES}` +
        `&redirect_uri={callbackUrl}&state={state}`,
      // Optional PKCE. The verifier is generated host-side and used
      // internally in the exchange step; the bundle never sees it.
      pkce: { method: "S256" },
      callback: {
        port: 0,                          // 0 = pick a free port (recommended)
        path: "/shopify/callback",        // default "/callback"
        successHtml: "<html>...</html>",  // a branded page ships by default
      },
      exchange: {
        url: `https://${shop}.myshopify.com/admin/oauth/access_token`,
        method: "POST",
        contentType: "json",              // "form" | "json"
        body: {
          client_id: CLIENT_ID,
          client_secret: CLIENT_SECRET,
          code: "{code}",                 // placeholder, substituted post-callback
          // Also available: {verifier} for PKCE flows, {state} if needed
        },
        accessTokenPath: "access_token",  // dotted path into the response
        extraResponsePaths: ["scope", "expires_in"],
      },
      storeAs: {
        source: "websiteCredential",
        key: `shopify:${resourceId}`,     // bundle owns naming
        provider: "shopify",              // optional tag for the cred store
        endpoint: `https://${shop}.myshopify.com/admin`,
      },
      timeoutSec: 300,                    // default 300 (5 minutes)
    });

    // result = {
    //   accessToken: "shpat_...",        returned ONCE for setup calls
    //   extras: { scope: "...", expires_in: ... },
    //   credentialKey: "shopify:res-abc",
    // }
    return { credentialKey: result.credentialKey };
  },
});

After the flow completes the token lives in the credential store. The bundle should not hold accessToken afterward: read it through http.requestAuthed, which pulls a fresh value from the store on every request.

Placeholders

The host substitutes these tokens for you:

  • {callbackUrl} in authorizeUrlTemplate, the full URL-encoded redirect URL the host bound.
  • {state} in authorizeUrlTemplate and exchange.body, the CSRF token the host generates and verifies.
  • {pkceChallenge} in authorizeUrlTemplate, present only when pkce is set.
  • {code} in exchange.body, the authorization code captured from the redirect.
  • {verifier} in exchange.body, the PKCE verifier (PKCE flows only).

How the host runs it

  1. Generate the CSRF state and, if requested, the PKCE verifier and SHA-256 challenge.
  2. Bind a localhost listener on the chosen port and path (port 0 lets the OS pick a free port).
  3. Substitute {callbackUrl}, {state}, and {pkceChallenge} in authorizeUrlTemplate.
  4. Open the authorize URL in the user’s default browser.
  5. Wait for the redirect (until timeoutSec), validate the CSRF state, and respond with successHtml.
  6. Substitute {code}, {verifier}, and {state} in exchange.body, then POST per contentType.
  7. Extract accessTokenPath and extraResponsePaths from the JSON response.
  8. Store the access token in the credential store and return.

Notes

  • Authorization Code grant only. Implicit grant and OAuth 1.0a are not supported.
  • POST exchange only.
  • PKCE is S256 only. Plain PKCE is deprecated.
  • Storage source is websiteCredential only. Account-level credentials are a tracked extension.
  • No automatic token refresh yet. Providers that issue refresh tokens need a separate refresh primitive.
  • Pair this with credentials and http.requestAuthed: OAuth writes the token, requestAuthed reads it on each call so the secret never enters the bundle.