objective categorical abstract machine language personal data server

Handle service auth to PDS

futur.blue 892efdd9 a5fb700f

verified
+140 -1
+128 -1
pegasus/lib/auth.ml
··· 169 169 Error "invalid authorization header" ) 170 170 | None -> 171 171 Error "missing authorization header" 172 + 173 + let is_service_auth_token req = 174 + match parse_header req "Bearer" with 175 + | Error _ -> 176 + false 177 + | Ok token -> ( 178 + match Jwt.decode_jwt token with 179 + | Error _ -> 180 + false 181 + | Ok (_header, payload) -> ( 182 + match Yojson.Safe.Util.member "lxm" payload with 183 + | `Null -> 184 + false 185 + | _ -> 186 + true ) ) 172 187 end 173 188 174 189 let parse_basic req = ··· 354 369 | Error _ -> 355 370 Lwt.return_error @@ Errors.auth_required "invalid authorization header" 356 371 372 + let user_service_auth : verifier = 373 + let check_actor_status did db = 374 + match%lwt Data_store.get_actor_by_identifier did db with 375 + | Some {deactivated_at= None; _} -> 376 + Lwt.return_ok (Access {did}) 377 + | Some {deactivated_at= Some _; _} -> 378 + Lwt.return_error 379 + @@ Errors.auth_required ~name:"AccountDeactivated" 380 + "account is deactivated" 381 + | None -> 382 + Lwt.return_error @@ Errors.auth_required "invalid credentials" 383 + in 384 + let verify_with_key token pubkey_multibase did db = 385 + let pubkey = 386 + Kleidos.parse_multikey_str pubkey_multibase 387 + in 388 + match Jwt.verify_jwt token ~pubkey with 389 + | Ok _ -> 390 + check_actor_status did db 391 + | Error e -> 392 + Dream.debug (fun log -> log "service jwt verification failed: %s" e) ; 393 + Lwt.return_error 394 + @@ Errors.auth_required "jwt signature does not match jwt issuer" 395 + in 396 + fun {req; db} -> 397 + match parse_bearer req with 398 + | Error e -> 399 + Lwt.return_error @@ Errors.auth_required e 400 + | Ok token -> ( 401 + match Jwt.decode_jwt token with 402 + | Error e -> 403 + Lwt.return_error @@ Errors.auth_required e 404 + | Ok (_header, payload) -> ( 405 + try 406 + let open Yojson.Safe.Util in 407 + let iss = payload |> member "iss" |> to_string in 408 + let aud = payload |> member "aud" |> to_string in 409 + let exp = payload |> member "exp" |> to_int in 410 + let lxm = payload |> member "lxm" |> to_string_option in 411 + let now = int_of_float (Unix.gettimeofday ()) in 412 + if exp < now then 413 + Lwt.return_error @@ Errors.auth_required "jwt expired" 414 + else if aud <> Env.did then 415 + Lwt.return_error 416 + @@ Errors.auth_required "jwt audience does not match service did" 417 + else 418 + let nsid = 419 + (Dream.path [@warning "-3"]) req |> List.rev |> List.hd 420 + in 421 + ( match lxm with 422 + | Some l when l <> nsid && l <> "*" -> 423 + Lwt.return_error 424 + @@ Errors.auth_required 425 + ("jwt lxm " ^ l ^ " does not match " ^ nsid) 426 + | _ -> 427 + let did = 428 + match String.split_on_char '#' iss with 429 + | did :: _ -> 430 + did 431 + | [] -> 432 + iss 433 + in 434 + ( match%lwt Id_resolver.Did.resolve did with 435 + | Error e -> 436 + Dream.debug (fun log -> 437 + log "failed to resolve did %s: %s" did e ) ; 438 + Lwt.return_error 439 + @@ Errors.auth_required "could not resolve issuer did" 440 + | Ok did_doc -> ( 441 + match 442 + Id_resolver.Did.Document.get_verification_key did_doc 443 + "#atproto" 444 + with 445 + | None -> 446 + Lwt.return_error 447 + @@ Errors.auth_required 448 + "missing or bad key in issuer did doc" 449 + | Some pubkey_multibase -> ( 450 + match%lwt verify_with_key token pubkey_multibase did db with 451 + | Ok creds -> 452 + Lwt.return_ok creds 453 + | Error _ -> 454 + (* try again, skipping cache in case of key rotation *) 455 + ( match%lwt 456 + Id_resolver.Did.resolve ~skip_cache:true did 457 + with 458 + | Error _ -> 459 + Lwt.return_error 460 + @@ Errors.auth_required 461 + "jwt signature does not match jwt issuer" 462 + | Ok fresh_doc -> ( 463 + match 464 + Id_resolver.Did.Document.get_verification_key 465 + fresh_doc "#atproto" 466 + with 467 + | None -> 468 + Lwt.return_error 469 + @@ Errors.auth_required 470 + "jwt signature does not match jwt issuer" 471 + | Some fresh_pubkey_multibase 472 + when fresh_pubkey_multibase = pubkey_multibase -> 473 + Lwt.return_error 474 + @@ Errors.auth_required 475 + "jwt signature does not match jwt issuer" 476 + | Some fresh_pubkey_multibase -> 477 + verify_with_key token fresh_pubkey_multibase did 478 + db ) ) ) ) ) ) 479 + with _ -> 480 + Lwt.return_error @@ Errors.auth_required "malformed service jwt" ) ) 481 + 357 482 let authorization : verifier = 358 483 fun ctx -> 359 484 match ··· 363 488 | Some ("Basic" :: _) -> 364 489 admin ctx 365 490 | Some ("Bearer" :: _) -> 366 - bearer ctx 491 + (* Check if it's a service auth token (has lxm claim) *) 492 + if is_service_auth_token ctx.req then user_service_auth ctx 493 + else bearer ctx 367 494 | Some ("DPoP" :: _) -> 368 495 oauth ctx 369 496 | _ -> (
+12
pegasus/lib/id_resolver.ml
··· 150 150 (fun (s : service) -> 151 151 if s.id = fragment then Some (get_service_endpoint s) else None ) 152 152 services 153 + 154 + let get_verification_key t fragment = 155 + match t.verification_method with 156 + | None -> 157 + None 158 + | Some methods -> 159 + List.find_map 160 + (fun (vm : verification_method) -> 161 + if vm.id = fragment || vm.id = t.id ^ fragment then 162 + vm.public_key_multibase 163 + else None ) 164 + methods 153 165 end 154 166 155 167 type document = Document.t