Attic is a cozy space with lofty ambitions. attic.social

cookie constants

dbushell.com b62ee852 5d78a193

verified
+72 -59
-1
src/app.d.ts
··· 15 did: Did; 16 handle: Handle; 17 displayName: string; 18 - avatar: string; 19 }; 20 } 21 // interface PageData {}
··· 15 did: Did; 16 handle: Handle; 17 displayName: string; 18 }; 19 } 20 // interface PageData {}
+3
src/lib/server/constants.ts
···
··· 1 + export const SESSION_COOKIE = "atproto_session"; 2 + export const HANDLE_COOKIE = "atproto_handle"; 3 + export const OAUTH_COOKIE_PREFIX = "atproto_oauth_";
+2 -1
src/lib/server/oauth.ts
··· 10 import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node"; 11 import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client"; 12 import { decryptText, encryptText } from "$lib/server/crypto.ts"; 13 import { env } from "$env/dynamic/private"; 14 import { dev } from "$app/environment"; 15 import { Buffer } from "node:buffer"; 16 17 class CookieStore<K extends string, V> implements Store<K, V> { 18 #cookies: Cookies; 19 - #prefix = "atproto_oauth_"; 20 #maxAge = 60 * 60 * 24 * 7; 21 22 constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) {
··· 10 import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node"; 11 import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client"; 12 import { decryptText, encryptText } from "$lib/server/crypto.ts"; 13 + import { OAUTH_COOKIE_PREFIX } from "$lib/server/constants.ts"; 14 import { env } from "$env/dynamic/private"; 15 import { dev } from "$app/environment"; 16 import { Buffer } from "node:buffer"; 17 18 class CookieStore<K extends string, V> implements Store<K, V> { 19 #cookies: Cookies; 20 + #prefix = OAUTH_COOKIE_PREFIX; 21 #maxAge = 60 * 60 * 24 * 7; 22 23 constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) {
+56 -21
src/lib/server/session.ts
··· 3 import { isDid, isHandle } from "@atcute/lexicons/syntax"; 4 import { createOAuthClient } from "$lib/server/oauth.ts"; 5 import { decryptText } from "./crypto.ts"; 6 import { env } from "$env/dynamic/private"; 7 8 - export const destroySession = async (event: RequestEvent): Promise<void> => { 9 - event.cookies.delete("atproto_session", { path: "/" }); 10 - if (event.locals.user === undefined) { 11 - return; 12 } 13 - try { 14 - const oAuthClient = createOAuthClient(event); 15 - // await event.locals.user.session.signOut(); 16 - await oAuthClient.revoke(event.locals.user.did); 17 - } catch { 18 - // Do nothing? 19 } 20 - event.locals.oAuthClient = undefined; 21 }; 22 23 export const restoreSession = async (event: RequestEvent): Promise<void> => { 24 const { cookies } = event; 25 - // Read the cookie 26 - const encrypted = cookies.get("atproto_session"); 27 if (encrypted === undefined) { 28 return; 29 } 30 - // Parse and validate or delete 31 let data; 32 try { 33 const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY); 34 data = JSON.parse(decrypted); 35 } catch { 36 - cookies.delete("atproto_session", { path: "/" }); 37 return; 38 } 39 - // [TODO] validate data type? 40 try { 41 - if ( 42 - isDid(data.did) === false || 43 - isHandle(data.handle) === false 44 - ) { 45 throw new Error(); 46 } 47 const oAuthClient = createOAuthClient(event); ··· 53 session, 54 }; 55 } catch { 56 - cookies.delete("atproto_session", { path: "/" }); 57 return; 58 } 59 };
··· 3 import { isDid, isHandle } from "@atcute/lexicons/syntax"; 4 import { createOAuthClient } from "$lib/server/oauth.ts"; 5 import { decryptText } from "./crypto.ts"; 6 + import { dev } from "$app/environment"; 7 import { env } from "$env/dynamic/private"; 8 + import { HANDLE_COOKIE, SESSION_COOKIE } from "./constants.ts"; 9 10 + /** 11 + * Logout 12 + */ 13 + export const destroySession = async ( 14 + event: RequestEvent, 15 + ): Promise<void> => { 16 + event.cookies.delete(SESSION_COOKIE, { path: "/" }); 17 + if (event.locals.user) { 18 + try { 19 + const oAuthClient = createOAuthClient(event); 20 + await oAuthClient.revoke(event.locals.user.did); 21 + } catch { 22 + // Do nothing? 23 + } 24 + event.locals.user = undefined; 25 } 26 + event.locals.oAuthClient = undefined; 27 + }; 28 + 29 + /** 30 + * Login 31 + * @returns {URL} OAuth redirect 32 + */ 33 + export const startSession = async ( 34 + event: RequestEvent, 35 + handle: string, 36 + ): Promise<URL> => { 37 + if (isHandle(handle) === false) { 38 + throw new Error("invalid handle"); 39 } 40 + const oAuthClient = createOAuthClient(event); 41 + const { url } = await oAuthClient.authorize({ 42 + target: { "type": "account", identifier: handle }, 43 + }); 44 + // Temporary to remember handle across oauth flow 45 + event.cookies.set( 46 + HANDLE_COOKIE, 47 + handle, 48 + { 49 + httpOnly: true, 50 + maxAge: 60 * 10, 51 + path: "/", 52 + sameSite: "lax", 53 + secure: !dev, 54 + }, 55 + ); 56 + return url; 57 }; 58 59 + /** 60 + * Setup OAuth client from cookies 61 + */ 62 export const restoreSession = async (event: RequestEvent): Promise<void> => { 63 const { cookies } = event; 64 + const encrypted = cookies.get(SESSION_COOKIE); 65 if (encrypted === undefined) { 66 return; 67 } 68 + // Parse and validate or delete cookie 69 let data; 70 try { 71 const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY); 72 data = JSON.parse(decrypted); 73 } catch { 74 + cookies.delete(SESSION_COOKIE, { path: "/" }); 75 return; 76 } 77 + // [TODO] ArkType data validation? 78 try { 79 + if (isDid(data.did) === false || isHandle(data.handle) === false) { 80 throw new Error(); 81 } 82 const oAuthClient = createOAuthClient(event); ··· 88 session, 89 }; 90 } catch { 91 + cookies.delete(SESSION_COOKIE, { path: "/" }); 92 return; 93 } 94 };
+1 -1
src/routes/+layout.server.ts
··· 4 let user = undefined; 5 if (event.locals.user) { 6 user = { 7 handle: event.locals.user.handle, 8 displayName: event.locals.user.displayName, 9 - avatar: event.locals.user.avatar, 10 }; 11 } 12 return {
··· 4 let user = undefined; 5 if (event.locals.user) { 6 user = { 7 + did: event.locals.user.did, 8 handle: event.locals.user.handle, 9 displayName: event.locals.user.displayName, 10 }; 11 } 12 return {
+5 -24
src/routes/+page.server.ts
··· 1 import { type Actions, fail, redirect } from "@sveltejs/kit"; 2 - import { isActorIdentifier } from "@atcute/lexicons/syntax"; 3 - import { createOAuthClient } from "$lib/server/oauth.ts"; 4 - import { destroySession } from "../lib/server/session.ts"; 5 - import { dev } from "$app/environment"; 6 7 export const actions = { 8 logout: async (event) => { ··· 12 login: async (event) => { 13 const formData = await event.request.formData(); 14 const handle = formData.get("handle"); 15 - if (isActorIdentifier(handle) === false) { 16 return fail(400, { handle, invalid: true }); 17 } 18 - const oAuthClient = createOAuthClient(event); 19 - const { url } = await oAuthClient.authorize({ 20 - target: { 21 - "type": "account", 22 - identifier: handle, 23 - }, 24 - }); 25 - // [TODO] encrypt handle necessary? 26 - event.cookies.set( 27 - "atproto_handle", 28 - handle, 29 - { 30 - httpOnly: true, 31 - maxAge: 60 * 5, 32 - path: "/", 33 - sameSite: "lax", 34 - secure: !dev, 35 - }, 36 - ); 37 redirect(303, url); 38 }, 39 } satisfies Actions;
··· 1 import { type Actions, fail, redirect } from "@sveltejs/kit"; 2 + import { destroySession, startSession } from "$lib/server/session.ts"; 3 4 export const actions = { 5 logout: async (event) => { ··· 9 login: async (event) => { 10 const formData = await event.request.formData(); 11 const handle = formData.get("handle"); 12 + let url: URL; 13 + try { 14 + url = await startSession(event, String(handle ?? "")); 15 + } catch { 16 return fail(400, { handle, invalid: true }); 17 } 18 redirect(303, url); 19 }, 20 } satisfies Actions;
+5 -11
src/routes/oauth/callback/+server.ts
··· 5 import { redirect } from "@sveltejs/kit"; 6 import { createOAuthClient } from "$lib/server/oauth.ts"; 7 import { encryptText } from "$lib/server/crypto.ts"; 8 import { env } from "$env/dynamic/private"; 9 import { dev } from "$app/environment"; 10 11 export const GET: RequestHandler = async (event) => { 12 const { url, cookies } = event; 13 14 - const handle = cookies.get("atproto_handle"); 15 if (handle === undefined) { 16 return redirect(303, "/?error=expired"); 17 } 18 - cookies.delete("atproto_handle", { path: "/" }); 19 20 let session: OAuthSession; 21 try { ··· 29 const data = { 30 handle, 31 did: session.did, 32 - displayName: "", 33 - avatar: "", 34 }; 35 36 try { ··· 40 params: { actor: session.did }, 41 }), 42 ); 43 - // if (profile.handle) { 44 - // data.handle = profile.handle; 45 - // } 46 if (profile.displayName) { 47 data.displayName = profile.displayName; 48 - } 49 - if (profile.avatar) { 50 - data.avatar = profile.avatar; 51 } 52 } catch { 53 // No Bluesky account? ··· 59 ); 60 61 cookies.set( 62 - "atproto_session", 63 encrypted, 64 { 65 httpOnly: true,
··· 5 import { redirect } from "@sveltejs/kit"; 6 import { createOAuthClient } from "$lib/server/oauth.ts"; 7 import { encryptText } from "$lib/server/crypto.ts"; 8 + import { HANDLE_COOKIE, SESSION_COOKIE } from "$lib/server/constants.ts"; 9 import { env } from "$env/dynamic/private"; 10 import { dev } from "$app/environment"; 11 12 export const GET: RequestHandler = async (event) => { 13 const { url, cookies } = event; 14 15 + const handle = cookies.get(HANDLE_COOKIE); 16 if (handle === undefined) { 17 return redirect(303, "/?error=expired"); 18 } 19 + cookies.delete(HANDLE_COOKIE, { path: "/" }); 20 21 let session: OAuthSession; 22 try { ··· 30 const data = { 31 handle, 32 did: session.did, 33 + displayName: handle, 34 }; 35 36 try { ··· 40 params: { actor: session.did }, 41 }), 42 ); 43 if (profile.displayName) { 44 data.displayName = profile.displayName; 45 } 46 } catch { 47 // No Bluesky account? ··· 53 ); 54 55 cookies.set( 56 + SESSION_COOKIE, 57 encrypted, 58 { 59 httpOnly: true,