headless_lms_server/domain/oauth/
dpop.rs1use actix_web::HttpRequest;
2use async_trait::async_trait;
3use dpop_verifier::{
4 DpopError, DpopVerifier,
5 actix_helpers::{dpop_header_str, expected_htu_from_actix},
6 replay::{ReplayContext, ReplayStore},
7};
8use headless_lms_models::library::oauth::Digest as TokenDigest;
9use headless_lms_models::oauth_dpop_proofs::OAuthDpopProof;
10use secrecy::{ExposeSecret, SecretBox};
11use sqlx::PgConnection;
12
13pub struct SqlxReplayStore<'c> {
14 pub conn: &'c mut PgConnection,
15}
16
17#[async_trait]
18impl<'c> ReplayStore for SqlxReplayStore<'c> {
19 async fn insert_once(
20 &mut self,
21 jti_hash: [u8; 32],
22 ctx: ReplayContext<'_>,
23 ) -> Result<bool, DpopError> {
24 let digest = TokenDigest::from(jti_hash);
25 let first_time = OAuthDpopProof::insert_once(
26 self.conn,
27 digest,
28 ctx.client_id,
29 ctx.jkt, ctx.htm, ctx.htu, Some(ctx.iat), )
34 .await
35 .map_err(|e| DpopError::Store(e.into()))?;
36
37 Ok(first_time)
38 }
39}
40
41pub async fn verify_dpop_from_actix(
42 conn: &mut PgConnection,
43 req: &HttpRequest,
44 method: &str, dpop_nonce_key: &SecretBox<String>,
46 access_token: Option<&str>, ) -> Result<String, DpopError> {
48 let hdr = dpop_header_str(req)?;
49
50 let htu = expected_htu_from_actix(req, true);
51
52 let mut store = SqlxReplayStore { conn };
53 let verifier = DpopVerifier::new()
54 .with_max_age_seconds(300)
55 .with_future_skew_seconds(5)
56 .with_nonce_mode(dpop_verifier::NonceMode::Hmac(
57 dpop_verifier::HmacConfig::new(
58 dpop_nonce_key.expose_secret().as_bytes(),
59 300,
60 true,
61 true,
62 true,
63 ),
64 ));
65 let verified = verifier
66 .verify(&mut store, hdr, &htu, method, access_token)
67 .await?;
68
69 Ok(verified.jkt)
70}