···197 write_type_and_argument t 5 (Int64.of_int len) ;
198 ordered_map_keys m
199 |> List.iter (fun k ->
200- write_string t k ;
201- write_value t (String_map.find k m) )
202 | `Link cid ->
203 write_cid t cid
204
···197 write_type_and_argument t 5 (Int64.of_int len) ;
198 ordered_map_keys m
199 |> List.iter (fun k ->
200+ write_string t k ;
201+ write_value t (String_map.find k m) )
202 | `Link cid ->
203 write_cid t cid
204
···1let handler =
2- Xrpc.handler ~auth:Authorization (fun {req; db; auth} ->
3 let did = Auth.get_authed_did_exn auth in
4 let%lwt body = Dream.body req in
5 let prefs =
···1let handler =
2+ Xrpc.handler ~auth:Authorization (fun {req; auth; db; _} ->
3 let did = Auth.get_authed_did_exn auth in
4 let%lwt body = Dream.body req in
5 let prefs =
+6-6
pegasus/lib/api/identity/resolveHandle.ml
···14 Dream.json @@ Yojson.Safe.to_string
15 @@ response_to_yojson {did= actor.did}
16 | None -> (
17- match%lwt Id_resolver.Handle.resolve handle with
18- | Ok did ->
19- Dream.json @@ Yojson.Safe.to_string @@ response_to_yojson {did}
20- | Error e ->
21- Errors.log_exn (Failure e) ;
22- Errors.internal_error ~msg:"could not resolve handle" () ) )
···14 Dream.json @@ Yojson.Safe.to_string
15 @@ response_to_yojson {did= actor.did}
16 | None -> (
17+ match%lwt Id_resolver.Handle.resolve handle with
18+ | Ok did ->
19+ Dream.json @@ Yojson.Safe.to_string @@ response_to_yojson {did}
20+ | Error e ->
21+ Errors.log_exn (Failure e) ;
22+ Errors.internal_error ~msg:"could not resolve handle" () ) )
+55-58
pegasus/lib/api/identity/updateHandle.ml
···1type request = {handle: string} [@@deriving yojson]
23let handler =
4- Xrpc.handler ~auth:Authorization (fun {req; auth; db} ->
5 let did = Auth.get_authed_did_exn auth in
6 let%lwt body = Dream.body req in
7 let handle =
···15 | Error e ->
16 raise e
17 | Ok () -> (
18- match%lwt Data_store.get_actor_by_identifier handle db with
19- | Some _ ->
20- Errors.invalid_request ~name:"InvalidHandle"
21- "handle already in use"
22- | None ->
23- let%lwt () = Data_store.update_actor_handle ~did ~handle db in
24- let%lwt _ =
25- if String.starts_with ~prefix:"did:plc:" did then
26- match%lwt Plc.get_audit_log did with
27- | Error e ->
28- Dream.error (fun log -> log ~request:req "%s" e) ;
29- Errors.internal_error ~msg:"failed to fetch did doc" ()
30- | Ok log -> (
31- let latest = List.rev log |> List.hd in
32- let aka =
33- match
34- List.mem ("at://" ^ handle)
35- latest.operation.also_known_as
36- with
37- | true ->
38- latest.operation.also_known_as
39- | false ->
40- ("at://" ^ handle) :: latest.operation.also_known_as
41- in
42- let%lwt signing_key =
43- match%lwt Data_store.get_actor_by_identifier did db with
44- | Some {signing_key; _} ->
45- Lwt.return @@ Kleidos.parse_multikey_str signing_key
46- | _ ->
47- Errors.internal_error ()
48- in
49- let signed =
50- Plc.sign_operation signing_key
51- (Operation
52- { type'= "plc_operation"
53- ; prev= Some latest.cid
54- ; also_known_as= aka
55- ; rotation_keys= latest.operation.rotation_keys
56- ; verification_methods=
57- latest.operation.verification_methods
58- ; services= latest.operation.services } )
59- in
60- match%lwt Plc.submit_operation did signed with
61- | Ok _ ->
62- Lwt.return_unit
63- | Error (status, msg) ->
64- Dream.error (fun log ->
65- log ~request:req "%d %s" status msg ) ;
66- Errors.internal_error
67- ~msg:"failed to submit plc operation" () )
68- else Lwt.return_unit
69- in
70- let () =
71- Ttl_cache.String_cache.remove Id_resolver.Did.cache did
72- in
73- let%lwt _ = Sequencer.sequence_identity db ~did ~handle () in
74- Dream.empty `OK ) )
···1type request = {handle: string} [@@deriving yojson]
23let handler =
4+ Xrpc.handler ~auth:Authorization (fun {req; auth; db; _} ->
5 let did = Auth.get_authed_did_exn auth in
6 let%lwt body = Dream.body req in
7 let handle =
···15 | Error e ->
16 raise e
17 | Ok () -> (
18+ match%lwt Data_store.get_actor_by_identifier handle db with
19+ | Some _ ->
20+ Errors.invalid_request ~name:"InvalidHandle" "handle already in use"
21+ | None ->
22+ let%lwt () = Data_store.update_actor_handle ~did ~handle db in
23+ let%lwt _ =
24+ if String.starts_with ~prefix:"did:plc:" did then
25+ match%lwt Plc.get_audit_log did with
26+ | Error e ->
27+ Dream.error (fun log -> log ~request:req "%s" e) ;
28+ Errors.internal_error ~msg:"failed to fetch did doc" ()
29+ | Ok log -> (
30+ let latest = List.rev log |> List.hd in
31+ let aka =
32+ match
33+ List.mem ("at://" ^ handle)
34+ latest.operation.also_known_as
35+ with
36+ | true ->
37+ latest.operation.also_known_as
38+ | false ->
39+ ("at://" ^ handle) :: latest.operation.also_known_as
40+ in
41+ let%lwt signing_key =
42+ match%lwt Data_store.get_actor_by_identifier did db with
43+ | Some {signing_key; _} ->
44+ Lwt.return @@ Kleidos.parse_multikey_str signing_key
45+ | _ ->
46+ Errors.internal_error ()
47+ in
48+ let signed =
49+ Plc.sign_operation signing_key
50+ (Operation
51+ { type'= "plc_operation"
52+ ; prev= Some latest.cid
53+ ; also_known_as= aka
54+ ; rotation_keys= latest.operation.rotation_keys
55+ ; verification_methods=
56+ latest.operation.verification_methods
57+ ; services= latest.operation.services } )
58+ in
59+ match%lwt Plc.submit_operation did signed with
60+ | Ok _ ->
61+ Lwt.return_unit
62+ | Error (status, msg) ->
63+ Dream.error (fun log ->
64+ log ~request:req "%d %s" status msg ) ;
65+ Errors.internal_error
66+ ~msg:"failed to submit plc operation" () )
67+ else Lwt.return_unit
68+ in
69+ let () = Ttl_cache.String_cache.remove Id_resolver.Did.cache did in
70+ let%lwt _ = Sequencer.sequence_identity db ~did ~handle () in
71+ Dream.empty `OK ) )
000
···1+open Oauth
2+3+let options_handler = Xrpc.handler (fun _ -> Dream.empty `No_Content)
4+5+let post_handler =
6+ Xrpc.handler ~auth:DPoP (fun ctx ->
7+ let%lwt req = Xrpc.parse_body ctx.req Types.token_request_of_yojson in
8+ let proof = Auth.get_dpop_proof_exn ctx.auth in
9+ match req.grant_type with
10+ | "authorization_code" -> (
11+ match req.code with
12+ | None ->
13+ Errors.invalid_request "code required"
14+ | Some code -> (
15+ let%lwt code_record = Queries.consume_auth_code ctx.db code in
16+ match code_record with
17+ | None ->
18+ Errors.invalid_request "invalid code"
19+ | Some code_rec -> (
20+ if Util.now_ms () > code_rec.expires_at then
21+ Errors.invalid_request "code expired"
22+ else
23+ match code_rec.authorized_by with
24+ | None ->
25+ Errors.invalid_request "code not authorized"
26+ | Some did -> (
27+ let%lwt par_req =
28+ Queries.get_par_request ctx.db code_rec.request_id
29+ in
30+ match par_req with
31+ | None ->
32+ Errors.internal_error ~msg:"request not found" ()
33+ | Some par_record ->
34+ let orig_req =
35+ Yojson.Safe.from_string par_record.request_data
36+ |> Types.par_request_of_yojson |> Result.get_ok
37+ in
38+ ( match req.redirect_uri with
39+ | None ->
40+ Errors.invalid_request "redirect_uri required"
41+ | Some uri when uri <> orig_req.redirect_uri ->
42+ Errors.invalid_request "redirect_uri mismatch"
43+ | _ ->
44+ () ) ;
45+ ( match req.code_verifier with
46+ | None ->
47+ Errors.invalid_request "code_verifier required"
48+ | Some verifier ->
49+ let computed =
50+ Digestif.SHA256.digest_string verifier
51+ |> Digestif.SHA256.to_raw_string
52+ |> Base64.(
53+ encode_exn ~pad:false
54+ ~alphabet:uri_safe_alphabet )
55+ in
56+ if orig_req.code_challenge <> computed then
57+ Errors.invalid_request "invalid code_verifier"
58+ ) ;
59+ ( match par_record.dpop_jkt with
60+ | Some stored when stored <> proof.jkt ->
61+ Errors.invalid_request "DPoP key mismatch"
62+ | _ ->
63+ () ) ;
64+ let token_id =
65+ "tok-"
66+ ^ Uuidm.to_string
67+ (Uuidm.v4_gen
68+ (Random.State.make_self_init ())
69+ () )
70+ in
71+ let refresh_token =
72+ "ref-"
73+ ^ Uuidm.to_string
74+ (Uuidm.v4_gen
75+ (Random.State.make_self_init ())
76+ () )
77+ in
78+ let now_sec = int_of_float (Unix.gettimeofday ()) in
79+ let expires_in =
80+ Constants.access_token_expiry_ms / 1000
81+ in
82+ let exp_sec = now_sec + expires_in in
83+ let expires_at = exp_sec * 1000 in
84+ let claims =
85+ `Assoc
86+ [ ("jti", `String token_id)
87+ ; ("sub", `String did)
88+ ; ("iat", `Int now_sec)
89+ ; ("exp", `Int exp_sec)
90+ ; ("scope", `String orig_req.scope)
91+ ; ("aud", `String ("https://" ^ Env.hostname))
92+ ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ]
93+ in
94+ let access_token =
95+ Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key
96+ in
97+ let%lwt () =
98+ Queries.insert_oauth_token ctx.db
99+ { refresh_token
100+ ; client_id= req.client_id
101+ ; did
102+ ; dpop_jkt= proof.jkt
103+ ; scope= orig_req.scope
104+ ; expires_at }
105+ in
106+ let nonce = Dpop.next_nonce () in
107+ Dream.json
108+ ~headers:
109+ [ ("DPoP-Nonce", nonce)
110+ ; ("Access-Control-Expose-Headers", "DPoP-Nonce")
111+ ; ("Cache-Control", "no-store") ]
112+ @@ Yojson.Safe.to_string
113+ @@ `Assoc
114+ [ ("access_token", `String access_token)
115+ ; ("token_type", `String "DPoP")
116+ ; ("refresh_token", `String refresh_token)
117+ ; ("expires_in", `Int expires_in)
118+ ; ("scope", `String orig_req.scope)
119+ ; ("sub", `String did) ] ) ) ) )
120+ | "refresh_token" -> (
121+ match req.refresh_token with
122+ | None ->
123+ Errors.invalid_request "refresh_token required"
124+ | Some refresh_token -> (
125+ let%lwt token_record =
126+ Queries.get_oauth_token_by_refresh ctx.db refresh_token
127+ in
128+ match token_record with
129+ | None ->
130+ Errors.invalid_request "invalid refresh token"
131+ | Some session ->
132+ if session.client_id <> req.client_id then
133+ Errors.invalid_request "client_id mismatch"
134+ else if session.dpop_jkt <> proof.jkt then
135+ Errors.invalid_request "DPoP key mismatch"
136+ else
137+ let new_token_id =
138+ "tok-"
139+ ^ Uuidm.to_string
140+ (Uuidm.v4_gen (Random.State.make_self_init ()) ())
141+ in
142+ let new_refresh =
143+ "ref-"
144+ ^ Uuidm.to_string
145+ (Uuidm.v4_gen (Random.State.make_self_init ()) ())
146+ in
147+ let now_sec = int_of_float (Unix.gettimeofday ()) in
148+ let expires_in = Constants.access_token_expiry_ms / 1000 in
149+ let exp_sec = now_sec + expires_in in
150+ let new_expires_at = exp_sec * 1000 in
151+ let claims =
152+ `Assoc
153+ [ ("jti", `String new_token_id)
154+ ; ("sub", `String session.did)
155+ ; ("iat", `Int now_sec)
156+ ; ("exp", `Int exp_sec)
157+ ; ("scope", `String session.scope)
158+ ; ("aud", `String ("https://" ^ Env.hostname))
159+ ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ]
160+ in
161+ let new_access_token =
162+ Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key
163+ in
164+ let%lwt () =
165+ Queries.update_oauth_token ctx.db
166+ ~old_refresh_token:refresh_token
167+ ~new_refresh_token:new_refresh ~expires_at:new_expires_at
168+ in
169+ Dream.json ~headers:[("Cache-Control", "no-store")]
170+ @@ Yojson.Safe.to_string
171+ @@ `Assoc
172+ [ ("access_token", `String new_access_token)
173+ ; ("token_type", `String "DPoP")
174+ ; ("refresh_token", `String new_refresh)
175+ ; ("expires_in", `Int expires_in)
176+ ; ("scope", `String session.scope)
177+ ; ("sub", `String session.did) ] ) )
178+ | _ ->
179+ Errors.invalid_request ("unsupported grant_type: " ^ req.grant_type) )
+10-10
pegasus/lib/api/repo/createAccount.ml
···57 let%lwt did =
58 match input.did with
59 | Some did -> (
60- match%lwt Data_store.get_actor_by_identifier did ctx.db with
61- | Some _ ->
62- Errors.invalid_request "an account with that did already exists"
63- | None ->
64- Lwt.return did )
65 | None -> (
66 let sk_did = Kleidos.K256.pubkey_to_did_key signing_pubkey in
67 let rotation_did_keys =
···79 let%lwt _ =
80 match input.invite_code with
81 | Some code -> (
82- match%lwt Data_store.use_invite ~code ctx.db with
83- | Some _ ->
84- Lwt.return ()
85- | None ->
86- failwith "failed to use invite code" )
87 | None ->
88 Lwt.return ()
89 in
···57 let%lwt did =
58 match input.did with
59 | Some did -> (
60+ match%lwt Data_store.get_actor_by_identifier did ctx.db with
61+ | Some _ ->
62+ Errors.invalid_request "an account with that did already exists"
63+ | None ->
64+ Lwt.return did )
65 | None -> (
66 let sk_did = Kleidos.K256.pubkey_to_did_key signing_pubkey in
67 let rotation_did_keys =
···79 let%lwt _ =
80 match input.invite_code with
81 | Some code -> (
82+ match%lwt Data_store.use_invite ~code ctx.db with
83+ | Some _ ->
84+ Lwt.return ()
85+ | None ->
86+ failwith "failed to use invite code" )
87 | None ->
88 Lwt.return ()
89 in
···15 | Admin
16 | Access of {did: string}
17 | Refresh of {did: string; jti: string}
001819let verify_bearer_jwt t token expected_scope =
20 match Jwt.verify_jwt token Env.jwt_key with
···42 match credentials with
43 | Admin ->
44 true
45- | Access {did= creds} when creds = did ->
46 true
47 | Refresh {did= creds; _} when creds = did && refresh ->
48 true
···50 false
5152let get_authed_did_exn = function
53- | Access {did} ->
54 did
55 | Refresh {did; _} ->
56 did
57 | _ ->
58- Errors.auth_required "Invalid authorization header"
0000005960let get_session_info identifier db =
61 let%lwt actor =
···84module Verifiers = struct
85 open struct
86 let parse_header req expected_type =
87- match Dream.header req "authorization" with
88 | Some header -> (
89 match String.split_on_char ' ' header with
90 | [typ; token]
···95 Error "invalid authorization header" )
96 | None ->
97 Error "missing authorization header"
09899- let parse_basic req =
100- match parse_header req "Basic" with
101- | Ok token -> (
102- match Base64.decode token with
103- | Ok decoded -> (
104- match Str.bounded_split (Str.regexp_string ":") decoded 2 with
105- | [username; password] ->
106- Ok (username, password)
107- | _ ->
108- Error "invalid basic authorization header" )
109- | Error _ ->
110 Error "invalid basic authorization header" )
111 | Error _ ->
112- Error "invalid basic authorization header"
00113114- let parse_bearer req = parse_header req "Bearer"
115- end
0116117 type ctx = {req: Dream.request; db: Data_store.t}
118···122 fun {req; _} ->
123 match Dream.header req "authorization" with
124 | Some _ ->
125- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
126 | None ->
127 Lwt.return_ok Unauthenticated
128···134 | "admin", p when p = Env.admin_password ->
135 Lwt.return_ok Admin
136 | _ ->
137- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
138 | Error _ ->
139- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
140141- let access : verifier =
142 fun {req; db} ->
143 match parse_bearer req with
144 | Ok jwt -> (
145- match%lwt verify_bearer_jwt db jwt "com.atproto.access" with
146- | Ok {sub= did; _} -> (
147- match%lwt Data_store.get_actor_by_identifier did db with
148- | Some {deactivated_at= None; _} ->
149- Lwt.return_ok (Access {did})
150- | Some {deactivated_at= Some _; _} ->
151- Lwt.return_error
152- @@ Errors.auth_required ~name:"AccountDeactivated"
153- "Account is deactivated"
154- | None ->
155- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
156- | Error _ ->
157- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
158 | Error _ ->
159- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
000000000000000000000000000000000000000000000000000000000000000000160161 let refresh : verifier =
162 fun {req; db} ->
163 match parse_bearer req with
164 | Ok jwt -> (
165- match%lwt verify_bearer_jwt db jwt "com.atproto.refresh" with
166- | Ok {sub= did; jti; _} -> (
167- match%lwt Data_store.get_actor_by_identifier did db with
168- | Some {deactivated_at= None; _} ->
169- Lwt.return_ok (Refresh {did; jti})
170- | Some {deactivated_at= Some _; _} ->
171- Lwt.return_error
172- @@ Errors.auth_required ~name:"AccountDeactivated"
173- "Account is deactivated"
174- | None ->
175- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
176- | Error "" | Error _ ->
177- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
178 | Error _ ->
179- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
180181 let authorization : verifier =
182 fun ctx ->
···187 | Some ("Basic" :: _) ->
188 admin ctx
189 | Some ("Bearer" :: _) ->
190- access ctx
00191 | _ ->
192 Lwt.return_error
193 @@ Errors.auth_required ~name:"InvalidToken"
194- "Unexpected authorization type"
195196 let any : verifier =
197 fun ctx -> try authorization ctx with _ -> unauthenticated ctx
198199- type t = Unauthenticated | Admin | Access | Refresh | Authorization | Any
00000000200201 let of_t = function
202 | Unauthenticated ->
203 unauthenticated
204 | Admin ->
205 admin
206- | Access ->
207- access
0000208 | Refresh ->
209 refresh
210 | Authorization ->
···15 | Admin
16 | Access of {did: string}
17 | Refresh of {did: string; jti: string}
18+ | OAuth of {did: string; proof: Oauth.Dpop.proof}
19+ | DPoP of {proof: Oauth.Dpop.proof}
2021let verify_bearer_jwt t token expected_scope =
22 match Jwt.verify_jwt token Env.jwt_key with
···44 match credentials with
45 | Admin ->
46 true
47+ | (Access {did= creds} | OAuth {did= creds; _}) when creds = did ->
48 true
49 | Refresh {did= creds; _} when creds = did && refresh ->
50 true
···52 false
5354let get_authed_did_exn = function
55+ | Access {did} | OAuth {did; _} ->
56 did
57 | Refresh {did; _} ->
58 did
59 | _ ->
60+ Errors.auth_required "invalid authorization header"
61+62+let get_dpop_proof_exn = function
63+ | OAuth {proof; _} | DPoP {proof} ->
64+ proof
65+ | _ ->
66+ Errors.invalid_request "invalid DPoP header"
6768let get_session_info identifier db =
69 let%lwt actor =
···92module Verifiers = struct
93 open struct
94 let parse_header req expected_type =
95+ match Dream.header req "Authorization" with
96 | Some header -> (
97 match String.split_on_char ' ' header with
98 | [typ; token]
···103 Error "invalid authorization header" )
104 | None ->
105 Error "missing authorization header"
106+ end
107108+ let parse_basic req =
109+ match parse_header req "Basic" with
110+ | Ok token -> (
111+ match Base64.decode token with
112+ | Ok decoded -> (
113+ match Str.bounded_split (Str.regexp_string ":") decoded 2 with
114+ | [username; password] ->
115+ Ok (username, password)
116+ | _ ->
00117 Error "invalid basic authorization header" )
118 | Error _ ->
119+ Error "invalid basic authorization header" )
120+ | Error _ ->
121+ Error "invalid basic authorization header"
122123+ let parse_bearer req = parse_header req "Bearer"
124+125+ let parse_dpop req = parse_header req "DPoP"
126127 type ctx = {req: Dream.request; db: Data_store.t}
128···132 fun {req; _} ->
133 match Dream.header req "authorization" with
134 | Some _ ->
135+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
136 | None ->
137 Lwt.return_ok Unauthenticated
138···144 | "admin", p when p = Env.admin_password ->
145 Lwt.return_ok Admin
146 | _ ->
147+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
148 | Error _ ->
149+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
150151+ let bearer : verifier =
152 fun {req; db} ->
153 match parse_bearer req with
154 | Ok jwt -> (
155+ match%lwt verify_bearer_jwt db jwt "com.atproto.access" with
156+ | Ok {sub= did; _} -> (
157+ match%lwt Data_store.get_actor_by_identifier did db with
158+ | Some {deactivated_at= None; _} ->
159+ Lwt.return_ok (Access {did})
160+ | Some {deactivated_at= Some _; _} ->
161+ Lwt.return_error
162+ @@ Errors.auth_required ~name:"AccountDeactivated"
163+ "account is deactivated"
164+ | None ->
165+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
166+ | Error _ ->
167+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
168 | Error _ ->
169+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
170+171+ let dpop : verifier =
172+ fun {req; _} ->
173+ let dpop_header = Dream.header req "DPoP" in
174+ match
175+ Oauth.Dpop.verify_dpop_proof
176+ ~mthd:(Dream.method_to_string @@ Dream.method_ req)
177+ ~url:(Dream.target req) ~dpop_header ()
178+ with
179+ | Error "use_dpop_nonce" ->
180+ Lwt.return_error @@ Errors.use_dpop_nonce ()
181+ | Error e ->
182+ Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e)
183+ | Ok proof ->
184+ Lwt.return_ok (DPoP {proof})
185+186+ let oauth : verifier =
187+ fun {req; db} ->
188+ match parse_dpop req with
189+ | Error e ->
190+ Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e)
191+ | Ok token -> (
192+ match%lwt dpop {req; db} with
193+ | Error e ->
194+ Lwt.return_error e
195+ | Ok (DPoP {proof}) -> (
196+ match Jwt.verify_jwt token Env.jwt_key with
197+ | Error e ->
198+ Lwt.return_error @@ Errors.auth_required e
199+ | Ok (_header, claims) -> (
200+ let open Yojson.Safe.Util in
201+ try
202+ let did = claims |> member "sub" |> to_string in
203+ let exp = claims |> member "exp" |> to_int in
204+ let jkt_claim =
205+ claims |> member "cnf" |> member "jkt" |> to_string
206+ in
207+ let now = int_of_float (Unix.gettimeofday ()) in
208+ if jkt_claim <> proof.jkt then
209+ Lwt.return_error @@ Errors.auth_required "dpop key mismatch"
210+ else if exp < now then
211+ Lwt.return_error @@ Errors.auth_required "token expired"
212+ else
213+ let%lwt session =
214+ try%lwt
215+ let%lwt sess = get_session_info did db in
216+ Lwt.return_ok sess
217+ with _ ->
218+ Lwt.return_error
219+ @@ Errors.auth_required "invalid credentials"
220+ in
221+ match session with
222+ | Ok {active= Some true; _} ->
223+ Lwt.return_ok (OAuth {did; proof})
224+ | Ok _ ->
225+ Lwt.return_error
226+ @@ Errors.auth_required ~name:"AccountDeactivated"
227+ "account is deactivated"
228+ | Error _ ->
229+ Lwt.return_error
230+ @@ Errors.auth_required "invalid credentials"
231+ with _ ->
232+ Lwt.return_error @@ Errors.auth_required "malformed JWT claims" )
233+ )
234+ | Ok _ ->
235+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
236237 let refresh : verifier =
238 fun {req; db} ->
239 match parse_bearer req with
240 | Ok jwt -> (
241+ match%lwt verify_bearer_jwt db jwt "com.atproto.refresh" with
242+ | Ok {sub= did; jti; _} -> (
243+ match%lwt Data_store.get_actor_by_identifier did db with
244+ | Some {deactivated_at= None; _} ->
245+ Lwt.return_ok (Refresh {did; jti})
246+ | Some {deactivated_at= Some _; _} ->
247+ Lwt.return_error
248+ @@ Errors.auth_required ~name:"AccountDeactivated"
249+ "account is deactivated"
250+ | None ->
251+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
252+ | Error "" | Error _ ->
253+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
254 | Error _ ->
255+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
256257 let authorization : verifier =
258 fun ctx ->
···263 | Some ("Basic" :: _) ->
264 admin ctx
265 | Some ("Bearer" :: _) ->
266+ bearer ctx
267+ | Some ("DPoP" :: _) ->
268+ oauth ctx
269 | _ ->
270 Lwt.return_error
271 @@ Errors.auth_required ~name:"InvalidToken"
272+ "unexpected authorization type"
273274 let any : verifier =
275 fun ctx -> try authorization ctx with _ -> unauthenticated ctx
276277+ type t =
278+ | Unauthenticated
279+ | Admin
280+ | Bearer
281+ | DPoP
282+ | OAuth
283+ | Refresh
284+ | Authorization
285+ | Any
286287 let of_t = function
288 | Unauthenticated ->
289 unauthenticated
290 | Admin ->
291 admin
292+ | Bearer ->
293+ bearer
294+ | DPoP ->
295+ dpop
296+ | OAuth ->
297+ oauth
298 | Refresh ->
299 refresh
300 | Authorization ->
+115-23
pegasus/lib/data_store.ml
···36 created_at INTEGER NOT NULL,
37 deactivated_at INTEGER
38 )
39- |sql}]
40 () conn
41 in
42 let$! () =
···52 [%rapper
53 execute
54 {sql| CREATE TABLE IF NOT EXISTS invite_codes (
55- code TEXT PRIMARY KEY,
56- did TEXT NOT NULL,
57- remaining INTEGER NOT NULL
58- )
59- |sql}]
60 () conn
61 in
62 let$! () =
63 [%rapper
64 execute
65 {sql| CREATE TABLE IF NOT EXISTS firehose (
66- seq INTEGER PRIMARY KEY,
67- time INTEGER NOT NULL,
68- t TEXT NOT NULL,
69- data BLOB NOT NULL
70- )
71- |sql}]
72 () conn
73 in
74- [%rapper
75- execute
76- (* no need to store issued tokens, just revoked ones; stolen from millipds https://github.com/DavidBuchanan314/millipds/blob/8f89a01e7d367a2a46f379960e9ca50347dcce71/src/millipds/database.py#L253 *)
77- {sql| CREATE TABLE IF NOT EXISTS revoked_tokens (
78- did TEXT NOT NULL,
79- jti TEXT NOT NULL,
80- revoked_at INTEGER NOT NULL,
81- PRIMARY KEY (did, jti)
82- )
83- |sql}]
84- () conn
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008586 let create_actor =
87 [%rapper
···221type t = Util.caqti_pool
222223let connect ?create ?write () : t Lwt.t =
00224 Util.connect_sqlite ?create ?write Util.Constants.pegasus_db_location
225226let init conn : unit Lwt.t = Util.use_pool conn Queries.create_tables
···36 created_at INTEGER NOT NULL,
37 deactivated_at INTEGER
38 )
39+ |sql}]
40 () conn
41 in
42 let$! () =
···52 [%rapper
53 execute
54 {sql| CREATE TABLE IF NOT EXISTS invite_codes (
55+ code TEXT PRIMARY KEY,
56+ did TEXT NOT NULL,
57+ remaining INTEGER NOT NULL
58+ )
59+ |sql}]
60 () conn
61 in
62 let$! () =
63 [%rapper
64 execute
65 {sql| CREATE TABLE IF NOT EXISTS firehose (
66+ seq INTEGER PRIMARY KEY,
67+ time INTEGER NOT NULL,
68+ t TEXT NOT NULL,
69+ data BLOB NOT NULL
70+ )
71+ |sql}]
72 () conn
73 in
74+ let$! () =
75+ [%rapper
76+ execute
77+ (* no need to store issued tokens, just revoked ones; stolen from millipds https://github.com/DavidBuchanan314/millipds/blob/8f89a01e7d367a2a46f379960e9ca50347dcce71/src/millipds/database.py#L253 *)
78+ {sql| CREATE TABLE IF NOT EXISTS revoked_tokens (
79+ did TEXT NOT NULL,
80+ jti TEXT NOT NULL,
81+ revoked_at INTEGER NOT NULL,
82+ PRIMARY KEY (did, jti)
83+ )
84+ |sql}]
85+ () conn
86+ in
87+ let$! () =
88+ [%rapper
89+ execute
90+ {sql| CREATE TABLE IF NOT EXISTS oauth_requests (
91+ request_id TEXT PRIMARY KEY,
92+ client_id TEXT NOT NULL,
93+ request_data TEXT NOT NULL,
94+ dpop_jkt TEXT,
95+ expires_at INTEGER NOT NULL,
96+ created_at INTEGER NOT NULL
97+ )
98+ |sql}]
99+ () conn
100+ in
101+ let$! () =
102+ [%rapper
103+ execute
104+ {sql| CREATE TABLE IF NOT EXISTS oauth_codes (
105+ code TEXT PRIMARY KEY,
106+ request_id TEXT NOT NULL REFERENCES oauth_requests(request_id) ON DELETE CASCADE,
107+ authorized_by TEXT,
108+ authorized_at INTEGER,
109+ expires_at INTEGER NOT NULL,
110+ used BOOLEAN DEFAULT FALSE
111+ )
112+ |sql}]
113+ () conn
114+ in
115+ let$! () =
116+ [%rapper
117+ execute
118+ {sql| CREATE TABLE IF NOT EXISTS oauth_tokens (
119+ refresh_token TEXT UNIQUE NOT NULL,
120+ client_id TEXT NOT NULL,
121+ did TEXT NOT NULL,
122+ dpop_jkt TEXT,
123+ scope TEXT NOT NULL,
124+ expires_at INTEGER NOT NULL
125+ )
126+ |sql}]
127+ () conn
128+ in
129+ let$! () =
130+ [%rapper
131+ execute
132+ {sql| CREATE INDEX IF NOT EXISTS oauth_requests_expires_idx ON oauth_requests(expires_at);
133+ CREATE INDEX IF NOT EXISTS oauth_codes_expires_idx ON oauth_codes(expires_at);
134+ CREATE INDEX IF NOT EXISTS oauth_tokens_refresh_idx ON oauth_tokens(refresh_token);
135+ |sql}]
136+ () conn
137+ in
138+ let$! () =
139+ [%rapper
140+ execute
141+ {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_requests
142+ AFTER INSERT ON oauth_requests
143+ BEGIN
144+ DELETE FROM oauth_requests WHERE expires_at < unixepoch() * 1000;
145+ END
146+ |sql}
147+ syntax_off]
148+ () conn
149+ in
150+ let$! () =
151+ [%rapper
152+ execute
153+ {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_codes
154+ AFTER INSERT ON oauth_codes
155+ BEGIN
156+ DELETE FROM oauth_codes WHERE expires_at < unixepoch() * 1000 OR used = 1;
157+ END
158+ |sql}
159+ syntax_off]
160+ () conn
161+ in
162+ let$! () =
163+ [%rapper
164+ execute
165+ {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_tokens
166+ AFTER INSERT ON oauth_tokens
167+ BEGIN
168+ DELETE FROM oauth_tokens WHERE expires_at < unixepoch() * 1000;
169+ END
170+ |sql}
171+ syntax_off]
172+ () conn
173+ in
174+ Lwt.return_ok ()
175176 let create_actor =
177 [%rapper
···311type t = Util.caqti_pool
312313let connect ?create ?write () : t Lwt.t =
314+ if create = Some true then
315+ Util.mkfile_p Util.Constants.pegasus_db_filepath ~perm:0o644 ;
316 Util.connect_sqlite ?create ?write Util.Constants.pegasus_db_location
317318let init conn : unit Lwt.t = Util.use_pool conn Queries.create_tables
···1open Cohttp_lwt
2-open Cohttp_lwt_unix
34let did_regex =
5 Str.regexp {|^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$|}
···12 let uri =
13 Uri.of_string ("https://" ^ handle ^ "/.well-known/atproto-did")
14 in
15- let%lwt {status; _}, body = Client.get uri in
16 match status with
17 | `OK ->
18 let%lwt did = Body.to_string body in
···164 ~path:(Uri.pct_encode did) ()
165 in
166 let%lwt {status; _}, body =
167- Client.get uri
168 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
169 in
170 match status with
···186 ~path:"/.well-known/did.json" ()
187 in
188 let%lwt {status; _}, body =
189- Client.get uri
190 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
191 in
192 match status with
···1open Cohttp_lwt
023let did_regex =
4 Str.regexp {|^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$|}
···11 let uri =
12 Uri.of_string ("https://" ^ handle ^ "/.well-known/atproto-did")
13 in
14+ let%lwt {status; _}, body = Util.http_get uri in
15 match status with
16 | `OK ->
17 let%lwt did = Body.to_string body in
···163 ~path:(Uri.pct_encode did) ()
164 in
165 let%lwt {status; _}, body =
166+ Util.http_get uri
167 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
168 in
169 match status with
···185 ~path:"/.well-known/did.json" ()
186 in
187 let%lwt {status; _}, body =
188+ Util.http_get uri
189 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
190 in
191 match status with
+20-26
pegasus/lib/jwt.ml
···19let b64_decode str =
20 match Base64.decode ~pad:false ~alphabet:Base64.uri_safe_alphabet str with
21 | Ok s ->
22- Ok s
23 | Error (`Msg e) ->
24- Error e
2526let extract_signature_components signature =
27 if Bytes.length signature <> 64 then failwith "expected 64 byte jwt signature"
···30 let s = Bytes.sub signature 32 32 in
31 (r, s)
3233-let sign_jwt payload signing_key =
34 let _, (module Curve : Kleidos.CURVE) = signing_key in
35 let alg =
36 match Curve.name with
···51 failwith "invalid curve"
52 in
53 let header_json =
54- `Assoc [("alg", `String alg); ("crv", `String crv); ("typ", `String "JWT")]
55 in
56 let encoded_header = header_json |> Yojson.Safe.to_string |> b64_encode in
57 let encoded_payload = payload |> Yojson.Safe.to_string |> b64_encode in
···65let decode_jwt jwt =
66 match String.split_on_char '.' jwt with
67 | [header_b64; payload_b64; _] -> (
68- match (b64_decode header_b64, b64_decode payload_b64) with
69- | Ok header_str, Ok payload_str -> (
70- try
71- let header = Yojson.Safe.from_string header_str in
72- let payload = Yojson.Safe.from_string payload_str in
73- Ok (header, payload)
74- with _ -> Error "invalid json in jwt" )
75- | Error e, _ | _, Error e ->
76- Error e )
77 | _ ->
78 Error "invalid jwt format"
7980let verify_jwt jwt pubkey =
81 match String.split_on_char '.' jwt with
82- | [header_b64; payload_b64; signature_b64] -> (
83- match b64_decode signature_b64 with
84- | Error e ->
85- Error e
86- | Ok signature_str ->
87- let signature = Bytes.of_string signature_str in
88- let signing_input = header_b64 ^ "." ^ payload_b64 in
89- let verified =
90- Kleidos.verify ~pubkey ~msg:(Bytes.of_string signing_input) ~signature
91- in
92- if verified then decode_jwt jwt
93- else Error "jwt signature verification failed" )
94 | _ ->
95 Error "invalid jwt format"
96···98 let now_s = int_of_float (Unix.gettimeofday ()) in
99 let access_exp = now_s + Defaults.access_token_exp in
100 let refresh_exp = now_s + Defaults.refresh_token_exp in
101- let jti = Uuidm.v4_gen (Random.get_state ()) () |> Uuidm.to_string in
00102 let access_payload =
103 symmetric_jwt_to_yojson
104 { scope= "com.atproto.access"
···19let b64_decode str =
20 match Base64.decode ~pad:false ~alphabet:Base64.uri_safe_alphabet str with
21 | Ok s ->
22+ s
23 | Error (`Msg e) ->
24+ failwith e
2526let extract_signature_components signature =
27 if Bytes.length signature <> 64 then failwith "expected 64 byte jwt signature"
···30 let s = Bytes.sub signature 32 32 in
31 (r, s)
3233+let sign_jwt payload ?(typ = "JWT") signing_key =
34 let _, (module Curve : Kleidos.CURVE) = signing_key in
35 let alg =
36 match Curve.name with
···51 failwith "invalid curve"
52 in
53 let header_json =
54+ `Assoc [("alg", `String alg); ("crv", `String crv); ("typ", `String typ)]
55 in
56 let encoded_header = header_json |> Yojson.Safe.to_string |> b64_encode in
57 let encoded_payload = payload |> Yojson.Safe.to_string |> b64_encode in
···65let decode_jwt jwt =
66 match String.split_on_char '.' jwt with
67 | [header_b64; payload_b64; _] -> (
68+ try
69+ let header = Yojson.Safe.from_string (b64_decode header_b64) in
70+ let payload = Yojson.Safe.from_string (b64_decode payload_b64) in
71+ Ok (header, payload)
72+ with _ -> Error "invalid jwt" )
000073 | _ ->
74 Error "invalid jwt format"
7576let verify_jwt jwt pubkey =
77 match String.split_on_char '.' jwt with
78+ | [header_b64; payload_b64; signature_b64] ->
79+ let signature = Bytes.of_string (b64_decode signature_b64) in
80+ let signing_input = header_b64 ^ "." ^ payload_b64 in
81+ let verified =
82+ Kleidos.verify ~pubkey ~msg:(Bytes.of_string signing_input) ~signature
83+ in
84+ if verified then decode_jwt jwt
85+ else Error "jwt signature verification failed"
000086 | _ ->
87 Error "invalid jwt format"
88···90 let now_s = int_of_float (Unix.gettimeofday ()) in
91 let access_exp = now_s + Defaults.access_token_exp in
92 let refresh_exp = now_s + Defaults.refresh_token_exp in
93+ let jti =
94+ Uuidm.v4_gen (Random.State.make_self_init ()) () |> Uuidm.to_string
95+ in
96 let access_payload =
97 symmetric_jwt_to_yojson
98 { scope= "com.atproto.access"
+45
pegasus/lib/oauth/client.ml
···000000000000000000000000000000000000000000000
···1+open Types
2+3+let fetch_client_metadata client_id : client_metadata Lwt.t =
4+ let%lwt {status; _}, res = Util.http_get (Uri.of_string client_id) in
5+ if status <> `OK then
6+ let%lwt () = Cohttp_lwt.Body.drain_body res in
7+ failwith
8+ (Printf.sprintf "client metadata not found; http %d"
9+ (Cohttp.Code.code_of_status status) )
10+ else
11+ let%lwt body = Cohttp_lwt.Body.to_string res in
12+ let json = Yojson.Safe.from_string body in
13+ let metadata =
14+ match client_metadata_of_yojson json with
15+ | Ok metadata ->
16+ metadata
17+ | Error err ->
18+ failwith err
19+ in
20+ if metadata.client_id <> client_id then failwith "client_id mismatch"
21+ else
22+ let scopes = String.split_on_char ' ' metadata.scope in
23+ if not (List.mem "atproto" scopes) then
24+ failwith "scope must include 'atproto'"
25+ else
26+ List.iter
27+ (function
28+ | "authorization_code" | "refresh_token" ->
29+ ()
30+ | grant ->
31+ failwith ("invalid grant type: " ^ grant) )
32+ metadata.grant_types ;
33+ List.iter
34+ (fun uri ->
35+ let u = Uri.of_string uri in
36+ let host = Uri.host u in
37+ match Uri.scheme u with
38+ | Some "https" when host <> Some "localhost" ->
39+ ()
40+ | Some "http" when host = Some "127.0.0.1" || host = Some "[::1]" ->
41+ ()
42+ | _ ->
43+ failwith ("invalid redirect_uri: " ^ uri) )
44+ metadata.redirect_uris ;
45+ Lwt.return metadata
···302 did
303 in
304 let headers = Http.Header.init_with "Accept" "application/json" in
305- let%lwt res, body = Client.get ~headers uri in
306 match res.status with
307 | `OK ->
308 let%lwt body = Body.to_string body in
···302 did
303 in
304 let headers = Http.Header.init_with "Accept" "application/json" in
305+ let%lwt res, body = Util.http_get ~headers uri in
306 match res.status with
307 | `OK ->
308 let%lwt body = Body.to_string body in
+11-11
pegasus/lib/repository.ml
···180 let%lwt map = get_map t in
181 String_map.bindings map
182 |> List.filter (fun (path, _) ->
183- String.starts_with ~prefix:(path ^ "/") collection )
184 |> Lwt_list.fold_left_s
185 (fun acc (path, cid) ->
186 match%lwt User_store.get_record t.db path with
···320 let%lwt () =
321 match old_cid with
322 | Some _ -> (
323- match%lwt User_store.get_record t.db path with
324- | Some record ->
325- let refs =
326- Util.find_blob_refs record.value
327- |> List.map (fun (r : Mist.Blob_ref.t) -> r.ref)
328- in
329- let%lwt () = User_store.clear_blob_refs t.db path refs in
330- Lwt.return_unit
331- | None ->
332- Lwt.return_unit )
333 | None ->
334 Lwt.return_unit
335 in
···180 let%lwt map = get_map t in
181 String_map.bindings map
182 |> List.filter (fun (path, _) ->
183+ String.starts_with ~prefix:(path ^ "/") collection )
184 |> Lwt_list.fold_left_s
185 (fun acc (path, cid) ->
186 match%lwt User_store.get_record t.db path with
···320 let%lwt () =
321 match old_cid with
322 | Some _ -> (
323+ match%lwt User_store.get_record t.db path with
324+ | Some record ->
325+ let refs =
326+ Util.find_blob_refs record.value
327+ |> List.map (fun (r : Mist.Blob_ref.t) -> r.ref)
328+ in
329+ let%lwt () = User_store.clear_blob_refs t.db path refs in
330+ Lwt.return_unit
331+ | None ->
332+ Lwt.return_unit )
333 | None ->
334 Lwt.return_unit
335 in
+28-28
pegasus/lib/sequencer.ml
···330 let blobs =
331 j |> member "blobs" |> to_list
332 |> List.filter_map (fun x ->
333- match Cid.of_yojson x with Ok c -> Some c | _ -> None )
334 in
335 let prev_data =
336 match j |> member "prevData" with
···342 let ops =
343 j |> member "ops" |> to_list
344 |> List.map (fun opj ->
345- let action =
346- match opj |> member "action" |> to_string with
347- | "create" ->
348- `Create
349- | "update" ->
350- `Update
351- | "delete" ->
352- `Delete
353- | _ ->
354- `Create
355- in
356- let path = opj |> member "path" |> to_string in
357- let cid =
358- match opj |> member "cid" with
359- | `Null ->
360- None
361- | v -> (
362- match Cid.of_yojson v with Ok c -> Some c | _ -> None )
363- in
364- let prev =
365- match opj |> member "prev" with
366- | `Null ->
367- None
368- | v -> (
369- match Cid.of_yojson v with Ok c -> Some c | _ -> None )
370- in
371- {action; path; cid; prev} )
372 in
373 Ok
374 { rebase
···330 let blobs =
331 j |> member "blobs" |> to_list
332 |> List.filter_map (fun x ->
333+ match Cid.of_yojson x with Ok c -> Some c | _ -> None )
334 in
335 let prev_data =
336 match j |> member "prevData" with
···342 let ops =
343 j |> member "ops" |> to_list
344 |> List.map (fun opj ->
345+ let action =
346+ match opj |> member "action" |> to_string with
347+ | "create" ->
348+ `Create
349+ | "update" ->
350+ `Update
351+ | "delete" ->
352+ `Delete
353+ | _ ->
354+ `Create
355+ in
356+ let path = opj |> member "path" |> to_string in
357+ let cid =
358+ match opj |> member "cid" with
359+ | `Null ->
360+ None
361+ | v -> (
362+ match Cid.of_yojson v with Ok c -> Some c | _ -> None )
363+ in
364+ let prev =
365+ match opj |> member "prev" with
366+ | `Null ->
367+ None
368+ | v -> (
369+ match Cid.of_yojson v with Ok c -> Some c | _ -> None )
370+ in
371+ {action; path; cid; prev} )
372 in
373 Ok
374 { rebase