objective categorical abstract machine language personal data server
65
fork

Configure Feed

Select the types of activity you want to include in your feed.

dedupe dpop verification

futurGH 9de6c2a8 ade2c30a

+234 -255
+33 -47
pegasus/lib/api/oauth_/par.ml
··· 12 12 [@@deriving yojson] 13 13 14 14 let handler = 15 - Xrpc.handler (fun ctx -> 16 - let proof = 17 - Dpop.verify_dpop_proof 18 - ~mthd:(Dream.method_to_string @@ Dream.method_ ctx.req) 19 - ~url:(Dream.target ctx.req) 20 - ~dpop_header:(Dream.header ctx.req "DPoP") 21 - () 15 + Xrpc.handler ~auth:DPoP (fun ctx -> 16 + let proof = Auth.get_dpop_proof_exn ctx.auth in 17 + let%lwt req = Xrpc.parse_body ctx.req request_of_yojson in 18 + let%lwt client = 19 + try%lwt Client.fetch_client_metadata req.client_id 20 + with e -> 21 + Errors.log_exn ~req:ctx.req e ; 22 + Errors.invalid_request "failed to fetch client metadata" 22 23 in 23 - match proof with 24 - | Error "use_dpop_nonce" -> 25 - Dream.json ~status:`Bad_Request 26 - @@ Yojson.Safe.to_string 27 - @@ `Assoc [("error", `String "use_dpop_nonce")] 28 - | Error e -> 29 - Errors.invalid_request e 30 - | Ok proof -> 31 - let%lwt req = Xrpc.parse_body ctx.req request_of_yojson in 32 - let%lwt client = 33 - try%lwt Client.fetch_client_metadata req.client_id 34 - with e -> 35 - Errors.log_exn ~req:ctx.req e ; 36 - Errors.invalid_request "failed to fetch client metadata" 37 - in 38 - if req.response_type <> "code" then 39 - Errors.invalid_request "only response_type=code supported" 40 - else if req.code_challenge_method <> "S256" then 41 - Errors.invalid_request "only code_challenge_method=S256 supported" 42 - else if not (List.mem req.redirect_uri client.redirect_uris) then 43 - Errors.invalid_request "invalid redirect_uri" 44 - else 45 - let request_id = 46 - "req-" ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ()) 47 - in 48 - let request_uri = Constants.request_uri_prefix ^ request_id in 49 - let expires_at = Util.now_ms () + Constants.par_request_ttl_ms in 50 - let%lwt () = 51 - Queries.insert_par_request ctx.db 52 - { request_id 53 - ; client_id= req.client_id 54 - ; request_data= Yojson.Safe.to_string (request_to_yojson req) 55 - ; dpop_jkt= Some proof.jkt 56 - ; expires_at 57 - ; created_at= Util.now_ms () } 58 - in 59 - Dream.json ~status:`Created 60 - @@ Yojson.Safe.to_string 61 - @@ `Assoc 62 - [("request_uri", `String request_uri); ("expires_in", `Int 300)] ) 24 + if req.response_type <> "code" then 25 + Errors.invalid_request "only response_type=code supported" 26 + else if req.code_challenge_method <> "S256" then 27 + Errors.invalid_request "only code_challenge_method=S256 supported" 28 + else if not (List.mem req.redirect_uri client.redirect_uris) then 29 + Errors.invalid_request "invalid redirect_uri" 30 + else 31 + let request_id = 32 + "req-" ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ()) 33 + in 34 + let request_uri = Constants.request_uri_prefix ^ request_id in 35 + let expires_at = Util.now_ms () + Constants.par_request_ttl_ms in 36 + let%lwt () = 37 + Queries.insert_par_request ctx.db 38 + { request_id 39 + ; client_id= req.client_id 40 + ; request_data= Yojson.Safe.to_string (request_to_yojson req) 41 + ; dpop_jkt= Some proof.jkt 42 + ; expires_at 43 + ; created_at= Util.now_ms () } 44 + in 45 + Dream.json ~status:`Created 46 + @@ Yojson.Safe.to_string 47 + @@ `Assoc 48 + [("request_uri", `String request_uri); ("expires_in", `Int 300)] )
+165 -182
pegasus/lib/api/oauth_/token.ml
··· 1 1 open Oauth 2 2 3 3 let handler = 4 - Xrpc.handler (fun ctx -> 4 + Xrpc.handler ~auth:DPoP (fun ctx -> 5 5 let%lwt req = Xrpc.parse_body ctx.req Types.token_request_of_yojson in 6 - let dpop_header = Dream.header ctx.req "DPoP" in 7 - let full_url = "https://" ^ Env.hostname ^ Dream.target ctx.req in 8 - let dpop_result = 9 - Dpop.verify_dpop_proof 10 - ~mthd:(Dream.method_to_string @@ Dream.method_ ctx.req) 11 - ~url:full_url ~dpop_header () 12 - in 13 - match dpop_result with 14 - | Error "use_dpop_nonce" -> 15 - Dream.json ~status:`Bad_Request 16 - @@ Yojson.Safe.to_string 17 - @@ `Assoc [("error", `String "use_dpop_nonce")] 18 - | Error e -> 19 - Errors.invalid_request ("DPoP error: " ^ e) 20 - | Ok proof -> ( 21 - match req.grant_type with 22 - | "authorization_code" -> ( 23 - match req.code with 24 - | None -> 25 - Errors.invalid_request "code required" 26 - | Some code -> ( 27 - let%lwt code_record = Queries.consume_auth_code ctx.db code in 28 - match code_record with 29 - | None -> 30 - Errors.invalid_request "invalid code" 31 - | Some code_rec -> ( 32 - if Util.now_ms () > code_rec.expires_at then 33 - Errors.invalid_request "code expired" 34 - else 35 - match code_rec.authorized_by with 36 - | None -> 37 - Errors.invalid_request "code not authorized" 38 - | Some did -> ( 39 - let%lwt par_req = 40 - Queries.get_par_request ctx.db code_rec.request_id 41 - in 42 - match par_req with 43 - | None -> 44 - Errors.internal_error ~msg:"request not found" () 45 - | Some par_record -> 46 - let orig_req = 47 - Yojson.Safe.from_string par_record.request_data 48 - |> Types.par_request_of_yojson |> Result.get_ok 49 - in 50 - ( match req.redirect_uri with 51 - | None -> 52 - Errors.invalid_request "redirect_uri required" 53 - | Some uri when uri <> orig_req.redirect_uri -> 54 - Errors.invalid_request "redirect_uri mismatch" 55 - | _ -> 56 - () ) ; 57 - ( match req.code_verifier with 58 - | None -> 59 - Errors.invalid_request "code_verifier required" 60 - | Some verifier -> 61 - let computed = 62 - Digestif.SHA256.digest_string verifier 63 - |> Digestif.SHA256.to_raw_string 64 - |> Base64.encode_exn ~pad:false 65 - in 66 - if orig_req.code_challenge <> computed then 67 - Errors.invalid_request "invalid code_verifier" 68 - ) ; 69 - ( match par_record.dpop_jkt with 70 - | Some stored when stored <> proof.jkt -> 71 - Errors.invalid_request "DPoP key mismatch" 72 - | _ -> 73 - () ) ; 74 - let token_id = 75 - "tok-" 76 - ^ Uuidm.to_string 77 - (Uuidm.v4_gen (Random.get_state ()) ()) 78 - in 79 - let refresh_token = 80 - "ref-" 81 - ^ Uuidm.to_string 82 - (Uuidm.v4_gen (Random.get_state ()) ()) 83 - in 84 - let now_sec = int_of_float (Unix.gettimeofday ()) in 85 - let expires_in = 86 - Constants.access_token_expiry_ms / 1000 87 - in 88 - let exp_sec = now_sec + expires_in in 89 - let expires_at = exp_sec * 1000 in 90 - let claims = 91 - `Assoc 92 - [ ("jti", `String token_id) 93 - ; ("sub", `String did) 94 - ; ("iat", `Int now_sec) 95 - ; ("exp", `Int exp_sec) 96 - ; ("scope", `String orig_req.scope) 97 - ; ("aud", `String ("https://" ^ Env.hostname)) 98 - ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) 99 - ] 100 - in 101 - let access_token = 102 - Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key 103 - in 104 - let%lwt () = 105 - Queries.insert_oauth_token ctx.db 106 - { refresh_token 107 - ; client_id= req.client_id 108 - ; did 109 - ; dpop_jkt= proof.jkt 110 - ; scope= orig_req.scope 111 - ; expires_at } 112 - in 113 - let nonce = Dpop.next_nonce () in 114 - Dream.json 115 - ~headers: 116 - [ ("DPoP-Nonce", nonce) 117 - ; ("Access-Control-Expose-Headers", "DPoP-Nonce") 118 - ; ("Cache-Control", "no-store") ] 119 - @@ Yojson.Safe.to_string 120 - @@ `Assoc 121 - [ ("access_token", `String access_token) 122 - ; ("token_type", `String "DPoP") 123 - ; ("refresh_token", `String refresh_token) 124 - ; ("expires_in", `Int expires_in) 125 - ; ("scope", `String orig_req.scope) 126 - ; ("sub", `String did) ] ) ) ) ) 127 - | "refresh_token" -> ( 128 - match req.refresh_token with 129 - | None -> 130 - Errors.invalid_request "refresh_token required" 131 - | Some refresh_token -> ( 132 - let%lwt token_record = 133 - Queries.get_oauth_token_by_refresh ctx.db refresh_token 134 - in 135 - match token_record with 136 - | None -> 137 - Errors.invalid_request "invalid refresh token" 138 - | Some session -> 139 - if session.client_id <> req.client_id then 140 - Errors.invalid_request "client_id mismatch" 141 - else if session.dpop_jkt <> proof.jkt then 142 - Errors.invalid_request "DPoP key mismatch" 143 - else 144 - let new_token_id = 145 - "tok-" 146 - ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ()) 147 - in 148 - let new_refresh = 149 - "ref-" 150 - ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ()) 151 - in 152 - let now_sec = int_of_float (Unix.gettimeofday ()) in 153 - let expires_in = Constants.access_token_expiry_ms / 1000 in 154 - let exp_sec = now_sec + expires_in in 155 - let new_expires_at = exp_sec * 1000 in 156 - let claims = 157 - `Assoc 158 - [ ("jti", `String new_token_id) 159 - ; ("sub", `String session.did) 160 - ; ("iat", `Int now_sec) 161 - ; ("exp", `Int exp_sec) 162 - ; ("scope", `String session.scope) 163 - ; ("aud", `String ("https://" ^ Env.hostname)) 164 - ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ] 165 - in 166 - let new_access_token = 167 - Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key 168 - in 169 - let%lwt () = 170 - Queries.update_oauth_token ctx.db 171 - ~old_refresh_token:refresh_token ~new_token_id 172 - ~new_refresh_token:new_refresh 173 - ~expires_at:new_expires_at 174 - in 175 - Dream.json ~headers:[("Cache-Control", "no-store")] 176 - @@ Yojson.Safe.to_string 177 - @@ `Assoc 178 - [ ("access_token", `String new_access_token) 179 - ; ("token_type", `String "DPoP") 180 - ; ("refresh_token", `String new_refresh) 181 - ; ("expires_in", `Int expires_in) 182 - ; ("scope", `String session.scope) 183 - ; ("sub", `String session.did) ] ) ) 184 - | _ -> 185 - Errors.invalid_request ("unsupported grant_type: " ^ req.grant_type) 186 - ) ) 6 + let proof = Auth.get_dpop_proof_exn ctx.auth in 7 + match req.grant_type with 8 + | "authorization_code" -> ( 9 + match req.code with 10 + | None -> 11 + Errors.invalid_request "code required" 12 + | Some code -> ( 13 + let%lwt code_record = Queries.consume_auth_code ctx.db code in 14 + match code_record with 15 + | None -> 16 + Errors.invalid_request "invalid code" 17 + | Some code_rec -> ( 18 + if Util.now_ms () > code_rec.expires_at then 19 + Errors.invalid_request "code expired" 20 + else 21 + match code_rec.authorized_by with 22 + | None -> 23 + Errors.invalid_request "code not authorized" 24 + | Some did -> ( 25 + let%lwt par_req = 26 + Queries.get_par_request ctx.db code_rec.request_id 27 + in 28 + match par_req with 29 + | None -> 30 + Errors.internal_error ~msg:"request not found" () 31 + | Some par_record -> 32 + let orig_req = 33 + Yojson.Safe.from_string par_record.request_data 34 + |> Types.par_request_of_yojson |> Result.get_ok 35 + in 36 + ( match req.redirect_uri with 37 + | None -> 38 + Errors.invalid_request "redirect_uri required" 39 + | Some uri when uri <> orig_req.redirect_uri -> 40 + Errors.invalid_request "redirect_uri mismatch" 41 + | _ -> 42 + () ) ; 43 + ( match req.code_verifier with 44 + | None -> 45 + Errors.invalid_request "code_verifier required" 46 + | Some verifier -> 47 + let computed = 48 + Digestif.SHA256.digest_string verifier 49 + |> Digestif.SHA256.to_raw_string 50 + |> Base64.encode_exn ~pad:false 51 + in 52 + if orig_req.code_challenge <> computed then 53 + Errors.invalid_request "invalid code_verifier" 54 + ) ; 55 + ( match par_record.dpop_jkt with 56 + | Some stored when stored <> proof.jkt -> 57 + Errors.invalid_request "DPoP key mismatch" 58 + | _ -> 59 + () ) ; 60 + let token_id = 61 + "tok-" 62 + ^ Uuidm.to_string 63 + (Uuidm.v4_gen (Random.get_state ()) ()) 64 + in 65 + let refresh_token = 66 + "ref-" 67 + ^ Uuidm.to_string 68 + (Uuidm.v4_gen (Random.get_state ()) ()) 69 + in 70 + let now_sec = int_of_float (Unix.gettimeofday ()) in 71 + let expires_in = 72 + Constants.access_token_expiry_ms / 1000 73 + in 74 + let exp_sec = now_sec + expires_in in 75 + let expires_at = exp_sec * 1000 in 76 + let claims = 77 + `Assoc 78 + [ ("jti", `String token_id) 79 + ; ("sub", `String did) 80 + ; ("iat", `Int now_sec) 81 + ; ("exp", `Int exp_sec) 82 + ; ("scope", `String orig_req.scope) 83 + ; ("aud", `String ("https://" ^ Env.hostname)) 84 + ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ] 85 + in 86 + let access_token = 87 + Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key 88 + in 89 + let%lwt () = 90 + Queries.insert_oauth_token ctx.db 91 + { refresh_token 92 + ; client_id= req.client_id 93 + ; did 94 + ; dpop_jkt= proof.jkt 95 + ; scope= orig_req.scope 96 + ; expires_at } 97 + in 98 + let nonce = Dpop.next_nonce () in 99 + Dream.json 100 + ~headers: 101 + [ ("DPoP-Nonce", nonce) 102 + ; ("Access-Control-Expose-Headers", "DPoP-Nonce") 103 + ; ("Cache-Control", "no-store") ] 104 + @@ Yojson.Safe.to_string 105 + @@ `Assoc 106 + [ ("access_token", `String access_token) 107 + ; ("token_type", `String "DPoP") 108 + ; ("refresh_token", `String refresh_token) 109 + ; ("expires_in", `Int expires_in) 110 + ; ("scope", `String orig_req.scope) 111 + ; ("sub", `String did) ] ) ) ) ) 112 + | "refresh_token" -> ( 113 + match req.refresh_token with 114 + | None -> 115 + Errors.invalid_request "refresh_token required" 116 + | Some refresh_token -> ( 117 + let%lwt token_record = 118 + Queries.get_oauth_token_by_refresh ctx.db refresh_token 119 + in 120 + match token_record with 121 + | None -> 122 + Errors.invalid_request "invalid refresh token" 123 + | Some session -> 124 + if session.client_id <> req.client_id then 125 + Errors.invalid_request "client_id mismatch" 126 + else if session.dpop_jkt <> proof.jkt then 127 + Errors.invalid_request "DPoP key mismatch" 128 + else 129 + let new_token_id = 130 + "tok-" 131 + ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ()) 132 + in 133 + let new_refresh = 134 + "ref-" 135 + ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ()) 136 + in 137 + let now_sec = int_of_float (Unix.gettimeofday ()) in 138 + let expires_in = Constants.access_token_expiry_ms / 1000 in 139 + let exp_sec = now_sec + expires_in in 140 + let new_expires_at = exp_sec * 1000 in 141 + let claims = 142 + `Assoc 143 + [ ("jti", `String new_token_id) 144 + ; ("sub", `String session.did) 145 + ; ("iat", `Int now_sec) 146 + ; ("exp", `Int exp_sec) 147 + ; ("scope", `String session.scope) 148 + ; ("aud", `String ("https://" ^ Env.hostname)) 149 + ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ] 150 + in 151 + let new_access_token = 152 + Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key 153 + in 154 + let%lwt () = 155 + Queries.update_oauth_token ctx.db 156 + ~old_refresh_token:refresh_token ~new_token_id 157 + ~new_refresh_token:new_refresh ~expires_at:new_expires_at 158 + in 159 + Dream.json ~headers:[("Cache-Control", "no-store")] 160 + @@ Yojson.Safe.to_string 161 + @@ `Assoc 162 + [ ("access_token", `String new_access_token) 163 + ; ("token_type", `String "DPoP") 164 + ; ("refresh_token", `String new_refresh) 165 + ; ("expires_in", `Int expires_in) 166 + ; ("scope", `String session.scope) 167 + ; ("sub", `String session.did) ] ) ) 168 + | _ -> 169 + Errors.invalid_request ("unsupported grant_type: " ^ req.grant_type) )
+36 -26
pegasus/lib/auth.ml
··· 15 15 | Admin 16 16 | Access of {did: string} 17 17 | Refresh of {did: string; jti: string} 18 + | OAuth of {did: string; proof: Oauth.Dpop.proof} 18 19 19 20 let verify_bearer_jwt t token expected_scope = 20 21 match Jwt.verify_jwt token Env.jwt_key with ··· 42 43 match credentials with 43 44 | Admin -> 44 45 true 45 - | Access {did= creds} when creds = did -> 46 + | (Access {did= creds} | OAuth {did= creds; _}) when creds = did -> 46 47 true 47 48 | Refresh {did= creds; _} when creds = did && refresh -> 48 49 true ··· 50 51 false 51 52 52 53 let get_authed_did_exn = function 53 - | Access {did} -> 54 + | Access {did} | OAuth {did; _} -> 54 55 did 55 56 | Refresh {did; _} -> 56 57 did 57 58 | _ -> 58 - Errors.auth_required "Invalid authorization header" 59 + Errors.auth_required "invalid authorization header" 60 + 61 + let get_dpop_proof_exn = function 62 + | OAuth {proof; _} -> 63 + proof 64 + | _ -> 65 + Errors.invalid_request "invalid DPoP header" 59 66 60 67 let get_session_info identifier db = 61 68 let%lwt actor = ··· 160 167 | Error _ -> 161 168 Lwt.return_error @@ Errors.auth_required "invalid authorization header" 162 169 163 - let oauth : verifier = 170 + let dpop : verifier = 164 171 fun {req; db} -> 165 172 match parse_dpop req with 166 - | Error _ -> 167 - Lwt.return_error @@ Errors.auth_required "missing authorization header" 173 + | Error e -> 174 + Errors.invalid_request ("dpop error: " ^ e) 168 175 | Ok token -> ( 169 176 let dpop_header = Dream.header req "DPoP" in 170 177 match ··· 172 179 ~mthd:(Dream.method_to_string @@ Dream.method_ req) 173 180 ~url:(Dream.target req) ~dpop_header ~access_token:token () 174 181 with 182 + | Error "use_dpop_nonce" -> 183 + Lwt.return_error 184 + (* error must be this object; see https://datatracker.ietf.org/doc/html/rfc9449#section-8 *) 185 + @@ Errors.invalid_request {|{ "error": "use_dpop_nonce" }|} 175 186 | Error e -> 176 - Lwt.return_error @@ Errors.auth_required ("dpop: " ^ e) 187 + Errors.invalid_request ("dpop error: " ^ e) 177 188 | Ok proof -> ( 178 189 match Jwt.verify_jwt token Env.jwt_key with 179 190 | Error e -> ··· 192 203 else if exp < now then 193 204 Lwt.return_error @@ Errors.auth_required "token expired" 194 205 else 195 - match%lwt Data_store.get_actor_by_identifier did db with 196 - | Some {deactivated_at= None; _} -> 197 - Lwt.return_ok (Access {did}) 198 - | Some {deactivated_at= Some _; _} -> 199 - Lwt.return_error 200 - @@ Errors.auth_required ~name:"AccountDeactivated" 201 - "account is deactivated" 202 - | None -> 203 - Lwt.return_error 204 - @@ Errors.auth_required "invalid credentials" 206 + let%lwt {active; _} = 207 + try%lwt get_session_info did db 208 + with _ -> Errors.auth_required "invalid credentials" 209 + in 210 + if active <> Some true then 211 + Lwt.return_error 212 + @@ Errors.auth_required ~name:"AccountDeactivated" 213 + "account is deactivated" 214 + else Lwt.return_ok (Access {did}) 205 215 with _ -> 206 216 Lwt.return_error @@ Errors.auth_required "malformed JWT claims" 207 217 ) ) ) ··· 218 228 | Some {deactivated_at= Some _; _} -> 219 229 Lwt.return_error 220 230 @@ Errors.auth_required ~name:"AccountDeactivated" 221 - "Account is deactivated" 231 + "account is deactivated" 222 232 | None -> 223 - Lwt.return_error @@ Errors.auth_required "Invalid credentials" ) 233 + Lwt.return_error @@ Errors.auth_required "invalid credentials" ) 224 234 | Error "" | Error _ -> 225 - Lwt.return_error @@ Errors.auth_required "Invalid credentials" ) 235 + Lwt.return_error @@ Errors.auth_required "invalid credentials" ) 226 236 | Error _ -> 227 - Lwt.return_error @@ Errors.auth_required "Invalid authorization header" 237 + Lwt.return_error @@ Errors.auth_required "invalid authorization header" 228 238 229 239 let authorization : verifier = 230 240 fun ctx -> ··· 237 247 | Some ("Bearer" :: _) -> 238 248 bearer ctx 239 249 | Some ("DPoP" :: _) -> 240 - oauth ctx 250 + dpop ctx 241 251 | _ -> 242 252 Lwt.return_error 243 253 @@ Errors.auth_required ~name:"InvalidToken" 244 - "Unexpected authorization type" 254 + "unexpected authorization type" 245 255 246 256 let any : verifier = 247 257 fun ctx -> try authorization ctx with _ -> unauthenticated ctx ··· 250 260 | Unauthenticated 251 261 | Admin 252 262 | Bearer 253 - | Oauth 263 + | DPoP 254 264 | Refresh 255 265 | Authorization 256 266 | Any ··· 262 272 admin 263 273 | Bearer -> 264 274 bearer 265 - | Oauth -> 266 - oauth 275 + | DPoP -> 276 + dpop 267 277 | Refresh -> 268 278 refresh 269 279 | Authorization ->