package oidc

  1. Overview
  2. Docs

Source file IDToken.ml

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
open Utils

let src =
  Logs.Src.create "oidc.id_token" ~doc:"logs OIDC events in the IDToken module"

module Log = (val Logs.src_log src : Logs.LOG)

type validation_error =
  [ `Expired
  | `Iat_in_future
  | `Invalid_nonce
  | `Invalid_signature
  | `Invalid_sub_length
  | `Missing_aud
  | `Missing_exp
  | `Missing_iat
  | `Missing_iss
  | `Missing_nonce
  | `Missing_sub
  | `Not_json
  | `Not_supported
  | `Msg of string
  | `No_jwk_provided
  | `Unexpected_nonce
  | `Unsafe
  | `Wrong_aud_value of string
  | `Wrong_iss_value of string
  ]

let validation_error_to_string = function
  | `Msg e -> e
  | `Expired -> "expired"
  | `Missing_exp -> "Missing exp"
  | `Invalid_signature -> "Invalid signature"
  | `Invalid_nonce -> "Invalid nonce"
  | `Missing_nonce -> "Missing nonce"
  | `Unexpected_nonce -> "Got nonce when not expected"
  | `Invalid_sub_length -> "Invalid sub length"
  | `Missing_sub -> "Missing sub"
  | `Not_json -> "Not JSON"
  | `Not_supported -> "Not supported"
  | `Wrong_aud_value aud -> "Wrong aud " ^ aud
  | `Missing_aud -> "aud is missing"
  | `Wrong_iss_value iss -> "Wrong iss value " ^ iss
  | `Missing_iss -> "iss is missing"
  | `Iat_in_future -> "iat is in future"
  | `Missing_iat -> "Missing iat"
  | `No_jwk_provided -> "No jwk provided but is needed"
  | `Unsafe -> "Unsafe action"

let ( >>= ) = RResult.( >>= )

let get_string_member member payload =
  Yojson.Safe.Util.member member payload |> Yojson.Safe.Util.to_string_option

let get_int_member member payload =
  Yojson.Safe.Util.member member payload |> Yojson.Safe.Util.to_int_option

let validate_sub (jwt : Jose.Jwt.t) =
  match get_string_member "sub" jwt.payload with
  | Some sub when String.length sub < 257 ->
    Log.debug (fun m -> m "sub is valid");
    Ok jwt
  | Some _sub ->
    Log.debug (fun m -> m "sub has invalid length");
    Error `Invalid_sub_length
  | None ->
    Log.debug (fun m -> m "sub is missing");
    Error `Missing_sub

let validate_exp ?(clock_tolerance = 0) (jwt : Jose.Jwt.t) =
  let module Json = Yojson.Safe.Util in
  match get_int_member "exp" jwt.payload with
  | Some exp when exp > int_of_float (Unix.time ()) - clock_tolerance ->
    Log.debug (fun m -> m "exp is valid");
    Ok jwt
  | Some _exp ->
    Log.debug (fun m -> m "exp is the past");
    Error `Expired
  | None ->
    Log.debug (fun m -> m "exp is missing");
    Error `Missing_exp

let validate_iat ?(clock_tolerance = 0) (jwt : Jose.Jwt.t) =
  let now = int_of_float (Unix.time ()) + clock_tolerance in
  match get_int_member "iat" jwt.payload with
  | Some iat when iat <= now ->
    Log.debug (fun m -> m "iat is valid");
    Ok jwt (* TODO: Make the time diff configurable *)
  | Some _iat ->
    Log.debug (fun m -> m "iat is in the future");
    Error `Iat_in_future
  | None ->
    Log.debug (fun m -> m "iat is missing");
    Error `Missing_iat

let validate_iss ~issuer (jwt : Jose.Jwt.t) =
  match get_string_member "iss" jwt.payload with
  | Some iss when iss = issuer ->
    Log.debug (fun m -> m "iss is valid, %s" issuer);
    Ok jwt
  (* Microsoft has a special case because they use a strange templated format *)
  | Some iss when String.starts_with ~prefix:"https://sts.windows.net" iss ->
    Ok jwt
  | Some iss ->
    Log.debug (fun m -> m "iss is invalid, expected %s, got %s" issuer iss);
    Error (`Wrong_iss_value iss)
  | None ->
    Log.debug (fun m -> m "iss is missing");
    Error `Missing_iss

let validate_aud ~(client : Client.t) (jwt : Jose.Jwt.t) =
  match Yojson.Safe.Util.member "aud" jwt.payload with
  | `String aud when aud = client.id ->
    Log.debug (fun m -> m "aud is valid");
    Ok jwt
  | `String aud ->
    Log.debug (fun m -> m "aud is invalid, expected %s got %s" client.id aud);
    Error (`Wrong_aud_value aud)
  | `List json ->
    Log.debug (fun m -> m "aud is list");
    let maybe_client_id = List.find_opt (fun v -> v = `String client.id) json in
    (match maybe_client_id with
    | Some _ ->
      Log.debug (fun m -> m "aud list includes %s" client.id);
      Ok jwt
    | None ->
      Log.debug (fun m ->
        m "aud list does not include expected value %s" client.id);
      Error (`Wrong_aud_value ""))
    (* TODO: Check azp as well if audience is longer than 1 *)
  | _ ->
    Log.debug (fun m -> m "aud is missing");
    Error `Missing_aud

let validate_nonce ?nonce (jwt : Jose.Jwt.t) =
  let jwt_nonce = get_string_member "nonce" jwt.payload in
  match nonce, jwt_nonce with
  | Some nonce, Some jwt_nonce ->
    if nonce = jwt_nonce
    then (
      Log.debug (fun m -> m "nonce is valid");
      Ok jwt)
    else (
      Log.debug (fun m ->
        m "nonce is invalid, expected %s got %s" nonce jwt_nonce);
      Error `Invalid_nonce)
  | None, Some _ ->
    Log.debug (fun m -> m "Got nonce but did not expect to");
    Error `Unexpected_nonce
  | Some _, None ->
    Log.debug (fun m -> m "nonce is missing when expected");
    Error `Missing_nonce
  | None, None ->
    Log.debug (fun m -> m "no nonce provided");
    Ok jwt

let validate
      ?clock_tolerance
      ?nonce
      ?jwk
      ?(now = Unix.gettimeofday () |> Ptime.of_float_s |> Option.get)
      ~(client : Client.t)
      ~issuer
      (jwt : Jose.Jwt.t)
  =
  let issuer = Uri.to_string issuer in
  (match jwt.header.alg, jwk with
    | `None, _ -> Ok jwt
    | _, Some jwk -> Jose.Jwt.validate ~now ~jwk jwt
    | _, None -> Error `No_jwk_provided)
  >>= validate_iss ~issuer
  >>= validate_exp
  >>= validate_iat ?clock_tolerance
  >>= validate_sub
  >>= validate_aud ~client
  >>= validate_nonce ?nonce
  |> fun jwt ->
  let () =
    match jwt with
    | Ok _ -> Log.debug (fun m -> m "JWT is valid")
    | Error _ -> Log.debug (fun m -> m "JWT is invalid")
  in
  jwt