-
-
Notifications
You must be signed in to change notification settings - Fork 136
/
Copy pathmormot.crypt.jwt.pas
1791 lines (1628 loc) · 65.8 KB
/
mormot.crypt.jwt.pas
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
/// Framework Core JSON Web Tokens (JWT) Support
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.crypt.jwt;
{
*****************************************************************************
JSON Web Tokens (JWT) Implementation - see RFC 7797
- Abstract JWT Parsing and Computation
- JWT Implementation of HS* and S3* Symmetric Algorithms
- JWT Implementation of ES256 Asymmetric Algorithm
- JWT Implementation of RS256/RS384/RS512 Asymmetric Algorithms
- TJwtCrypt Implementation via ICryptPublicKey/ICryptPrivateKey
Uses optimized mormot.crypt.core.pas and mormot.crypt.ecc for its process.
Include mormot.crypt.openssl.pas to support all other JWT algorithms.
The TJwtCrypt class is the way to go to support JWT in your projects.
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
uses
classes,
sysutils,
mormot.core.base,
mormot.core.os,
mormot.core.rtti,
mormot.core.unicode,
mormot.core.text,
mormot.core.buffers,
mormot.core.data,
mormot.core.datetime,
mormot.core.variants,
mormot.core.json,
mormot.crypt.core,
mormot.crypt.secure,
mormot.crypt.ecc256r1,
mormot.crypt.ecc,
mormot.crypt.rsa;
{ **************** Abstract JWT Parsing and Computation }
type
/// JWT Registered Claims, as defined in RFC 7519
// - known registered claims have a specific name and behavior, and will be
// handled automatically by TJwtAbstract
// - corresponding field names are iss,sub,aud,exp,nbf,iat,jti - as defined
// in JWT_CLAIMS_TEXT constant
// - jrcIssuer identifies the server which originated the token, e.g.
// "iss":"https://example.auth0.com/" when the token comes from Auth0 servers
// - jrcSubject is the application-specific extent which is protected by this
// JWT, e.g. an User or Resource ID, e.g. "sub":"auth0|57fe9f1bad961aa242870e"
// - jrcAudience claims that the token is valid only for one or several
// resource servers (may be a JSON string or a JSON array of strings), e.g.
// "aud":["https://myshineyfileserver.sometld"] - TJwtAbstract will check
// that the supplied "aud" field does match an expected list of identifiers
// - jrcExpirationTime contains the Unix timestamp in seconds after which
// the token must not be granted access, e.g. "exp":1477474667
// - jrcNotBefore contains the Unix timestamp in seconds before which the
// token must not be granted access, e.g. "nbf":147745438
// - jrcIssuedAt contains the Unix timestamp in seconds when the token was
// generated, e.g. "iat":1477438667
// - jrcJwtID provides a unique identifier for the JWT, to prevent any replay;
// TJwtAbstract.Compute will set an obfuscated TSynUniqueIdentifierGenerator
// hexadecimal value stored as "jti" payload field
// - jrcData does not reflect any RFC 7519 registered claim, it is set when
// TJwtContent.data TDocVariant has been filled with some non-standard fields
TJwtClaim = (
jrcIssuer,
jrcSubject,
jrcAudience,
jrcExpirationTime,
jrcNotBefore,
jrcIssuedAt,
jrcJwtID,
jrcData);
/// set of JWT Registered Claims, as in TJwtAbstract.Claims
TJwtClaims = set of TJwtClaim;
/// Exception raised when running JSON Web Tokens
EJwtException = class(ESynException);
/// TJwtContent.result codes after TJwtAbstract.Verify method call
TJwtResult = (
jwtValid,
jwtNoToken,
jwtWrongFormat,
jwtInvalidAlgorithm,
jwtInvalidPayload,
jwtUnexpectedClaim,
jwtMissingClaim,
jwtUnknownAudience,
jwtExpired,
jwtNotBeforeFailed,
jwtInvalidIssuedAt,
jwtInvalidID,
jwtInvalidSignature);
//// set of TJwtContent.result codes
TJwtResults = set of TJwtResult;
/// JWT decoded content, as processed by TJwtAbstract
// - optionally cached in memory
TJwtContent = record
/// store latest Verify() result
result: TJwtResult;
/// set of known/registered claims, as stored in the JWT payload
claims: TJwtClaims;
/// match TJwtAbstract.Audience[] indexes for reg[jrcAudience]
// - is not decoded if joNoAudienceCheck option was defined
audience: set of 0..15;
/// known/registered claims UTF-8 values, as stored in the JWT payload
// - e.g. reg[jrcSubject]='1234567890' and reg[jrcIssuer]='' for
// $ {"sub": "1234567890","name": "John Doe","admin": true}
// - jrcData claim is stored in the data TDocVariant field
reg: array[low(TJwtClaim)..jrcJwtID] of RawUtf8;
/// custom/unregistered claim values, as stored in the JWT payload
// - registered claims will be available from reg[], not in this field
// - e.g. data.U['name']='John Doe' and data.B['admin']=true for
// $ {"sub": "1234567890","name": "John Doe","admin": true}
// but data.U['sub'] if not defined, and reg[jrcSubject]='1234567890'
data: TDocVariantData;
/// match the jrcJwtID "jti" claim 64-bit desobfuscated value
// - is not decoded if joNoJwtIDCheck option was defined
id: TSynUniqueIdentifierBits;
end;
/// pointer to a JWT decoded content, as processed by TJwtAbstract
PJwtContent = ^TJwtContent;
/// used to store a list of JWT decoded content
// - as used e.g. by TJwtAbstract cache
TJwtContentDynArray = array of TJwtContent;
/// available options for TJwtAbstract process
// - joHeaderParse won't expect a fixed '{"alg":"%","typ":"JWT"}' header, but
// parse for any valid variant (slower)
// - joAllowUnexpectedClaims won't reject JWT with unknown TJwtAbstract.Claims
// - joAllowUnexpectedAudience won't reject JWT with unknown "aud" item(s)
// - joNoJwtIDGenerate won't compute a new "jti" item, but expect it to be
// supplied as a DataNameValue pair to TJwtAbstract.Compute()
// - joNoJwtIDCheck won't decode/deobfuscate the "jti" item
// - joNoAudienceCheck won't decode and check the "aud" - so is faster
// - joDoubleInData will allow double floting point values in TJwtContent.data
TJwtOption = (
joHeaderParse,
joAllowUnexpectedClaims,
joAllowUnexpectedAudience,
joNoJwtIDGenerate,
joNoJwtIDCheck,
joNoAudienceCheck,
joDoubleInData);
/// store options for TJwtAbstract process
TJwtOptions = set of TJwtOption;
/// abstract parent class for implementing JSON Web Tokens
// - to represent claims securely between two parties, as defined in industry
// standard @http://tools.ietf.org/html/rfc7519
// - you should never use this abstract class directly, but e.g. TJwtHS256,
// TJwtHS384, TJwtHS512 or TJwtEs256 inherited classes
// - for security reasons, one inherited class is implementing a single
// algorithm, as is very likely to be the case on production: you pickup one
// "alg", then you stick to it; if your server needs more than one algorithm
// for compatibility reasons, use a separate key and URI - this design will
// reduce attack surface, and fully avoid weaknesses as described in
// @https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries
// and @http://tools.ietf.org/html/rfc7518#section-8.5
TJwtAbstract = class(TSynPersistent)
protected
fAlgorithm: RawUtf8;
fHeader: RawUtf8;
fHeaderB64: RawUtf8;
fClaims: TJwtClaims;
fOptions: TJwtOptions;
fAudience: TRawUtf8DynArray;
fExpirationSeconds: integer;
fIDGen: TSynUniqueIdentifierGenerator;
fCacheTimeoutSeconds: integer;
fCacheResults: TJwtResults;
fCache: TSynDictionary; // TRawUtf8DynArray/TJwtContentDynArray
procedure SetCacheTimeoutSeconds(value: integer); virtual;
function PayloadToJson(const DataNameValue: array of const;
const Issuer, Subject, Audience: RawUtf8; NotBefore: TDateTime;
ExpirationMinutes: cardinal): RawUtf8; virtual;
procedure Parse(const Token: RawUtf8; var Jwt: TJwtContent;
out headpayload: RawUtf8; out signature: RawByteString;
excluded: TJwtClaims); virtual;
function CheckAgainstActualTimestamp(var Jwt: TJwtContent): boolean;
// abstract methods which should be overriden by inherited classes
function ComputeSignature(const headpayload: RawUtf8): RawUtf8;
virtual; abstract;
procedure CheckSignature(const headpayload: RawUtf8;
const signature: RawByteString; var jwt: TJwtContent); virtual; abstract;
public
/// initialize the JWT processing instance
// - the supplied set of claims are expected to be defined in the JWT payload
// - aAudience are the allowed values for the jrcAudience claim
// - aExpirationMinutes is the deprecation time for the jrcExpirationTime claim
// - aIDIdentifier and aIDObfuscationKey/aIDObfuscationKeyNewKdf are passed
// to a TSynUniqueIdentifierGenerator instance used for jrcJwtID claim
constructor Create(const aAlgorithm: RawUtf8; aClaims: TJwtClaims;
const aAudience: array of RawUtf8; aExpirationMinutes: integer;
aIDIdentifier: TSynUniqueIdentifierProcess; aIDObfuscationKey: RawUtf8;
aIDObfuscationKeyNewKdf: integer = 0); reintroduce;
/// finalize the instance
destructor Destroy; override;
/// compute a new JWT for a given payload
// - here the data payload is supplied as Name,Value pairs - by convention,
// some registered Names (see TJwtClaim) should not be used here, and private
// claims names are expected to be short (typically 3 chars), or an URI
// - depending on the instance Claims, you should also specify associated
// Issuer, Subject, Audience and NotBefore values; expected 'exp', 'nbf',
// 'iat', 'jti' claims will also be generated and included, if needed
// - you can override the aExpirationMinutes value as defined in Create()
// - Audience is usually a single text, serialized as a JSON string, but
// if the value supplied starts with '[', it is expected to be an array
// of text values, already serialized as a JSON array of strings
// - this method is thread-safe
function Compute(const DataNameValue: array of const;
const Issuer: RawUtf8 = ''; const Subject: RawUtf8 = '';
const Audience: RawUtf8 = ''; NotBefore: TDateTime = 0;
ExpirationMinutes: integer = 0; Signature: PRawUtf8 = nil): RawUtf8;
/// compute a HTTP Authorization header containing a JWT for a given payload
// - just a wrapper around Compute(), returned the HTTP header value:
// $ Authorization: <HttpAuthorizationHeader>
// following the expected pattern:
// $ Authorization: Bearer <Token>
// - this method is thread-safe
function ComputeAuthorizationHeader(const DataNameValue: array of const;
const Issuer: RawUtf8 = ''; const Subject: RawUtf8 = '';
const Audience: RawUtf8 = ''; NotBefore: TDateTime = 0;
ExpirationMinutes: integer = 0): RawUtf8;
/// check a JWT value, and its signature
// - will validate all expected Claims (minus ExcludedClaims optional
// parameter, jrcData being for JWT.data), and the associated signature
// - verification state is returned in JWT.result (jwtValid for a valid JWT),
// together with all parsed payload information
// - supplied JWT is transmitted e.g. in HTTP header:
// $ Authorization: Bearer <Token>
// - this method is thread-safe
procedure Verify(const Token: RawUtf8; out Jwt: TJwtContent;
ExcludedClaims: TJwtClaims = []); overload;
/// check a JWT value, and its signature
// - will validate all expected Claims, and the associated signature
// - verification state is returned as function result
// - supplied JWT is transmitted e.g. in HTTP header:
// $ Authorization: Bearer <Token>
// - this method is thread-safe
function Verify(const Token: RawUtf8): TJwtResult; overload;
/// check a HTTP Authorization header value as JWT, and its signature
// - will validate all expected Claims, and the associated signature
// - verification state is returned in Jwt.result (jwtValid for a valid JWT),
// together with all parsed payload information
// - expect supplied HttpAuthorizationHeader as transmitted in HTTP header:
// $ Authorization: <HttpAuthorizationHeader>
// - this method is thread-safe
function VerifyAuthorizationHeader(const HttpAuthorizationHeader: RawUtf8;
out Jwt: TJwtContent): boolean; overload;
/// in-place decoding and quick check of the JWT header and paylod
// - it won't check the signature, only the header and payload
// - the header's algorithm is checked against ExpectedAlgo (if not '')
// - it will parse the JWT payload and check for its expiration, and some
// mandatory fied values - you can optionally retrieve the Expiration time,
// the ending Signature, and/or the Payload decoded as TDocVariant
// - NotBeforeDelta allows to define some time frame for the "nbf" field
// - may be used on client side to quickly validate a JWT received from
// server, without knowing the exact algorithm or secret keys
class function VerifyPayload(const Token,
ExpectedAlgo, ExpectedSubject, ExpectedIssuer, ExpectedAudience: RawUtf8;
Expiration: PUnixTime; Signature, Subject, Issuer, HeadPayload: PRawUtf8;
Payload: PVariant = nil;
IgnoreTime: boolean = false; NotBeforeDelta: TUnixTime = 15): TJwtResult;
/// in-place decoding of the JWT header, returning the algorithm
// - checking there is a payload and a signature, without decoding them
// - could be used to quickly check if a token is likely to be a JWT
class function ExtractAlgo(const Token: RawUtf8): RawUtf8;
/// in-place check of the JWT header algorithm
// - just a wrapper around PropNameEquals(MatchAlgo(Token), Algo);
class function MatchAlgo(const Token, Algo: RawUtf8): boolean;
published
/// the name of the algorithm used by this instance (e.g. 'HS256')
property Algorithm: RawUtf8
read fAlgorithm;
/// allow to tune the Verify and Compute method process
property Options: TJwtOptions
read fOptions write fOptions;
/// the JWT Registered Claims, as implemented by this instance
// - Verify() method will ensure all claims are defined in the payload,
// then fill TJwtContent.reg[] with all corresponding values
property Claims: TJwtClaims
read fClaims;
/// the period, in seconds, for the "exp" claim
property ExpirationSeconds: integer
read fExpirationSeconds;
/// the audience string values associated with this instance
// - will be checked by Verify() method, and set in TJwtContent.audience
property Audience: TRawUtf8DynArray
read fAudience;
/// delay of optional in-memory cache of Verify() TJwtContent
// - equals 0 by default, i.e. cache is disabled
// - may be useful if the signature process is very resource consumming
// (e.g. for TJwtEs256 or even HMAC-SHA-256) - see also CacheResults
// - each time this property is assigned, internal cache content is flushed
property CacheTimeoutSeconds: integer
read fCacheTimeoutSeconds write SetCacheTimeoutSeconds;
/// which TJwtContent.result should be stored in in-memory cache
// - default is [jwtValid] but you may also include jwtInvalidSignature
// if signature checking uses a lot of resources
// - only used if CacheTimeoutSeconds>0
property CacheResults: TJwtResults
read fCacheResults write fCacheResults;
/// access to the low-level generator associated with jrcJwtID "jti" claim
property IDGen: TSynUniqueIdentifierGenerator
read fIDGen;
end;
/// class-reference type (metaclass) of a JWT algorithm process
TJwtAbstractClass = class of TJwtAbstract;
/// abstract parent class for implementing JWT with asymmetric cryptography
TJwtAsym = class(TJwtAbstract)
public
/// returns the algorithm used to compute the JWT digital signature
class function GetAsymAlgo: TCryptAsymAlgo; virtual; abstract;
end;
/// implements JSON Web Tokens using 'none' algorithm
// - as defined in @http://tools.ietf.org/html/rfc7518 paragraph 3.6
// - you should never use this weak algorithm in production, unless your
// communication is already secured by other means, and use JWT as cookies
TJwtNone = class(TJwtAbstract)
protected
function ComputeSignature(const headpayload: RawUtf8): RawUtf8; override;
procedure CheckSignature(const headpayload: RawUtf8;
const signature: RawByteString; var Jwt: TJwtContent); override;
public
/// initialize the JWT processing using the 'none' algorithm
// - the supplied set of claims are expected to be defined in the JWT payload
// - aAudience are the allowed values for the jrcAudience claim
// - aExpirationMinutes is the deprecation time for the jrcExpirationTime claim
// - aIDIdentifier and aIDObfuscationKey/aIDObfuscationKeyNewKdf are passed
// to a TSynUniqueIdentifierGenerator instance used for jrcJwtID claim
constructor Create(aClaims: TJwtClaims; const aAudience: array of RawUtf8;
aExpirationMinutes: integer = 0;
aIDIdentifier: TSynUniqueIdentifierProcess = 0;
aIDObfuscationKey: RawUtf8 = '';
aIDObfuscationKeyNewKdf: integer = 0); reintroduce;
end;
const
/// the text field names of the registerd claims, as defined by RFC 7519
// - see TJwtClaim enumeration and TJwtClaims set
// - RFC standard expects those to be case-sensitive
JWT_CLAIMS_TEXT: array[TJwtClaim] of RawUtf8 = (
'iss', // jrcIssuer
'sub', // jrcSubject
'aud', // jrcAudience
'exp', // jrcExpirationTime
'nbf', // jrcNotBefore
'iat', // jrcIssuedAt
'jti', // jrcJwtID
'data'); // jrcData
function ToText(res: TJwtResult): PShortString; overload;
function ToCaption(res: TJwtResult): string; overload;
function ToText(claim: TJwtClaim): PShortString; overload;
function ToText(claims: TJwtClaims): ShortString; overload;
/// try to recognize a JWT from a supplied text, which may be an URI
// - will ignore any trailing spaces, then extract any ending Base64-URI encoded
// text which matches the JWT 'algo.payload.sign' layout
// - returns '' if no JWT-like pattern was found
// - it won't validate the exact JWT format, nor any signature, only guess if
// there is a chance the supplied text contains a JWT, and extract it
function ParseTrailingJwt(const aText: RawUtf8; noDotCheck: boolean = false): RawUtf8;
{ **************** JWT Implementation of HS and S3 Algorithms }
type
/// abstract parent of JSON Web Tokens using HMAC-SHA2 or SHA-3 algorithms
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// but could be used as a safer (and sometimes faster) alternative to HMAC-SHA2
// - digital signature will be processed by an internal TSynSigner instance
// - never use this abstract class, but any inherited class, or
// JWT_CLASS[].Create to instantiate a JWT process from a given algorithm
TJwtSynSignerAbstract = class(TJwtAbstract)
protected
fSignPrepared: TSynSigner;
function GetAlgo: TSignAlgo; virtual; abstract;
function ComputeSignature(const headpayload: RawUtf8): RawUtf8; override;
procedure CheckSignature(const headpayload: RawUtf8;
const signature: RawByteString; var Jwt: TJwtContent); override;
public
/// initialize the JWT processing using SHA3 algorithm
// - the supplied set of claims are expected to be defined in the JWT payload
// - the supplied secret text will be used to compute the digital signature,
// directly if aSecretPbkdf2Round=0, or via PBKDF2 iterative key derivation
// if some number of rounds were specified
// - aAudience are the allowed values for the jrcAudience claim
// - aExpirationMinutes is the deprecation time for the jrcExpirationTime claim
// - aIDIdentifier and aIDObfuscationKey/aIDObfuscationKeyNewKdf are passed
// to a TSynUniqueIdentifierGenerator instance used for jrcJwtID claim
// - optionally return the PBKDF2 derivated key for aSecretPbkdf2Round>0
constructor Create(const aSecret: RawUtf8; aSecretPbkdf2Round: integer;
aClaims: TJwtClaims; const aAudience: array of RawUtf8;
aExpirationMinutes: integer = 0;
aIDIdentifier: TSynUniqueIdentifierProcess = 0;
aIDObfuscationKey: RawUtf8 = '';
aIDObfuscationKeyNewKdf: integer = 0;
aPBKDF2Secret: PHash512Rec = nil); reintroduce;
/// finalize the instance
destructor Destroy; override;
/// low-level read access to the internal signature structure
property SignPrepared: TSynSigner
read fSignPrepared;
{$ifndef ISDELPHI2009} // avoid Delphi 2009 F2084 Internal Error: DT5830
/// the digital signature size, in byte
property SignatureSize: integer
read fSignPrepared.SignatureSize;
/// the TSynSigner raw algorithm used for digital signature
property SignatureAlgo: TSignAlgo
read fSignPrepared.Algo;
{$endif ISDELPHI2009}
end;
/// meta-class for TJwtSynSignerAbstract creations
TJwtSynSignerAbstractClass = class of TJwtSynSignerAbstract;
type
/// implements JSON Web Tokens using 'HS256' (HMAC SHA-256) algorithm
// - as defined in @http://tools.ietf.org/html/rfc7518 paragraph 3.2
// - our HMAC SHA-256 implementation used is thread safe, and very fast
// (x86: 3us, x64: 2.5us) so cache is not needed
// - resulting signature size will be of 256 bits
TJwtHS256 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// implements JSON Web Tokens using 'HS384' (HMAC SHA-384) algorithm
// - as defined in @http://tools.ietf.org/html/rfc7518 paragraph 3.2
// - our HMAC SHA-384 implementation used is thread safe, and very fast
// even on x86 (if the CPU supports SSE3 opcodes)
// - resulting signature size will be of 384 bits
TJwtHS384 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// implements JSON Web Tokens using 'HS512' (HMAC SHA-512) algorithm
// - as defined in @http://tools.ietf.org/html/rfc7518 paragraph 3.2
// - our HMAC SHA-512 implementation used is thread safe, and very fast
// even on x86 (if the CPU supports SSE3 opcodes)
// - resulting signature size will be of 512 bits
TJwtHS512 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// experimental JSON Web Tokens using 'S3224' (SHA3-224) algorithm
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// but could be used as a safer (and sometimes faster) alternative to HMAC-SHA2
// - resulting signature size will be of 224 bits
TJwtS3224 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// experimental JSON Web Tokens using 'S3256' (SHA3-256) algorithm
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// but could be used as a safer (and sometimes faster) alternative to HMAC-SHA2
// - resulting signature size will be of 256 bits
TJwtS3256 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// experimental JSON Web Tokens using 'S3384' (SHA3-384) algorithm
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// but could be used as a safer (and sometimes faster) alternative to HMAC-SHA2
// - resulting signature size will be of 384 bits
TJwtS3384 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// experimental JSON Web Tokens using 'S3512' (SHA3-512) algorithm
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// but could be used as a safer (and sometimes faster) alternative to HMAC-SHA2
// - resulting signature size will be of 512 bits
TJwtS3512 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// experimental JSON Web Tokens using 'S3S128' (SHA3-SHAKE128) algorithm
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// but could be used as a safer (and sometimes faster) alternative to HMAC-SHA2
// - resulting signature size will be of 256 bits
TJwtS3S128 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
/// experimental JSON Web Tokens using 'S3S256' (SHA3-SHAKE256) algorithm
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// but could be used as a safer (and sometimes faster) alternative to HMAC-SHA2
// - resulting signature size will be of 512 bits
TJwtS3S256 = class(TJwtSynSignerAbstract)
protected
function GetAlgo: TSignAlgo; override;
end;
const
/// how TJwtSynSignerAbstract algorithms are identified in the JWT
// - SHA-1 will fallback to HS256 (since there will never be SHA-1 support)
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
JWT_TEXT: array[TSignAlgo] of RawUtf8 = (
'HS256',
'HS256',
'HS384',
'HS512',
'S3224',
'S3256',
'S3384',
'S3512',
'S3S128',
'S3S256');
/// able to instantiate any of the TJwtSynSignerAbstract instance expected
// - SHA-1 will fallback to TJwtHS256 (since SHA-1 will never be supported)
// - SHA-3 is not yet officially defined in @http://tools.ietf.org/html/rfc7518
// - typical use is the following:
// ! result := JWT_CLASS[algo].Create(master, round, claims, [], expirationMinutes);
JWT_CLASS: array[TSignAlgo] of TJwtSynSignerAbstractClass = (
TJwtHS256,
TJwtHS256,
TJwtHS384,
TJwtHS512,
TJwtS3224,
TJwtS3256,
TJwtS3384,
TJwtS3512,
TJwtS3S128,
TJwtS3S256);
{ ************** JWT Implementation of ES256 Algorithm }
type
/// implements JSON Web Tokens using 'ES256' algorithm
// - i.e. ECDSA using the P-256 curve and the SHA-256 hash algorithm
// - as defined in http://tools.ietf.org/html/rfc7518 paragraph 3.4
// - since ECDSA signature and verification is CPU consumming (especially
// under x86) you may enable CacheTimeoutSeconds
// - will use the OpenSSL library if available - about 5 times faster than
// our pascal/asm code - here are some numbers on x86_64:
// $ TJwtEs256 pascal: 100 ES256 in 33.57ms i.e. 2.9K/s, aver. 335us
// $ TJwtEs256 OpenSSL: 100 ES256 in 6.90ms i.e. 14.1K/s, aver. 69us
// - our direct OpenSSL access is even slightly faster than TJwtEs256Osl:
// $ TJwtEs256Osl: 100 ES256 in 8.64ms i.e. 11.3K/s, aver. 86us
TJwtEs256 = class(TJwtAsym)
protected
fCertificate: TEccCertificate;
fVerify: TEcc256r1VerifyAbstract; // includes pre-computed public key
fOwnCertificate: boolean;
function ComputeSignature(const headpayload: RawUtf8): RawUtf8; override;
procedure CheckSignature(const headpayload: RawUtf8; const signature: RawByteString;
var jwt: TJwtContent); override;
public
/// initialize the JWT processing instance using ECDSA P-256 algorithm
// - the supplied set of claims are expected to be defined in the JWT payload
// - the supplied ECC certificate should be a TEccCertificate storing the
// public key needed for Verify(), or a TEccCertificateSecret storing also
// the private key required by Compute()
// - aCertificate is owned by this instance if property OwnCertificate is true
// - aAudience are the allowed values for the jrcAudience claim
// - aExpirationMinutes is the deprecation time for the jrcExpirationTime claim
// - aIDIdentifier and aIDObfuscationKey/aIDObfuscationKeyNewKdf are passed
// to a TSynUniqueIdentifierGenerator instance used for jrcJwtID claim
constructor Create(aCertificate: TEccCertificate; aClaims: TJwtClaims;
const aAudience: array of RawUtf8; aExpirationMinutes: integer = 0;
aIDIdentifier: TSynUniqueIdentifierProcess = 0;
aIDObfuscationKey: RawUtf8 = '';
aIDObfuscationKeyNewKdf: integer = 0); reintroduce;
/// finalize the instance
destructor Destroy; override;
/// overriden to return caaES256
class function GetAsymAlgo: TCryptAsymAlgo; override;
/// access to the associated TEccCertificate instance
// - which may be a TEccCertificateSecret for Compute() private key
property Certificate: TEccCertificate
read fCertificate;
/// if the associated TEccCertificate is to be owned by this instance
property OwnCertificate: boolean
read fOwnCertificate write fOwnCertificate;
end;
{ ************** JWT Implementation of RS256/RS384/RS512 Algorithms }
type
/// abstract parent for JSON Web Tokens using our mormot.crypt.rsa unit
// - inherited TJwtRs256/TJwtRs384/TJwtRs512 classes implement proper
// RS256/RS384/RS512 algorithms as defined in https://jwt.io
TJwtRsa = class(TJwtAsym)
protected
fRsa: TRsa;
fHash: THashAlgo;
function ComputeSignature(const headpayload: RawUtf8): RawUtf8; override;
procedure CheckSignature(const headpayload: RawUtf8; const signature: RawByteString;
var jwt: TJwtContent); override;
public
/// initialize the JWT processing instance calling SetAlgorithm abstract method
// - should supply one RSA key, eigher private or public, in PEM or raw DER
// binary format, of at least 2048-bit (as required by NIST SP800-131A) but
// it could be 3072-bit for support up to 2030
// - the supplied set of claims are expected to be defined in the JWT payload
// - aAudience are the allowed values for the jrcAudience claim
// - aExpirationMinutes is the deprecation time for the jrcExpirationTime claim
// - aIDIdentifier and aIDObfuscationKey/aIDObfuscationKeyNewKdf are passed
// to a TSynUniqueIdentifierGenerator instance used for jrcJwtID claim
constructor Create(const aKey: RawByteString; aClaims: TJwtClaims;
const aAudience: array of RawUtf8; aExpirationMinutes: integer = 0;
aIDIdentifier: TSynUniqueIdentifierProcess = 0;
aIDObfuscationKey: RawUtf8 = ''; aIDObfuscationKeyNewKdf: integer = 0);
reintroduce;
/// finalize this JWT instance and its stored key
destructor Destroy; override;
/// access to the low-level associated TRsa instance
property Rsa: TRsa
read fRsa;
end;
/// meta-class of TJwtRsa classes
TJwtRsaClass = class of TJwtRsa;
/// implements 'RS256' RSA algorithm over SHA-256
// - you may consider faster TJwtRs256Osl from mormot.crypt.openssl instead
TJwtRs256 = class(TJwtRsa)
public
class function GetAsymAlgo: TCryptAsymAlgo; override;
end;
/// implements 'RS384' RSA algorithm over SHA-384
// - you may consider faster TJwtRs384Osl from mormot.crypt.openssl instead
TJwtRs384 = class(TJwtRsa)
public
class function GetAsymAlgo: TCryptAsymAlgo; override;
end;
/// implements 'RS512' RSA algorithm over SHA-512
// - you may consider faster TJwtRs512Osl from mormot.crypt.openssl instead
TJwtRs512 = class(TJwtRsa)
public
class function GetAsymAlgo: TCryptAsymAlgo; override;
end;
/// implements 'PS256' PSA algorithm over SHA-256
// - you may consider faster TJwtPs256Osl from mormot.crypt.openssl instead
TJwtPs256 = class(TJwtRsa)
public
class function GetAsymAlgo: TCryptAsymAlgo; override;
end;
/// implements 'PS384' PSA algorithm over SHA-384
// - you may consider faster TJwtPs384Osl from mormot.crypt.openssl instead
TJwtPs384 = class(TJwtRsa)
public
class function GetAsymAlgo: TCryptAsymAlgo; override;
end;
/// implements 'PS512' PSA algorithm over SHA-512
// - you may consider faster TJwtPs512Osl from mormot.crypt.openssl instead
TJwtPs512 = class(TJwtRsa)
public
class function GetAsymAlgo: TCryptAsymAlgo; override;
end;
{ *********** JWT Implementation via ICryptPublicKey/ICryptPrivateKey Factories }
type
/// implements JSON Web Tokens using ICryptPublicKey/ICryptPrivateKey wrappers
// - this may be the easiest way to work with JWT in our framework
// - you may try the other dedicated classes, from this unit (TJwtEs256 ..
// TJwtPs512) or from mormot.crypt.openssl (the TJwt*Osl classes) but this
// class seems to be the fastest, cleanest, and with the less overhead
TJwtCrypt = class(TJwtAbstract)
protected
fAsymAlgo: TCryptAsymAlgo;
fKeyAlgo: TCryptKeyAlgo;
fPublicKey: ICryptPublicKey;
fPrivateKey: ICryptPrivateKey;
function ComputeSignature(const headpayload: RawUtf8): RawUtf8; override;
procedure CheckSignature(const headpayload: RawUtf8; const signature: RawByteString;
var jwt: TJwtContent); override;
public
/// check if a given algorithm is supported by this class
// - just a wrapper to check that CryptPublicKey[aAlgo] factory do exist
class function Supports(aAlgo: TCryptAsymAlgo): boolean;
/// initialize this JWT instance from a supplied public key and algorithm
// - aPublicKey is expected to be a public key in PEM or DER format, but
// a private key with no password encryption is also accepted here
// - if no aPublicKey is supplied, it will generate a new key pair and the
// PublicKey/PrivateKey properties could be used for proper persistence
// (warning: generating a key pair could be very slow with RSA/RSAPSS)
// - the supplied set of claims are expected to be defined in the JWT payload
// - aAudience are the allowed values for the jrcAudience claim
// - aExpirationMinutes is the deprecation time for the jrcExpirationTime claim
// - aIDIdentifier and aIDObfuscationKey/aIDObfuscationKeyNewKdf are passed
// to a TSynUniqueIdentifierGenerator instance used for jrcJwtID claim
constructor Create(aAlgo: TCryptAsymAlgo; const aPublicKey: RawByteString;
aClaims: TJwtClaims; const aAudience: array of RawUtf8;
aExpirationMinutes: integer = 0; aIDIdentifier: TSynUniqueIdentifierProcess = 0;
aIDObfuscationKey: RawUtf8 = ''; aIDObfuscationKeyNewKdf: integer = 0);
reintroduce;
/// add a private key to this instance execution context
// - so that the Compute() method could be used
// - will check that the supplied private key do match aPublicKey as
// supplied to the constructor
function LoadPrivateKey(const aPrivateKey: RawByteString;
const aPassword: SpiUtf8 = ''): boolean;
/// the asymmetric algorithm with hashing, as supplied to the constructor
property AsymAlgo: TCryptAsymAlgo
read fAsymAlgo;
/// the asymmetric key algorithm of this instance
property KeyAlgo: TCryptKeyAlgo
read fKeyAlgo;
/// low-level access to the associated public key, as supplied to Create()
property PublicKey: ICryptPublicKey
read fPublicKey;
/// low-level access to an associated private key
// - if you want the Compute method to be able to sign a new JWT, you should
// either call LoadPrivateKey() or initialize and assign to this property
// a ICryptPrivateKey instance
property PrivateKey: ICryptPrivateKey
read fPrivateKey write fPrivateKey;
end;
implementation
{ **************** Abstract JWT Parsing and Computation }
var
_TJwtResult: array[TJwtResult] of PShortString;
_TJwtClaim: array[TJwtClaim] of PShortString;
function ToText(res: TJwtResult): PShortString;
begin
result := _TJwtResult[res];
end;
function ToCaption(res: TJwtResult): string;
begin
GetCaptionFromTrimmed(_TJwtResult[res], result);
end;
function ToText(claim: TJwtClaim): PShortString;
begin
result := _TJwtClaim[claim];
end;
function ToText(claims: TJwtClaims): ShortString;
begin
GetSetNameShort(TypeInfo(TJwtClaims), claims, result);
end;
function ParseTrailingJwt(const aText: RawUtf8; noDotCheck: boolean): RawUtf8;
var
txtlen, beg, dotcount: PtrInt;
tc: PTextCharSet;
begin
result := ''; // no JWT found
txtlen := length(aText);
while (txtlen > 10) and
(aText[txtlen] <= ' ') do // trim right
dec(txtlen);
beg := txtlen + 1;
dotcount := 0;
tc := @TEXT_CHARS;
while (beg > 1) and
(tcURIUnreserved in tc[aText[beg - 1]]) do // search backward end of JWT
begin
dec(beg);
if aText[beg] = '.' then
inc(dotcount);
end;
dec(txtlen, beg - 1);
if not noDotCheck then
if (dotcount <> 2) or
(txtlen <= 10) then
exit;
result := copy(aText, beg, txtlen); // trim base64 encoded part
end;
{ TJwtAbstract }
constructor TJwtAbstract.Create(const aAlgorithm: RawUtf8; aClaims: TJwtClaims;
const aAudience: array of RawUtf8; aExpirationMinutes: integer;
aIDIdentifier: TSynUniqueIdentifierProcess; aIDObfuscationKey: RawUtf8;
aIDObfuscationKeyNewKdf: integer);
begin
inherited Create; // may have been overriden
if aAlgorithm = '' then
EJwtException.RaiseUtf8('%.Create(algo?)', [self]);
if high(aAudience) >= 0 then
begin
fAudience := TRawUtf8DynArrayFrom(aAudience);
include(aClaims, jrcAudience);
end;
if aExpirationMinutes > 0 then
begin
include(aClaims, jrcExpirationTime);
fExpirationSeconds := aExpirationMinutes * 60;
end
else
exclude(aClaims, jrcExpirationTime);
fAlgorithm := aAlgorithm;
fClaims := aClaims;
if jrcJwtID in aClaims then
fIDGen := TSynUniqueIdentifierGenerator.Create(
aIDIdentifier, aIDObfuscationKey, aIDObfuscationKeyNewKdf);
if fHeader = '' then
FormatUtf8('{"alg":"%","typ":"JWT"}', [aAlgorithm], fHeader);
fHeaderB64 := BinToBase64Uri(fHeader) + '.';
fCacheResults := [jwtValid];
end;
destructor TJwtAbstract.Destroy;
begin
fIDGen.Free;
fCache.Free;
inherited;
end;
const
JWT_MAXSIZE = 4096; // coherent with HTTP headers limitations
function TJwtAbstract.Compute(const DataNameValue: array of const;
const Issuer, Subject, Audience: RawUtf8; NotBefore: TDateTime;
ExpirationMinutes: integer; Signature: PRawUtf8): RawUtf8;
var
payload, headpayload, sig: RawUtf8;
begin
result := '';
if self = nil then
exit;
payload := PayloadToJson(DataNameValue, Issuer, Subject, Audience,
NotBefore, ExpirationMinutes);
headpayload := fHeaderB64 + BinToBase64Uri(payload);
sig := ComputeSignature(headpayload);
result := headpayload + '.' + sig;
if length(result) > JWT_MAXSIZE then
EJwtException.RaiseUtf8('%.Compute oversize: len=%',
[self, length(result)]);
if Signature <> nil then
Signature^ := sig;
end;
function TJwtAbstract.ComputeAuthorizationHeader(
const DataNameValue: array of const; const Issuer, Subject, Audience: RawUtf8;
NotBefore: TDateTime; ExpirationMinutes: integer): RawUtf8;
begin
if self = nil then
result := ''
else
result := 'Bearer ' + Compute(DataNameValue, Issuer, Subject, Audience,
NotBefore, ExpirationMinutes);
end;
function TJwtAbstract.PayloadToJson(const DataNameValue: array of const;
const Issuer, Subject, Audience: RawUtf8; NotBefore: TDateTime;
ExpirationMinutes: cardinal): RawUtf8;
procedure RaiseMissing(c: TJwtClaim);
begin
EJwtException.RaiseUtf8('%.PayloadToJson: missing % (''%'')',
[self, _TJwtClaim[c]^, JWT_CLAIMS_TEXT[c]]);
end;
var
payload: TDocVariantData;
begin
result := '';
payload.InitObject(DataNameValue, JSON_FAST);
if jrcIssuer in fClaims then
if Issuer = '' then
RaiseMissing(jrcIssuer)
else
payload.AddValueFromText(JWT_CLAIMS_TEXT[jrcIssuer], Issuer, true);
if jrcSubject in fClaims then
if Subject = '' then
RaiseMissing(jrcSubject)
else
payload.AddValueFromText(JWT_CLAIMS_TEXT[jrcSubject], Subject, true);
if jrcAudience in fClaims then
if Audience = '' then
RaiseMissing(jrcAudience)
else if Audience[1] = '[' then
payload.AddOrUpdateValue(JWT_CLAIMS_TEXT[jrcAudience], _JsonFast(Audience))
else
payload.AddValueFromText(JWT_CLAIMS_TEXT[jrcAudience], Audience, true);
if jrcNotBefore in fClaims then
if NotBefore <= 0 then
payload.AddOrUpdateValue(JWT_CLAIMS_TEXT[jrcNotBefore], UnixTimeUtc)
else
payload.AddOrUpdateValue(JWT_CLAIMS_TEXT[jrcNotBefore], DateTimeToUnixTime(NotBefore));
if jrcIssuedAt in fClaims then
payload.AddOrUpdateValue(JWT_CLAIMS_TEXT[jrcIssuedAt], UnixTimeUtc);
if jrcExpirationTime in fClaims then
begin
if ExpirationMinutes = 0 then
ExpirationMinutes := fExpirationSeconds
else
ExpirationMinutes := ExpirationMinutes * 60;
payload.AddOrUpdateValue(JWT_CLAIMS_TEXT[jrcExpirationTime],
UnixTimeUtc + ExpirationMinutes);
end;
if jrcJwtID in fClaims then
if joNoJwtIDGenerate in fOptions then
begin
if payload.GetValueIndex(JWT_CLAIMS_TEXT[jrcJwtID]) < 0 then
exit; // not generated, but should be supplied
end
else
payload.AddValue(JWT_CLAIMS_TEXT[jrcJwtID],
RawUtf8ToVariant(fIDGen.ToObfuscated(fIDGen.ComputeNew)));
result := payload.ToJson;
end;
procedure TJwtAbstract.SetCacheTimeoutSeconds(value: integer);
begin
fCacheTimeoutSeconds := value;
FreeAndNil(fCache);
if (value > 0) and
(fCacheResults <> []) then
fCache := TSynDictionary.Create(
TypeInfo(TRawUtf8DynArray), TypeInfo(TJwtContentDynArray), false, value);
end;
procedure TJwtAbstract.Verify(const Token: RawUtf8; out Jwt: TJwtContent;
ExcludedClaims: TJwtClaims);
var
headpayload: RawUtf8;
signature: RawByteString;
fromcache: boolean;
begin
Jwt.result := jwtNoToken;
if Token = '' then
exit;
if (self = nil) or
(fCache = nil) then
fromcache := false
else
begin
fromcache := fCache.FindAndCopy(Token, Jwt);
fCache.DeleteDeprecated;
end;
if not fromcache then
Parse(Token, Jwt, headpayload, signature, ExcludedClaims);
if Jwt.result in [jwtValid, jwtNotBeforeFailed] then
if CheckAgainstActualTimestamp(Jwt) and
not fromcache then
// depending on the algorithm used
CheckSignature(headpayload{%H-}, signature{%H-}, Jwt);
if not fromcache and
(self <> nil) and
(fCache <> nil) and
(Jwt.result in fCacheResults) then
fCache.Add(Token, Jwt);
end;
function TJwtAbstract.Verify(const Token: RawUtf8): TJwtResult;
var
jwt: TJwtContent;
begin
Verify(Token, jwt, [jrcData]); // we won't use jwt.data for sure
result := jwt.result;
end;
function TJwtAbstract.CheckAgainstActualTimestamp(var Jwt: TJwtContent): boolean;
var