~james/core

core/status/src/access.test.ts -rw-r--r-- 3.7 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 14 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { webcrypto } from 'node:crypto';
import { VerifiedAccessIdentity, type TeamCertKey } from './access.ts';

const TEAM = 'tangled.cloudflareaccess.com';
const AUD = 'a'.repeat(32);
const env = { ACCESS_TEAM_DOMAIN: TEAM, ACCESS_AUD: AUD };

const generated = (await crypto.subtle.generateKey(
  { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
  true,
  ['sign', 'verify'],
)) as webcrypto.CryptoKeyPair;
const jwk = {
  private: (await crypto.subtle.exportKey('jwk', generated.privateKey)) as TeamCertKey,
  public: (await crypto.subtle.exportKey('jwk', generated.publicKey)) as TeamCertKey,
};

let servedKeys: TeamCertKey[] = [{ ...jwk.public, kid: 'test-key', use: 'sig', alg: 'RS256' }];
globalThis.fetch = (async () => Response.json({ keys: servedKeys })) as typeof fetch;

const b64url = (bytes: Uint8Array): string => Buffer.from(bytes).toString('base64url');

const signedToken = async (claims: Record<string, unknown>, kid = 'test-key'): Promise<string> => {
  const head = b64url(new TextEncoder().encode(JSON.stringify({ alg: 'RS256', kid, typ: 'JWT' })));
  const payload = b64url(new TextEncoder().encode(JSON.stringify(claims)));
  const key = await crypto.subtle.importKey(
    'jwk',
    jwk.private,
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const signature = await crypto.subtle.sign(
    'RSASSA-PKCS1-v1_5',
    key,
    new TextEncoder().encode(`${head}.${payload}`) as unknown as ArrayBuffer,
  );
  return `${head}.${payload}.${b64url(new Uint8Array(signature))}`;
};

const now = Math.floor(Date.now() / 1000);
const claims = (over: Record<string, unknown> = {}) => ({
  iss: `https://${TEAM}`,
  aud: AUD,
  iat: now,
  exp: now + 600,
  email: 'lewis@cia.nel.pet',
  ...over,
});

test('team-signed token gives bearer email', async () => {
  const identity = await VerifiedAccessIdentity.verify(await signedToken(claims()), env);
  assert.equal(identity?.email, 'lewis@cia.nel.pet');
  const arrayAud = await VerifiedAccessIdentity.verify(await signedToken(claims({ aud: [AUD] })), env);
  assert.equal(arrayAud?.email, 'lewis@cia.nel.pet');
});

test('a token outside policy isnot a full ident', async () => {
  const [head, payload] = (await signedToken(claims())).split('.');
  const cases: Record<string, Promise<string>> = {
    'tampered signature': Promise.resolve(`${head}.${payload}.AAAA`),
    'another team': signedToken(claims({ iss: 'https://elsewhere.cloudflareaccess.com' })),
    'another audience': signedToken(claims({ aud: 'b'.repeat(32) })),
    'superstring audience': signedToken(claims({ aud: `${AUD}tail` })),
    'prefixed audience': signedToken(claims({ aud: `head${AUD}` })),
    expired: signedToken(claims({ exp: now - 3600 })),
    'issued in the future': signedToken(claims({ iat: now + 3600 })),
    'without exp': signedToken(claims({ exp: undefined })),
    'without iat': signedToken(claims({ iat: undefined })),
  };
  for (const [name, token] of Object.entries(cases)) {
    assert.equal(await VerifiedAccessIdentity.verify(await token, env), null, name);
  }
});

test('token that is in rotated key kid will still be verifiable if served by certs', async () => {
  servedKeys = [{ ...jwk.public, kid: 'rotated-key', use: 'sig', alg: 'RS256' }];
  const token = await signedToken(claims(), 'rotated-key');
  const identity = await VerifiedAccessIdentity.verify(token, env);
  assert.equal(identity?.email, 'lewis@cia.nel.pet');
});

test('w/o team or aud configured should be closed', async () => {
  assert.equal(await VerifiedAccessIdentity.verify(await signedToken(claims()), {}), null);
  assert.equal(await VerifiedAccessIdentity.verify(null, env), null);
});