tangled
alpha
login
or
join now
dbushell.com
/
attic.social
6
fork
atom
Attic is a cozy space with lofty ambitions.
attic.social
6
fork
atom
overview
issues
pulls
pipelines
cookie constants
dbushell.com
1 week ago
b62ee852
5d78a193
verified
This commit was signed with the committer's
known signature
.
dbushell.com
SSH Key Fingerprint:
SHA256:Sj5AfJ6VbC0PEnnQD2kGGEiGFwHdFBS/ypN5oifzzFI=
+72
-59
7 changed files
expand all
collapse all
unified
split
src
app.d.ts
lib
server
constants.ts
oauth.ts
session.ts
routes
+layout.server.ts
+page.server.ts
oauth
callback
+server.ts
-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;
0
18
};
19
}
20
// interface PageData {}
+3
src/lib/server/constants.ts
···
0
0
0
···
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";
0
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";
0
6
import { env } from "$env/dynamic/private";
0
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;
0
0
0
0
0
0
0
0
0
0
0
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?
0
0
0
0
0
0
0
19
}
20
-
event.locals.oAuthClient = undefined;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
21
};
22
0
0
0
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);
0
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) {
0
0
0
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 = {
0
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,
0
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) {
0
0
0
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";
0
0
0
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
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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";
0
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,
0
34
};
35
36
try {
···
40
params: { actor: session.did },
41
}),
42
);
0
0
0
43
if (profile.displayName) {
44
data.displayName = profile.displayName;
0
0
0
45
}
46
} catch {
47
// No Bluesky account?
···
53
);
54
55
cookies.set(
56
+
SESSION_COOKIE,
57
encrypted,
58
{
59
httpOnly: true,