Skip to content

Commit 9cd9ab4

Browse files
Merge pull request #67 from SpellcraftAI/cors-tests
test: granular CORS tests
2 parents 4d02346 + 4bbd95d commit 9cd9ab4

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed

__tests__/oauth-provider.test.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,9 @@ describe('OAuthProvider', () => {
12101210
expect(data.success).toBe(true);
12111211
expect(data.user).toEqual({ userId: 'test-user-123', username: 'TestUser' });
12121212
});
1213+
});
12131214

1215+
describe('CORS Support', () => {
12141216
it('should handle CORS preflight for API requests', async () => {
12151217
const preflightRequest = createMockRequest('https://example.com/api/test', 'OPTIONS', {
12161218
Origin: 'https://client.example.com',
@@ -1225,6 +1227,215 @@ describe('OAuthProvider', () => {
12251227
expect(preflightResponse.headers.get('Access-Control-Allow-Methods')).toBe('*');
12261228
expect(preflightResponse.headers.get('Access-Control-Allow-Headers')).toContain('Authorization');
12271229
});
1230+
1231+
it('should add CORS headers to OAuth metadata discovery endpoint', async () => {
1232+
const request = createMockRequest('https://example.com/.well-known/oauth-authorization-server', 'GET', {
1233+
Origin: 'https://client.example.com',
1234+
});
1235+
1236+
const response = await oauthProvider.fetch(request, mockEnv, mockCtx);
1237+
1238+
expect(response.status).toBe(200);
1239+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://client.example.com');
1240+
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
1241+
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1242+
expect(response.headers.get('Access-Control-Max-Age')).toBe('86400');
1243+
});
1244+
1245+
it('should handle OPTIONS preflight for metadata discovery endpoint', async () => {
1246+
const preflightRequest = createMockRequest('https://example.com/.well-known/oauth-authorization-server', 'OPTIONS', {
1247+
Origin: 'https://spa.example.com',
1248+
'Access-Control-Request-Method': 'GET',
1249+
});
1250+
1251+
const response = await oauthProvider.fetch(preflightRequest, mockEnv, mockCtx);
1252+
1253+
expect(response.status).toBe(204);
1254+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://spa.example.com');
1255+
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
1256+
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1257+
expect(response.headers.get('Access-Control-Max-Age')).toBe('86400');
1258+
expect(response.headers.get('Content-Length')).toBe('0');
1259+
});
1260+
1261+
it('should add CORS headers to token endpoint responses', async () => {
1262+
// First create a client and get auth code
1263+
const clientData = {
1264+
redirect_uris: ['https://client.example.com/callback'],
1265+
client_name: 'CORS Test Client',
1266+
token_endpoint_auth_method: 'client_secret_basic',
1267+
};
1268+
1269+
const registerRequest = createMockRequest(
1270+
'https://example.com/oauth/register',
1271+
'POST',
1272+
{ 'Content-Type': 'application/json' },
1273+
JSON.stringify(clientData)
1274+
);
1275+
1276+
const registerResponse = await oauthProvider.fetch(registerRequest, mockEnv, mockCtx);
1277+
const client = await registerResponse.json<any>();
1278+
const clientId = client.client_id;
1279+
const clientSecret = client.client_secret;
1280+
const redirectUri = 'https://client.example.com/callback';
1281+
1282+
const authRequest = createMockRequest(
1283+
`https://example.com/authorize?response_type=code&client_id=${clientId}` +
1284+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
1285+
`&scope=read%20write&state=xyz123`
1286+
);
1287+
1288+
const authResponse = await oauthProvider.fetch(authRequest, mockEnv, mockCtx);
1289+
const location = authResponse.headers.get('Location')!;
1290+
const code = new URL(location).searchParams.get('code')!;
1291+
1292+
// Now test token exchange with CORS
1293+
const params = new URLSearchParams();
1294+
params.append('grant_type', 'authorization_code');
1295+
params.append('code', code);
1296+
params.append('redirect_uri', redirectUri);
1297+
params.append('client_id', clientId);
1298+
params.append('client_secret', clientSecret);
1299+
1300+
const tokenRequest = createMockRequest(
1301+
'https://example.com/oauth/token',
1302+
'POST',
1303+
{
1304+
'Content-Type': 'application/x-www-form-urlencoded',
1305+
'Origin': 'https://webapp.example.com'
1306+
},
1307+
params.toString()
1308+
);
1309+
1310+
const tokenResponse = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx);
1311+
1312+
expect(tokenResponse.status).toBe(200);
1313+
expect(tokenResponse.headers.get('Access-Control-Allow-Origin')).toBe('https://webapp.example.com');
1314+
expect(tokenResponse.headers.get('Access-Control-Allow-Methods')).toBe('*');
1315+
expect(tokenResponse.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1316+
expect(tokenResponse.headers.get('Access-Control-Max-Age')).toBe('86400');
1317+
});
1318+
1319+
it('should handle OPTIONS preflight for token endpoint', async () => {
1320+
const preflightRequest = createMockRequest('https://example.com/oauth/token', 'OPTIONS', {
1321+
Origin: 'https://mobile.example.com',
1322+
'Access-Control-Request-Method': 'POST',
1323+
'Access-Control-Request-Headers': 'Content-Type',
1324+
});
1325+
1326+
const response = await oauthProvider.fetch(preflightRequest, mockEnv, mockCtx);
1327+
1328+
expect(response.status).toBe(204);
1329+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://mobile.example.com');
1330+
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
1331+
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1332+
expect(response.headers.get('Access-Control-Max-Age')).toBe('86400');
1333+
});
1334+
1335+
it('should add CORS headers to client registration endpoint', async () => {
1336+
const clientData = {
1337+
redirect_uris: ['https://newapp.example.com/callback'],
1338+
client_name: 'New CORS Test Client',
1339+
token_endpoint_auth_method: 'client_secret_basic',
1340+
};
1341+
1342+
const request = createMockRequest(
1343+
'https://example.com/oauth/register',
1344+
'POST',
1345+
{
1346+
'Content-Type': 'application/json',
1347+
'Origin': 'https://admin.example.com'
1348+
},
1349+
JSON.stringify(clientData)
1350+
);
1351+
1352+
const response = await oauthProvider.fetch(request, mockEnv, mockCtx);
1353+
1354+
expect(response.status).toBe(201);
1355+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://admin.example.com');
1356+
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
1357+
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1358+
});
1359+
1360+
it('should handle OPTIONS preflight for client registration endpoint', async () => {
1361+
const preflightRequest = createMockRequest('https://example.com/oauth/register', 'OPTIONS', {
1362+
Origin: 'https://dashboard.example.com',
1363+
'Access-Control-Request-Method': 'POST',
1364+
'Access-Control-Request-Headers': 'Content-Type, Authorization',
1365+
});
1366+
1367+
const response = await oauthProvider.fetch(preflightRequest, mockEnv, mockCtx);
1368+
1369+
expect(response.status).toBe(204);
1370+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://dashboard.example.com');
1371+
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
1372+
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1373+
});
1374+
1375+
it('should not add CORS headers when no Origin header is present', async () => {
1376+
const request = createMockRequest('https://example.com/.well-known/oauth-authorization-server');
1377+
const response = await oauthProvider.fetch(request, mockEnv, mockCtx);
1378+
1379+
expect(response.status).toBe(200);
1380+
expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull();
1381+
expect(response.headers.get('Access-Control-Allow-Methods')).toBeNull();
1382+
expect(response.headers.get('Access-Control-Allow-Headers')).toBeNull();
1383+
});
1384+
1385+
it('should add CORS headers to API error responses', async () => {
1386+
const apiRequest = createMockRequest('https://example.com/api/test', 'GET', {
1387+
Origin: 'https://client.example.com',
1388+
// No Authorization header - should get 401 error with CORS headers
1389+
});
1390+
1391+
const response = await oauthProvider.fetch(apiRequest, mockEnv, mockCtx);
1392+
1393+
expect(response.status).toBe(401);
1394+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://client.example.com');
1395+
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
1396+
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1397+
1398+
const error = await response.json<any>();
1399+
expect(error.error).toBe('invalid_token');
1400+
});
1401+
1402+
it('should add CORS headers to token endpoint error responses', async () => {
1403+
const params = new URLSearchParams();
1404+
params.append('grant_type', 'authorization_code');
1405+
params.append('code', 'invalid-code');
1406+
params.append('client_id', 'invalid-client');
1407+
1408+
const tokenRequest = createMockRequest(
1409+
'https://example.com/oauth/token',
1410+
'POST',
1411+
{
1412+
'Content-Type': 'application/x-www-form-urlencoded',
1413+
'Origin': 'https://evil.example.com'
1414+
},
1415+
params.toString()
1416+
);
1417+
1418+
const response = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx);
1419+
1420+
expect(response.status).toBe(401); // Should be an error
1421+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://evil.example.com');
1422+
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
1423+
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *');
1424+
});
1425+
1426+
it('should not add CORS headers to default handler responses', async () => {
1427+
const defaultRequest = createMockRequest('https://example.com/some-other-route', 'GET', {
1428+
Origin: 'https://client.example.com',
1429+
});
1430+
1431+
const response = await oauthProvider.fetch(defaultRequest, mockEnv, mockCtx);
1432+
1433+
expect(response.status).toBe(200);
1434+
// CORS headers should NOT be added to default handler responses
1435+
expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull();
1436+
expect(response.headers.get('Access-Control-Allow-Methods')).toBeNull();
1437+
expect(response.headers.get('Access-Control-Allow-Headers')).toBeNull();
1438+
});
12281439
});
12291440

12301441
describe('Token Exchange Callback', () => {

0 commit comments

Comments
 (0)