···11open Oauth
2233-let handler =
33+let options_handler = Xrpc.handler (fun _ -> Dream.empty `No_Content)
44+55+let post_handler =
46 Xrpc.handler ~auth:DPoP (fun ctx ->
57 let%lwt req = Xrpc.parse_body ctx.req Types.token_request_of_yojson in
68 let proof = Auth.get_dpop_proof_exn ctx.auth in
···4749 let computed =
4850 Digestif.SHA256.digest_string verifier
4951 |> Digestif.SHA256.to_raw_string
5050- |> Base64.encode_exn ~pad:false
5252+ |> Base64.(
5353+ encode_exn ~pad:false
5454+ ~alphabet:uri_safe_alphabet )
5155 in
5256 if orig_req.code_challenge <> computed then
5357 Errors.invalid_request "invalid code_verifier"
···6064 let token_id =
6165 "tok-"
6266 ^ Uuidm.to_string
6363- (Uuidm.v4_gen (Random.get_state ()) ())
6767+ (Uuidm.v4_gen
6868+ (Random.State.make_self_init ())
6969+ () )
6470 in
6571 let refresh_token =
6672 "ref-"
6773 ^ Uuidm.to_string
6868- (Uuidm.v4_gen (Random.get_state ()) ())
7474+ (Uuidm.v4_gen
7575+ (Random.State.make_self_init ())
7676+ () )
6977 in
7078 let now_sec = int_of_float (Unix.gettimeofday ()) in
7179 let expires_in =
···128136 else
129137 let new_token_id =
130138 "tok-"
131131- ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ())
139139+ ^ Uuidm.to_string
140140+ (Uuidm.v4_gen (Random.State.make_self_init ()) ())
132141 in
133142 let new_refresh =
134143 "ref-"
135135- ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ())
144144+ ^ Uuidm.to_string
145145+ (Uuidm.v4_gen (Random.State.make_self_init ()) ())
136146 in
137147 let now_sec = int_of_float (Unix.gettimeofday ()) in
138148 let expires_in = Constants.access_token_expiry_ms / 1000 in
···153163 in
154164 let%lwt () =
155165 Queries.update_oauth_token ctx.db
156156- ~old_refresh_token:refresh_token ~new_token_id
166166+ ~old_refresh_token:refresh_token
157167 ~new_refresh_token:new_refresh ~expires_at:new_expires_at
158168 in
159169 Dream.json ~headers:[("Cache-Control", "no-store")]
+61-40
pegasus/lib/auth.ml
···1616 | Access of {did: string}
1717 | Refresh of {did: string; jti: string}
1818 | OAuth of {did: string; proof: Oauth.Dpop.proof}
1919+ | DPoP of {proof: Oauth.Dpop.proof}
19202021let verify_bearer_jwt t token expected_scope =
2122 match Jwt.verify_jwt token Env.jwt_key with
···5960 Errors.auth_required "invalid authorization header"
60616162let get_dpop_proof_exn = function
6262- | OAuth {proof; _} ->
6363+ | OAuth {proof; _} | DPoP {proof} ->
6364 proof
6465 | _ ->
6566 Errors.invalid_request "invalid DPoP header"
···168169 Lwt.return_error @@ Errors.auth_required "invalid authorization header"
169170170171 let dpop : verifier =
172172+ fun {req; _} ->
173173+ let dpop_header = Dream.header req "DPoP" in
174174+ match
175175+ Oauth.Dpop.verify_dpop_proof
176176+ ~mthd:(Dream.method_to_string @@ Dream.method_ req)
177177+ ~url:(Dream.target req) ~dpop_header ()
178178+ with
179179+ | Error "use_dpop_nonce" ->
180180+ Lwt.return_error @@ Errors.use_dpop_nonce ()
181181+ | Error e ->
182182+ Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e)
183183+ | Ok proof ->
184184+ Lwt.return_ok (DPoP {proof})
185185+186186+ let oauth : verifier =
171187 fun {req; db} ->
172188 match parse_dpop req with
173189 | Error e ->
174174- Errors.invalid_request ("dpop error: " ^ e)
190190+ Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e)
175191 | Ok token -> (
176176- let dpop_header = Dream.header req "DPoP" in
177177- match
178178- Oauth.Dpop.verify_dpop_proof
179179- ~mthd:(Dream.method_to_string @@ Dream.method_ req)
180180- ~url:(Dream.target req) ~dpop_header ~access_token:token ()
181181- with
182182- | Error "use_dpop_nonce" ->
183183- Lwt.return_error
184184- (* error must be this object; see https://datatracker.ietf.org/doc/html/rfc9449#section-8 *)
185185- @@ Errors.invalid_request {|{ "error": "use_dpop_nonce" }|}
192192+ match%lwt dpop {req; db} with
193193+ | Error e ->
194194+ Lwt.return_error e
195195+ | Ok (DPoP {proof}) -> (
196196+ match Jwt.verify_jwt token Env.jwt_key with
186197 | Error e ->
187187- Errors.invalid_request ("dpop error: " ^ e)
188188- | Ok proof -> (
189189- match Jwt.verify_jwt token Env.jwt_key with
190190- | Error e ->
191191- Lwt.return_error @@ Errors.auth_required e
192192- | Ok (_header, claims) -> (
193193- let open Yojson.Safe.Util in
194194- try
195195- let did = claims |> member "sub" |> to_string in
196196- let exp = claims |> member "exp" |> to_int in
197197- let jkt_claim =
198198- claims |> member "cnf" |> member "jkt" |> to_string
198198+ Lwt.return_error @@ Errors.auth_required e
199199+ | Ok (_header, claims) -> (
200200+ let open Yojson.Safe.Util in
201201+ try
202202+ let did = claims |> member "sub" |> to_string in
203203+ let exp = claims |> member "exp" |> to_int in
204204+ let jkt_claim =
205205+ claims |> member "cnf" |> member "jkt" |> to_string
206206+ in
207207+ let now = int_of_float (Unix.gettimeofday ()) in
208208+ if jkt_claim <> proof.jkt then
209209+ Lwt.return_error @@ Errors.auth_required "dpop key mismatch"
210210+ else if exp < now then
211211+ Lwt.return_error @@ Errors.auth_required "token expired"
212212+ else
213213+ let%lwt session =
214214+ try%lwt
215215+ let%lwt sess = get_session_info did db in
216216+ Lwt.return_ok sess
217217+ with _ ->
218218+ Lwt.return_error
219219+ @@ Errors.auth_required "invalid credentials"
199220 in
200200- let now = int_of_float (Unix.gettimeofday ()) in
201201- if jkt_claim <> proof.jkt then
202202- Lwt.return_error @@ Errors.auth_required "dpop key mismatch"
203203- else if exp < now then
204204- Lwt.return_error @@ Errors.auth_required "token expired"
205205- else
206206- let%lwt {active; _} =
207207- try%lwt get_session_info did db
208208- with _ -> Errors.auth_required "invalid credentials"
209209- in
210210- if active <> Some true then
221221+ match session with
222222+ | Ok {active= Some true; _} ->
223223+ Lwt.return_ok (OAuth {did; proof})
224224+ | Ok _ ->
211225 Lwt.return_error
212226 @@ Errors.auth_required ~name:"AccountDeactivated"
213227 "account is deactivated"
214214- else Lwt.return_ok (Access {did})
215215- with _ ->
216216- Lwt.return_error @@ Errors.auth_required "malformed JWT claims"
217217- ) ) )
228228+ | Error _ ->
229229+ Lwt.return_error
230230+ @@ Errors.auth_required "invalid credentials"
231231+ with _ ->
232232+ Lwt.return_error @@ Errors.auth_required "malformed JWT claims" )
233233+ )
234234+ | Ok _ ->
235235+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
218236219237 let refresh : verifier =
220238 fun {req; db} ->
···247265 | Some ("Bearer" :: _) ->
248266 bearer ctx
249267 | Some ("DPoP" :: _) ->
250250- dpop ctx
268268+ oauth ctx
251269 | _ ->
252270 Lwt.return_error
253271 @@ Errors.auth_required ~name:"InvalidToken"
···261279 | Admin
262280 | Bearer
263281 | DPoP
282282+ | OAuth
264283 | Refresh
265284 | Authorization
266285 | Any
···274293 bearer
275294 | DPoP ->
276295 dpop
296296+ | OAuth ->
297297+ oauth
277298 | Refresh ->
278299 refresh
279300 | Authorization ->
+3-1
pegasus/lib/data_store.ml
···103103 execute
104104 {sql| CREATE TABLE IF NOT EXISTS oauth_codes (
105105 code TEXT PRIMARY KEY,
106106- request_id TEXT NOT NULL REFERENCES oauth_requests(request_id),
106106+ request_id TEXT NOT NULL REFERENCES oauth_requests(request_id) ON DELETE CASCADE,
107107 authorized_by TEXT,
108108 authorized_at INTEGER,
109109 expires_at INTEGER NOT NULL,
···311311type t = Util.caqti_pool
312312313313let connect ?create ?write () : t Lwt.t =
314314+ if create = Some true then
315315+ Util.mkfile_p Util.Constants.pegasus_db_filepath ~perm:0o644 ;
314316 Util.connect_sqlite ?create ?write Util.Constants.pegasus_db_location
315317316318let init conn : unit Lwt.t = Util.use_pool conn Queries.create_tables
+3-2
pegasus/lib/env.ml
···2121 match Sys.getenv_opt "DPOP_NONCE_SECRET" with
2222 | Some sec ->
2323 let secret =
2424- Base64.(decode_exn ~alphabet:uri_safe_alphabet) sec |> Bytes.of_string
2424+ Base64.(decode_exn ~alphabet:uri_safe_alphabet ~pad:false) sec
2525+ |> Bytes.of_string
2526 in
2627 if Bytes.length secret = 32 then secret
2727- else failwith "DPOP_NONCE_SECRET must be 32 bytes in base64"
2828+ else failwith "DPOP_NONCE_SECRET must be 32 bytes in base64uri"
2829 | None ->
2930 let secret = Mirage_crypto_rng_unix.getrandom 32 in
3031 Dream.warning (fun log ->
···11open Cohttp_lwt
22-open Cohttp_lwt_unix
3243let did_regex =
54 Str.regexp {|^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$|}
···1211 let uri =
1312 Uri.of_string ("https://" ^ handle ^ "/.well-known/atproto-did")
1413 in
1515- let%lwt {status; _}, body = Client.get uri in
1414+ let%lwt {status; _}, body = Util.http_get uri in
1615 match status with
1716 | `OK ->
1817 let%lwt did = Body.to_string body in
···164163 ~path:(Uri.pct_encode did) ()
165164 in
166165 let%lwt {status; _}, body =
167167- Client.get uri
166166+ Util.http_get uri
168167 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
169168 in
170169 match status with
···186185 ~path:"/.well-known/did.json" ()
187186 in
188187 let%lwt {status; _}, body =
189189- Client.get uri
188188+ Util.http_get uri
190189 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
191190 in
192191 match status with
+3-1
pegasus/lib/jwt.ml
···9090 let now_s = int_of_float (Unix.gettimeofday ()) in
9191 let access_exp = now_s + Defaults.access_token_exp in
9292 let refresh_exp = now_s + Defaults.refresh_token_exp in
9393- let jti = Uuidm.v4_gen (Random.get_state ()) () |> Uuidm.to_string in
9393+ let jti =
9494+ Uuidm.v4_gen (Random.State.make_self_init ()) () |> Uuidm.to_string
9595+ in
9496 let access_payload =
9597 symmetric_jwt_to_yojson
9698 { scope= "com.atproto.access"
+8-4
pegasus/lib/oauth/client.ml
···11open Types
2233let fetch_client_metadata client_id : client_metadata Lwt.t =
44- let%lwt {status; _}, res =
55- Cohttp_lwt_unix.Client.get (Uri.of_string client_id)
66- in
44+ let%lwt {status; _}, res = Util.http_get (Uri.of_string client_id) in
75 if status <> `OK then
86 let%lwt () = Cohttp_lwt.Body.drain_body res in
97 failwith
···1210 else
1311 let%lwt body = Cohttp_lwt.Body.to_string res in
1412 let json = Yojson.Safe.from_string body in
1515- let metadata = client_metadata_of_yojson json |> Result.get_ok in
1313+ let metadata =
1414+ match client_metadata_of_yojson json with
1515+ | Ok metadata ->
1616+ metadata
1717+ | Error err ->
1818+ failwith err
1919+ in
1620 if metadata.client_id <> client_id then failwith "client_id mismatch"
1721 else
1822 let scopes = String.split_on_char ' ' metadata.scope in
···302302 did
303303 in
304304 let headers = Http.Header.init_with "Accept" "application/json" in
305305- let%lwt res, body = Client.get ~headers uri in
305305+ let%lwt res, body = Util.http_get ~headers uri in
306306 match res.status with
307307 | `OK ->
308308 let%lwt body = Body.to_string body in