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
oauthclient cookie store
dbushell.com
2 weeks ago
5d78a193
65509e82
verified
This commit was signed with the committer's
known signature
.
dbushell.com
SSH Key Fingerprint:
SHA256:Sj5AfJ6VbC0PEnnQD2kGGEiGFwHdFBS/ypN5oifzzFI=
+99
-31
5 changed files
expand all
collapse all
unified
split
src
app.d.ts
lib
server
oauth.ts
session.ts
routes
+page.server.ts
oauth
callback
+server.ts
+2
-1
src/app.d.ts
···
1
1
-
import type { OAuthSession } from "@atcute/oauth-node-client";
1
1
+
import type { OAuthClient, OAuthSession } from "@atcute/oauth-node-client";
2
2
import type { Client } from "@atcute/client";
3
3
import type { Did, Handle } from "@atcute/lexicons";
4
4
···
8
8
namespace App {
9
9
// interface Error {}
10
10
interface Locals {
11
11
+
oAuthClient?: OAuthClient;
11
12
user?: {
12
13
client: Client;
13
14
session: OAuthSession;
+85
-21
src/lib/server/oauth.ts
···
1
1
+
import type { Cookies } from "@sveltejs/kit";
1
2
import {
2
3
CompositeDidDocumentResolver,
3
4
CompositeHandleResolver,
···
7
8
WellKnownHandleResolver,
8
9
} from "@atcute/identity-resolver";
9
10
import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node";
10
10
-
import {
11
11
-
MemoryStore,
12
12
-
OAuthClient,
13
13
-
scope,
14
14
-
type StoredState,
15
15
-
} from "@atcute/oauth-node-client";
11
11
+
import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client";
12
12
+
import { decryptText, encryptText } from "$lib/server/crypto.ts";
13
13
+
import { env } from "$env/dynamic/private";
14
14
+
import { dev } from "$app/environment";
15
15
+
import { Buffer } from "node:buffer";
16
16
+
17
17
+
class CookieStore<K extends string, V> implements Store<K, V> {
18
18
+
#cookies: Cookies;
19
19
+
#prefix = "atproto_oauth_";
20
20
+
#maxAge = 60 * 60 * 24 * 7;
21
21
+
22
22
+
constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) {
23
23
+
this.#cookies = event.cookies;
24
24
+
if (options?.maxAge) {
25
25
+
this.#maxAge = options.maxAge;
26
26
+
}
27
27
+
}
28
28
+
29
29
+
cookieName(key: K) {
30
30
+
const name = Buffer.from(key).toString("base64url");
31
31
+
return `${this.#prefix}${name}`;
32
32
+
}
16
33
17
17
-
const TEN_MINUTES_MS = 10 * 60_000;
34
34
+
async get(key: K) {
35
35
+
const cookieName = this.cookieName(key);
36
36
+
const cookieValue = this.#cookies.get(cookieName);
37
37
+
if (cookieValue === undefined) {
38
38
+
return undefined;
39
39
+
}
40
40
+
try {
41
41
+
const value = await decryptText(
42
42
+
cookieValue,
43
43
+
env.PRIVATE_COOKIE_KEY,
44
44
+
);
45
45
+
return JSON.parse(value);
46
46
+
} catch {
47
47
+
return undefined;
48
48
+
}
49
49
+
}
18
50
19
19
-
/**
20
20
-
* {@link https://github.com/mary-ext/atcute/tree/trunk/packages/oauth/node-client-public-example}
21
21
-
*/
22
22
-
export function createOAuthClient(): OAuthClient {
51
51
+
async set(key: K, value: V) {
52
52
+
const cookieName = this.cookieName(key);
53
53
+
const cookieValue = await encryptText(
54
54
+
JSON.stringify(value),
55
55
+
env.PRIVATE_COOKIE_KEY,
56
56
+
);
57
57
+
if (cookieValue.length > 4000) {
58
58
+
throw new Error("too large");
59
59
+
}
60
60
+
this.#cookies.set(
61
61
+
cookieName,
62
62
+
cookieValue,
63
63
+
{
64
64
+
httpOnly: true,
65
65
+
maxAge: this.#maxAge,
66
66
+
path: "/",
67
67
+
sameSite: "lax",
68
68
+
secure: !dev,
69
69
+
},
70
70
+
);
71
71
+
}
72
72
+
73
73
+
delete(key: K) {
74
74
+
const cookieName = this.cookieName(key);
75
75
+
this.#cookies.delete(cookieName, { path: "/" });
76
76
+
}
77
77
+
78
78
+
clear() {
79
79
+
for (const { name } of this.#cookies.getAll()) {
80
80
+
if (name.startsWith(this.#prefix)) {
81
81
+
this.#cookies.delete(name, { path: "/" });
82
82
+
}
83
83
+
}
84
84
+
}
85
85
+
}
86
86
+
87
87
+
export function createOAuthClient(
88
88
+
event: { cookies: Cookies; locals: App.Locals },
89
89
+
): OAuthClient {
90
90
+
if (event.locals.oAuthClient) {
91
91
+
return event.locals.oAuthClient;
92
92
+
}
93
93
+
23
94
// [TODO] dynamic hostname/port
24
95
const redirectUri = `http://127.0.0.1:5173/oauth/callback`;
25
96
···
28
99
redirect_uris: [redirectUri],
29
100
scope: [scope.rpc({ lxm: ["app.bsky.actor.getProfile"], aud: "*" })],
30
101
},
31
31
-
32
102
actorResolver: new LocalActorResolver({
33
103
handleResolver: new CompositeHandleResolver({
34
104
methods: {
···
43
113
},
44
114
}),
45
115
}),
46
46
-
// [TODO] custom database K/V store
47
116
stores: {
48
48
-
sessions: new MemoryStore({ maxSize: 10 }),
49
49
-
states: new MemoryStore<string, StoredState>({
50
50
-
maxSize: 10,
51
51
-
ttl: TEN_MINUTES_MS,
52
52
-
ttlAutopurge: true,
53
53
-
}),
117
117
+
sessions: new CookieStore(event),
118
118
+
states: new CookieStore(event, { maxAge: 60 * 10 }),
54
119
},
55
120
});
56
121
122
122
+
event.locals.oAuthClient = client;
57
123
return client;
58
124
}
59
59
-
60
60
-
export const oAuthClient = createOAuthClient();
+4
-1
src/lib/server/session.ts
···
1
1
import type { RequestEvent } from "@sveltejs/kit";
2
2
import { Client } from "@atcute/client";
3
3
import { isDid, isHandle } from "@atcute/lexicons/syntax";
4
4
-
import { oAuthClient } from "$lib/server/oauth.ts";
4
4
+
import { createOAuthClient } from "$lib/server/oauth.ts";
5
5
import { decryptText } from "./crypto.ts";
6
6
import { env } from "$env/dynamic/private";
7
7
···
11
11
return;
12
12
}
13
13
try {
14
14
+
const oAuthClient = createOAuthClient(event);
14
15
// await event.locals.user.session.signOut();
15
16
await oAuthClient.revoke(event.locals.user.did);
16
17
} catch {
17
18
// Do nothing?
18
19
}
20
20
+
event.locals.oAuthClient = undefined;
19
21
};
20
22
21
23
export const restoreSession = async (event: RequestEvent): Promise<void> => {
···
42
44
) {
43
45
throw new Error();
44
46
}
47
47
+
const oAuthClient = createOAuthClient(event);
45
48
const session = await oAuthClient.restore(data.did);
46
49
const client = new Client({ handler: session });
47
50
event.locals.user = {
+6
-5
src/routes/+page.server.ts
···
1
1
import { type Actions, fail, redirect } from "@sveltejs/kit";
2
2
import { isActorIdentifier } from "@atcute/lexicons/syntax";
3
3
-
import { oAuthClient } from "$lib/server/oauth.ts";
3
3
+
import { createOAuthClient } from "$lib/server/oauth.ts";
4
4
import { destroySession } from "../lib/server/session.ts";
5
5
import { dev } from "$app/environment";
6
6
···
9
9
await destroySession(event);
10
10
redirect(303, "/");
11
11
},
12
12
-
login: async ({ cookies, request }) => {
13
13
-
const formData = await request.formData();
12
12
+
login: async (event) => {
13
13
+
const formData = await event.request.formData();
14
14
const handle = formData.get("handle");
15
15
if (isActorIdentifier(handle) === false) {
16
16
return fail(400, { handle, invalid: true });
17
17
}
18
18
+
const oAuthClient = createOAuthClient(event);
18
19
const { url } = await oAuthClient.authorize({
19
20
target: {
20
21
"type": "account",
21
22
identifier: handle,
22
23
},
23
24
});
24
24
-
// [TODO] encrypt handle?
25
25
-
cookies.set(
25
25
+
// [TODO] encrypt handle necessary?
26
26
+
event.cookies.set(
26
27
"atproto_handle",
27
28
handle,
28
29
{
+2
-3
src/routes/oauth/callback/+server.ts
···
3
3
import { AppBskyActorGetProfile } from "@atcute/bluesky";
4
4
import { Client, ok } from "@atcute/client";
5
5
import { redirect } from "@sveltejs/kit";
6
6
-
import { oAuthClient } from "$lib/server/oauth.ts";
6
6
+
import { createOAuthClient } from "$lib/server/oauth.ts";
7
7
import { encryptText } from "$lib/server/crypto.ts";
8
8
import { env } from "$env/dynamic/private";
9
9
import { dev } from "$app/environment";
···
11
11
export const GET: RequestHandler = async (event) => {
12
12
const { url, cookies } = event;
13
13
14
14
-
// [TODO] delete / handled by @atcute?
15
14
const handle = cookies.get("atproto_handle");
16
15
if (handle === undefined) {
17
16
return redirect(303, "/?error=expired");
···
20
19
21
20
let session: OAuthSession;
22
21
try {
22
22
+
const oAuthClient = createOAuthClient(event);
23
23
session = (await oAuthClient.callback(url.searchParams)).session;
24
24
} catch (err) {
25
25
console.error(err);
26
26
redirect(303, "/?error=session");
27
27
}
28
28
29
29
-
// [TODO] remember handle from login form
30
29
const data = {
31
30
handle,
32
31
did: session.did,