AT Protocol OAuth template in Deno, Hono, HTMX
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

init

zeudev 70e133c8

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