-
-
Notifications
You must be signed in to change notification settings - Fork 783
Expand file tree
/
Copy pathdefaults.js
More file actions
2909 lines (2709 loc) · 115 KB
/
defaults.js
File metadata and controls
2909 lines (2709 loc) · 115 KB
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* eslint-disable no-shadow */
/* eslint-disable no-unused-vars */
import * as crypto from 'node:crypto';
import * as os from 'node:os';
import * as attention from './attention.js';
import nanoid from './nanoid.js';
import { base as defaultPolicy } from './interaction_policy/index.js';
import htmlSafe from './html_safe.js';
import * as errors from './errors.js';
const warned = new Set();
function shouldChange(name, msg) {
if (!warned.has(name)) {
warned.add(name);
attention.info(`default ${name} function called, you SHOULD change it in order to ${msg}.`);
}
}
function mustChange(name, msg) {
if (!warned.has(name)) {
warned.add(name);
attention.warn(`default ${name} function called, you MUST change it in order to ${msg}.`);
}
}
function clientBasedCORS(ctx, origin, client) {
mustChange('clientBasedCORS', 'control CORS allowed Origins based on the client making a CORS request');
return false;
}
function getCertificate(ctx) {
mustChange('features.mTLS.getCertificate', 'retrieve the PEM-formatted client certificate from the request context');
throw new Error('features.mTLS.getCertificate function not configured');
}
function certificateAuthorized(ctx) {
mustChange('features.mTLS.certificateAuthorized', 'determine if the client certificate is verified and comes from a trusted CA');
throw new Error('features.mTLS.certificateAuthorized function not configured');
}
function certificateSubjectMatches(ctx, property, expected) {
mustChange('features.mTLS.certificateSubjectMatches', 'verify that the tls_client_auth_* registered client property value matches the certificate one');
throw new Error('features.mTLS.certificateSubjectMatches function not configured');
}
function deviceInfo(ctx) {
return {
ip: ctx.ip,
ua: ctx.get('user-agent'),
};
}
function fetch(url, options) {
/* eslint-disable no-param-reassign */
options.signal = AbortSignal.timeout(2500);
options.headers = new Headers(options.headers);
options.headers.set('user-agent', ''); // removes the user-agent header in Node's global fetch()
// eslint-disable-next-line no-undef
return globalThis.fetch(url, options);
/* eslint-enable no-param-reassign */
}
async function userCodeInputSource(ctx, form, out, err) {
// @param ctx - koa request context
// @param form - form source (id="op.deviceInputForm") to be embedded in the page and submitted
// by the End-User.
// @param out - if an error is returned the out object contains details that are fit to be
// rendered, i.e. does not include internal error messages
// @param err - error object with an optional userCode property passed when the form is being
// re-rendered due to code missing/invalid/expired
shouldChange('features.deviceFlow.userCodeInputSource', 'customize the look of the user code input page');
let msg;
if (err && (err.userCode || err.name === 'NoCodeError')) {
msg = '<p class="red">The code you entered is incorrect. Try again</p>';
} else if (err && err.name === 'AbortedError') {
msg = '<p class="red">The Sign-in request was interrupted</p>';
} else if (err) {
msg = '<p class="red">There was an error processing your request</p>';
} else {
msg = '<p>Enter the code displayed on your device</p>';
}
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Sign-in</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1,h1+p{font-weight:100;text-align:center}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}p.red{color:#d50000}input[type=email],input[type=password],input[type=text]{height:44px;font-size:16px;width:100%;margin-bottom:10px;-webkit-appearance:none;background:#fff;border:1px solid #d9d9d9;border-top:1px solid silver;padding:0 8px;box-sizing:border-box;-moz-box-sizing:border-box}[type=submit]{width:100%;display:block;margin-bottom:10px;position:relative;text-align:center;font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe;cursor:pointer}[type=submit]:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}input[type=text]{text-transform:uppercase;text-align: center}input[type=text]::placeholder{text-transform: none}
</style>
</head>
<body>
<div class="container">
<h1>Sign-in</h1>
${msg}
${form}
<button type="submit" form="op.deviceInputForm">Continue</button>
</div>
</body>
</html>`;
}
function requireNonce(ctx) {
return false;
}
async function userCodeConfirmSource(ctx, form, client, deviceInfo, userCode) {
// @param ctx - koa request context
// @param form - form source (id="op.deviceConfirmForm") to be embedded in the page and
// submitted by the End-User.
// @param deviceInfo - device information from the device_authorization_endpoint call
// @param userCode - formatted user code by the configured mask
shouldChange('features.deviceFlow.userCodeConfirmSource', 'customize the look of the user code confirmation page');
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Device Login Confirmation</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);.help,h1,h1+p{text-align:center}h1,h1+p{font-weight:100}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#f7f7f7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}button[autofocus]{width:100%;display:block;margin-bottom:10px;position:relative;font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe;cursor:pointer}button[autofocus]:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}button[name=abort]{background:0 0!important;border:none;padding:0!important;font:inherit;cursor:pointer}a,button[name=abort]{text-decoration:none;color:#666;font-weight:400;display:inline-block;opacity:.6}.help{width:100%;font-size:12px}code{font-size:2em}
</style>
</head>
<body>
<div class="container">
<h1>Confirm Device</h1>
<p>
<strong>${ctx.oidc.client.clientName || ctx.oidc.client.clientId}</strong>
<br/><br/>
The following code should be displayed on your device<br/><br/>
<code>${userCode}</code>
<br/><br/>
<small>If you did not initiate this action, the code does not match or are unaware of such device in your possession please close this window or click abort.</small>
</p>
${form}
<button autofocus type="submit" form="op.deviceConfirmForm">Continue</button>
<div class="help">
<button type="submit" form="op.deviceConfirmForm" value="yes" name="abort">[ Abort ]</button>
</div>
</div>
</body>
</html>`;
}
async function successSource(ctx) {
// @param ctx - koa request context
shouldChange('features.deviceFlow.successSource', 'customize the look of the device code success page');
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Sign-in Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1,h1+p{font-weight:100;text-align:center}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}
</style>
</head>
<body>
<div class="container">
<h1>Sign-in Success</h1>
<p>Your sign-in ${ctx.oidc.client.clientName ? `with ${ctx.oidc.client.clientName}` : ''} was successful, you can now close this page.</p>
</div>
</body>
</html>`;
}
async function introspectionAllowedPolicy(ctx, client, token) {
// @param ctx - koa request context
// @param client - authenticated client making the request
// @param token - token being introspected
shouldChange('features.introspection.allowedPolicy', 'to check whether the caller is authorized to receive the introspection response');
if (
client.clientAuthMethod === 'none'
&& token.clientId !== ctx.oidc.client.clientId
) {
return false;
}
return true;
}
async function revocationAllowedPolicy(ctx, client, token) {
// @param ctx - koa request context
// @param client - authenticated client making the request
// @param token - token being revoked
shouldChange('features.revocation.allowedPolicy', 'to check whether the caller is authorized to revoke the token');
if (token.clientId !== client.clientId) {
if (client.clientAuthMethod === 'none') {
// do not revoke but respond as success to disallow guessing valid tokens
return false;
}
throw new errors.InvalidRequest('client is not authorized to revoke the presented token');
}
return true;
}
function idFactory(ctx) {
return nanoid();
}
async function secretFactory(ctx) {
return crypto.randomBytes(64).toString('base64url');
}
async function defaultResource(ctx, client, oneOf) {
// @param ctx - koa request context
// @param client - client making the request
// @param oneOf {string[]} - The authorization server needs to select **one** of the values provided.
// Default is that the array is provided so that the request will fail.
// This argument is only provided when called during
// Authorization Code / Refresh Token / Device Code exchanges.
if (oneOf) return oneOf;
return undefined;
}
async function useGrantedResource(ctx, model) {
// @param ctx - koa request context
// @param model - depending on the request's grant_type this can be either an AuthorizationCode, BackchannelAuthenticationRequest,
// RefreshToken, or DeviceCode model instance.
return false;
}
async function getResourceServerInfo(ctx, resourceIndicator, client) {
// @param ctx - koa request context
// @param resourceIndicator - resource indicator value either requested or resolved by the defaultResource helper.
// @param client - client making the request
mustChange('features.resourceIndicators.getResourceServerInfo', 'to provide details about the Resource Server identified by the Resource Indicator');
throw new errors.InvalidTarget();
}
async function extraTokenClaims(ctx, token) {
return undefined;
}
async function expiresWithSession(ctx, code) {
return !code.scopes.has('offline_access');
}
async function issueRefreshToken(ctx, client, code) {
return (
client.grantTypeAllowed('refresh_token')
&& code.scopes.has('offline_access')
);
}
function pkceRequired(ctx, client) {
// All public clients MUST use PKCE as per
// https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.1-2.1
if (client.clientAuthMethod === 'none') {
return true;
}
const fapiProfile = ctx.oidc.isFapi('2.0', '1.0 Final');
// FAPI 2.0 as per
// https://openid.net/specs/fapi-security-profile-2_0-final.html#section-5.3.2.2-2.5
if (fapiProfile === '2.0') {
return true;
}
// FAPI 1.0 Advanced as per
// https://openid.net/specs/openid-financial-api-part-2-1_0-final.html#authorization-server
if (fapiProfile === '1.0 Final' && ctx.oidc.route === 'pushed_authorization_request') {
return true;
}
// In all other cases use of PKCE is RECOMMENDED as per
// https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.1-2.2
// but the server doesn't force them to.
return false;
}
async function pairwiseIdentifier(ctx, accountId, client) {
mustChange('pairwiseIdentifier', 'provide an implementation for pairwise identifiers, the default one uses `os.hostname()` as salt and is therefore not fit for anything else than development');
return crypto
.createHash('sha256')
.update(client.sectorIdentifier)
.update(accountId)
.update(os.hostname()) // put your own unique salt here, or implement other mechanism
.digest('hex');
}
function AccessTokenTTL(ctx, token, client) {
shouldChange('ttl.AccessToken', 'define the expiration for AccessToken artifacts');
return token.resourceServer?.accessTokenTTL || 60 * 60; // 1 hour in seconds
}
function AuthorizationCodeTTL(ctx, code, client) {
return 60; // 1 minute in seconds
}
function ClientCredentialsTTL(ctx, token, client) {
shouldChange('ttl.ClientCredentials', 'define the expiration for ClientCredentials artifacts');
return token.resourceServer?.accessTokenTTL || 10 * 60; // 10 minutes in seconds
}
function DeviceCodeTTL(ctx, deviceCode, client) {
shouldChange('ttl.DeviceCode', 'define the expiration for DeviceCode artifacts');
return 10 * 60; // 10 minutes in seconds
}
function BackchannelAuthenticationRequestTTL(ctx, request, client) {
shouldChange('ttl.BackchannelAuthenticationRequest', 'define the expiration for BackchannelAuthenticationRequest artifacts');
if (ctx?.oidc?.params.requested_expiry) {
return Math.min(10 * 60, +ctx.oidc.params.requested_expiry); // 10 minutes in seconds or requested_expiry, whichever is shorter
}
return 10 * 60; // 10 minutes in seconds
}
function IdTokenTTL(ctx, token, client) {
shouldChange('ttl.IdToken', 'define the expiration for IdToken artifacts');
return 60 * 60; // 1 hour in seconds
}
function RefreshTokenTTL(ctx, token, client) {
shouldChange('ttl.RefreshToken', 'define the expiration for RefreshToken artifacts');
if (
ctx?.oidc?.entities.RotatedRefreshToken
&& client.applicationType === 'web'
&& client.clientAuthMethod === 'none'
&& !token.isSenderConstrained()
) {
// Non-Sender Constrained SPA RefreshTokens do not have infinite expiration through rotation
return ctx.oidc.entities.RotatedRefreshToken.remainingTTL;
}
return 14 * 24 * 60 * 60; // 14 days in seconds
}
function InteractionTTL(ctx, interaction) {
shouldChange('ttl.Interaction', 'define the expiration for Interaction artifacts');
return 60 * 60; // 1 hour in seconds
}
function SessionTTL(ctx, session) {
shouldChange('ttl.Session', 'define the expiration for Session artifacts');
return 14 * 24 * 60 * 60; // 14 days in seconds
}
function GrantTTL(ctx, grant, client) {
shouldChange('ttl.Grant', 'define the expiration for Grant artifacts');
return 14 * 24 * 60 * 60; // 14 days in seconds
}
function extraClientMetadataValidator(ctx, key, value, metadata) {
// @param ctx - koa request context (only provided when a client is being constructed during
// Client Registration Request or Client Update Request
// @param key - the client metadata property name
// @param value - the property value
// @param metadata - the current accumulated client metadata
// @param ctx - koa request context (only provided when a client is being constructed during
// Client Registration Request or Client Update Request
}
async function postLogoutSuccessSource(ctx) {
// @param ctx - koa request context
shouldChange('features.rpInitiatedLogout.postLogoutSuccessSource', 'customize the look of the default post logout success page');
const display = ctx.oidc.client?.clientName || ctx.oidc.client?.clientId;
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Sign-out Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1,h1+p{font-weight:100;text-align:center}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}
</style>
</head>
<body>
<div class="container">
<h1>Sign-out Success</h1>
<p>Your sign-out ${display ? `with ${display}` : ''} was successful.</p>
</div>
</body>
</html>`;
}
async function logoutSource(ctx, form) {
// @param ctx - koa request context
// @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by
// the End-User
shouldChange('features.rpInitiatedLogout.logoutSource', 'customize the look of the logout page');
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Logout Request</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);button,h1{text-align:center}h1{font-weight:100;font-size:1.3em}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}button{font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px;width:100%;display:block;margin-bottom:10px;position:relative;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe;cursor:pointer}button:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}
</style>
</head>
<body>
<div class="container">
<h1>Do you want to sign-out from ${ctx.host}?</h1>
${form}
<button autofocus type="submit" form="op.logoutForm" value="yes" name="logout">Yes, sign me out</button>
<button type="submit" form="op.logoutForm">No, stay signed in</button>
</div>
</body>
</html>`;
}
async function renderError(ctx, out, error) {
shouldChange('renderError', 'customize the look of the error page');
ctx.type = 'html';
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>oops! something went wrong</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1{font-weight:100;text-align:center;font-size:2.3em}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;margin:0 0 0 1em;text-indent:-1em}
</style>
</head>
<body>
<div class="container">
<h1>oops! something went wrong</h1>
${Object.entries(out).map(([key, value]) => `<pre><strong>${key}</strong>: ${htmlSafe(value)}</pre>`).join('')}
</div>
</body>
</html>`;
}
async function interactionsUrl(ctx, interaction) {
return `/interaction/${interaction.uid}`;
}
async function findAccount(ctx, sub, token) {
// @param ctx - koa request context
// @param sub {string} - account identifier (subject)
// @param token - is a reference to the token used for which a given account is being loaded,
// is undefined in scenarios where claims are returned from authorization endpoint
mustChange('findAccount', 'use your own account model');
return {
accountId: sub,
// @param use {string} - can either be "id_token" or "userinfo", depending on
// where the specific claims are intended to be put in
// @param scope {string} - the intended scope, while oidc-provider will mask
// claims depending on the scope automatically you might want to skip
// loading some claims from external resources or through db projection etc. based on this
// detail or not return them in ID Tokens but only UserInfo and so on
// @param claims {object} - the part of the claims authorization parameter for either
// "id_token" or "userinfo" (depends on the "use" param)
// @param rejected {Array[String]} - claim names that were rejected by the end-user, you might
// want to skip loading some claims from external resources or through db projection
async claims(use, scope, claims, rejected) {
return { sub };
},
};
}
function rotateRefreshToken(ctx) {
const { RefreshToken: refreshToken, Client: client } = ctx.oidc.entities;
// cap the maximum amount of time a refresh token can be
// rotated for up to 1 year, afterwards its TTL is final
if (refreshToken.totalLifetime() >= 365.25 * 24 * 60 * 60) {
return false;
}
// rotate non sender-constrained public client refresh tokens
if (
client.clientAuthMethod === 'none'
&& !refreshToken.isSenderConstrained()
) {
return true;
}
// rotate if the token is nearing expiration (it's beyond 70% of its lifetime)
return refreshToken.ttlPercentagePassed() >= 70;
}
async function loadExistingGrant(ctx) {
const grantId = ctx.oidc.result?.consent?.grantId
|| ctx.oidc.session.grantIdFor(ctx.oidc.client.clientId);
if (grantId) {
return ctx.oidc.provider.Grant.find(grantId);
}
return undefined;
}
function revokeGrantPolicy(ctx) {
if (ctx.oidc.route === 'revocation' && ctx.oidc.entities.AccessToken) {
return false;
}
return true;
}
function sectorIdentifierUriValidate(client) {
// @param client - the Client instance
return true;
}
async function processLoginHintToken(ctx, loginHintToken) {
// @param ctx - koa request context
// @param loginHintToken - string value of the login_hint_token parameter
mustChange('features.ciba.processLoginHintToken', 'process the login_hint_token parameter and return the accountId value to use for processsing the request');
throw new Error('features.ciba.processLoginHintToken not implemented');
}
async function processLoginHint(ctx, loginHint) {
// @param ctx - koa request context
// @param loginHint - string value of the login_hint parameter
mustChange('features.ciba.processLoginHint', 'process the login_hint parameter and return the accountId value to use for processsing the request');
throw new Error('features.ciba.processLoginHint not implemented');
}
async function verifyUserCode(ctx, account, userCode) {
// @param ctx - koa request context
// @param account -
// @param userCode - string value of the user_code parameter, when not provided it is undefined
mustChange('features.ciba.verifyUserCode', 'verify the user_code parameter is present when required and verify its value');
throw new Error('features.ciba.verifyUserCode not implemented');
}
async function validateBindingMessage(ctx, bindingMessage) {
// @param ctx - koa request context
// @param bindingMessage - string value of the binding_message parameter, when not provided it is undefined
shouldChange('features.ciba.validateBindingMessage', 'verify the binding_message parameter is present when required and verify its value');
if (bindingMessage?.match(/^[a-zA-Z0-9-._+/!?#]{1,20}$/) === null) {
throw new errors.InvalidBindingMessage(
'the binding_message value, when provided, needs to be 1 - 20 characters in length and use only a basic set of characters (matching the regex: ^[a-zA-Z0-9-._+/!?#]{1,20}$ )',
);
}
}
async function validateRequestContext(ctx, requestContext) {
// @param ctx - koa request context
// @param requestContext - string value of the request_context parameter, when not provided it is undefined
mustChange('features.ciba.validateRequestContext', 'verify the request_context parameter is present when required and verify its value');
throw new Error('features.ciba.validateRequestContext not implemented');
}
async function triggerAuthenticationDevice(ctx, request, account, client) {
// @param ctx - koa request context
// @param request - the BackchannelAuthenticationRequest instance
// @param account - the account object retrieved by findAccount
// @param client - the Client instance
mustChange('features.ciba.triggerAuthenticationDevice', "to trigger the authentication and authorization process on end-user's Authentication Device");
throw new Error('features.ciba.triggerAuthenticationDevice not implemented');
}
async function assertClaimsParameter(ctx, claims, client) {
// @param ctx - koa request context
// @param claims - parsed claims parameter
// @param client - the Client instance
}
async function assertJwtClientAuthClaimsAndHeader(ctx, claims, header, client) {
// @param ctx - koa request context
// @param claims - parsed JWT Client Authentication Assertion Claims Set as object
// @param header - parsed JWT Client Authentication Assertion Headers as object
// @param client - the Client instance
if (ctx.oidc.isFapi('2.0') && claims.aud !== ctx.oidc.issuer) {
throw new errors.InvalidClientAuth(
'audience (aud) must equal the issuer identifier url',
);
}
}
async function assertJwtClaimsAndHeader(ctx, claims, header, client) {
// @param ctx - koa request context
// @param claims - parsed Request Object JWT Claims Set as object
// @param header - parsed Request Object JWT Headers as object
// @param client - the Client instance
const requiredClaims = [];
const fapiProfile = ctx.oidc.isFapi('1.0 Final', '2.0');
if (fapiProfile) {
requiredClaims.push('exp', 'aud', 'nbf');
}
if (ctx.oidc.route === 'backchannel_authentication') {
requiredClaims.push('exp', 'iat', 'nbf', 'jti');
}
for (const claim of new Set(requiredClaims)) {
if (claims[claim] === undefined) {
throw new errors.InvalidRequestObject(
`Request Object is missing the '${claim}' claim`,
);
}
}
if (fapiProfile) {
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject(
"Request Object 'exp' claim too far from 'nbf' claim",
);
}
}
}
function makeDefaults() {
const defaults = {
/*
* acrValues
*
* description: Array of strings, the Authentication Context Class References that the authorization server supports.
*/
acrValues: [],
/*
* adapter
*
* description: The provided example and any new instance of oidc-provider will use the basic
* in-memory adapter for storing issued tokens, codes, user sessions, dynamically registered
* clients, etc. This is fine as long as you develop, configure and generally just play around
* since every time you restart your process all information will be lost. As soon as you cannot
* live with this limitation you will be required to provide your own custom adapter constructor
* for oidc-provider to use. This constructor will be called for every model accessed the first
* time it is needed.
*
* see: [The interface oidc-provider expects](/example/my_adapter.js)
* see: [Example MongoDB adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1308)
* see: [Example Redis adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1309)
* see: [Example Redis w/ JSON Adapter](https://github.com/panva/node-oidc-provider/discussions/1310)
* see: [Default in-memory adapter implementation](/lib/adapters/memory_adapter.js)
* see: [Community Contributed Adapter Archive](https://github.com/panva/node-oidc-provider/discussions/1311)
*
* @nodefault
*/
adapter: undefined,
/*
* claims
*
* description: Describes the claims that the OpenID Provider MAY be able to supply values for.
*
* It is used to achieve two different things related to claims:
* - which additional claims are available to RPs (configure as `{ claimName: null }`)
* - which claims fall under what scope (configure `{ scopeName: ['claim', 'another-claim'] }`)
*
* see: [Configuring OpenID Connect 1.0 Standard Claims](https://github.com/panva/node-oidc-provider/discussions/1299)
*/
claims: {
acr: null,
sid: null,
auth_time: null,
iss: null,
openid: ['sub'],
},
/*
* clientBasedCORS
*
* description: Function used to check whether a given CORS request should be allowed
* based on the request's client.
*
* see: [Configuring Client Metadata-based CORS Origin allow list](https://github.com/panva/node-oidc-provider/discussions/1298)
*/
clientBasedCORS,
/*
* clients
*
* description: Array of objects representing client metadata. These clients are referred to as
* static, they don't expire, never reload, are always available. In addition to these
* clients the authorization server will use your adapter's `find` method when a non-static client_id is
* encountered. If you only wish to support statically configured clients and
* no dynamic registration then make it so that your adapter resolves client find calls with a
* falsy value (e.g. `return Promise.resolve()`) and don't take unnecessary DB trips.
*
* Client's metadata is validated as defined by the respective specification they've been defined
* in.
*
* example: Available Metadata
*
* application_type, client_id, client_name, client_secret, client_uri, contacts,
* default_acr_values, default_max_age, grant_types, id_token_signed_response_alg,
* initiate_login_uri, jwks, jwks_uri, logo_uri, policy_uri, post_logout_redirect_uris,
* redirect_uris, require_auth_time, response_types, response_modes, scope, sector_identifier_uri,
* subject_type, token_endpoint_auth_method, tos_uri, userinfo_signed_response_alg
*
* <br/><br/>The following metadata is available but may not be recognized depending on your
* provider's configuration.<br/><br/>
*
* authorization_encrypted_response_alg, authorization_encrypted_response_enc,
* authorization_signed_response_alg, backchannel_logout_session_required, backchannel_logout_uri,
* id_token_encrypted_response_alg,
* id_token_encrypted_response_enc, introspection_encrypted_response_alg,
* introspection_encrypted_response_enc, introspection_signed_response_alg,
* request_object_encryption_alg, request_object_encryption_enc, request_object_signing_alg,
* tls_client_auth_san_dns, tls_client_auth_san_email, tls_client_auth_san_ip,
* tls_client_auth_san_uri, tls_client_auth_subject_dn,
* tls_client_certificate_bound_access_tokens,
* use_mtls_endpoint_aliases, token_endpoint_auth_signing_alg,
* userinfo_encrypted_response_alg, userinfo_encrypted_response_enc
*
*/
clients: [],
/*
* clientDefaults
*
* description: Default client metadata to be assigned when unspecified by the client metadata,
* e.g. during Dynamic Client Registration or for statically configured clients. The default value
* does not represent all default values, but merely copies its subset. You can provide any used
* client metadata property in this object.
*
* example: Changing the default client token_endpoint_auth_method
*
* To change the default client token_endpoint_auth_method configure `clientDefaults` to be an
* object like so:
*
* ```js
* {
* token_endpoint_auth_method: 'client_secret_post'
* }
* ```
* example: Changing the default client response type to `code id_token`
*
* To change the default client response_types configure `clientDefaults` to be an
* object like so:
*
* ```js
* {
* response_types: ['code id_token'],
* grant_types: ['authorization_code', 'implicit'],
* }
* ```
*
*/
clientDefaults: {
grant_types: ['authorization_code'],
id_token_signed_response_alg: 'RS256',
response_types: ['code'],
token_endpoint_auth_method: 'client_secret_basic',
},
/*
* clockTolerance
*
* description: A `Number` value (in seconds) describing the allowed system clock skew for
* validating client-provided JWTs, e.g. Request Objects, DPoP Proofs and otherwise comparing
* timestamps
* recommendation: Only set this to a reasonable value when needed to cover server-side client and
* oidc-provider server clock skew.
*/
clockTolerance: 15,
/*
* conformIdTokenClaims
*
* title: ID Token only contains End-User claims when the requested `response_type` is `id_token`
*
* description: [`OIDC Core 1.0` - Requesting Claims using Scope Values](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ScopeClaims)
* defines that claims requested using the `scope` parameter are only returned from the UserInfo
* Endpoint unless the `response_type` is `id_token`.
*
* Despite of this configuration the ID Token always includes claims requested using the `scope`
* parameter when the userinfo endpoint is disabled, or when issuing an Access Token not applicable
* for access to the userinfo endpoint.
*
*/
conformIdTokenClaims: true,
/*
* loadExistingGrant
*
* description: Helper function used to load existing but also just in time pre-established Grants
* to attempt to resolve an Authorization Request with. Default: loads a grant based on the
* interaction result `consent.grantId` first, falls back to the existing grantId for the client
* in the current session.
*/
loadExistingGrant,
/*
* allowOmittingSingleRegisteredRedirectUri
*
* title: Allow omitting the redirect_uri parameter when only a single one is registered for a client.
*/
allowOmittingSingleRegisteredRedirectUri: true,
/*
* acceptQueryParamAccessTokens
*
* description: Several OAuth 2.0 / OIDC profiles prohibit the use of query strings to carry
* access tokens. This setting either allows (true) or prohibits (false) that mechanism to be
* used.
*
*/
acceptQueryParamAccessTokens: false,
/*
* cookies
*
* description: Options for the [cookies module](https://github.com/pillarjs/cookies#cookiesset-name--value---options--)
* used to keep track of various User-Agent states. The options `maxAge` and `expires` are ignored. Use `ttl.Session`
* and `ttl.Interaction` to configure the ttl and in turn the cookie expiration values for Session and Interaction
* models.
* @nodefault
*/
cookies: {
/*
* cookies.names
*
* description: Cookie names used to store and transfer various states.
*/
names: {
session: '_session', // used for main session reference
interaction: '_interaction', // used by the interactions for interaction session reference
resume: '_interaction_resume', // used when interactions resume authorization for interaction session reference
},
/*
* cookies.long
*
* description: Options for long-term cookies
*/
long: {
httpOnly: true, // cookies are not readable by client-side javascript
sameSite: 'lax',
},
/*
* cookies.short
*
* description: Options for short-term cookies
*/
short: {
httpOnly: true, // cookies are not readable by client-side javascript
sameSite: 'lax',
},
/*
* cookies.keys
*
* description: [Keygrip](https://www.npmjs.com/package/keygrip) Signing keys used for cookie
* signing to prevent tampering. You may also pass your own KeyGrip instance.
*
* recommendation: Rotate regularly (by prepending new keys) with a reasonable interval and keep
* a reasonable history of keys to allow for returning user session cookies to still be valid
* and re-signed
*
* @skip
*/
keys: [],
},
/*
* discovery
*
* description: Pass additional properties to this object to extend the discovery document
*/
discovery: {
claim_types_supported: ['normal'],
claims_locales_supported: undefined,
display_values_supported: undefined,
op_policy_uri: undefined,
op_tos_uri: undefined,
service_documentation: undefined,
ui_locales_supported: undefined,
},
/*
* extraParams
*
* description: Pass an iterable object (i.e. array or Set of strings) to extend the parameters
* recognised by the authorization, device authorization, backchannel authentication, and
* pushed authorization request endpoints. These parameters are then available in `ctx.oidc.params`
* as well as passed to interaction session details.
*
*
* This may also be a plain object with string properties representing parameter names and values being
* either a function or async function to validate said parameter value. These validators are executed
* regardless of the parameters' presence or value such that this can be used to validate presence of
* custom parameters as well as to assign default values for them. If the value is `null` or
* `undefined` the parameter is added without a validator. Note that these validators execute near the very end
* of the request's validation process and changes to (such as assigning default values) other parameters
* will not trigger any re-validation of the whole request again.
*
* example: registering an extra `origin` parameter with its validator
*
* ```js
* import { errors } from 'oidc-provider';
*
* const extraParams = {
* async origin(ctx, value, client) {
* // @param ctx - koa request context
* // @param value - the `origin` parameter value (string or undefined)
* // @param client - client making the request
*
* if (hasDefaultOrigin(client)) {
* // assign default
* ctx.oidc.params.origin ||= value ||= getDefaultOrigin(client);
* }
*
* if (!value && requiresOrigin(ctx, client)) {
* // reject when missing but required
* throw new errors.InvalidRequest('"origin" is required for this request')
* }
*
* if (!allowedOrigin(value, client)) {
* // reject when not allowed
* throw new errors.InvalidRequest('requested "origin" is not allowed for this client')
* }
* }
* }
* ```
*/
extraParams: [],
/*
* features
*
* description: Enable/disable features.
*
* Some features may be experimental.
* Enabling those will produce a warning and you must
* be aware that breaking changes may occur and that those changes
* will be published as minor versions of oidc-provider. See the example below on how to
* acknowledge an experimental feature version (this will remove the warning) and ensure
* the Provider configuration will throw an error if a new version of oidc-provider includes
* breaking changes to this experimental feature.
*
* example: Acknowledging an experimental feature
*
* ```js
* import * as oidc from 'oidc-provider'
*
* new oidc.Provider('http://localhost:3000', {
* features: {
* webMessageResponseMode: {
* enabled: true,
* },
* },
* });
*
* // The above code produces this NOTICE
* // NOTICE: The following experimental features are enabled and their implemented version not acknowledged
* // NOTICE: - OAuth 2.0 Web Message Response Mode - draft 01 (Acknowledging this feature's implemented version can be done with the value 'individual-draft-01')
* // NOTICE: Breaking changes between experimental feature updates may occur and these will be published as MINOR semver oidc-provider updates.
* // NOTICE: You may disable this notice and be warned when breaking updates occur by acknowledging the current experiment's version. See the documentation for more details.
*
* new oidc.Provider('http://localhost:3000', {
* features: {
* webMessageResponseMode: {
* enabled: true,
* ack: 'individual-draft-01',
* },
* },
* });
* // No more NOTICE, at this point if the experimental was updated and contained no breaking
* // changes, you're good to go, still no NOTICE, your code is safe to run.
*
* // Now lets assume you upgrade oidc-provider version and it includes a breaking change in
* // this experimental feature
* new oidc.Provider('http://localhost:3000', {
* features: {
* webMessageResponseMode: {
* enabled: true,
* ack: 'individual-draft-01',
* },
* },
* });
* // Thrown:
* // Error: An unacknowledged version of an experimental feature is included in this oidc-provider version.
* ```
* @nodefault
*/
features: {
/*
* features.devInteractions
*
* description: Development-ONLY out of the box interaction views bundled with the library allow
* you to skip the boring frontend part while experimenting with oidc-provider. Enter any
* username (will be used as sub claim value) and any password to proceed.
*
* Be sure to disable and replace this feature with your actual frontend flows and End-User
* authentication flows as soon as possible. These views are not meant to ever be seen by actual
* users.
*/
devInteractions: { enabled: true },
/*
* features.dPoP
*
* title: [`RFC9449`](https://www.rfc-editor.org/rfc/rfc9449.html) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (`DPoP`)
*