···9898 *)
9999 Dream.html "" ) ) ) )
100100101101-let post_handler pool =
101101+let post_handler =
102102 Xrpc.handler (fun ctx ->
103103 match%lwt get_session_user ctx with
104104 | None ->
···117117 (String.length request_uri - String.length prefix)
118118 in
119119 let%lwt req_record =
120120- Oauth.Queries.get_par_request pool request_id
120120+ Oauth.Queries.get_par_request ctx.db request_id
121121 in
122122 match req_record with
123123 | Some rec_ ->
···141141 | None ->
142142 Errors.invalid_request "request expired" )
143143 | Some "allow", Some code, Some _request_uri -> (
144144- let%lwt code_record = Oauth.Queries.get_auth_code pool code in
144144+ let%lwt code_record =
145145+ Oauth.Queries.get_auth_code ctx.db code
146146+ in
145147 match code_record with
146148 | None ->
147149 Errors.invalid_request "invalid code"
···154156 Errors.invalid_request "code expired"
155157 else
156158 let%lwt () =
157157- Oauth.Queries.activate_auth_code pool code user_did
159159+ Oauth.Queries.activate_auth_code ctx.db code user_did
158160 in
159161 let%lwt req_record =
160160- Oauth.Queries.get_par_request pool code_rec.request_id
162162+ Oauth.Queries.get_par_request ctx.db
163163+ code_rec.request_id
161164 in
162165 match req_record with
163166 | None ->
+1-1
pegasus/lib/api/oauth_/par.ml
···4141 Errors.invalid_request "invalid redirect_uri"
4242 else
4343 let request_id =
4444- "req-" ^ (Uuidm.v4_gen (Random.get_state ()) () |> Uuidm.to_string)
4444+ "req-" ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ())
4545 in
4646 let request_uri = Oauth.Constants.request_uri_prefix ^ request_id in
4747 let expires_at =
+186
pegasus/lib/api/oauth_/token.ml
···11+open Oauth
22+33+let handler =
44+ Xrpc.handler (fun ctx ->
55+ let%lwt req = Xrpc.parse_body ctx.req Types.token_request_of_yojson in
66+ let dpop_header = Dream.header ctx.req "DPoP" in
77+ let full_url = "https://" ^ Env.hostname ^ Dream.target ctx.req in
88+ let dpop_result =
99+ Dpop.verify_dpop_proof
1010+ ~mthd:(Dream.method_to_string @@ Dream.method_ ctx.req)
1111+ ~url:full_url ~dpop_header ()
1212+ in
1313+ match dpop_result with
1414+ | Error "use_dpop_nonce" ->
1515+ Dream.json ~status:`Bad_Request
1616+ @@ Yojson.Safe.to_string
1717+ @@ `Assoc [("error", `String "use_dpop_nonce")]
1818+ | Error e ->
1919+ Errors.invalid_request ("DPoP error: " ^ e)
2020+ | Ok proof -> (
2121+ match req.grant_type with
2222+ | "authorization_code" -> (
2323+ match req.code with
2424+ | None ->
2525+ Errors.invalid_request "code required"
2626+ | Some code -> (
2727+ let%lwt code_record = Queries.consume_auth_code ctx.db code in
2828+ match code_record with
2929+ | None ->
3030+ Errors.invalid_request "invalid code"
3131+ | Some code_rec -> (
3232+ if Util.now_ms () > code_rec.expires_at then
3333+ Errors.invalid_request "code expired"
3434+ else
3535+ match code_rec.authorized_by with
3636+ | None ->
3737+ Errors.invalid_request "code not authorized"
3838+ | Some did -> (
3939+ let%lwt par_req =
4040+ Queries.get_par_request ctx.db code_rec.request_id
4141+ in
4242+ match par_req with
4343+ | None ->
4444+ Errors.internal_error ~msg:"request not found" ()
4545+ | Some par_record ->
4646+ let orig_req =
4747+ Yojson.Safe.from_string par_record.request_data
4848+ |> Types.par_request_of_yojson |> Result.get_ok
4949+ in
5050+ ( match req.redirect_uri with
5151+ | None ->
5252+ Errors.invalid_request "redirect_uri required"
5353+ | Some uri when uri <> orig_req.redirect_uri ->
5454+ Errors.invalid_request "redirect_uri mismatch"
5555+ | _ ->
5656+ () ) ;
5757+ ( match req.code_verifier with
5858+ | None ->
5959+ Errors.invalid_request "code_verifier required"
6060+ | Some verifier ->
6161+ let computed =
6262+ Digestif.SHA256.digest_string verifier
6363+ |> Digestif.SHA256.to_raw_string
6464+ |> Base64.encode_exn ~pad:false
6565+ in
6666+ if orig_req.code_challenge <> computed then
6767+ Errors.invalid_request "invalid code_verifier"
6868+ ) ;
6969+ ( match par_record.dpop_jkt with
7070+ | Some stored when stored <> proof.jkt ->
7171+ Errors.invalid_request "DPoP key mismatch"
7272+ | _ ->
7373+ () ) ;
7474+ let token_id =
7575+ "tok-"
7676+ ^ Uuidm.to_string
7777+ (Uuidm.v4_gen (Random.get_state ()) ())
7878+ in
7979+ let refresh_token =
8080+ "ref-"
8181+ ^ Uuidm.to_string
8282+ (Uuidm.v4_gen (Random.get_state ()) ())
8383+ in
8484+ let now_sec = int_of_float (Unix.gettimeofday ()) in
8585+ let expires_in =
8686+ Constants.access_token_expiry_ms / 1000
8787+ in
8888+ let exp_sec = now_sec + expires_in in
8989+ let expires_at = exp_sec * 1000 in
9090+ let claims =
9191+ `Assoc
9292+ [ ("jti", `String token_id)
9393+ ; ("sub", `String did)
9494+ ; ("iat", `Int now_sec)
9595+ ; ("exp", `Int exp_sec)
9696+ ; ("scope", `String orig_req.scope)
9797+ ; ("aud", `String ("https://" ^ Env.hostname))
9898+ ; ("cnf", `Assoc [("jkt", `String proof.jkt)])
9999+ ]
100100+ in
101101+ let access_token =
102102+ Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key
103103+ in
104104+ let%lwt () =
105105+ Queries.insert_oauth_token ctx.db
106106+ { refresh_token
107107+ ; client_id= req.client_id
108108+ ; did
109109+ ; dpop_jkt= proof.jkt
110110+ ; scope= orig_req.scope
111111+ ; expires_at }
112112+ in
113113+ let nonce = Dpop.next_nonce () in
114114+ Dream.json
115115+ ~headers:
116116+ [ ("DPoP-Nonce", nonce)
117117+ ; ("Access-Control-Expose-Headers", "DPoP-Nonce")
118118+ ; ("Cache-Control", "no-store") ]
119119+ @@ Yojson.Safe.to_string
120120+ @@ `Assoc
121121+ [ ("access_token", `String access_token)
122122+ ; ("token_type", `String "DPoP")
123123+ ; ("refresh_token", `String refresh_token)
124124+ ; ("expires_in", `Int expires_in)
125125+ ; ("scope", `String orig_req.scope)
126126+ ; ("sub", `String did) ] ) ) ) )
127127+ | "refresh_token" -> (
128128+ match req.refresh_token with
129129+ | None ->
130130+ Errors.invalid_request "refresh_token required"
131131+ | Some refresh_token -> (
132132+ let%lwt token_record =
133133+ Queries.get_oauth_token_by_refresh ctx.db refresh_token
134134+ in
135135+ match token_record with
136136+ | None ->
137137+ Errors.invalid_request "invalid refresh token"
138138+ | Some session ->
139139+ if session.client_id <> req.client_id then
140140+ Errors.invalid_request "client_id mismatch"
141141+ else if session.dpop_jkt <> proof.jkt then
142142+ Errors.invalid_request "DPoP key mismatch"
143143+ else
144144+ let new_token_id =
145145+ "tok-"
146146+ ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ())
147147+ in
148148+ let new_refresh =
149149+ "ref-"
150150+ ^ Uuidm.to_string (Uuidm.v4_gen (Random.get_state ()) ())
151151+ in
152152+ let now_sec = int_of_float (Unix.gettimeofday ()) in
153153+ let expires_in = Constants.access_token_expiry_ms / 1000 in
154154+ let exp_sec = now_sec + expires_in in
155155+ let new_expires_at = exp_sec * 1000 in
156156+ let claims =
157157+ `Assoc
158158+ [ ("jti", `String new_token_id)
159159+ ; ("sub", `String session.did)
160160+ ; ("iat", `Int now_sec)
161161+ ; ("exp", `Int exp_sec)
162162+ ; ("scope", `String session.scope)
163163+ ; ("aud", `String ("https://" ^ Env.hostname))
164164+ ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ]
165165+ in
166166+ let new_access_token =
167167+ Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key
168168+ in
169169+ let%lwt () =
170170+ Queries.update_oauth_token ctx.db
171171+ ~old_refresh_token:refresh_token ~new_token_id
172172+ ~new_refresh_token:new_refresh
173173+ ~expires_at:new_expires_at
174174+ in
175175+ Dream.json ~headers:[("Cache-Control", "no-store")]
176176+ @@ Yojson.Safe.to_string
177177+ @@ `Assoc
178178+ [ ("access_token", `String new_access_token)
179179+ ; ("token_type", `String "DPoP")
180180+ ; ("refresh_token", `String new_refresh)
181181+ ; ("expires_in", `Int expires_in)
182182+ ; ("scope", `String session.scope)
183183+ ; ("sub", `String session.did) ] ) )
184184+ | _ ->
185185+ Errors.invalid_request ("unsupported grant_type: " ^ req.grant_type)
186186+ ) )
+1-5
pegasus/lib/data_store.ml
···116116 [%rapper
117117 execute
118118 {sql| CREATE TABLE IF NOT EXISTS oauth_tokens (
119119- id INTEGER PRIMARY KEY,
120120- token_id TEXT UNIQUE NOT NULL,
121119 refresh_token TEXT UNIQUE NOT NULL,
122120 client_id TEXT NOT NULL,
123121 did TEXT NOT NULL,
124122 dpop_jkt TEXT,
125123 scope TEXT NOT NULL,
126126- created_at INTEGER NOT NULL,
127127- expires_at INTEGER NOT NULL,
128128- last_refreshed_at INTEGER NOT NULL
124124+ expires_at INTEGER NOT NULL
129125 )
130126 |sql}]
131127 () conn