···1616 | Access of {did: string}
1717 | Refresh of {did: string; jti: string}
18181919+let dpop_nonce_state = ref (Oauth.Dpop.create_nonce_state Env.dpop_nonce_secret)
2020+1921let verify_bearer_jwt t token expected_scope =
2022 match Jwt.verify_jwt token Env.jwt_key with
2123 | Error err ->
+23-15
pegasus/lib/oauth/client.ml
···1818 in
1919 if status <> `OK then
2020 let%lwt () = Cohttp_lwt.Body.drain_body res in
2121- Errors.invalid_request "client metadata not found"
2121+ failwith
2222+ (Printf.sprintf "client metadata not found; http %d"
2323+ (Cohttp.Code.code_of_status status) )
2224 else
2325 let%lwt body = Cohttp_lwt.Body.to_string res in
2426 let json = Yojson.Safe.from_string body in
···4042 ; dpop_bound_access_tokens=
4143 json |> member "dpop_bound_access_tokens" |> to_bool
4244 ; jwks_uri= json |> member "jwks_uri" |> to_string_option
4343- ; jwks= json |> member "jwks" |> to_option Fun.id }
4545+ ; jwks= json |> member "jwks" |> to_option (fun j -> j) }
4446 in
4545- if metadata.client_id <> client_id then
4646- Errors.invalid_request "client_id mismatch"
4747+ if metadata.client_id <> client_id then failwith "client_id mismatch"
4748 else
4849 let scopes = String.split_on_char ' ' metadata.scope in
4950 if not (List.mem "atproto" scopes) then
5050- Errors.invalid_request "scope must include 'atproto'"
5151+ failwith "scope must include 'atproto'"
5152 else
5253 List.iter
5353- (fun uri ->
5454- let u = Uri.of_string uri in
5555- match Uri.scheme u with
5656- | Some "https" ->
5757- ()
5858- | Some "http"
5959- when Uri.host u = Some "127.0.0.1" || Uri.host u = Some "[::1]" ->
5454+ (function
5555+ | "authorization_code" | "refresh_token" ->
6056 ()
6161- | _ ->
6262- Errors.invalid_request ("invalid redirect_uri: " ^ uri) )
6363- metadata.redirect_uris ;
5757+ | grant ->
5858+ failwith ("invalid grant type: " ^ grant) )
5959+ metadata.grant_types ;
6060+ List.iter
6161+ (fun uri ->
6262+ let u = Uri.of_string uri in
6363+ let host = Uri.host u in
6464+ match Uri.scheme u with
6565+ | Some "https" when host <> Some "localhost" ->
6666+ ()
6767+ | Some "http" when host = Some "127.0.0.1" || host = Some "[::1]" ->
6868+ ()
6969+ | _ ->
7070+ failwith ("invalid redirect_uri: " ^ uri) )
7171+ metadata.redirect_uris ;
6472 Lwt.return metadata
+38-32
pegasus/lib/oauth/dpop.ml
···8899type proof = {jti: string; jkt: string; htm: string; htu: string}
10101111-type context = {nonce_state: nonce_state; jti_cache: (string, int) Hashtbl.t}
1111+let max_age_s = 60
1212+1313+let rotation_interval_ms = 60_000L
1414+1515+let jti_ttl_s = 3600
1616+1717+let jti_cache_size = 10_000
1818+1919+let jti_cache : (string, int) Hashtbl.t = Hashtbl.create jti_cache_size
2020+2121+let cleanup_jti_cache () =
2222+ let now = int_of_float (Unix.gettimeofday ()) in
2323+ Hashtbl.filter_map_inplace
2424+ (fun _ expires_at -> if expires_at > now then Some expires_at else None)
2525+ jti_cache
2626+2727+let compute_nonce secret counter =
2828+ let data = Bytes.create 8 in
2929+ Bytes.set_int64_be data 0 counter ;
3030+ Digestif.SHA256.(
3131+ hmac_bytes ~key:(Bytes.to_string secret) data
3232+ |> to_raw_string
3333+ |> Base64.encode_exn ~pad:false )
12341313-let create_nonce_state ?(rotation_interval_ms = 60_000L) secret =
3535+let create_nonce_state secret =
1436 let counter =
1537 Int64.div
1638 (Int64.of_float (Unix.gettimeofday () *. 1000.))
1739 rotation_interval_ms
1840 in
1919- let compute_nonce cnt =
2020- let data = Bytes.create 8 in
2121- Bytes.set_int64_be data 0 cnt ;
2222- Digestif.SHA256.(
2323- hmac_bytes ~key:(Bytes.to_string secret) data
2424- |> to_raw_string
2525- |> Base64.encode_exn ~pad:false )
2626- in
2741 { secret
2842 ; counter
2929- ; prev= compute_nonce (Int64.pred counter)
3030- ; curr= compute_nonce counter
3131- ; next= compute_nonce (Int64.succ counter)
4343+ ; prev= compute_nonce secret (Int64.pred counter)
4444+ ; curr= compute_nonce secret counter
4545+ ; next= compute_nonce secret (Int64.succ counter)
3246 ; rotation_interval_ms }
33473448let next_nonce state =
···4054 if now_counter <> state.counter then (
4155 state.prev <- state.curr ;
4256 state.curr <- state.next ;
4343- let data = Bytes.create 8 in
4444- Bytes.set_int64_be data 0 (Int64.succ now_counter) ;
4545- state.next <-
4646- Digestif.SHA256.(
4747- hmac_bytes ~key:(Bytes.to_string state.secret) data
4848- |> to_raw_string
4949- |> Base64.encode_exn ~pad:false ) ;
5757+ state.next <- compute_nonce state.secret (Int64.succ now_counter) ;
5058 state.counter <- now_counter ) ;
5159 state.next
5260···6674 digest_string tp |> to_raw_string |> Base64.encode_exn ~pad:false )
67756876let normalize_url url =
6969- (* Remove query params and fragment, normalize to https://host/path *)
7077 let uri = Uri.of_string url in
7178 Uri.make ~scheme:"https"
7279 ~host:(Uri.host uri |> Option.get)
···8390 Digestif.SHA256.(digest_string signing_input |> to_raw_string)
8491 |> Bytes.of_string
8592 in
8686- let x = jwk |> member "x" |> to_string |> Base64.decode_exn in
8787- let y = jwk |> member "y" |> to_string |> Base64.decode_exn in
9393+ let x = jwk |> member "x" |> to_string |> Base64.decode_exn ~pad:false in
9494+ let y = jwk |> member "y" |> to_string |> Base64.decode_exn ~pad:false in
9595+ let pubkey =
9696+ Bytes.cat (Bytes.of_string "\x04") (Bytes.of_string (x ^ y))
9797+ in
8898 let pubkey =
8989- ( Bytes.of_string (x ^ y)
9999+ ( pubkey
90100 , match alg with
91101 | "ES256K" ->
92102 (module Kleidos.K256 : Kleidos.CURVE)
···103113 | _ ->
104114 false
105115106106-let verify_dpop_proof {nonce_state; jti_cache} ~mthd ~url ~dpop_header
107107- ?access_token () =
116116+let verify_dpop_proof ~nonce_state ~mthd ~url ~dpop_header ?access_token () =
108117 match dpop_header with
109118 | None ->
110119 Lwt.return_error "missing dpop header"
···142151 then Lwt.return_error "htu mismatch"
143152 else
144153 let now = int_of_float (Unix.gettimeofday ()) in
145145- if now - iat > 60 then Lwt.return_error "dpop proof too old"
154154+ if now - iat > max_age_s then
155155+ Lwt.return_error "dpop proof too old"
146156 else if Hashtbl.mem jti_cache jti then
147157 Lwt.return_error "dpop proof replay detected"
148158 else (
149149- Hashtbl.add jti_cache jti (now + 3600) ;
159159+ Hashtbl.add jti_cache jti (now + jti_ttl_s) ;
150160 if not (verify_signature jwt jwk alg) then
151161 Lwt.return_error "invalid dpop signature"
152162 else
···175185 else Lwt.return_ok {jti; jkt; htm; htu} ) )
176186 | _ ->
177187 Lwt.return_error "invalid dpop jwt" )
178178-179179-let create_context ?rotation_interval_ms secret =
180180- { nonce_state= create_nonce_state secret ?rotation_interval_ms
181181- ; jti_cache= Hashtbl.create 1000 }