a tool for shared writing and social publishing
at update/reader 119 lines 3.6 kB view raw
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}