Skip to content
File

Blob: worker/src/server/auth.ts

typescript60 lines
1import { getCookie, setCookie } from 'hono/cookie';
2import type { Context } from 'hono';
3import { demand } from './rpc.ts';
4import { equal } from './crypto.ts';
5import type { HttpApp } from './http.ts';
6 
7async function signature(value: string, secret: string): Promise<string> {
8 const key = await crypto.subtle.importKey(
9 'raw',
10 new TextEncoder().encode(secret),
11 { name: 'HMAC', hash: 'SHA-256' },
12 false,
13 ['sign'],
14 );
15 const bytes = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value));
16 return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('');
17}
18export function assertSameOrigin(request: Request): void {
19 const origin = request.headers.get('Origin');
20 demand(!origin || origin === new URL(request.url).origin, 403, 'Origin not allowed.');
21 demand(
22 request.headers.get('Sec-Fetch-Site') !== 'cross-site',
23 403,
24 'Cross-site request rejected.',
25 );
26}
27export async function authenticated(c: Context<HttpApp>): Promise<boolean> {
28 const request = c.req.raw,
29 env = c.env;
30 const url = new URL(request.url);
31 if (['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) return true;
32 const cookie = getCookie(c, 'radio_auth');
33 if (!cookie) return false;
34 const [expires, nonce, mac] = cookie.split('.');
35 if (
36 !expires ||
37 !nonce ||
38 !mac ||
39 !Number.isFinite(Number(expires)) ||
40 Number(expires) < Date.now()
41 )
42 return false;
43 return equal(mac, await signature(`${expires}.${nonce}`, env.VIEWER_PASSWORD));
44}
45export async function login(password: string, c: Context<HttpApp>): Promise<Response> {
46 const request = c.req.raw,
47 env = c.env;
48 demand(await equal(password, env.VIEWER_PASSWORD), 401, 'That password did not match.');
49 const value = `${Date.now() + 86400000}.${crypto.randomUUID()}`;
50 const cookie = `${value}.${await signature(value, env.VIEWER_PASSWORD)}`;
51 setCookie(c, 'radio_auth', cookie, {
52 httpOnly: true,
53 sameSite: 'Strict',
54 path: '/api',
55 maxAge: 86400,
56 secure: new URL(request.url).protocol === 'https:',
57 });
58 return c.json({ ok: true });
59}