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
41struct BufferedReplayEntry {
43 jti_hash: [u8; 32],
44 client_id: Option<String>,
45 jkt: Option<String>,
46 htm: Option<String>,
47 htu: Option<String>,
48 iat: Option<i64>,
49}
50
51pub struct DeferredReplayStore {
55 buffer: Option<BufferedReplayEntry>,
56}
57
58impl Default for DeferredReplayStore {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl DeferredReplayStore {
65 pub fn new() -> Self {
66 Self { buffer: None }
67 }
68
69 pub async fn flush(&mut self, conn: &mut PgConnection) -> Result<(), DpopError> {
72 if let Some(entry) = self.buffer.take() {
73 let digest = TokenDigest::from(entry.jti_hash);
74 let first_time = OAuthDpopProof::insert_once(
75 conn,
76 digest,
77 entry.client_id.as_deref(),
78 entry.jkt.as_deref(),
79 entry.htm.as_deref(),
80 entry.htu.as_deref(),
81 entry.iat,
82 )
83 .await
84 .map_err(|e| DpopError::Store(e.into()))?;
85 if !first_time {
86 return Err(DpopError::Replay);
87 }
88 }
89 Ok(())
90 }
91}
92
93#[async_trait]
94impl ReplayStore for DeferredReplayStore {
95 async fn insert_once(
96 &mut self,
97 jti_hash: [u8; 32],
98 ctx: ReplayContext<'_>,
99 ) -> Result<bool, DpopError> {
100 self.buffer = Some(BufferedReplayEntry {
101 jti_hash,
102 client_id: ctx.client_id.map(String::from),
103 jkt: ctx.jkt.map(String::from),
104 htm: ctx.htm.map(String::from),
105 htu: ctx.htu.map(String::from),
106 iat: Some(ctx.iat),
107 });
108 Ok(true)
110 }
111}
112
113pub async fn verify_dpop_from_actix(
114 conn: &mut PgConnection,
115 req: &HttpRequest,
116 method: &str, dpop_nonce_key: &SecretBox<String>,
118 access_token: Option<&str>, ) -> Result<String, DpopError> {
120 let hdr = dpop_header_str(req)?;
121
122 let htu = expected_htu_from_actix(req, true);
123
124 let mut store = SqlxReplayStore { conn };
125 let verifier = DpopVerifier::new()
126 .with_max_age_seconds(300)
127 .with_future_skew_seconds(5)
128 .with_nonce_mode(dpop_verifier::NonceMode::Hmac(
129 dpop_verifier::HmacConfig::new(
130 dpop_nonce_key.expose_secret().as_bytes(),
131 300,
132 true,
133 true,
134 true,
135 ),
136 ));
137 let verified = verifier
138 .verify(&mut store, hdr, &htu, method, access_token)
139 .await?;
140
141 Ok(verified.jkt)
142}
143
144pub async fn verify_dpop_from_actix_for_token(
148 conn: &mut PgConnection,
149 req: &HttpRequest,
150 dpop_nonce_key: &SecretBox<String>,
151) -> Result<String, DpopError> {
152 let hdr = dpop_header_str(req)?;
153 let htu = expected_htu_from_actix(req, true);
154
155 let mut store = DeferredReplayStore::new();
156 let verifier = DpopVerifier::new()
157 .with_max_age_seconds(300)
158 .with_future_skew_seconds(5)
159 .with_nonce_mode(dpop_verifier::NonceMode::Hmac(
160 dpop_verifier::HmacConfig::new(
161 dpop_nonce_key.expose_secret().as_bytes(),
162 300,
163 true,
164 true,
165 true,
166 ),
167 ));
168 let result = verifier
169 .verify(&mut store, hdr, &htu, "POST", None::<&str>)
170 .await;
171
172 match result {
173 Ok(verified) => {
174 store.flush(conn).await?;
175 Ok(verified.jkt)
176 }
177 Err(e) => Err(e),
178 }
179}