-
Notifications
You must be signed in to change notification settings - Fork 0
/
session.test.ts
206 lines (170 loc) · 8.29 KB
/
session.test.ts
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// @ts-ignore
import express from 'express';
import { exportJWK, generateKeyPair, KeyLike, SignJWT } from 'jose';
// @ts-ignore
import http from 'http';
import { BaseError, httpStatusCodes, ValidationError } from '../../src/errors';
import { ValidationErrorNames } from '../../src/errors/validationError';
import SessionService from '../../src/services/sessionService';
const app = express();
const PORT = 8081;
const TEST_USER_ID = '12345';
const TEST_USER_FULL_NAME = 'Test Name';
let validPrivateKey: KeyLike;
let invalidPrivateKey: KeyLike;
let publicKeyJwk: any;
async function initializeKeys() {
const { privateKey: key, publicKey } = await generateKeyPair('RS256');
validPrivateKey = key;
const { privateKey: invalidKey } = await generateKeyPair('RS256');
invalidPrivateKey = invalidKey;
publicKeyJwk = await exportJWK(publicKey);
publicKeyJwk.kid = 'kid123';
}
// @ts-ignore
app.get('/jwks', (req, res) => {
res.json({ keys: [publicKeyJwk] });
});
async function startJWKSserver() {
await initializeKeys();
return new Promise<http.Server>((resolve) => {
const server = app.listen(PORT, () => {
console.log(`JWKS server is running at http://localhost:${PORT}`);
resolve(server);
});
});
}
async function generateJWT(
issuer: string,
expiresIn: number,
notBefore: number,
privateKey: KeyLike,
): Promise<string> {
return await new SignJWT({
sub: TEST_USER_ID,
name: TEST_USER_FULL_NAME,
iss: issuer,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + expiresIn,
nbf: Math.floor(Date.now() / 1000) + notBefore,
})
.setProtectedHeader({
alg: 'RS256',
kid: 'kid123',
})
.sign(privateKey);
}
function createSessionService(issuer: string): SessionService {
return new SessionService(
'cbo_session_token',
issuer,
`http://localhost:${PORT}/jwks`,
10,
'pro-1',
);
}
describe('Session Service Unit Tests', () => {
let server: http.Server;
beforeAll(async () => {
server = await startJWKSserver();
});
afterAll(async () => {
server.close();
});
test('should throw error if required parameters are missing in constructor', () => {
expect(() => new SessionService('', 'https://pro-1.frontendapi.cloud.corbado.io', `http://localhost:${PORT}/jwks`, 10, 'pro-1')).toThrow(
'Required parameter is empty',
);
expect(() => new SessionService('cbo_session_token', '', `http://localhost:${PORT}/jwks`, 10, 'pro-1')).toThrow(
'Required parameter is empty',
);
expect(() => new SessionService('cbo_session_token', 'https://pro-1.frontendapi.cloud.corbado.io', '', 10, 'pro-1')).toThrow(
'Required parameter is empty',
);
});
test('should throw ValidationError if JWT is empty', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
await expect(sessionService.validateToken('')).rejects.toThrow(BaseError);
await expect(sessionService.validateToken('')).rejects.toHaveProperty(
'statusCode',
httpStatusCodes.EMPTY_STRING.code,
);
});
test('should throw ValidationError if JWT is too short', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
await expect(sessionService.validateToken('short')).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken('short')).rejects.toHaveProperty(
'name',
ValidationErrorNames.InvalidShortSession,
);
});
test('should throw ValidationError if JWT has an invalid signature', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZDEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGguYWNtZS5jb20iLCJpYXQiOjE3MjY0OTE4MDcsImV4cCI6MTcyNjQ5MTkwNywibmJmIjoxNzI2NDkxNzA3LCJzdWIiOiJ1c3ItMTIzNDU2Nzg5MCIsIm5hbWUiOiJuYW1lIiwiZW1haWwiOiJlbWFpbCIsInBob25lX251bWJlciI6InBob25lTnVtYmVyIiwib3JpZyI6Im9yaWcifQ.invalid';
await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty(
'name',
ValidationErrorNames.JWTInvalid,
);
});
test('should throw ValidationError using invalid private key', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', 600, 0, invalidPrivateKey);
await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.JWTInvalid);
});
test('should throw ValidationError if JWT is not yet valid (nbf claim in the future)', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', 600, 600, validPrivateKey);
await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty(
'name',
ValidationErrorNames.JWTClaimValidationFailed,
);
});
test('should throw ValidationError using an expired JWT', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', -600, 0, validPrivateKey);
await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.JWTExpired);
});
test('should throw ValidationError if issuer is empty', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
const jwt = await generateJWT('', 600, 0, validPrivateKey);
await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.EmptyIssuer);
});
test('should throw ValidationError if issuer is mismatch 1', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.corbado.io')
const jwt = await generateJWT('https://pro-2.frontendapi.cloud.corbado.io', 600, 0, validPrivateKey);
await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.InvalidIssuer);
});
test('should throw ValidationError if issuer is mismatch 2', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
const jwt = await generateJWT('https://pro-2.frontendapi.corbado.io', 600, 0, validPrivateKey);
await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError);
await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.InvalidIssuer);
});
test('should return user using old Frontend API URL as issuer in JWT', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io')
const jwt = await generateJWT('https://pro-1.frontendapi.corbado.io', 600, 0, validPrivateKey);
const user = await sessionService.validateToken(jwt);
expect(user.userId).toBe(TEST_USER_ID);
expect(user.fullName).toBe(TEST_USER_FULL_NAME);
});
test('should return user using old Frontend API URL as issuer in config', async () => {
const sessionService = createSessionService('https://pro-1.frontendapi.corbado.io')
const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', 600, 0, validPrivateKey);
const user = await sessionService.validateToken(jwt);
expect(user.userId).toBe(TEST_USER_ID);
expect(user.fullName).toBe(TEST_USER_FULL_NAME);
});
test('should return user data using CNAME', async () => {
const sessionService = createSessionService('https://auth.acme.com')
const jwt = await generateJWT('https://auth.acme.com', 600, 0, validPrivateKey);
const user = await sessionService.validateToken(jwt);
expect(user.userId).toBe(TEST_USER_ID);
expect(user.fullName).toBe(TEST_USER_FULL_NAME);
});
});