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