objective categorical abstract machine language personal data server

Return invalid_token 401 error for expired oauth token

futur.blue 496a898e fd38dacc

verified
+49 -22
+2
pegasus/lib/api/account_/permissions.ml
··· 95 95 Oauth.Queries.delete_oauth_tokens_by_client ctx.db ~did 96 96 ~client_id 97 97 in 98 + Oauth.Dpop.revoke_tokens_for_did did ; 98 99 Dream.redirect ctx.req "/account/permissions" 99 100 | None -> 100 101 Dream.redirect ctx.req "/account/permissions" ) ··· 113 114 Oauth.Queries.delete_oauth_tokens_by_device ctx.db ~did 114 115 ~last_ip ~last_user_agent 115 116 in 117 + Oauth.Dpop.revoke_tokens_for_did did ; 116 118 Dream.redirect ctx.req "/account/permissions" 117 119 | _ -> 118 120 Dream.redirect ctx.req "/account/permissions" )
+5 -2
pegasus/lib/auth.ml
··· 258 258 ~url:(Dream.target req) ~dpop_header () 259 259 with 260 260 | Error "use_dpop_nonce" -> 261 - Lwt.return_error @@ Errors.use_dpop_nonce_auth () 261 + Lwt.return_error @@ Errors.dpop_auth "use_dpop_nonce" 262 262 | Error e -> 263 263 Log.debug (fun log -> log "dpop error: %s" e) ; 264 264 Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e) ··· 279 279 ~access_token:token () 280 280 with 281 281 | Error "use_dpop_nonce" -> 282 - Lwt.return_error @@ Errors.use_dpop_nonce_resource () 282 + Lwt.return_error @@ Errors.dpop_resource "use_dpop_nonce" 283 283 | Error e -> 284 284 Log.debug (fun log -> log "dpop error: %s" e) ; 285 285 Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e) ··· 292 292 let open Yojson.Safe.Util in 293 293 try 294 294 let did = claims |> member "sub" |> to_string in 295 + let iat = claims |> member "iat" |> to_int in 295 296 let exp = claims |> member "exp" |> to_int in 296 297 let jkt_claim = 297 298 claims |> member "cnf" |> member "jkt" |> to_string ··· 307 308 else if exp < now then 308 309 Lwt.return_error 309 310 @@ Errors.invalid_request ~name:"ExpiredToken" "token expired" 311 + else if Oauth.Dpop.is_token_revoked ~did ~iat then 312 + Lwt.return_error @@ Errors.dpop_resource "invalid_token" 310 313 else if not (Oauth.Scopes.has_atproto scopes) then 311 314 Lwt.return_error 312 315 @@ Errors.invalid_request ~name:"InvalidToken"
+17 -17
pegasus/lib/errors.ml
··· 8 8 9 9 exception Redirect of string 10 10 11 - (* HTTP 400, { error: "use_dpop_nonce" } — https://datatracker.ietf.org/doc/html/rfc9449#section-8 *) 12 - exception UseDpopNonceAuthError 11 + (* HTTP 400, { error: "..." } — https://datatracker.ietf.org/doc/html/rfc9449#section-8 *) 12 + exception DpopAuthError of string 13 13 14 - (* HTTP 401, WWW-Authenticate — https://datatracker.ietf.org/doc/html/rfc9449#section-9 *) 15 - exception UseDpopNonceResourceError 14 + (* HTTP 401, WWW-Authenticate=DPoP error="..." — https://datatracker.ietf.org/doc/html/rfc9449#section-9 *) 15 + exception DpopResourceError of string 16 16 17 17 let is_xrpc_error = function 18 18 | InvalidRequestError _ ··· 34 34 35 35 let not_found ?(name = "NotFound") msg = raise (NotFoundError (name, msg)) 36 36 37 - let use_dpop_nonce_auth () = raise UseDpopNonceAuthError 37 + let dpop_auth error = raise (DpopAuthError error) 38 38 39 - let use_dpop_nonce_resource () = raise UseDpopNonceResourceError 39 + let dpop_resource error = raise (DpopResourceError error) 40 40 41 41 let printer = function 42 42 | InvalidRequestError (error, message) -> ··· 47 47 Some (Printf.sprintf "Auth error (%s): %s" error message) 48 48 | NotFoundError (error, message) -> 49 49 Some (Printf.sprintf "Not found (%s): %s" error message) 50 - | UseDpopNonceAuthError -> 51 - Some "Use DPoP nonce" 52 - | UseDpopNonceResourceError -> 53 - Some "Use DPoP nonce" 50 + | DpopAuthError error -> 51 + Some (Printf.sprintf "DPoP auth error (%s)" error) 52 + | DpopResourceError error -> 53 + Some (Printf.sprintf "DPoP resource error (%s)" error) 54 54 | _ -> 55 55 None 56 56 ··· 72 72 | NotFoundError (error, message) -> 73 73 Log.debug (fun log -> log "not found error: %s - %s" error message) ; 74 74 format_response error message `Not_Found 75 - | UseDpopNonceAuthError -> 76 - Log.debug (fun log -> log "use_dpop_nonce auth error") ; 75 + | DpopAuthError e -> 76 + Log.debug (fun log -> log "dpop auth error") ; 77 77 Dream.json ~status:`Bad_Request 78 78 ~headers:[("Access-Control-Expose-Headers", "DPoP-Nonce")] 79 - {|{ "error": "use_dpop_nonce" }|} 80 - | UseDpopNonceResourceError -> 81 - Log.debug (fun log -> log "use_dpop_nonce resource error") ; 79 + (Format.sprintf {|{ "error": "%s" }|} e) 80 + | DpopResourceError e -> 81 + Log.debug (fun log -> log "dpop resource error") ; 82 82 Dream.json ~status:`Unauthorized 83 83 ~headers: 84 - [ ("WWW-Authenticate", {|DPoP error="use_dpop_nonce"|}) 84 + [ ("WWW-Authenticate", Format.sprintf {|DPoP error="%s"|} e) 85 85 ; ("Access-Control-Expose-Headers", "DPoP-Nonce, WWW-Authenticate") ] 86 - {|{ "error": "use_dpop_nonce" }|} 86 + (Format.sprintf {|{ "error": "%s" }|} e) 87 87 | e -> 88 88 Log.warn (fun log -> 89 89 log "unexpected internal error: %s" (Printexc.to_string e) ) ;
+22
pegasus/lib/oauth/dpop.ml
··· 20 20 (fun _ expires_at -> if expires_at > now then Some expires_at else None) 21 21 jti_cache 22 22 23 + let revocation_cache : (string, int) Hashtbl.t = Hashtbl.create 1000 24 + 25 + let cleanup_revocation_cache () = 26 + let now_s = int_of_float (Unix.gettimeofday ()) in 27 + let max_token_age_s = Constants.access_token_expiry_ms / 1000 in 28 + Hashtbl.filter_map_inplace 29 + (fun _ revoked_at -> 30 + if now_s - revoked_at < max_token_age_s then Some revoked_at else None ) 31 + revocation_cache 32 + 33 + let revoke_tokens_for_did did = 34 + let now_s = int_of_float (Unix.gettimeofday ()) in 35 + Hashtbl.replace revocation_cache did now_s ; 36 + if Hashtbl.length revocation_cache mod 50 = 0 then cleanup_revocation_cache () 37 + 38 + let is_token_revoked ~did ~iat = 39 + match Hashtbl.find_opt revocation_cache did with 40 + | Some revoked_at -> 41 + iat < revoked_at 42 + | None -> 43 + false 44 + 23 45 let compute_nonce secret counter = 24 46 let data = Bytes.create 8 in 25 47 Bytes.set_int64_be data 0 (Int64.of_int counter) ;
+3 -3
pegasus/lib/xrpc.ml
··· 108 108 in 109 109 let () = 110 110 let to_expose = 111 - (* see comments on UseDpopNonce____Error in errors.ml *) 111 + (* see comments on Dpop____Error in errors.ml *) 112 112 if Dream.status res = `Unauthorized then "DPoP-Nonce, WWW-Authenticate" 113 113 else if Dream.status res = `Bad_Request then "DPoP-Nonce" 114 114 else "" ··· 146 146 let%lwt res = exn_to_response e in 147 147 Lwt.return 148 148 ( match e with 149 - | UseDpopNonceAuthError | UseDpopNonceResourceError -> 149 + | DpopAuthError _ | DpopResourceError _ -> 150 150 add_dpop_nonce_if_needed res 151 151 | _ -> 152 152 res ) ··· 155 155 Dream.redirect init.req r 156 156 | Rate_limiter.Rate_limit_exceeded status -> 157 157 rate_limit_response status 158 - | (UseDpopNonceAuthError | UseDpopNonceResourceError) as e -> 158 + | (DpopAuthError _ | DpopResourceError _) as e -> 159 159 let%lwt res = exn_to_response e in 160 160 Lwt.return (add_dpop_nonce_if_needed res) 161 161 | e ->