@boxyhq/saml-jackson 0.2.3-beta.210 → 0.2.3-beta.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +13 -0
- package/package.json +2 -2
- package/prettier.config.js +4 -0
- package/src/controller/api.ts +225 -0
- package/src/controller/error.ts +13 -0
- package/src/controller/oauth/allowed.ts +22 -0
- package/src/controller/oauth/code-verifier.ts +11 -0
- package/src/controller/oauth/redirect.ts +12 -0
- package/src/controller/oauth.ts +337 -0
- package/src/controller/utils.ts +17 -0
- package/src/db/db.ts +100 -0
- package/src/db/encrypter.ts +38 -0
- package/src/db/mem.ts +128 -0
- package/src/db/mongo.ts +110 -0
- package/src/db/redis.ts +103 -0
- package/src/db/sql/entity/JacksonIndex.ts +44 -0
- package/src/db/sql/entity/JacksonStore.ts +43 -0
- package/src/db/sql/entity/JacksonTTL.ts +17 -0
- package/src/db/sql/model/JacksonIndex.ts +3 -0
- package/src/db/sql/model/JacksonStore.ts +8 -0
- package/src/db/sql/sql.ts +184 -0
- package/src/db/store.ts +49 -0
- package/src/db/utils.ts +26 -0
- package/src/env.ts +42 -0
- package/src/index.ts +79 -0
- package/src/jackson.ts +171 -0
- package/src/read-config.ts +29 -0
- package/src/saml/claims.ts +41 -0
- package/src/saml/saml.ts +234 -0
- package/src/saml/x509.ts +51 -0
- package/src/test/api.test.ts +271 -0
- package/src/test/data/metadata/boxyhq.js +6 -0
- package/src/test/data/metadata/boxyhq.xml +30 -0
- package/src/test/data/saml_response +1 -0
- package/src/test/db.test.ts +313 -0
- package/src/test/oauth.test.ts +353 -0
- package/src/typings.ts +167 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +26 -0
- package/.nyc_output/36a3e9e1-42eb-468d-a9ec-8d206fedcd3e.json +0 -1
- package/.nyc_output/8c0af85a-b807-45bb-8331-20c3aabe15df.json +0 -1
- package/.nyc_output/ad148b90-7401-4df2-959f-3fdcf81a06ec.json +0 -1
- package/.nyc_output/processinfo/36a3e9e1-42eb-468d-a9ec-8d206fedcd3e.json +0 -1
- package/.nyc_output/processinfo/8c0af85a-b807-45bb-8331-20c3aabe15df.json +0 -1
- package/.nyc_output/processinfo/ad148b90-7401-4df2-959f-3fdcf81a06ec.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
@@ -0,0 +1,313 @@
|
|
1
|
+
import {
|
2
|
+
DatabaseEngine,
|
3
|
+
DatabaseOption,
|
4
|
+
EncryptionKey,
|
5
|
+
Storable,
|
6
|
+
} from 'saml-jackson';
|
7
|
+
import tap from 'tap';
|
8
|
+
import DB from '../db/db';
|
9
|
+
|
10
|
+
const encryptionKey: EncryptionKey = '3yGrTcnKPBqqHoH3zZMAU6nt4bmIYb2q';
|
11
|
+
|
12
|
+
let configStores: Storable[] = [];
|
13
|
+
let ttlStores: Storable[] = [];
|
14
|
+
const ttl = 3;
|
15
|
+
|
16
|
+
const record1 = {
|
17
|
+
id: '1',
|
18
|
+
name: 'Deepak',
|
19
|
+
city: 'London',
|
20
|
+
};
|
21
|
+
|
22
|
+
const record2 = {
|
23
|
+
id: '2',
|
24
|
+
name: 'Sama',
|
25
|
+
city: 'London',
|
26
|
+
};
|
27
|
+
|
28
|
+
const memDbConfig = <DatabaseOption>{
|
29
|
+
engine: 'mem',
|
30
|
+
ttl: 1,
|
31
|
+
};
|
32
|
+
|
33
|
+
const redisDbConfig = <DatabaseOption>{
|
34
|
+
engine: 'redis',
|
35
|
+
url: 'redis://localhost:6379',
|
36
|
+
};
|
37
|
+
|
38
|
+
const postgresDbConfig = <DatabaseOption>{
|
39
|
+
engine: 'sql',
|
40
|
+
url: 'postgresql://postgres:postgres@localhost:5432/postgres',
|
41
|
+
type: 'postgres',
|
42
|
+
ttl: 1,
|
43
|
+
cleanupLimit: 1,
|
44
|
+
};
|
45
|
+
|
46
|
+
const mongoDbConfig = <DatabaseOption>{
|
47
|
+
engine: 'mongo',
|
48
|
+
url: 'mongodb://localhost:27017/jackson',
|
49
|
+
};
|
50
|
+
|
51
|
+
const mysqlDbConfig = <DatabaseOption>{
|
52
|
+
engine: 'sql',
|
53
|
+
url: 'mysql://root:mysql@localhost:3307/mysql',
|
54
|
+
type: 'mysql',
|
55
|
+
ttl: 1,
|
56
|
+
cleanupLimit: 1,
|
57
|
+
};
|
58
|
+
|
59
|
+
const mariadbDbConfig = <DatabaseOption>{
|
60
|
+
engine: 'sql',
|
61
|
+
url: 'mariadb://root@localhost:3306/mysql',
|
62
|
+
type: 'mariadb',
|
63
|
+
ttl: 1,
|
64
|
+
cleanupLimit: 1,
|
65
|
+
};
|
66
|
+
|
67
|
+
const dbs = [
|
68
|
+
{
|
69
|
+
...memDbConfig,
|
70
|
+
},
|
71
|
+
{
|
72
|
+
...memDbConfig,
|
73
|
+
encryptionKey,
|
74
|
+
},
|
75
|
+
{
|
76
|
+
...redisDbConfig,
|
77
|
+
},
|
78
|
+
{
|
79
|
+
...redisDbConfig,
|
80
|
+
encryptionKey,
|
81
|
+
},
|
82
|
+
{
|
83
|
+
...postgresDbConfig,
|
84
|
+
},
|
85
|
+
{
|
86
|
+
...postgresDbConfig,
|
87
|
+
encryptionKey,
|
88
|
+
},
|
89
|
+
{
|
90
|
+
...mongoDbConfig,
|
91
|
+
},
|
92
|
+
{
|
93
|
+
...mongoDbConfig,
|
94
|
+
encryptionKey,
|
95
|
+
},
|
96
|
+
{
|
97
|
+
...mysqlDbConfig,
|
98
|
+
},
|
99
|
+
{
|
100
|
+
...mysqlDbConfig,
|
101
|
+
encryptionKey,
|
102
|
+
},
|
103
|
+
{
|
104
|
+
...mariadbDbConfig,
|
105
|
+
},
|
106
|
+
{
|
107
|
+
...mariadbDbConfig,
|
108
|
+
encryptionKey,
|
109
|
+
},
|
110
|
+
];
|
111
|
+
|
112
|
+
tap.before(async () => {
|
113
|
+
for (const idx in dbs) {
|
114
|
+
const opts = dbs[idx];
|
115
|
+
const db = await DB.new(opts);
|
116
|
+
|
117
|
+
configStores.push(db.store('saml:config'));
|
118
|
+
ttlStores.push(db.store('oauth:session', ttl));
|
119
|
+
}
|
120
|
+
});
|
121
|
+
|
122
|
+
tap.teardown(async () => {
|
123
|
+
process.exit(0);
|
124
|
+
});
|
125
|
+
|
126
|
+
tap.test('dbs', ({ end }) => {
|
127
|
+
for (const idx in configStores) {
|
128
|
+
const configStore = configStores[idx];
|
129
|
+
const ttlStore = ttlStores[idx];
|
130
|
+
let dbEngine = dbs[idx].engine;
|
131
|
+
|
132
|
+
if (dbs[idx].type) {
|
133
|
+
dbEngine += ': ' + dbs[idx].type;
|
134
|
+
}
|
135
|
+
|
136
|
+
tap.test('put(): ' + dbEngine, async (t) => {
|
137
|
+
await configStore.put(
|
138
|
+
record1.id,
|
139
|
+
record1,
|
140
|
+
{
|
141
|
+
// secondary index on city
|
142
|
+
name: 'city',
|
143
|
+
value: record1.city,
|
144
|
+
},
|
145
|
+
{
|
146
|
+
// secondary index on name
|
147
|
+
name: 'name',
|
148
|
+
value: record1.name,
|
149
|
+
}
|
150
|
+
);
|
151
|
+
|
152
|
+
await configStore.put(
|
153
|
+
record2.id,
|
154
|
+
record2,
|
155
|
+
{
|
156
|
+
// secondary index on city
|
157
|
+
name: 'city',
|
158
|
+
value: record2.city,
|
159
|
+
},
|
160
|
+
{
|
161
|
+
// secondary index on name
|
162
|
+
name: 'name',
|
163
|
+
value: record2.name,
|
164
|
+
}
|
165
|
+
);
|
166
|
+
|
167
|
+
t.end();
|
168
|
+
});
|
169
|
+
|
170
|
+
tap.test('get(): ' + dbEngine, async (t) => {
|
171
|
+
const ret1 = await configStore.get(record1.id);
|
172
|
+
const ret2 = await configStore.get(record2.id);
|
173
|
+
|
174
|
+
t.same(ret1, record1, 'unable to get record1');
|
175
|
+
t.same(ret2, record2, 'unable to get record2');
|
176
|
+
|
177
|
+
t.end();
|
178
|
+
});
|
179
|
+
|
180
|
+
tap.test('getByIndex(): ' + dbEngine, async (t) => {
|
181
|
+
const ret1 = await configStore.getByIndex({
|
182
|
+
name: 'name',
|
183
|
+
value: record1.name,
|
184
|
+
});
|
185
|
+
|
186
|
+
const ret2 = await configStore.getByIndex({
|
187
|
+
name: 'city',
|
188
|
+
value: record1.city,
|
189
|
+
});
|
190
|
+
|
191
|
+
t.same(ret1, [record1], 'unable to get index "name"');
|
192
|
+
t.same(
|
193
|
+
ret2.sort((a, b) => a.id.localeCompare(b.id)),
|
194
|
+
[record1, record2].sort((a, b) => a.id.localeCompare(b.id)),
|
195
|
+
'unable to get index "city"'
|
196
|
+
);
|
197
|
+
|
198
|
+
t.end();
|
199
|
+
});
|
200
|
+
|
201
|
+
tap.test('delete(): ' + dbEngine, async (t) => {
|
202
|
+
await configStore.delete(record1.id);
|
203
|
+
|
204
|
+
const ret0 = await configStore.getByIndex({
|
205
|
+
name: 'city',
|
206
|
+
value: record1.city,
|
207
|
+
});
|
208
|
+
|
209
|
+
t.same(ret0, [record2], 'unable to get index "city" after delete');
|
210
|
+
|
211
|
+
await configStore.delete(record2.id);
|
212
|
+
|
213
|
+
const ret1 = await configStore.get(record1.id);
|
214
|
+
const ret2 = await configStore.get(record2.id);
|
215
|
+
|
216
|
+
const ret3 = await configStore.getByIndex({
|
217
|
+
name: 'name',
|
218
|
+
value: record1.name,
|
219
|
+
});
|
220
|
+
const ret4 = await configStore.getByIndex({
|
221
|
+
name: 'city',
|
222
|
+
value: record1.city,
|
223
|
+
});
|
224
|
+
|
225
|
+
t.same(ret1, null, 'delete for record1 failed');
|
226
|
+
t.same(ret2, null, 'delete for record2 failed');
|
227
|
+
|
228
|
+
t.same(ret3, [], 'delete for record1 failed');
|
229
|
+
t.same(ret4, [], 'delete for record2 failed');
|
230
|
+
|
231
|
+
t.end();
|
232
|
+
});
|
233
|
+
|
234
|
+
tap.test('ttl indexes: ' + dbEngine, async (t) => {
|
235
|
+
try {
|
236
|
+
await ttlStore.put(
|
237
|
+
record1.id,
|
238
|
+
record1,
|
239
|
+
{
|
240
|
+
// secondary index on city
|
241
|
+
name: 'city',
|
242
|
+
value: record1.city,
|
243
|
+
},
|
244
|
+
{
|
245
|
+
// secondary index on name
|
246
|
+
name: 'name',
|
247
|
+
value: record1.name,
|
248
|
+
}
|
249
|
+
);
|
250
|
+
|
251
|
+
t.fail('expecting a secondary indexes not allow on a store with ttl');
|
252
|
+
} catch (err) {
|
253
|
+
t.ok(err, 'got expected error');
|
254
|
+
}
|
255
|
+
|
256
|
+
t.end();
|
257
|
+
});
|
258
|
+
|
259
|
+
tap.test('ttl put(): ' + dbEngine, async (t) => {
|
260
|
+
await ttlStore.put(record1.id, record1);
|
261
|
+
|
262
|
+
await ttlStore.put(record2.id, record2);
|
263
|
+
|
264
|
+
t.end();
|
265
|
+
});
|
266
|
+
|
267
|
+
tap.test('ttl get(): ' + dbEngine, async (t) => {
|
268
|
+
const ret1 = await ttlStore.get(record1.id);
|
269
|
+
const ret2 = await ttlStore.get(record2.id);
|
270
|
+
|
271
|
+
t.same(ret1, record1, 'unable to get record1');
|
272
|
+
t.same(ret2, record2, 'unable to get record2');
|
273
|
+
|
274
|
+
t.end();
|
275
|
+
});
|
276
|
+
|
277
|
+
tap.test('ttl expiry: ' + dbEngine, async (t) => {
|
278
|
+
// mongo runs ttl task every 60 seconds
|
279
|
+
if (dbEngine.startsWith('mongo')) {
|
280
|
+
t.end();
|
281
|
+
return;
|
282
|
+
}
|
283
|
+
|
284
|
+
await new Promise((resolve) =>
|
285
|
+
setTimeout(resolve, (2 * ttl + 0.5) * 1000)
|
286
|
+
);
|
287
|
+
|
288
|
+
const ret1 = await ttlStore.get(record1.id);
|
289
|
+
const ret2 = await ttlStore.get(record2.id);
|
290
|
+
|
291
|
+
t.same(ret1, null, 'ttl for record1 failed');
|
292
|
+
t.same(ret2, null, 'ttl for record2 failed');
|
293
|
+
|
294
|
+
t.end();
|
295
|
+
});
|
296
|
+
}
|
297
|
+
|
298
|
+
tap.test('db.new() error', async (t) => {
|
299
|
+
try {
|
300
|
+
await DB.new(<DatabaseOption>{
|
301
|
+
engine: <DatabaseEngine>'somedb',
|
302
|
+
});
|
303
|
+
|
304
|
+
t.fail('expecting an unsupported db error');
|
305
|
+
} catch (err) {
|
306
|
+
t.ok(err, 'got expected error');
|
307
|
+
}
|
308
|
+
|
309
|
+
t.end();
|
310
|
+
});
|
311
|
+
|
312
|
+
end();
|
313
|
+
});
|
@@ -0,0 +1,353 @@
|
|
1
|
+
import crypto from 'crypto';
|
2
|
+
import { promises as fs } from 'fs';
|
3
|
+
import path from 'path';
|
4
|
+
import { JacksonOption } from 'saml-jackson';
|
5
|
+
import sinon from 'sinon';
|
6
|
+
import tap from 'tap';
|
7
|
+
import { JacksonError } from '../controller/error';
|
8
|
+
import readConfig from '../read-config';
|
9
|
+
import saml from '../saml/saml';
|
10
|
+
|
11
|
+
// TODO: Add type
|
12
|
+
let apiController;
|
13
|
+
let oauthController;
|
14
|
+
|
15
|
+
const code = '1234567890';
|
16
|
+
const token = '24c1550190dd6a5a9bd6fe2a8ff69d593121c7b9';
|
17
|
+
|
18
|
+
const metadataPath = path.join(__dirname, '/data/metadata');
|
19
|
+
|
20
|
+
const options = {
|
21
|
+
externalUrl: 'https://my-cool-app.com',
|
22
|
+
samlAudience: 'https://saml.boxyhq.com',
|
23
|
+
samlPath: '/sso/oauth/saml',
|
24
|
+
db: {
|
25
|
+
engine: 'mem',
|
26
|
+
},
|
27
|
+
} as JacksonOption;
|
28
|
+
|
29
|
+
const samlConfig = {
|
30
|
+
tenant: 'boxyhq.com',
|
31
|
+
product: 'crm',
|
32
|
+
redirectUrl: '["http://localhost:3000/*"]',
|
33
|
+
defaultRedirectUrl: 'http://localhost:3000/login/saml',
|
34
|
+
rawMetadata: null,
|
35
|
+
};
|
36
|
+
|
37
|
+
const addMetadata = async (metadataPath) => {
|
38
|
+
const configs = await readConfig(metadataPath);
|
39
|
+
|
40
|
+
for (const config of configs) {
|
41
|
+
await apiController.config(config);
|
42
|
+
}
|
43
|
+
};
|
44
|
+
|
45
|
+
tap.before(async () => {
|
46
|
+
const controller = await (await import('../index')).default(options);
|
47
|
+
|
48
|
+
apiController = controller.apiController;
|
49
|
+
oauthController = controller.oauthController;
|
50
|
+
|
51
|
+
await addMetadata(metadataPath);
|
52
|
+
});
|
53
|
+
|
54
|
+
tap.teardown(async () => {
|
55
|
+
process.exit(0);
|
56
|
+
});
|
57
|
+
|
58
|
+
tap.test('authorize()', async (t) => {
|
59
|
+
t.test('Should throw an error if `redirect_uri` null', async (t) => {
|
60
|
+
const body = {
|
61
|
+
redirect_uri: null,
|
62
|
+
state: 'state',
|
63
|
+
};
|
64
|
+
|
65
|
+
try {
|
66
|
+
await oauthController.authorize(body);
|
67
|
+
t.fail('Expecting JacksonError.');
|
68
|
+
} catch (err) {
|
69
|
+
const { message, statusCode } = err as JacksonError;
|
70
|
+
t.equal(
|
71
|
+
message,
|
72
|
+
'Please specify a redirect URL.',
|
73
|
+
'got expected error message'
|
74
|
+
);
|
75
|
+
t.equal(statusCode, 400, 'got expected status code');
|
76
|
+
}
|
77
|
+
|
78
|
+
t.end();
|
79
|
+
});
|
80
|
+
|
81
|
+
t.test('Should throw an error if `state` null', async (t) => {
|
82
|
+
const body = {
|
83
|
+
redirect_uri: 'https://example.com/',
|
84
|
+
state: null,
|
85
|
+
};
|
86
|
+
|
87
|
+
try {
|
88
|
+
await oauthController.authorize(body);
|
89
|
+
|
90
|
+
t.fail('Expecting JacksonError.');
|
91
|
+
} catch (err) {
|
92
|
+
const { message, statusCode } = err as JacksonError;
|
93
|
+
t.equal(
|
94
|
+
message,
|
95
|
+
'Please specify a state to safeguard against XSRF attacks.',
|
96
|
+
'got expected error message'
|
97
|
+
);
|
98
|
+
t.equal(statusCode, 400, 'got expected status code');
|
99
|
+
}
|
100
|
+
|
101
|
+
t.end();
|
102
|
+
});
|
103
|
+
|
104
|
+
t.test('Should throw an error if `client_id` is invalid', async (t) => {
|
105
|
+
const body = {
|
106
|
+
redirect_uri: 'https://example.com/',
|
107
|
+
state: 'state-123',
|
108
|
+
client_id: '27fa9a11875ec3a0',
|
109
|
+
};
|
110
|
+
|
111
|
+
try {
|
112
|
+
await oauthController.authorize(body);
|
113
|
+
|
114
|
+
t.fail('Expecting JacksonError.');
|
115
|
+
} catch (err) {
|
116
|
+
const { message, statusCode } = err as JacksonError;
|
117
|
+
t.equal(
|
118
|
+
message,
|
119
|
+
'SAML configuration not found.',
|
120
|
+
'got expected error message'
|
121
|
+
);
|
122
|
+
t.equal(statusCode, 403, 'got expected status code');
|
123
|
+
}
|
124
|
+
|
125
|
+
t.end();
|
126
|
+
});
|
127
|
+
|
128
|
+
t.test(
|
129
|
+
'Should throw an error if `redirect_uri` is not allowed',
|
130
|
+
async (t) => {
|
131
|
+
const body = {
|
132
|
+
redirect_uri: 'https://example.com/',
|
133
|
+
state: 'state-123',
|
134
|
+
client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
|
135
|
+
};
|
136
|
+
|
137
|
+
try {
|
138
|
+
await oauthController.authorize(body);
|
139
|
+
|
140
|
+
t.fail('Expecting JacksonError.');
|
141
|
+
} catch (err) {
|
142
|
+
const { message, statusCode } = err as JacksonError;
|
143
|
+
t.equal(
|
144
|
+
message,
|
145
|
+
'Redirect URL is not allowed.',
|
146
|
+
'got expected error message'
|
147
|
+
);
|
148
|
+
t.equal(statusCode, 403, 'got expected status code');
|
149
|
+
}
|
150
|
+
|
151
|
+
t.end();
|
152
|
+
}
|
153
|
+
);
|
154
|
+
|
155
|
+
t.test('Should return the Idp SSO URL', async (t) => {
|
156
|
+
const body = {
|
157
|
+
redirect_uri: samlConfig.defaultRedirectUrl,
|
158
|
+
state: 'state-123',
|
159
|
+
client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
|
160
|
+
};
|
161
|
+
|
162
|
+
const response = await oauthController.authorize(body);
|
163
|
+
const params = new URLSearchParams(new URL(response.redirect_url).search);
|
164
|
+
|
165
|
+
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
166
|
+
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
167
|
+
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
168
|
+
|
169
|
+
t.end();
|
170
|
+
});
|
171
|
+
|
172
|
+
t.end();
|
173
|
+
});
|
174
|
+
|
175
|
+
tap.test('samlResponse()', async (t) => {
|
176
|
+
const authBody = {
|
177
|
+
redirect_uri: samlConfig.defaultRedirectUrl,
|
178
|
+
state: 'state-123',
|
179
|
+
client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
|
180
|
+
};
|
181
|
+
|
182
|
+
const { redirect_url } = await oauthController.authorize(authBody);
|
183
|
+
|
184
|
+
const relayState = new URLSearchParams(new URL(redirect_url).search).get(
|
185
|
+
'RelayState'
|
186
|
+
);
|
187
|
+
|
188
|
+
const rawResponse = await fs.readFile(
|
189
|
+
path.join(__dirname, '/data/saml_response'),
|
190
|
+
'utf8'
|
191
|
+
);
|
192
|
+
|
193
|
+
t.test('Should throw an error if `RelayState` is missing', async (t) => {
|
194
|
+
const responseBody = {
|
195
|
+
SAMLResponse: rawResponse,
|
196
|
+
};
|
197
|
+
|
198
|
+
try {
|
199
|
+
await oauthController.samlResponse(responseBody);
|
200
|
+
|
201
|
+
t.fail('Expecting JacksonError.');
|
202
|
+
} catch (err) {
|
203
|
+
const { message, statusCode } = err as JacksonError;
|
204
|
+
t.equal(
|
205
|
+
message,
|
206
|
+
'IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.',
|
207
|
+
'got expected error message'
|
208
|
+
);
|
209
|
+
|
210
|
+
t.equal(statusCode, 403, 'got expected status code');
|
211
|
+
}
|
212
|
+
|
213
|
+
t.end();
|
214
|
+
});
|
215
|
+
|
216
|
+
t.test(
|
217
|
+
'Should return a URL with code and state as query params',
|
218
|
+
async (t) => {
|
219
|
+
const responseBody = {
|
220
|
+
SAMLResponse: rawResponse,
|
221
|
+
RelayState: relayState,
|
222
|
+
};
|
223
|
+
|
224
|
+
const stubValidateAsync = sinon
|
225
|
+
.stub(saml, 'validateAsync')
|
226
|
+
.resolves({ audience: '', claims: {}, issuer: '', sessionIndex: '' });
|
227
|
+
//@ts-ignore
|
228
|
+
const stubRandomBytes = sinon.stub(crypto, 'randomBytes').returns(code);
|
229
|
+
|
230
|
+
const response = await oauthController.samlResponse(responseBody);
|
231
|
+
|
232
|
+
const params = new URLSearchParams(new URL(response.redirect_url).search);
|
233
|
+
|
234
|
+
t.ok(stubValidateAsync.calledOnce, 'validateAsync called once');
|
235
|
+
t.ok(stubRandomBytes.calledOnce, 'randomBytes called once');
|
236
|
+
t.ok('redirect_url' in response, 'response contains redirect_url');
|
237
|
+
t.ok(params.has('code'), 'query string includes code');
|
238
|
+
t.ok(params.has('state'), 'query string includes state');
|
239
|
+
t.match(params.get('state'), authBody.state, 'state value is valid');
|
240
|
+
|
241
|
+
stubRandomBytes.restore();
|
242
|
+
stubValidateAsync.restore();
|
243
|
+
|
244
|
+
t.end();
|
245
|
+
}
|
246
|
+
);
|
247
|
+
|
248
|
+
t.end();
|
249
|
+
});
|
250
|
+
|
251
|
+
tap.test('token()', (t) => {
|
252
|
+
t.test(
|
253
|
+
'Should throw an error if `grant_type` is not `authorization_code`',
|
254
|
+
async (t) => {
|
255
|
+
const body = {
|
256
|
+
grant_type: 'authorization_code_1',
|
257
|
+
};
|
258
|
+
|
259
|
+
try {
|
260
|
+
await oauthController.token(body);
|
261
|
+
|
262
|
+
t.fail('Expecting JacksonError.');
|
263
|
+
} catch (err) {
|
264
|
+
const { message, statusCode } = err as JacksonError;
|
265
|
+
t.equal(
|
266
|
+
message,
|
267
|
+
'Unsupported grant_type',
|
268
|
+
'got expected error message'
|
269
|
+
);
|
270
|
+
t.equal(statusCode, 400, 'got expected status code');
|
271
|
+
}
|
272
|
+
|
273
|
+
t.end();
|
274
|
+
}
|
275
|
+
);
|
276
|
+
|
277
|
+
t.test('Should throw an error if `code` is missing', async (t) => {
|
278
|
+
const body = {
|
279
|
+
grant_type: 'authorization_code',
|
280
|
+
};
|
281
|
+
|
282
|
+
try {
|
283
|
+
await oauthController.token(body);
|
284
|
+
|
285
|
+
t.fail('Expecting JacksonError.');
|
286
|
+
} catch (err) {
|
287
|
+
const { message, statusCode } = err as JacksonError;
|
288
|
+
t.equal(message, 'Please specify code', 'got expected error message');
|
289
|
+
t.equal(statusCode, 400, 'got expected status code');
|
290
|
+
}
|
291
|
+
|
292
|
+
t.end();
|
293
|
+
});
|
294
|
+
|
295
|
+
t.test('Should throw an error if `code` is invalid', async (t) => {
|
296
|
+
const body = {
|
297
|
+
grant_type: 'authorization_code',
|
298
|
+
client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
|
299
|
+
client_secret: 'some-secret',
|
300
|
+
redirect_uri: null,
|
301
|
+
code: 'invalid-code',
|
302
|
+
};
|
303
|
+
|
304
|
+
try {
|
305
|
+
await oauthController.token(body);
|
306
|
+
|
307
|
+
t.fail('Expecting JacksonError.');
|
308
|
+
} catch (err) {
|
309
|
+
const { message, statusCode } = err as JacksonError;
|
310
|
+
t.equal(message, 'Invalid code', 'got expected error message');
|
311
|
+
t.equal(statusCode, 403, 'got expected status code');
|
312
|
+
}
|
313
|
+
|
314
|
+
t.end();
|
315
|
+
});
|
316
|
+
|
317
|
+
t.test('Should return the `access_token` for a valid request', async (t) => {
|
318
|
+
const body = {
|
319
|
+
grant_type: 'authorization_code',
|
320
|
+
client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
|
321
|
+
client_secret: 'some-secret',
|
322
|
+
redirect_uri: null,
|
323
|
+
code: code,
|
324
|
+
};
|
325
|
+
|
326
|
+
const stubRandomBytes = sinon
|
327
|
+
.stub(crypto, 'randomBytes')
|
328
|
+
.onFirstCall()
|
329
|
+
//@ts-ignore
|
330
|
+
.returns(token);
|
331
|
+
|
332
|
+
const response = await oauthController.token(body);
|
333
|
+
|
334
|
+
t.ok(stubRandomBytes.calledOnce, 'randomBytes called once');
|
335
|
+
t.ok('access_token' in response, 'includes access_token');
|
336
|
+
t.ok('token_type' in response, 'includes token_type');
|
337
|
+
t.ok('expires_in' in response, 'includes expires_in');
|
338
|
+
t.match(response.access_token, token);
|
339
|
+
t.match(response.token_type, 'bearer');
|
340
|
+
t.match(response.expires_in, 300);
|
341
|
+
|
342
|
+
stubRandomBytes.restore();
|
343
|
+
|
344
|
+
t.end();
|
345
|
+
});
|
346
|
+
|
347
|
+
// TODO
|
348
|
+
t.test('Handle invalid client_id', async (t) => {
|
349
|
+
t.end();
|
350
|
+
});
|
351
|
+
|
352
|
+
t.end();
|
353
|
+
});
|