AtAuth
1/**
2 * Forward-Auth Proxy Utilities Tests
3 */
4import { describe, it, expect, vi } from 'vitest';
5import {
6 createSessionCookie,
7 verifySessionCookie,
8 createProxyCookie,
9 verifyProxyCookie,
10 createAdminCookie,
11 verifyAdminCookie,
12 createAuthTicket,
13 verifyAuthTicket,
14 parseCookies,
15 isAllowedRedirect,
16 extractOrigin,
17 SESSION_COOKIE_NAME,
18 PROXY_COOKIE_NAME,
19 ADMIN_COOKIE_NAME,
20} from './proxy-auth.js';
21
22const TEST_SECRET = 'test-secret-for-hmac-signing-32b!';
23
24describe('Session Cookie', () => {
25 it('should create and verify a session cookie', () => {
26 const cookie = createSessionCookie('session-123', TEST_SECRET, 3600);
27 const result = verifySessionCookie(cookie, TEST_SECRET);
28 expect(result).toBe('session-123');
29 });
30
31 it('should reject an expired cookie', () => {
32 vi.useFakeTimers();
33 const cookie = createSessionCookie('session-123', TEST_SECRET, 60);
34 // Advance past expiry
35 vi.advanceTimersByTime(61 * 1000);
36 const result = verifySessionCookie(cookie, TEST_SECRET);
37 expect(result).toBeNull();
38 vi.useRealTimers();
39 });
40
41 it('should reject a tampered cookie', () => {
42 const cookie = createSessionCookie('session-123', TEST_SECRET, 3600);
43 const tampered = cookie.slice(0, -1) + 'x';
44 const result = verifySessionCookie(tampered, TEST_SECRET);
45 expect(result).toBeNull();
46 });
47
48 it('should reject a cookie with wrong secret', () => {
49 const cookie = createSessionCookie('session-123', TEST_SECRET, 3600);
50 const result = verifySessionCookie(cookie, 'wrong-secret');
51 expect(result).toBeNull();
52 });
53
54 it('should reject malformed cookies', () => {
55 expect(verifySessionCookie('', TEST_SECRET)).toBeNull();
56 expect(verifySessionCookie('no-dots', TEST_SECRET)).toBeNull();
57 expect(verifySessionCookie('too.many.dots', TEST_SECRET)).toBeNull();
58 expect(verifySessionCookie('invalid.base64!', TEST_SECRET)).toBeNull();
59 });
60});
61
62describe('Proxy Cookie', () => {
63 it('should create and verify a proxy cookie', () => {
64 const cookie = createProxyCookie('session-456', TEST_SECRET, 86400);
65 const result = verifyProxyCookie(cookie, TEST_SECRET);
66 expect(result).toBe('session-456');
67 });
68});
69
70describe('Cookie Confusion Prevention', () => {
71 it('should reject a proxy cookie used as a session cookie', () => {
72 const proxyCookie = createProxyCookie('session-123', TEST_SECRET, 3600);
73 const result = verifySessionCookie(proxyCookie, TEST_SECRET);
74 expect(result).toBeNull();
75 });
76
77 it('should reject a session cookie used as a proxy cookie', () => {
78 const sessionCookie = createSessionCookie('session-123', TEST_SECRET, 3600);
79 const result = verifyProxyCookie(sessionCookie, TEST_SECRET);
80 expect(result).toBeNull();
81 });
82});
83
84describe('Auth Ticket', () => {
85 it('should create and verify an auth ticket', () => {
86 const ticket = createAuthTicket(
87 'session-789', 'did:plc:abc123', 'user.bsky.social',
88 'https://search.example.com', TEST_SECRET,
89 );
90 const result = verifyAuthTicket(ticket, TEST_SECRET);
91 expect(result).not.toBeNull();
92 expect(result!.sid).toBe('session-789');
93 expect(result!.did).toBe('did:plc:abc123');
94 expect(result!.handle).toBe('user.bsky.social');
95 expect(result!.origin).toBe('https://search.example.com');
96 });
97
98 it('should reject an expired ticket', () => {
99 vi.useFakeTimers();
100 const ticket = createAuthTicket(
101 'session-789', 'did:plc:abc123', 'user.bsky.social',
102 'https://search.example.com', TEST_SECRET,
103 );
104 // Advance past 60s expiry
105 vi.advanceTimersByTime(61 * 1000);
106 const result = verifyAuthTicket(ticket, TEST_SECRET);
107 expect(result).toBeNull();
108 vi.useRealTimers();
109 });
110
111 it('should reject a ticket with wrong origin', () => {
112 const ticket = createAuthTicket(
113 'session-789', 'did:plc:abc123', 'user.bsky.social',
114 'https://search.example.com', TEST_SECRET,
115 );
116 const result = verifyAuthTicket(ticket, TEST_SECRET, 'https://evil.example.com');
117 expect(result).toBeNull();
118 });
119
120 it('should accept a ticket with matching expected origin', () => {
121 const ticket = createAuthTicket(
122 'session-789', 'did:plc:abc123', 'user.bsky.social',
123 'https://search.example.com', TEST_SECRET,
124 );
125 const result = verifyAuthTicket(ticket, TEST_SECRET, 'https://search.example.com');
126 expect(result).not.toBeNull();
127 expect(result!.sid).toBe('session-789');
128 });
129
130 it('should reject a tampered ticket', () => {
131 const ticket = createAuthTicket(
132 'session-789', 'did:plc:abc123', 'user.bsky.social',
133 'https://search.example.com', TEST_SECRET,
134 );
135 const tampered = ticket.slice(0, -1) + 'x';
136 const result = verifyAuthTicket(tampered, TEST_SECRET);
137 expect(result).toBeNull();
138 });
139});
140
141describe('parseCookies', () => {
142 it('should parse a standard cookie header', () => {
143 const result = parseCookies('foo=bar; baz=qux');
144 expect(result).toEqual({ foo: 'bar', baz: 'qux' });
145 });
146
147 it('should handle cookies with = in the value', () => {
148 const result = parseCookies('token=abc=def=ghi');
149 expect(result).toEqual({ token: 'abc=def=ghi' });
150 });
151
152 it('should return empty object for undefined', () => {
153 expect(parseCookies(undefined)).toEqual({});
154 });
155
156 it('should return empty object for empty string', () => {
157 expect(parseCookies('')).toEqual({});
158 });
159
160 it('should trim whitespace', () => {
161 const result = parseCookies(' foo = bar ; baz = qux ');
162 expect(result).toEqual({ foo: 'bar', baz: 'qux' });
163 });
164});
165
166describe('isAllowedRedirect', () => {
167 const allowed = ['https://search.example.com', 'https://element.example.com'];
168
169 it('should allow a URL with an allowed origin', () => {
170 expect(isAllowedRedirect('https://search.example.com/some/path', allowed)).toBe(true);
171 expect(isAllowedRedirect('https://element.example.com/', allowed)).toBe(true);
172 });
173
174 it('should reject a URL with a disallowed origin', () => {
175 expect(isAllowedRedirect('https://evil.example.com/search', allowed)).toBe(false);
176 });
177
178 it('should reject an invalid URL', () => {
179 expect(isAllowedRedirect('not-a-url', allowed)).toBe(false);
180 });
181
182 it('should reject a URL with different port', () => {
183 expect(isAllowedRedirect('https://search.example.com:8443/path', allowed)).toBe(false);
184 });
185
186 it('should reject a URL with different scheme', () => {
187 expect(isAllowedRedirect('http://search.example.com/path', allowed)).toBe(false);
188 });
189});
190
191describe('extractOrigin', () => {
192 it('should extract origin from a full URL', () => {
193 expect(extractOrigin('https://search.example.com/some/path?q=test')).toBe('https://search.example.com');
194 });
195
196 it('should return null for invalid URL', () => {
197 expect(extractOrigin('not-a-url')).toBeNull();
198 });
199});
200
201describe('Admin Cookie', () => {
202 it('should create and verify an admin cookie', () => {
203 const cookie = createAdminCookie(TEST_SECRET, 86400);
204 expect(verifyAdminCookie(cookie, TEST_SECRET)).toBe(true);
205 });
206
207 it('should reject an expired admin cookie', () => {
208 vi.useFakeTimers();
209 const cookie = createAdminCookie(TEST_SECRET, 60);
210 vi.advanceTimersByTime(61 * 1000);
211 expect(verifyAdminCookie(cookie, TEST_SECRET)).toBe(false);
212 vi.useRealTimers();
213 });
214
215 it('should reject admin cookie with wrong secret', () => {
216 const cookie = createAdminCookie(TEST_SECRET, 86400);
217 expect(verifyAdminCookie(cookie, 'wrong-secret')).toBe(false);
218 });
219
220 it('should reject session cookie as admin cookie', () => {
221 const sessionCookie = createSessionCookie('sid', TEST_SECRET, 3600);
222 expect(verifyAdminCookie(sessionCookie, TEST_SECRET)).toBe(false);
223 });
224
225 it('should reject proxy cookie as admin cookie', () => {
226 const proxyCookie = createProxyCookie('sid', TEST_SECRET, 3600);
227 expect(verifyAdminCookie(proxyCookie, TEST_SECRET)).toBe(false);
228 });
229});
230
231describe('Cookie names', () => {
232 it('should export expected cookie names', () => {
233 expect(SESSION_COOKIE_NAME).toBe('_atauth_session');
234 expect(PROXY_COOKIE_NAME).toBe('_atauth_proxy');
235 expect(ADMIN_COOKIE_NAME).toBe('_atauth_admin');
236 });
237});