a tool for shared writing and social publishing
1import {
2 NodeOAuthClient,
3 NodeSavedSession,
4 NodeSavedState,
5 RuntimeLock,
6 OAuthSession,
7} from "@atproto/oauth-client-node";
8import { JoseKey } from "@atproto/jwk-jose";
9import { oauth_metadata } from "app/api/oauth/[route]/oauth-metadata";
10import { supabaseServerClient } from "supabase/serverClient";
11
12import Client from "ioredis";
13import Redlock from "redlock";
14import { Result, Ok, Err } from "./result";
15export async function createOauthClient() {
16 let keyset =
17 process.env.NODE_ENV === "production"
18 ? await Promise.all([
19 JoseKey.fromImportable(process.env.JOSE_PRIVATE_KEY_1!),
20 ])
21 : undefined;
22 let requestLock: RuntimeLock | undefined;
23 if (process.env.NODE_ENV === "production" && process.env.REDIS_URL) {
24 const client = new Client(process.env.REDIS_URL);
25 const redlock = new Redlock([client]);
26 requestLock = async (key, fn) => {
27 // 30 seconds should be enough. Since we will be using one lock per user id
28 // we can be quite liberal with the lock duration here.
29 const lock = await redlock.acquire([key], 45e3);
30 try {
31 return await fn();
32 } finally {
33 await lock.release();
34 }
35 };
36 }
37 return new NodeOAuthClient({
38 // This object will be used to build the payload of the /client-metadata.json
39 // endpoint metadata, exposing the client metadata to the OAuth server.
40 clientMetadata: oauth_metadata,
41
42 // Used to authenticate the client to the token endpoint. Will be used to
43 // build the jwks object to be exposed on the "jwks_uri" endpoint.
44 keyset,
45
46 // Interface to store authorization state data (during authorization flows)
47 stateStore,
48 // Interface to store authenticated session data
49 sessionStore,
50 requestLock,
51 });
52}
53
54let stateStore = {
55 async set(key: string, state: NodeSavedState): Promise<void> {
56 await supabaseServerClient.from("oauth_state_store").upsert({ key, state });
57 },
58 async get(key: string): Promise<NodeSavedState | undefined> {
59 let { data } = await supabaseServerClient
60 .from("oauth_state_store")
61 .select("state")
62 .eq("key", key)
63 .single();
64 return (data?.state as NodeSavedState) || undefined;
65 },
66 async del(key: string): Promise<void> {
67 await supabaseServerClient
68 .from("oauth_state_store")
69 .delete()
70 .eq("key", key);
71 },
72};
73
74let sessionStore = {
75 async set(key: string, session: NodeSavedSession): Promise<void> {
76 await supabaseServerClient
77 .from("oauth_session_store")
78 .upsert({ key, session });
79 },
80 async get(key: string): Promise<NodeSavedSession | undefined> {
81 let { data } = await supabaseServerClient
82 .from("oauth_session_store")
83 .select("session")
84 .eq("key", key)
85 .single();
86 return (data?.session as NodeSavedSession) || undefined;
87 },
88 async del(key: string): Promise<void> {
89 await supabaseServerClient
90 .from("oauth_session_store")
91 .delete()
92 .eq("key", key);
93 },
94};
95
96export type OAuthSessionError = {
97 type: "oauth_session_expired";
98 message: string;
99 did: string;
100};
101
102export async function restoreOAuthSession(
103 did: string
104): Promise<Result<OAuthSession, OAuthSessionError>> {
105 try {
106 const oauthClient = await createOauthClient();
107 const session = await oauthClient.restore(did);
108 return Ok(session);
109 } catch (error) {
110 return Err({
111 type: "oauth_session_expired",
112 message:
113 error instanceof Error
114 ? error.message
115 : "OAuth session expired or invalid",
116 did,
117 });
118 }
119}