1use crate::oauth_access_token::{NewAccessTokenParams, OAuthAccessToken, TokenType};
2use crate::{library::oauth::Digest, prelude::*};
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sqlx::{FromRow, PgConnection};
6use uuid::Uuid;
7
8#[derive(Debug, Serialize, Deserialize, FromRow)]
21pub struct OAuthRefreshTokens {
22 pub digest: Digest,
23 pub user_id: Uuid,
24 pub client_id: Uuid,
25 pub expires_at: DateTime<Utc>,
26 pub scopes: Vec<String>,
27 pub audience: Option<Vec<String>>,
28 pub jti: Uuid,
29 pub dpop_jkt: Option<String>,
31 pub metadata: serde_json::Value,
32 pub revoked: bool,
33 pub rotated_from: Option<Digest>,
34}
35
36#[derive(Debug, Clone)]
37pub struct NewRefreshTokenParams<'a> {
38 pub digest: &'a Digest,
39 pub user_id: Uuid,
40 pub client_id: Uuid,
41 pub scopes: &'a [String],
42 pub audience: Option<&'a [String]>,
43 pub expires_at: DateTime<Utc>,
44 pub rotated_from: Option<&'a Digest>,
45 pub metadata: serde_json::Map<String, serde_json::Value>,
46 pub dpop_jkt: Option<&'a str>,
48}
49
50#[derive(Debug)]
52pub struct RotateRefreshTokenParams<'a> {
53 pub new_refresh_token_digest: &'a Digest,
54 pub new_access_token_digest: &'a Digest,
55 pub access_token_expires_at: DateTime<Utc>,
56 pub refresh_token_expires_at: DateTime<Utc>,
57 pub access_token_type: TokenType,
58 pub access_token_dpop_jkt: Option<&'a str>,
59 pub refresh_token_dpop_jkt: Option<&'a str>,
60}
61
62#[derive(Debug, Clone)]
64pub struct IssueTokensFromAuthCodeParams<'a> {
65 pub user_id: Uuid,
66 pub client_id: Uuid,
67 pub scopes: &'a [String],
68 pub access_token_digest: &'a Digest,
69 pub refresh_token_digest: &'a Digest,
70 pub access_token_expires_at: DateTime<Utc>,
71 pub refresh_token_expires_at: DateTime<Utc>,
72 pub access_token_type: TokenType,
73 pub access_token_dpop_jkt: Option<&'a str>,
74 pub refresh_token_dpop_jkt: Option<&'a str>,
75}
76
77impl OAuthRefreshTokens {
78 pub async fn insert(
79 conn: &mut PgConnection,
80 params: NewRefreshTokenParams<'_>,
81 ) -> ModelResult<()> {
82 sqlx::query!(
83 r#"
84 INSERT INTO oauth_refresh_tokens
85 (digest, user_id, client_id, scopes, audience, jti, expires_at, revoked, rotated_from, metadata, dpop_jkt)
86 VALUES
87 ($1, $2, $3, $4, $5, gen_random_uuid(), $6, false, $7, $8, $9)
88 "#,
89 params.digest.as_bytes(),
90 params.user_id,
91 params.client_id,
92 params.scopes,
93 params.audience,
94 params.expires_at,
95 params.rotated_from.map(|d| d.as_bytes() as &[u8]),
96 serde_json::Value::Object(params.metadata),
97 params.dpop_jkt
98 )
99 .execute(conn)
100 .await?;
101 Ok(())
102 }
103
104 pub async fn find_valid(
105 conn: &mut PgConnection,
106 digest: Digest,
107 ) -> ModelResult<OAuthRefreshTokens> {
108 let mut tx = conn.begin().await?;
109 let token = sqlx::query_as!(
110 OAuthRefreshTokens,
111 r#"
112 SELECT
113 digest as "digest: _",
114 user_id,
115 client_id,
116 expires_at,
117 scopes,
118 audience,
119 jti,
120 dpop_jkt,
121 metadata,
122 revoked,
123 rotated_from as "rotated_from: _"
124 FROM oauth_refresh_tokens
125 WHERE digest = $1
126 AND expires_at > now()
127 AND revoked = false
128 "#,
129 digest.as_bytes()
130 )
131 .fetch_one(&mut *tx)
132 .await?;
133 tx.commit().await?;
134 Ok(token)
135 }
136
137 pub async fn find_valid_for_sender(
139 conn: &mut PgConnection,
140 digest: Digest,
141 presented_jkt: Option<&str>,
142 ) -> ModelResult<OAuthRefreshTokens> {
143 let t = Self::find_valid(conn, digest).await?;
144 if let Some(expected) = t.dpop_jkt.as_deref() {
145 let Some(presented) = presented_jkt else {
146 return Err(ModelError::new(
147 ModelErrorType::PreconditionFailed,
148 "refresh token requires DPoP but no JKT presented",
149 None::<anyhow::Error>,
150 ));
151 };
152 if expected != presented {
153 return Err(ModelError::new(
154 ModelErrorType::PreconditionFailed,
155 "DPoP JKT mismatch for refresh token",
156 None::<anyhow::Error>,
157 ));
158 }
159 }
160 Ok(t)
161 }
162
163 pub async fn revoke_by_digest(conn: &mut PgConnection, digest: Digest) -> ModelResult<()> {
164 let mut tx = conn.begin().await?;
165 sqlx::query!(
166 r#"
167 UPDATE oauth_refresh_tokens
168 SET revoked = true
169 WHERE digest = $1
170 "#,
171 digest.as_bytes()
172 )
173 .execute(&mut *tx)
174 .await?;
175 tx.commit().await?;
176 Ok(())
177 }
178
179 pub async fn revoke_all_by_user_client(
180 conn: &mut PgConnection,
181 user_id: Uuid,
182 client_id: Uuid,
183 ) -> ModelResult<()> {
184 let mut tx = conn.begin().await?;
185 sqlx::query!(
186 r#"
187 UPDATE oauth_refresh_tokens
188 SET revoked = true
189 WHERE user_id = $1 AND client_id = $2
190 "#,
191 user_id,
192 client_id
193 )
194 .execute(&mut *tx)
195 .await?;
196 tx.commit().await?;
197 Ok(())
198 }
199
200 pub async fn consume_in_transaction(
208 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
209 digest: Digest,
210 client_id: Uuid,
211 ) -> ModelResult<OAuthRefreshTokens> {
212 let row = sqlx::query_as!(
213 OAuthRefreshTokens,
214 r#"
215 UPDATE oauth_refresh_tokens
216 SET revoked = true
217 WHERE digest = $1
218 AND client_id = $2
219 AND revoked = false
220 AND expires_at > now()
221 RETURNING
222 digest as "digest: _",
223 user_id,
224 client_id,
225 expires_at,
226 scopes,
227 audience,
228 jti,
229 dpop_jkt,
230 metadata,
231 revoked,
232 rotated_from as "rotated_from: _"
233 "#,
234 digest.as_bytes(),
235 client_id
236 )
237 .fetch_one(&mut **tx)
238 .await?;
239 Ok(row)
240 }
241
242 pub async fn complete_refresh_token_rotation_in_transaction(
250 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
251 old_token: &OAuthRefreshTokens,
252 params: RotateRefreshTokenParams<'_>,
253 ) -> ModelResult<()> {
254 sqlx::query!(
256 r#"
257 UPDATE oauth_refresh_tokens
258 SET revoked = true
259 WHERE user_id = $1 AND client_id = $2
260 "#,
261 old_token.user_id,
262 old_token.client_id
263 )
264 .execute(&mut **tx)
265 .await?;
266
267 sqlx::query!(
269 r#"
270 INSERT INTO oauth_refresh_tokens
271 (digest, user_id, client_id, scopes, audience, jti, expires_at, revoked, rotated_from, metadata, dpop_jkt)
272 VALUES
273 ($1, $2, $3, $4, $5, gen_random_uuid(), $6, false, $7, $8, $9)
274 "#,
275 params.new_refresh_token_digest.as_bytes(),
276 old_token.user_id,
277 old_token.client_id,
278 &old_token.scopes,
279 old_token.audience.as_deref(),
280 params.refresh_token_expires_at,
281 old_token.digest.as_bytes(),
282 serde_json::Value::Object(serde_json::Map::new()),
283 params.refresh_token_dpop_jkt
284 )
285 .execute(&mut **tx)
286 .await?;
287
288 OAuthAccessToken::insert(
290 tx,
291 NewAccessTokenParams {
292 digest: params.new_access_token_digest,
293 user_id: Some(old_token.user_id),
294 client_id: old_token.client_id,
295 scopes: &old_token.scopes,
296 audience: old_token.audience.as_deref(),
297 token_type: params.access_token_type,
298 dpop_jkt: params.access_token_dpop_jkt,
299 metadata: serde_json::Map::new(),
300 expires_at: params.access_token_expires_at,
301 },
302 )
303 .await?;
304
305 Ok(())
306 }
307
308 pub async fn issue_tokens_from_auth_code_in_transaction(
316 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
317 params: IssueTokensFromAuthCodeParams<'_>,
318 ) -> ModelResult<()> {
319 OAuthAccessToken::insert(
321 tx,
322 NewAccessTokenParams {
323 digest: params.access_token_digest,
324 user_id: Some(params.user_id),
325 client_id: params.client_id,
326 scopes: params.scopes,
327 audience: None,
328 token_type: params.access_token_type,
329 dpop_jkt: params.access_token_dpop_jkt,
330 metadata: serde_json::Map::new(),
331 expires_at: params.access_token_expires_at,
332 },
333 )
334 .await?;
335
336 sqlx::query!(
338 r#"
339 UPDATE oauth_refresh_tokens
340 SET revoked = true
341 WHERE user_id = $1 AND client_id = $2
342 "#,
343 params.user_id,
344 params.client_id
345 )
346 .execute(&mut **tx)
347 .await?;
348
349 sqlx::query!(
351 r#"
352 INSERT INTO oauth_refresh_tokens
353 (digest, user_id, client_id, scopes, audience, jti, expires_at, revoked, rotated_from, metadata, dpop_jkt)
354 VALUES
355 ($1, $2, $3, $4, $5, gen_random_uuid(), $6, false, NULL, $7, $8)
356 "#,
357 params.refresh_token_digest.as_bytes(),
358 params.user_id,
359 params.client_id,
360 params.scopes,
361 Option::<Vec<String>>::None as Option<Vec<String>>,
362 params.refresh_token_expires_at,
363 serde_json::Value::Object(serde_json::Map::new()),
364 params.refresh_token_dpop_jkt
365 )
366 .execute(&mut **tx)
367 .await?;
368
369 Ok(())
370 }
371}