signIn
uses node:crypto
Sign in as who (a handle or DID) with OAuth, in the browser, for scope: as AT Protocol's loopback client (no
client metadata to host), with PKCE and DPoP-bound tokens. tell says what's happening.
import { authServerFor, b64url, dpopFetch, loopbackServer, oauthAnswer, okJson, openBrowser, resolveIdentity, } from "#codebase/tcmts/atproto"; import type { OAuthSession, TokenAnswer } from "#codebase/tcmts/atproto"; import crypto from "node:crypto"; export async function signIn( who: string, scope: string, tell: (line: string) => void = console.log, ): Promise<OAuthSession> { const id = await resolveIdentity(who); const as = await authServerFor(id.pds); const dpopKey = crypto .generateKeyPairSync("ec", { namedCurve: "P-256" }) .privateKey.export({ format: "jwk" }); const verifier = b64url(crypto.randomBytes(32)); const state = b64url(crypto.randomBytes(16)); const back = await loopbackServer(undefined, state); try { const redirect = `http://127.0.0.1:${back.port}/callback`; const clientId = `http://localhost?${new URLSearchParams({ redirect_uri: redirect, scope })}`; const form = { "content-type": "application/x-www-form-urlencoded" }; const par = await okJson<{ request_uri: string }>( await dpopFetch(dpopKey, as.pushed_authorization_request_endpoint, { method: "POST", headers: form, body: new URLSearchParams({ client_id: clientId, response_type: "code", code_challenge: b64url(crypto.createHash("sha256").update(verifier).digest()), code_challenge_method: "S256", redirect_uri: redirect, scope, state, login_hint: id.handle ?? id.did, }).toString(), }), `signing in at ${as.issuer}`, ); const url = `${as.authorization_endpoint}?${new URLSearchParams({ client_id: clientId, request_uri: par.request_uri })}`; tell(`signing in as ${id.handle ?? id.did}, in the browser (if it doesn't open: ${url})`); openBrowser(url); const q = await back.answer; const code = oauthAnswer(q, state, as.issuer); const t = await okJson<TokenAnswer>( await dpopFetch(dpopKey, as.token_endpoint, { method: "POST", headers: form, body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirect, client_id: clientId, code_verifier: verifier, }).toString(), }), `signing in at ${as.issuer}`, ); if (t.sub !== id.did) throw new Error(`signed in as ${t.sub}, not ${id.did}`); return { did: id.did, ...(id.handle ? { handle: id.handle } : {}), pds: id.pds, issuer: as.issuer, tokenEndpoint: as.token_endpoint, clientId, dpopKey, accessToken: t.access_token, ...(t.refresh_token ? { refreshToken: t.refresh_token } : {}), expiresAt: Date.now() + (t.expires_in ?? 300) * 1000, scope: t.scope ?? scope, }; } finally { back.close(); } }
uses
- tcmts.atproto.OAuthSession
- tcmts.atproto.TokenAnswer
- tcmts.atproto.authServerFor
- tcmts.atproto.b64url
- tcmts.atproto.dpopFetch
- tcmts.atproto.loopbackServer
- tcmts.atproto.oauthAnswer
- tcmts.atproto.okJson
- tcmts.atproto.openBrowser
- tcmts.atproto.resolveIdentity