docs/src/routes/auth.ts 3.8 K raw
1
import { Hono } from "hono";
2
import { createOAuthClient } from "../lib/oauth-client";
3
import {
4
	getSessionDid,
5
	setSessionCookie,
6
	clearSessionCookie,
7
	getReturnToCookie,
8
	clearReturnToCookie,
9
} from "../lib/session";
10
11
interface Env {
12
	SEQUOIA_SESSIONS: KVNamespace;
13
	CLIENT_URL: string;
14
}
15
16
const auth = new Hono<{ Bindings: Env }>();
17
18
// OAuth client metadata endpoint
19
auth.get("/client-metadata.json", (c) => {
20
	const clientId = `${c.env.CLIENT_URL}/oauth/client-metadata.json`;
21
	const redirectUri = `${c.env.CLIENT_URL}/oauth/callback`;
22
23
	return c.json({
24
		client_id: clientId,
25
		client_name: "Sequoia",
26
		client_uri: c.env.CLIENT_URL,
27
		redirect_uris: [redirectUri],
28
		grant_types: ["authorization_code", "refresh_token"],
29
		response_types: ["code"],
30
		scope: "atproto site.standard.graph.subscription",
31
		token_endpoint_auth_method: "none",
32
		application_type: "web",
33
		dpop_bound_access_tokens: true,
34
	});
35
});
36
37
// Start OAuth login flow
38
auth.get("/login", async (c) => {
39
	try {
40
		const handle = c.req.query("handle");
41
		if (!handle) {
42
			return c.redirect(`${c.env.CLIENT_URL}/?error=missing_handle`);
43
		}
44
45
		const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL);
46
		const authUrl = await client.authorize(handle, {
47
			scope: "atproto site.standard.graph.subscription",
48
		});
49
50
		return c.redirect(authUrl.toString());
51
	} catch (error) {
52
		console.error("Login error:", error);
53
		return c.redirect(`${c.env.CLIENT_URL}/?error=login_failed`);
54
	}
55
});
56
57
// OAuth callback handler
58
auth.get("/callback", async (c) => {
59
	try {
60
		const params = new URLSearchParams(c.req.url.split("?")[1] || "");
61
62
		if (params.get("error")) {
63
			const error = params.get("error");
64
			console.error("OAuth error:", error, params.get("error_description"));
65
			return c.redirect(
66
				`${c.env.CLIENT_URL}/?error=${encodeURIComponent(error!)}`,
67
			);
68
		}
69
70
		const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL);
71
		const { session } = await client.callback(params);
72
73
		// Resolve handle from DID
74
		let handle: string | undefined;
75
		try {
76
			const identity = await client.identityResolver.resolve(session.did);
77
			handle = identity.handle;
78
		} catch {
79
			// Handle resolution is best-effort
80
		}
81
82
		// Store handle in KV alongside the session for quick lookup
83
		if (handle) {
84
			await c.env.SEQUOIA_SESSIONS.put(`oauth_handle:${session.did}`, handle, {
85
				expirationTtl: 60 * 60 * 24 * 14,
86
			});
87
		}
88
89
		setSessionCookie(c, session.did, c.env.CLIENT_URL);
90
91
		// If a subscribe flow set a return URL before initiating OAuth, honor it
92
		const returnTo = getReturnToCookie(c);
93
		clearReturnToCookie(c, c.env.CLIENT_URL);
94
95
		return c.redirect(returnTo ?? `${c.env.CLIENT_URL}/`);
96
	} catch (error) {
97
		console.error("Callback error:", error);
98
		return c.redirect(`${c.env.CLIENT_URL}/?error=callback_failed`);
99
	}
100
});
101
102
// Logout endpoint
103
auth.post("/logout", async (c) => {
104
	const did = getSessionDid(c);
105
106
	if (did) {
107
		try {
108
			const client = createOAuthClient(
109
				c.env.SEQUOIA_SESSIONS,
110
				c.env.CLIENT_URL,
111
			);
112
			await client.revoke(did);
113
		} catch (error) {
114
			console.error("Revoke error:", error);
115
		}
116
		await c.env.SEQUOIA_SESSIONS.delete(`oauth_handle:${did}`);
117
	}
118
119
	clearSessionCookie(c, c.env.CLIENT_URL);
120
	return c.json({ success: true });
121
});
122
123
// Check auth status
124
auth.get("/status", async (c) => {
125
	const did = getSessionDid(c);
126
127
	if (!did) {
128
		return c.json({ authenticated: false });
129
	}
130
131
	try {
132
		const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL);
133
		const session = await client.restore(did);
134
135
		const handle = await c.env.SEQUOIA_SESSIONS.get(
136
			`oauth_handle:${session.did}`,
137
		);
138
139
		return c.json({
140
			authenticated: true,
141
			did: session.did,
142
			handle: handle || undefined,
143
		});
144
	} catch (error) {
145
		console.error("Session restore failed:", error);
146
		clearSessionCookie(c, c.env.CLIENT_URL);
147
		return c.json({ authenticated: false });
148
	}
149
});
150
151
export default auth;