/** * Lets this instance's users sign in to other gerbeur instances with their * account here, as `identity-provider-service.ts` describes. The client's * `/authorize` page calls the first two; the other instance's server calls the * last. * * - `GET /api/oauth/clients?client_id=` — signed in. Answers the instance * asking to sign the caller in (`OAuthClient`), and whether they already * let it; 400 for an account from another instance, 403 when that instance * is blocked here, 502 when it doesn't answer as a gerbeur. * - `POST /api/oauth/codes` — signed in. Records the caller's consent to the * request (`AuthorizeRequest`) and answers where to send them, the * instance's callback page with a code. * - `POST /api/oauth/tokens` — public, form-encoded, server to server. Trades * a code and its PKCE verifier for an identity token (`id_token`), bare * JSON as OAuth has it; 400 when anything doesn't match. Rate-limited per * client. * * @module */ import { Router } from "@oak/oak"; import type { APIResponse } from "../../shared/api.ts"; import type { AuthorizeResponse, OAuthClient, } from "../../shared/federation.ts"; import { APIException } from "../lib/api-exception.ts"; import { type RateLimit, takeToken } from "../lib/rate-limit.ts"; import { type AuthContext, authMiddleware } from "../middleware/auth.ts"; import { isAuthorizeRequest } from "../validators/federation.ts"; import { createAuthorizationCode, describeClient, exchangeCode, } from "../services/identity-provider-service.ts"; /** Allows a burst of 30 code trades per client instance, then one a second. */ const TOKEN_LIMIT: RateLimit = { capacity: 30, refill: 1 }; /** * Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware. * An `APIException` thrown by a handler becomes the JSON error response there. */ const router: Router = new Router({ prefix: "/api/oauth" }); router.get("/clients", authMiddleware, async (ctx: AuthContext) => { const clientId = ctx.request.url.searchParams.get("client_id"); if (!clientId) throw new APIException(400, "client_id is required"); const responseBody: APIResponse = { success: true, data: await describeClient(ctx.state.user.userId, clientId), }; ctx.response.body = responseBody; }); router.post("/codes", authMiddleware, async (ctx: AuthContext) => { const body: unknown = await ctx.request.body.json(); if (!isAuthorizeRequest(body)) throw new APIException(400, "Invalid request"); const responseBody: APIResponse = { success: true, data: { redirectTo: await createAuthorizationCode(ctx.state.user.userId, body), }, }; ctx.response.body = responseBody; }); router.post("/tokens", async (ctx) => { const params = new URLSearchParams(await ctx.request.body.text()); if (!takeToken(`oauth-token:${params.get("client_id")}`, TOKEN_LIMIT)) { throw new APIException(429, "Too many requests"); } ctx.response.headers.set("Cache-Control", "no-store"); ctx.response.body = await exchangeCode(params); }); export default router;