1 /**
2  * Module defining various test cases used in separate implementations.
3  */
4 module jwtlited.tests;
5 
6 version (assert):
7 
8 import jwtlited;
9 
10 enum Test
11 {
12     decode = 1,
13     encode = 2,
14 
15     all = decode | encode
16 }
17 
18 enum Valid
19 {
20     key = 1,
21     decode = 2,
22     encode = 4,
23 
24     none = 0,
25     all = key | decode | encode
26 }
27 
28 struct TestCase
29 {
30     string name;
31     JWTAlgorithm alg;
32     Test test;
33     Valid valid;
34     string key;
35     string pkey;
36     string payload;
37     string token;
38 }
39 
40 enum EC256_PUBKEY = `-----BEGIN PUBLIC KEY-----
41 MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMlFGAIxe+/zLanxz4bOxTI6daFBk
42 NGyQ+P4bc/RmNEq1NpsogiMB5eXC7jUcD/XqxP9HCIhdRBcQHx7aOo3ayQ==
43 -----END PUBLIC KEY-----`;
44 
45 enum EC256_PRIVKEY = `-----BEGIN EC PRIVATE KEY-----
46 MHcCAQEEILvM6E7mLOdndALDyFc3sOgUTb6iVjgwRBtBwYZngSuwoAoGCCqGSM49
47 AwEHoUQDQgAEMlFGAIxe+/zLanxz4bOxTI6daFBkNGyQ+P4bc/RmNEq1NpsogiMB
48 5eXC7jUcD/XqxP9HCIhdRBcQHx7aOo3ayQ==
49 -----END EC PRIVATE KEY-----`;
50 
51 enum EC512_PUBKEY = `-----BEGIN PUBLIC KEY-----
52 MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBb6j/jJ/+D25Ez8GuU3Al+DsnuZXF
53 AkU6V2eMMezgJu/6E0+FCUMcLaIi6MAuBo74FE6MSCCW4CQ3MGOG75jy67EBENM7
54 xGFOBWlBedku4S7N4cayJbpHVnxc+Z5uK50gchiUripQ1i78wi7W+5WjYYQBUE4l
55 4et/HP21c5n4LRCeasw=
56 -----END PUBLIC KEY-----`;
57 
58 enum EC512_PRIVKEY = `-----BEGIN EC PRIVATE KEY-----
59 MIHcAgEBBEIATbhPa+N94KjXbTzHx3ujwN+TwLVlQjyxA2e1jp8oYxogg8S/ceXU
60 CsS/169A1zf1EYKe7lEYm3LTSXcvdaXzT1ygBwYFK4EEACOhgYkDgYYABAFvqP+M
61 n/4PbkTPwa5TcCX4Oye5lcUCRTpXZ4wx7OAm7/oTT4UJQxwtoiLowC4GjvgUToxI
62 IJbgJDcwY4bvmPLrsQEQ0zvEYU4FaUF52S7hLs3hxrIlukdWfFz5nm4rnSByGJSu
63 KlDWLvzCLtb7laNhhAFQTiXh638c/bVzmfgtEJ5qzA==
64 -----END EC PRIVATE KEY-----`;
65 
66 enum RS_PRIVKEY = `-----BEGIN RSA PRIVATE KEY-----
67 MIIEowIBAAKCAQEA0hZf4ct1tvPkcqM7826L89TwCPBhuycWMn3xT+4MLeUqe51F
68 LMSm1VK6k+Ew8jrmQ9T9tOtp2vRJFEhoK/WVAb44Sg0PuX9zDLw8ncgW9jON4q6X
69 m9MeJNC2Mb5ogwc72S+kQWNWi3nNR9xrCXmczHfoolVC9/lnU0T+Tp7kj4MZNmSC
70 Cx0eACHpkZV5306TAs+FSlOpKgTL9Wazf1i7teTddkhD5Csm/OE5gBqdAQDqO/8q
71 aKDpmYJTdrjM8RebBq9eTuc5sp7zzIGH2hjveiBG7+/83dDgLwW5IUV1+EB/VqSx
72 jrlurQcH38zYfmXV65QCToJXbF5X3asUluSu9wIDAQABAoIBAQCfXV2qeJ55BBW9
73 aFnn1WnQsyzKex6Hy6So9KSDD36pqfdKAgkhZqNvmuvxlZd9iHR37C/wd8u6zihJ
74 fIuZHRfFVLh6Y+ITwrxRYtFQlyHj7UOqOurCx6lMIA61OU0qZ+hcXilpeKOD9gdk
75 ha2kaF4rNKKB0c+VL9nTbrjChwG2YkneqROL7KyszVHAumU9sZUtaYsxKvwALwZi
76 7GStXCa8yFb0AXuTANWzVQt5QsFvIO5GpXjQrmYJM36pwzKNVKBFCqrMrRoQhuwe
77 UfXOI/VF1tUM9BhZ78R/ccxBGyklQCJt2wO1GqnWKH1lUDHUTDv//V3kI4TF8Tba
78 lEn4l8fhAoGBAPYIVsjDZdi7LTnkXENlUTf+VvWGwM7Upb7QK0LK6rZkJrFeiLfT
79 vPd4TDEcNHcWVKz+dZubJ5m1rC8hh4IUsQv5CcZdcQuJ/dINZyPRyNkNU4O+kDmf
80 50xemRMm9JwpvJfSRsIzoFizzwNsvYeJpQm5ZbGHdVxM1kQBt0P05Hk/AoGBANqZ
81 PWLTcKh942GXDzlr6sg4067neYg5fKMeUU6QsDN5Zf6MmPBNDDVd5+oMTjxRQiSW
82 Q4SIqR2ssDDuowBGBSoAirQyTdiQ/lVo4/h9oQJX2fDEQvMsPSaby6MBzl9kSSPz
83 fBeqSM5fCt6HpkLvzIwS6AlQ4lFzj3fU7tZ3vuRJAoGAGr6FUIWNCKYwIF7meJ0G
84 2yNWqJHhW5pZ+gf+69/K69CvNBCmo/TsUapN/fim61sOEVAH0MZo45iQAv+OD2HY
85 bQjBO0LlCvARG0hBse8X+iAst+F7JAhxyCdwVFijtmYDDi3ZazrZb0r8cc7cO2OH
86 ASuaFlY3N7VShUn6dfSk8VkCgYB0RawUI9k5lfRbFUlgxpkUNL3Lu422OrWj4d1n
87 h6hhSMJKmihDMQg8Xp2brT3z8VjYMyDonvQtN4xkCpqi65uVksI0RMmJVt4hOfCA
88 XPpGT8o5uXrO84n3PkkbhDtsG+CXgcxQnh+pvX3/jXGPCxPmsavAQMiQgIIgQB9l
89 7j2YGQKBgERkwz7s29PN9jg/9D0UGynxhkvJhIo8EcN42/lrnr4MziHxIHN5CwBv
90 oNHVKMZXklzzZ7X2jZcqY5UbTIOwiDonwmjfch8SSHt4L50MIzaCrxzDaEQ//zd6
91 qT7bwBrcVfn7JUE8RRk5qEn5Z81Z/4AciYBFbsOowA/1NDhLoCZ5
92 -----END RSA PRIVATE KEY-----`;
93 
94     enum RS_PUBKEY = `-----BEGIN PUBLIC KEY-----
95 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0hZf4ct1tvPkcqM7826L
96 89TwCPBhuycWMn3xT+4MLeUqe51FLMSm1VK6k+Ew8jrmQ9T9tOtp2vRJFEhoK/WV
97 Ab44Sg0PuX9zDLw8ncgW9jON4q6Xm9MeJNC2Mb5ogwc72S+kQWNWi3nNR9xrCXmc
98 zHfoolVC9/lnU0T+Tp7kj4MZNmSCCx0eACHpkZV5306TAs+FSlOpKgTL9Wazf1i7
99 teTddkhD5Csm/OE5gBqdAQDqO/8qaKDpmYJTdrjM8RebBq9eTuc5sp7zzIGH2hjv
100 eiBG7+/83dDgLwW5IUV1+EB/VqSxjrlurQcH38zYfmXV65QCToJXbF5X3asUluSu
101 9wIDAQAB
102 -----END PUBLIC KEY-----`;
103 
104 /// Test cases to test correct validation and signature used with all implementations
105 immutable TestCase[] testCases = [
106     // NONE
107     TestCase(
108         "NONE - valid",
109         JWTAlgorithm.none,
110         Test.all, Valid.all,
111         null, null,
112         `{"sub":"1234567890","name":"John Doe","iat":1516239022}`,
113         "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.",
114     ),
115     TestCase(
116         "NONE - unexpected signature in token",
117         JWTAlgorithm.none,
118         Test.decode, Valid.none,
119         null, null, null,
120         "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.B02AWclotXRccJUoyHFSpYHMfg4gUvy4cvFrqwMracg",
121     ),
122     TestCase(
123         "NONE - valid token with different algorithm",
124         JWTAlgorithm.none,
125         Test.decode, Valid.none,
126         null, null, null,
127         "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.B02AWclotXRccJUoyHFSpYHMfg4gUvy4cvFrqwMracg",
128     ),
129     TestCase("NONE - invalid token 1", JWTAlgorithm.none, Test.decode, Valid.none, null, null, null, "aa.bb.cc"),
130     TestCase("NONE - invalid token 2", JWTAlgorithm.none, Test.decode, Valid.none, null, null, null, "aa.bb."),
131     TestCase("NONE - invalid token 3", JWTAlgorithm.none, Test.decode, Valid.none, null, null, null, "aa.bb"),
132     TestCase("NONE - invalid token 4", JWTAlgorithm.none, Test.decode, Valid.none, null, null, null, "aa."),
133     TestCase("NONE - invalid token 5", JWTAlgorithm.none, Test.decode, Valid.none, null, null, null, "aa"),
134 
135     // HMAC
136     TestCase(
137         "HS256 - valid decode",
138         JWTAlgorithm.HS256,
139         Test.decode, Valid.all,
140         "FOO BAR BAZ", null,
141         `{"sub":"1234567890","name":"John Doe","iat":1516239022}`,
142         "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.B02AWclotXRccJUoyHFSpYHMfg4gUvy4cvFrqwMracg",
143     ),
144     TestCase(
145         "HS256 - valid all",
146         JWTAlgorithm.HS256,
147         Test.all, Valid.all,
148         "FOO BAR BAZ", null,
149         `{"sub":"1234567890","name":"John Doe","iat":1516239022}`,
150         "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.9hBU4ACaixzXiNcKiy3P04e4dQmrqVD9gAHVpQoUuBM",
151     ),
152     TestCase(
153         "HS256 - not matching signature - sig",
154         JWTAlgorithm.HS256,
155         Test.decode, Valid.key,
156         "FOO BAR BAZ", null, null,
157         "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.B12AWclotXRccJUoyHFSpYHMfg4gUvy4cvFrqwMracg",
158     ),
159     TestCase(
160         "HS256 - not matching signature - pay",
161         JWTAlgorithm.HS256,
162         Test.decode, Valid.key,
163         "FOO BAR BAZ", null, null,
164         "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzcWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.B02AWclotXRccJUoyHFSpYHMfg4gUvy4cvFrqwMracg",
165     ),
166     TestCase(
167         "HS256 - not matching signature - key",
168         JWTAlgorithm.HS256,
169         Test.decode, Valid.key,
170         "FOOBARBAZ", null, null,
171         "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.B02AWclotXRccJUoyHFSpYHMfg4gUvy4cvFrqwMracg",
172     ),
173 
174     // RSA
175     TestCase(
176         "RS512 - valid",
177         JWTAlgorithm.RS512,
178         Test.all, Valid.all, RS_PUBKEY, RS_PRIVKEY,
179         `{"foo":42}`,
180         "eyJhbGciOiJSUzUxMiJ9.eyJmb28iOjQyfQ.HawDMFOZBczB6PozCDPMG4LVYqiy9UaOghs7-ccfQfOpm8nnUNjIcC7-_bUi_KK7tcKUqPIjnz7aR89szjNu5-CtiAef62xfbsPq6ZCTh0x57GNHHMa1DBYoLkvMeRmbKdFgEcamh6qEIoGiihGoe11O3J1JoX6-bktDHNmHEBAYK1BzyxLo2yYwYBPa6GoxOV-qM1ysX_4FEqIul9EpAahLX1S5H9MYwiWt4e7yhRYyJgLXM2qPNCzXrVG28QcF5dSnDjzzvdi8urScBFmVnFKWhZrQj3VbvwOUkJFbaYd4eN2rzTBNQV2YQjU9KZL0kJMn2AcnTeyG_IBj34E8Ig"
181     ),
182     TestCase(
183         "RS512 - different alg",
184         JWTAlgorithm.RS512,
185         Test.decode, Valid.key, RS_PUBKEY, RS_PRIVKEY,
186         `{"foo":42}`,
187         "eyJhbGciOiJSUzI1NiJ9.eyJmb28iOjQyfQ.BOHqKf2MlxWvUNEm8TQIlJ2XIigRN9Mesgt88VrpbnjPBhi5r3J_82pi-iX60RscddpXkFS5f0YZxB-rDslPaZEh_P12SXN9QCEuqspeh7_uFbZpG0E2RcKpjvs4N4UnOetEVkcn1GxjZoZEkzffH-tNGFlLQeRVyJPuk1D8cH13JsKbpmqGQayQOmlZjYsdM48Yy0Rb5Pcc0tR95zBXtQtkgeBNYNEBN5lfWaLBpTADm-fJV5dw4Hu3qKvOMOzW2CslIsCTq-1c-VT4I6-hSBlhy2GkozhKKtbGS6G4RpunolX5GOaPiqhKcQx12TnBVqotRMfEJKazkZe3MA80QA"
188     ),
189     TestCase(
190         "RS256 - valid",
191         JWTAlgorithm.RS256,
192         Test.all, Valid.all, RS_PUBKEY, RS_PRIVKEY,
193         `{"foo":42}`,
194         "eyJhbGciOiJSUzI1NiJ9.eyJmb28iOjQyfQ.BOHqKf2MlxWvUNEm8TQIlJ2XIigRN9Mesgt88VrpbnjPBhi5r3J_82pi-iX60RscddpXkFS5f0YZxB-rDslPaZEh_P12SXN9QCEuqspeh7_uFbZpG0E2RcKpjvs4N4UnOetEVkcn1GxjZoZEkzffH-tNGFlLQeRVyJPuk1D8cH13JsKbpmqGQayQOmlZjYsdM48Yy0Rb5Pcc0tR95zBXtQtkgeBNYNEBN5lfWaLBpTADm-fJV5dw4Hu3qKvOMOzW2CslIsCTq-1c-VT4I6-hSBlhy2GkozhKKtbGS6G4RpunolX5GOaPiqhKcQx12TnBVqotRMfEJKazkZe3MA80QA"
195     ),
196 
197     // ECDSA
198     TestCase(
199         "ES256 - valid",
200         JWTAlgorithm.ES256,
201         Test.all, Valid.all, EC256_PUBKEY, EC256_PRIVKEY,
202         `{"foo":42}`,
203         "eyJhbGciOiJFUzI1NiJ9.eyJmb28iOjQyfQ.R_MeWV0nLqRcNk9OrczuhykhKJn2wBZIgmwF87TivMlLGk2KB4Ekec9aXz0dOxBfYQflP6PwdSNjgLdYMECwRA"
204     ),
205     TestCase(
206         "ES512 - valid",
207         JWTAlgorithm.ES512,
208         Test.all, Valid.all, EC512_PUBKEY, EC512_PRIVKEY,
209         `{"foo":42}`,
210         "eyJhbGciOiJFUzUxMiJ9.eyJmb28iOjQyfQ.AGjGpLTYdQB2U2amD6-zJAWI0buCUiKgu-hT_JJgDmqyXYjSvJRQ3uaWID3DWj5fISsMoFZNdp29Pn8Rzwn4yWXYADX_4H4OLUb-IZ82qDfVgZNVMlygrvevbczGU-v1FpKac5Ov2CC7irEoCgus-kVhgFe2XscCz5T6UxUmn4V59Jc3"
211     ),
212 ];
213 
214 void evalTest(H)(auto ref H handler, ref immutable(TestCase) tc) @safe
215 {
216     import std.algorithm : countUntil;
217     import std.range : retro;
218     import std.stdio : writeln;
219     // writeln("Test case: ", tc.name);
220     scope (success) writeln("Test case PASSED: ", tc.name);
221     scope (failure) writeln("Test case FAILED: ", tc.name);
222 
223     char[512] buf;
224     if (tc.test & Test.decode)
225     {
226         assert(handler.decode(tc.token, buf[]) == !!(tc.valid & Valid.decode));
227         assert(!(tc.valid & Valid.decode) || buf[0..tc.payload.length] == tc.payload);
228         if (tc.valid & Valid.decode)
229             assert(handler.decode(tc.token, null)); // test validation without payload decode
230     }
231 
232     if (tc.test & Test.encode)
233     {
234         immutable len = handler.encode(buf[], tc.payload);
235         if (tc.valid & Valid.encode)
236         {
237             assert(len == tc.token.length);
238 
239             // some algorithms generates different signature, so we check if it's valid and only part vithout the signature equals
240             immutable idx = tc.token.retro.countUntil('.');
241             assert(idx >= 0); // NONE ends with '.'
242 
243             if (tc.test & Test.decode)
244                 assert(handler.validate(buf[0..len]));
245 
246             assert(buf[0..tc.token.length-idx] == tc.token[0..$-idx]);
247         }
248         else assert(len == -1);
249     }
250 }
251 
252 @("JWTAlgorithm.none")
253 @safe unittest
254 {
255     import std.algorithm : filter;
256     import core.memory : GC;
257 
258     immutable pre = () @trusted { return GC.stats(); }();
259     foreach (tc; testCases.filter!(a => a.alg == JWTAlgorithm.none))
260     {
261         evalTest(NoneHandler.init, tc);
262     }
263     assert((() @trusted { return GC.stats().usedSize; }() - pre.usedSize) == 0); // check for no GC allocations
264 }
265 
266 @("AnyAlgValidator")
267 @safe unittest
268 {
269     // valid token
270     enum tok = "eyJhbGciOiJub25lIn0.eyJmb28iOiJiYXIifQ.";
271     enum hdr = `{"alg":"none"}`;
272     enum pay = `{"foo":"bar"}`;
273     char[64] bh, bp;
274 
275     // decode header and payload
276     assert(decode(AnyAlgValidator.init, tok, bh[], bp[]));
277     assert(bh[0..hdr.length] == hdr);
278     assert(bp[0..pay.length] == pay);
279 
280     // decode payload
281     assert(decode(AnyAlgValidator.init, tok, bp[]));
282     assert(bp[0..pay.length] == pay);
283 
284     // just validate
285     assert(validate(AnyAlgValidator.init, tok));
286 
287     // invalid base64 signature
288     assert(!validate(AnyAlgValidator.init, "eyJhbGciOiJub25lIn0.eyJmb28iOiJiYXIifQ.blabla!"));
289 }
290 
291 @("Sink tests")
292 @safe unittest
293 {
294     NoneHandler none;
295     enum tok = "eyJhbGciOiJub25lIn0.eyJmb28iOiJiYXIifQ.";
296     enum pay = `{"foo":"bar"}`;
297     enum hdr = `{"alg":"none"}`;
298 
299     // char array
300     {
301         char[512] hbuf, pbuf, tbuf;
302         assert(none.decode(tok, hbuf[], pbuf[]));
303         assert(hbuf[0..hdr.length] == hdr);
304         assert(pbuf[0..pay.length] == pay);
305         assert(none.encode(tbuf[], pay) > 0);
306         assert(tbuf[0..tok.length] == tok);
307     }
308 
309     // appender
310     {
311         import std.array : Appender;
312         Appender!string hbuf, pbuf, tbuf;
313         assert(none.decode(tok, hbuf, pbuf));
314         assert(hbuf.data == hdr);
315         assert(pbuf.data == pay);
316         assert(none.encode(tbuf, pay) > 0);
317         assert(tbuf.data == tok);
318     }
319 
320     // String from bc.string
321     {
322         import bc.string : String;
323         String hbuf, pbuf, tbuf;
324         assert(none.decode(tok, hbuf, pbuf));
325         assert(hbuf == hdr);
326         assert(pbuf == pay);
327         assert(none.encode(tbuf, pay) > 0);
328         assert(tbuf == tok);
329     }
330 }