AtAuth
at main 237 lines 8.2 kB view raw
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});