forge
login
or
join now
zeu.dev
/
atproto-oauth-deno
star
17
fork
atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
AT Protocol OAuth template in Deno, Hono, HTMX
star
17
fork
atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
overview
issues
pulls
pipelines
init
zeudev
4 months ago
70e133c8
+221
7 changed files
expand all
collapse all
unified
split
.env.example
.gitignore
README.md
components.tsx
deno.json
deno.lock
main.tsx
+1
.env.example
reviewed
···
1
1
+
COOKIE_SECRET=<`openssl rand -base64 64`>
+3
.gitignore
reviewed
···
1
1
+
.env
2
2
+
.env.*
3
3
+
!.env.example
+3
README.md
reviewed
···
1
1
+
```
2
2
+
deno task start
3
3
+
```
+36
components.tsx
reviewed
···
1
1
+
import { Context } from "hono";
2
2
+
import { oauth } from "./main.tsx";
3
3
+
import { FC, PropsWithChildren } from "hono/jsx";
4
4
+
5
5
+
export const SiteLayout: FC<PropsWithChildren<{ context: Context }>> = async ({ context, children }) => {
6
6
+
const { session } = await oauth.getSessionFromRequest(context.req.raw);
7
7
+
8
8
+
return (
9
9
+
<html>
10
10
+
<head>
11
11
+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
12
12
+
</head>
13
13
+
<body>
14
14
+
{ session ? <LogoutForm /> : <LoginForm /> }
15
15
+
{children}
16
16
+
</body>
17
17
+
</html>
18
18
+
);
19
19
+
}
20
20
+
21
21
+
export const LoginForm: FC = () => {
22
22
+
return (
23
23
+
<form method="get" action="/login">
24
24
+
<input name="handle" type="text" />
25
25
+
<button type="submit">Login</button>
26
26
+
</form>
27
27
+
)
28
28
+
}
29
29
+
30
30
+
export const LogoutForm: FC = () => {
31
31
+
return (
32
32
+
<form hx-post="/api/auth/logout" hx-swap="outerHTML">
33
33
+
<button type="submit">Logout</button>
34
34
+
</form>
35
35
+
)
36
36
+
}
+14
deno.json
reviewed
···
1
1
+
{
2
2
+
"imports": {
3
3
+
"@tijs/atproto-oauth": "jsr:@tijs/atproto-oauth@^2.9.0",
4
4
+
"@tijs/atproto-storage": "jsr:@tijs/atproto-storage@^1.0.0",
5
5
+
"hono": "jsr:@hono/hono@^4.11.9"
6
6
+
},
7
7
+
"tasks": {
8
8
+
"start": "deno run --allow-net --allow-env --env-file --watch-hmr main.tsx"
9
9
+
},
10
10
+
"compilerOptions": {
11
11
+
"jsx": "precompile",
12
12
+
"jsxImportSource": "hono/jsx"
13
13
+
}
14
14
+
}
+101
deno.lock
reviewed
···
1
1
+
{
2
2
+
"version": "5",
3
3
+
"specifiers": {
4
4
+
"jsr:@hono/hono@^4.11.9": "4.11.9",
5
5
+
"jsr:@panva/jose@6.1.0": "6.1.0",
6
6
+
"jsr:@tijs/atproto-oauth@*": "2.4.0",
7
7
+
"jsr:@tijs/atproto-oauth@^2.9.0": "2.9.0",
8
8
+
"jsr:@tijs/atproto-sessions@2.1.0": "2.1.0",
9
9
+
"jsr:@tijs/atproto-storage@*": "1.0.0",
10
10
+
"jsr:@tijs/atproto-storage@0.1.1": "0.1.1",
11
11
+
"jsr:@tijs/atproto-storage@1": "1.0.0",
12
12
+
"jsr:@tijs/oauth-client-deno@4.0.2": "4.0.2",
13
13
+
"jsr:@tijs/oauth-client-deno@5.1.0": "5.1.0",
14
14
+
"npm:@atproto/syntax@0.3.0": "0.3.0",
15
15
+
"npm:@atproto/syntax@0.4.0": "0.4.0",
16
16
+
"npm:iron-session@8.0.4": "8.0.4"
17
17
+
},
18
18
+
"jsr": {
19
19
+
"@hono/hono@4.11.9": {
20
20
+
"integrity": "c82c6b846abc3c1879d921d8365287d77cdef8073019f509ff80bf53033bdcba"
21
21
+
},
22
22
+
"@panva/jose@6.1.0": {
23
23
+
"integrity": "9ecffef33d822f4326341ace652bf30eef30d4dc9f7134faf7901e5480c2e761"
24
24
+
},
25
25
+
"@tijs/atproto-oauth@2.4.0": {
26
26
+
"integrity": "1e38182d3a9cde5c767429a09701cd9012dfc8ab7548d944b839c6c5e785ab84",
27
27
+
"dependencies": [
28
28
+
"jsr:@tijs/atproto-sessions",
29
29
+
"jsr:@tijs/oauth-client-deno@4.0.2",
30
30
+
"npm:@atproto/syntax@0.3.0"
31
31
+
]
32
32
+
},
33
33
+
"@tijs/atproto-oauth@2.9.0": {
34
34
+
"integrity": "89f71b7c99960d595df38defedace9cbc0c56350a4771a4b2b7fe6cd505248a2",
35
35
+
"dependencies": [
36
36
+
"jsr:@tijs/atproto-sessions",
37
37
+
"jsr:@tijs/atproto-storage@0.1.1",
38
38
+
"jsr:@tijs/oauth-client-deno@5.1.0",
39
39
+
"npm:@atproto/syntax@0.3.0"
40
40
+
]
41
41
+
},
42
42
+
"@tijs/atproto-sessions@2.1.0": {
43
43
+
"integrity": "5b8779ca7af76e3825c515d7483bbda6982b0ab656e3670566da6eb9d39aef59",
44
44
+
"dependencies": [
45
45
+
"npm:iron-session"
46
46
+
]
47
47
+
},
48
48
+
"@tijs/atproto-storage@0.1.1": {
49
49
+
"integrity": "384643401a7d15915a6fc86b273ba142a1825d5d46b692b0da3405e9f938f8ab"
50
50
+
},
51
51
+
"@tijs/atproto-storage@1.0.0": {
52
52
+
"integrity": "a7f03f14ad4846fb9df19c90ac3cb007cfdc8a40df0e1b33314feaeaad439d2e"
53
53
+
},
54
54
+
"@tijs/oauth-client-deno@4.0.2": {
55
55
+
"integrity": "759d7fa655642a87ccf46afddce84065ca3a49b4c0d387ae1f0b09b66a9f1d26",
56
56
+
"dependencies": [
57
57
+
"jsr:@panva/jose",
58
58
+
"npm:@atproto/syntax@0.4.0"
59
59
+
]
60
60
+
},
61
61
+
"@tijs/oauth-client-deno@5.1.0": {
62
62
+
"integrity": "ab8b0a6a13030f4730452557dadb9d5b822e44924efcadaf4579befa1883cfa9",
63
63
+
"dependencies": [
64
64
+
"jsr:@panva/jose",
65
65
+
"npm:@atproto/syntax@0.4.0"
66
66
+
]
67
67
+
}
68
68
+
},
69
69
+
"npm": {
70
70
+
"@atproto/syntax@0.3.0": {
71
71
+
"integrity": "sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA=="
72
72
+
},
73
73
+
"@atproto/syntax@0.4.0": {
74
74
+
"integrity": "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="
75
75
+
},
76
76
+
"cookie@0.7.2": {
77
77
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
78
78
+
},
79
79
+
"iron-session@8.0.4": {
80
80
+
"integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==",
81
81
+
"dependencies": [
82
82
+
"cookie",
83
83
+
"iron-webcrypto",
84
84
+
"uncrypto"
85
85
+
]
86
86
+
},
87
87
+
"iron-webcrypto@1.2.1": {
88
88
+
"integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="
89
89
+
},
90
90
+
"uncrypto@0.1.3": {
91
91
+
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
92
92
+
}
93
93
+
},
94
94
+
"workspace": {
95
95
+
"dependencies": [
96
96
+
"jsr:@hono/hono@^4.11.9",
97
97
+
"jsr:@tijs/atproto-oauth@^2.9.0",
98
98
+
"jsr:@tijs/atproto-storage@1"
99
99
+
]
100
100
+
}
101
101
+
}
+63
main.tsx
reviewed
···
1
1
+
import { Context, Hono } from "hono";
2
2
+
import { createATProtoOAuth } from "@tijs/atproto-oauth";
3
3
+
import { MemoryStorage } from "@tijs/atproto-storage";
4
4
+
import { LoginForm, SiteLayout } from "./components.tsx";
5
5
+
6
6
+
const app = new Hono();
7
7
+
8
8
+
export const oauth = createATProtoOAuth({
9
9
+
baseUrl: "http://127.0.0.1:8000/",
10
10
+
appName: "Pedro",
11
11
+
cookieSecret: Deno.env.get("COOKIE_SECRET")!,
12
12
+
storage: new MemoryStorage(),
13
13
+
sessionTtl: 60 * 60 * 24 * 14, // 14 days
14
14
+
});
15
15
+
16
16
+
17
17
+
app.get("/login", (c) => oauth.handleLogin(c.req.raw));
18
18
+
app.get("/oauth/callback", (c) => oauth.handleCallback(c.req.raw));
19
19
+
app.get("/oauth-client-metadata.json", () => oauth.handleClientMetadata());
20
20
+
app.post("/api/auth/logout", async (c) => {
21
21
+
const res = await oauth.handleLogout(c.req.raw)
22
22
+
const { success } = await res.json();
23
23
+
if (success) {
24
24
+
return c.html(<LoginForm />)
25
25
+
}
26
26
+
return res;
27
27
+
});
28
28
+
29
29
+
// Mount OAuth routes
30
30
+
app.get("/", (c) => {
31
31
+
return c.html(
32
32
+
<SiteLayout context={c}>
33
33
+
<h1>Hello</h1>
34
34
+
</SiteLayout>
35
35
+
)
36
36
+
});
37
37
+
38
38
+
// Protected route example
39
39
+
app.get("/api/profile", async (c) => {
40
40
+
const { session, setCookieHeader, error } = await oauth.getSessionFromRequest(
41
41
+
c.req.raw,
42
42
+
);
43
43
+
44
44
+
if (!session) {
45
45
+
return c.json({ error: error?.message || "Not authenticated" }, 401);
46
46
+
}
47
47
+
48
48
+
// Make authenticated API call
49
49
+
const response = await session.makeRequest(
50
50
+
"GET",
51
51
+
`${session.pdsUrl}/xrpc/app.bsky.actor.getProfile?actor=${session.did}`,
52
52
+
);
53
53
+
54
54
+
const profile = await response.json();
55
55
+
56
56
+
const res = c.json(profile);
57
57
+
if (setCookieHeader) {
58
58
+
res.headers.set("Set-Cookie", setCookieHeader);
59
59
+
}
60
60
+
return res;
61
61
+
});
62
62
+
63
63
+
Deno.serve(app.fetch);