# Sign in to other services

> Connect a person's account on another service with OAuth, keep its tokens fresh, and ask the person to set up or reconnect when it stops working.

An application can act on a person's account on another service, once the person has signed in there
through it.

## Ask a person to set up or reconnect

When an action cannot work until the person sets your application up, or reconnects an account that
stopped working, a refused refresh token for instance, it says so with one response header:

```ts
return new Response(JSON.stringify({ error: 'Gmail is disconnected' }), {
  status: 409,
  headers: { 'Content-Type': 'application/json', 'X-Apigami-Settings': 'reconnect' },
})
```

An application that asks the person to set it up or reconnect needs a place where that happens: its home
or settings page, or, for a brick, the home of each application that calls it and its own settings page,
for its trials ([Expose your settings](/docs/app-dependencies/#expose-your-settings-to-the-applications-that-call-yours)). `setup` or `reconnect` records a need for this person and your application, whichever application
called yours: they see the same need under every application that uses yours, and repairing it once
clears it everywhere. `ok` clears it. Answer `ok` from the action that checks, the one your page calls
once the person has signed in again, or any action that just used the account. Apigami writes
only when the state changes, so `ok` on every successful answer costs nothing. Any other value is ignored.

Only your own answer counts, and only for the person the run serves: a run that serves nobody, such
as a trigger for the whole application, signals nothing. The header never reaches your caller, and
neither does any other `X-Apigami-*` header your code sets: those are Apigami speaking.

Under a run token, `GET /v1/me` lists `settings_needed`, the open needs of the called application and of
those it reaches, each with its `app_id`, `name`, `kind` and `since`. The link to give the person is always
the connect link of the application they connected to, `/connect?app_id={that app_id}`, whose home shows
what to set up: never one of your own pages, nor the link of a brick it calls. An assistant running your action with
`app_run` is told the same link.

A page that loads `apigami.js` shows it for you: when its application or one it calls needs the person, a
banner in Apigami's words names it and links to that connect link, in a new tab from the side panel. It is
refreshed each time the person comes back to the tab and after an action fails, and closing it hides it
until the page reloads. A home or settings page shows no banner, since it is where the need gets fixed;
`apigami.state.settings_needed` lists every open need.

Checking that the account that comes back is the same one is your job: compare the account the service
names with the one you stored for this `user_ref` before you answer `ok`. A run serving a `user_ref` may
come from the root's server, with its key, while the person is away: never take it as proof they are there.

## Sign in to a third-party service from your app: OAuth, PKCE, callback

Look for the service's OAuth before asking anyone for a key: `/.well-known/oauth-authorization-server` on its API,
or `/.well-known/oauth-protected-resource` on its MCP server.

When your actions need the person's account on another service, an OAuth provider or an MCP server,
your server signs them in and your actions keep the tokens, on `user_ref`. At your app's own address every
route knows the person ([Who is behind a request](/docs/app-page-sessions/#who-is-behind-a-request-x-apigami-user-ref)), so it is the
classic web flow:

1. A link or a button leads to your `GET /oauth/start`, in the same tab, or in a window
   [`apigami.open()`](/docs/app-dependencies/#show-the-settings-of-the-bricks-you-call) opened from the page of an application
   that calls yours.
2. `/oauth/start` reads the person in `x-apigami-user-ref`, keeps a `state` and a PKCE verifier in a
   `__Host-` cookie, and sends the browser to the service with `prompt=consent`, or the service's
   equivalent, so that connecting always shows the service's own screen.
3. The service sends the browser back to your `GET /oauth/callback`, which reads the person again, checks
   the `state` against the cookie, exchanges the code with the verifier and stores the tokens on that
   `user_ref`.

The cookie proves this browser started the sign-in, and the header names the person: a forwarded
`/oauth/start` link serves whoever opens it, under their own session. Never trust a person the URL names,
a `user_ref` in the query for instance: anyone can write that URL.

```ts title="oauth.ts"
// GET /oauth/start, /oauth/callback and /oauth/client.json, routed in main.ts beside /healthz.
const STATE_COOKIE = '__Host-oauth'
const base64url = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const cookieOf = (request: Request, name: string) =>
  request.headers.get('Cookie')?.split(/;\s*/).find((pair) => pair.startsWith(`${name}=`))?.slice(name.length + 1) ?? null

export const startSignIn = async (request: Request, env) => {
  const userRef = request.headers.get('x-apigami-user-ref')
  if (!userRef) return page('Open this from the application, once connected.', 401)
  // `https://acme-basket.apigami.tech`, or its `--staging` host: a browser asked, so this is the public address.
  const base = new URL(request.url).origin
  const state = base64url(crypto.getRandomValues(new Uint8Array(16)))
  const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)))
  const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))))
  const location = `${AUTHORIZE_URL}?${new URLSearchParams({
    // A registered client's id, else the URL of your client document (below).
    response_type: 'code', client_id: env.CLIENT_ID ?? `${base}/oauth/client.json`, redirect_uri: `${base}/oauth/callback`,
    code_challenge: challenge, code_challenge_method: 'S256', state, scope: 'read', prompt: 'consent',
  })}`
  // `__Host-`: no other application under apigami.tech can set it. `Lax`: it comes back with the service's redirect.
  return new Response(null, { status: 303, headers: {
    location, 'set-cookie': `${STATE_COOKIE}=${state}.${verifier}; Path=/; Max-Age=600; Secure; HttpOnly; SameSite=Lax`,
  } })
}

export const finishSignIn = async (request: Request, env) => {
  const userRef = request.headers.get('x-apigami-user-ref')
  const params = new URL(request.url).searchParams
  const [state, verifier] = (cookieOf(request, STATE_COOKIE) ?? '').split('.')
  if (!userRef || !state || params.get('state') !== state || !params.get('code')) {
    return page('The sign-in expired or was refused: start again from the application.', 400)
  }
  const redirectUri = `${new URL(request.url).origin}/oauth/callback`
  // POST TOKEN_URL: grant_type=authorization_code, code, redirectUri, client_id, code_verifier=verifier,
  // and env.CLIENT_SECRET for a registered client. Then store the tokens in oauth_tokens, on userRef.
  return page('Connected. You can close this window.', 200)
}

// The end of a sign-in. Connected, a window apigami.open() opened tells its opener, which reads its settings again,
// and closes: the message carries nothing, so any opener may receive it.
const page = (message: string, status: number) => new Response(`<!doctype html><p>${message}</p><p><a href="/">Back</a></p>
${status === 200 ? '<script>if (opener) { opener.postMessage({ type: \'apigami:settings-changed\' }, \'*\'); close() }</script>' : ''}`, {
  status, headers: { 'content-type': 'text/html; charset=utf-8', 'set-cookie': `${STATE_COOKIE}=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax` },
})
```

```ts title="actions/settings.ts"
import { personOf } from './basket.ts'

// What a page asks, `ok` once the tokens are there. An action reads the person from its run token,
// never from x-apigami-user-ref.
export const settingsStatus = async (request: Request, env) => {
  const userRef = await personOf(env, request.headers.get('X-Apigami-Run') ?? '')
  if (!userRef) return Response.json({ error: 'this action serves one person' }, { status: 401 })
  const tokens = await env.DB.prepare('select account from oauth_tokens where user_ref = ?1')
    .bind(userRef).first<{ account: string }>()
  const state = tokens ? 'ok' : 'setup'
  return Response.json({ state, account: tokens?.account ?? null }, { headers: { 'X-Apigami-Settings': state } })
}
```

`env.DB` and `personOf` are those of [Its database](/docs/app-database/#its-database).

**A service that sends the code back by a form**, `response_mode=form_post`, posts from its own site:
neither the session nor a `SameSite=Lax` cookie comes with that `POST`. Answer it with a `303` to a `GET` of
the same path carrying its fields, a top-level navigation, which carries both:

```ts
if (request.method === 'POST' && pathname === '/oauth/callback') {
  const fields = new URLSearchParams(await request.text())
  return new Response(null, { status: 303, headers: { location: `/oauth/callback?${fields}` } })
}
```

**The client.** Register one with the service wherever it offers it, confidential: give it both
redirect URIs, `https://{slug}.apigami.tech/oauth/callback` and
`https://{slug}--staging.apigami.tech/oauth/callback`, and keep its secret in `x-apigami.env` with
`"secret": true`. A code caught on its way back is then useless to anyone without the secret. Both
addresses are in the `address` of any version you write, before your first deploy. A
[domain of your own](/docs/app-domain/#your-own-address-a-custom-hostname) needs a third, `https://app.example.com/oauth/callback`,
registered before the domain goes active: from then on production's pages open there, and so does
`/oauth/start`.

**A public client works too**, where the service accepts nothing else, such as an MCP authorization
server that takes a Client ID Metadata Document: the URL of a JSON document you serve as `client_id`,
which it fetches. The code comes back to your own host, which no other application serves. Its
`client_id` field is that same URL, built from the request's own:

```ts
if (request.method === 'GET' && pathname === '/oauth/client.json') {
  const base = new URL(request.url).origin
  return Response.json({
    client_id: `${base}/oauth/client.json`, client_name: 'My app', redirect_uris: [`${base}/oauth/callback`],
    grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'none',
  })
}
```

A service may require the `redirect_uri` on the same origin as the `client_id`: one `client.json` per
origin, each naming its own callback, which building both from the request's origin gives you.
Production and `--staging` are then two clients, and so is a domain of your own: store the `client_id`
a sign-in used with its tokens, since a refresh sends the same one.

The browser and the service reach `/oauth/start`, `/oauth/client.json` and `/oauth/callback` at
`--staging` as in production: you can sign in there before you publish. On a page of your own site, which
has no session, open the service's sign-in in a popup on a click and send its code to one of your actions
with `apigami.call()` instead.

**Keep the tokens fresh.** Before each use, an action refreshes an access token about to expire with
its `refresh_token`, and stores what comes back. When the refresh is refused, the person has to sign in
again: answer `409 { "needs": "reconnect" }` with `X-Apigami-Settings: reconnect`
([Ask a person to set up or reconnect](#ask-a-person-to-set-up-or-reconnect)); `settingsStatus` answers
`ok` once the callback has stored new ones.