2026-02-26 11:30:30 +01:00
|
|
|
import { env, createExecutionContext, fetchMock } from 'cloudflare:test';
|
|
|
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
|
|
|
import { generateKeyPair, SignJWT, exportJWK } from 'jose';
|
2026-02-26 10:16:34 +01:00
|
|
|
import worker from '../src/index';
|
|
|
|
|
|
|
|
|
|
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
|
|
|
|
|
|
2026-02-26 11:30:30 +01:00
|
|
|
let privateKey: CryptoKey;
|
|
|
|
|
let jwksResponse: object;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
const { publicKey, privateKey: pk } = await generateKeyPair('RS256');
|
|
|
|
|
privateKey = pk;
|
|
|
|
|
|
|
|
|
|
const publicJwk = await exportJWK(publicKey);
|
|
|
|
|
publicJwk.alg = 'RS256';
|
|
|
|
|
publicJwk.use = 'sig';
|
|
|
|
|
publicJwk.kid = 'test-key';
|
|
|
|
|
jwksResponse = { keys: [publicJwk] };
|
|
|
|
|
|
|
|
|
|
fetchMock.activate();
|
|
|
|
|
fetchMock.disableNetConnect();
|
|
|
|
|
|
|
|
|
|
fetchMock
|
|
|
|
|
.get('https://id.guardianproject.info')
|
|
|
|
|
.intercept({ path: '/realms/gp/protocol/openid-connect/certs' })
|
|
|
|
|
.reply(200, JSON.stringify(jwksResponse), {
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
})
|
|
|
|
|
.persist();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function makeJWT(overrides: Record<string, unknown> = {}): Promise<string> {
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
const defaults = {
|
|
|
|
|
iss: env.KEYCLOAK_ISSUER,
|
|
|
|
|
aud: env.KEYCLOAK_AUDIENCE,
|
|
|
|
|
sub: 'test-user',
|
|
|
|
|
groups: [env.REQUIRED_GROUP],
|
|
|
|
|
iat: now,
|
|
|
|
|
exp: now + 3600,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const claims = { ...defaults, ...overrides };
|
|
|
|
|
|
|
|
|
|
let builder = new SignJWT(claims)
|
|
|
|
|
.setProtectedHeader({ alg: 'RS256', kid: 'test-key' });
|
|
|
|
|
|
|
|
|
|
if (claims.iss !== undefined) builder = builder.setIssuer(claims.iss as string);
|
|
|
|
|
if (claims.aud !== undefined) builder = builder.setAudience(claims.aud as string);
|
|
|
|
|
if (claims.exp !== undefined) builder = builder.setExpirationTime(claims.exp as number);
|
|
|
|
|
|
|
|
|
|
return builder.sign(privateKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('nix-cache auth', () => {
|
|
|
|
|
it('OPTIONS without auth returns 200', async () => {
|
|
|
|
|
const request = new IncomingRequest('http://example.com/', { method: 'OPTIONS' });
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with no Authorization header returns 401', async () => {
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info');
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).toBe(401);
|
|
|
|
|
expect(response.headers.get('WWW-Authenticate')).toBe('Bearer realm="nix-cache"');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with unrecognized auth scheme returns 401', async () => {
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: 'Token xyz' },
|
|
|
|
|
});
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).toBe(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with Bearer + valid JWT passes auth', async () => {
|
|
|
|
|
const token = await makeJWT();
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
|
});
|
2026-02-26 10:16:34 +01:00
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
2026-02-26 11:30:30 +01:00
|
|
|
expect(response.status).not.toBe(401);
|
2026-02-26 10:16:34 +01:00
|
|
|
});
|
|
|
|
|
|
2026-02-26 11:30:30 +01:00
|
|
|
it('GET with Basic auth :token format passes auth', async () => {
|
|
|
|
|
const token = await makeJWT();
|
|
|
|
|
const basic = btoa(':' + token);
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: `Basic ${basic}` },
|
|
|
|
|
});
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).not.toBe(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with Basic auth user:token format passes auth', async () => {
|
|
|
|
|
const token = await makeJWT();
|
|
|
|
|
const basic = btoa('user:' + token);
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: `Basic ${basic}` },
|
|
|
|
|
});
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).not.toBe(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with invalid Bearer token returns 401', async () => {
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: 'Bearer garbage-token' },
|
|
|
|
|
});
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).toBe(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with expired JWT returns 401', async () => {
|
|
|
|
|
const token = await makeJWT({ exp: Math.floor(Date.now() / 1000) - 3600 });
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).toBe(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with JWT missing groups claim returns 401', async () => {
|
|
|
|
|
const token = await makeJWT({ groups: undefined });
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).toBe(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('GET with JWT where groups does not contain required group returns 401', async () => {
|
|
|
|
|
const token = await makeJWT({ groups: ['some-other-group'] });
|
|
|
|
|
const request = new IncomingRequest('http://example.com/nix-cache-info', {
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
const ctx = createExecutionContext();
|
|
|
|
|
const response = await worker.fetch(request, env, ctx);
|
|
|
|
|
expect(response.status).toBe(401);
|
2026-02-26 10:16:34 +01:00
|
|
|
});
|
|
|
|
|
});
|