objective categorical abstract machine language personal data server
1open Alcotest
2
3(** helpers *)
4let test_string = testable Fmt.string String.equal
5
6(* create a minimal jwt
7 we only care about the (base64url encoded json) payload, so header and signature can be anything *)
8let make_jwt payload_json =
9 let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" in
10 (* {"alg":"HS256","typ":"JWT"} *)
11 let payload =
12 Base64.encode_string ~alphabet:Base64.uri_safe_alphabet ~pad:false
13 payload_json
14 in
15 let signature = "signature" in
16 header ^ "." ^ payload ^ "." ^ signature
17
18(* decoding a valid JWT *)
19let test_decode_valid () =
20 let now = int_of_float (Unix.time ()) in
21 let payload_json =
22 Printf.sprintf {|{"sub":"did:plc:test","iat":%d,"exp":%d}|} now (now + 3600)
23 in
24 let jwt = make_jwt payload_json in
25 match Hermes.Jwt.decode_payload jwt with
26 | Ok payload ->
27 check (option test_string) "sub" (Some "did:plc:test") payload.sub ;
28 check (option int) "iat" (Some now) payload.iat ;
29 check (option int) "exp" (Some (now + 3600)) payload.exp
30 | Error e ->
31 fail ("decode failed: " ^ e)
32
33(* decoding JWT with additional fields *)
34let test_decode_with_extra_fields () =
35 let now = int_of_float (Unix.time ()) in
36 let payload_json =
37 Printf.sprintf
38 {|{"sub":"did:plc:extra","iat":%d,"exp":%d,"scope":"atproto","aud":"did:web:server"}|}
39 now (now + 3600)
40 in
41 let jwt = make_jwt payload_json in
42 match Hermes.Jwt.decode_payload jwt with
43 | Ok payload ->
44 check (option test_string) "sub" (Some "did:plc:extra") payload.sub ;
45 check (option int) "exp" (Some (now + 3600)) payload.exp ;
46 check (option test_string) "aud" (Some "did:web:server") payload.aud
47 | Error e ->
48 fail ("decode failed: " ^ e)
49
50(* decoding invalid JWT format *)
51let test_decode_invalid_format () =
52 let invalid = "not.a.jwt.with.wrong.parts" in
53 match Hermes.Jwt.decode_payload invalid with
54 | Ok _ ->
55 fail "should have failed"
56 | Error e ->
57 check bool "has error" true (String.length e > 0)
58
59let test_decode_no_dots () =
60 let invalid = "nodots" in
61 match Hermes.Jwt.decode_payload invalid with
62 | Ok _ ->
63 fail "should have failed"
64 | Error _ ->
65 ()
66
67(* decoding JWT with invalid base64 *)
68let test_decode_invalid_base64 () =
69 let invalid = "header.!!!invalid!!!.signature" in
70 match Hermes.Jwt.decode_payload invalid with
71 | Ok _ ->
72 fail "should have failed"
73 | Error _ ->
74 ()
75
76(* decoding JWT with invalid JSON payload *)
77let test_decode_invalid_json () =
78 let header = "eyJhbGciOiJIUzI1NiJ9" in
79 let payload =
80 Base64.encode_string ~alphabet:Base64.uri_safe_alphabet ~pad:false
81 "not json"
82 in
83 let jwt = header ^ "." ^ payload ^ ".sig" in
84 match Hermes.Jwt.decode_payload jwt with
85 | Ok _ ->
86 fail "should have failed"
87 | Error _ ->
88 ()
89
90(* test is_expired with expired token *)
91let test_expired_token () =
92 let past = int_of_float (Unix.time ()) - 3600 in
93 (* 1 hour ago *)
94 let payload_json =
95 Printf.sprintf {|{"sub":"test","iat":%d,"exp":%d}|} past past
96 in
97 let jwt = make_jwt payload_json in
98 check bool "is expired" true (Hermes.Jwt.is_expired jwt)
99
100(* test is_expired with valid token *)
101let test_valid_token () =
102 let now = int_of_float (Unix.time ()) in
103 let future = now + 3600 in
104 (* 1 hour from now *)
105 let payload_json =
106 Printf.sprintf {|{"sub":"test","iat":%d,"exp":%d}|} now future
107 in
108 let jwt = make_jwt payload_json in
109 check bool "is not expired" false (Hermes.Jwt.is_expired jwt)
110
111(* test is_expired with buffer *)
112let test_expired_with_buffer () =
113 let now = int_of_float (Unix.time ()) in
114 let almost_expired = now + 30 in
115 (* expires in 30 seconds *)
116 let payload_json =
117 Printf.sprintf {|{"sub":"test","iat":%d,"exp":%d}|} now almost_expired
118 in
119 let jwt = make_jwt payload_json in
120 (* default buffer is 60 seconds, so this should be considered expired *)
121 check bool "expired with buffer" true (Hermes.Jwt.is_expired jwt)
122
123(* is_expired with invalid JWT returns true *)
124let test_expired_invalid_jwt () =
125 check bool "invalid JWT treated as expired" true
126 (Hermes.Jwt.is_expired "invalid")
127
128(* test get_expiration *)
129let test_get_expiration () =
130 let now = int_of_float (Unix.time ()) in
131 let exp_time = now + 3600 in
132 let payload_json = Printf.sprintf {|{"sub":"test","exp":%d}|} exp_time in
133 let jwt = make_jwt payload_json in
134 check (option int) "expiration" (Some exp_time)
135 (Hermes.Jwt.get_expiration jwt)
136
137let test_get_expiration_missing () =
138 let payload_json = {|{"sub":"test"}|} in
139 let jwt = make_jwt payload_json in
140 check (option int) "no expiration" None (Hermes.Jwt.get_expiration jwt)
141
142(** tests *)
143
144let decode_tests =
145 [ ("decode valid JWT", `Quick, test_decode_valid)
146 ; ("decode with extra fields", `Quick, test_decode_with_extra_fields)
147 ; ("decode invalid format", `Quick, test_decode_invalid_format)
148 ; ("decode no dots", `Quick, test_decode_no_dots)
149 ; ("decode invalid base64", `Quick, test_decode_invalid_base64)
150 ; ("decode invalid json", `Quick, test_decode_invalid_json) ]
151
152let expiry_tests =
153 [ ("expired token", `Quick, test_expired_token)
154 ; ("valid token", `Quick, test_valid_token)
155 ; ("expired with buffer", `Quick, test_expired_with_buffer)
156 ; ("invalid JWT is expired", `Quick, test_expired_invalid_jwt)
157 ; ("get_expiration", `Quick, test_get_expiration)
158 ; ("get_expiration missing", `Quick, test_get_expiration_missing) ]
159
160let () = run "Jwt" [("decode", decode_tests); ("expiry", expiry_tests)]