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 pub created_at: DateTime<Utc>,
35 pub updated_at: DateTime<Utc>,
36}
37
38#[derive(Debug, Clone)]
39pub struct NewRefreshTokenParams<'a> {
40 pub digest: &'a Digest,
41 pub user_id: Uuid,
42 pub client_id: Uuid,
43 pub scopes: &'a [String],
44 pub audience: Option<&'a [String]>,
45 pub expires_at: DateTime<Utc>,
46 pub rotated_from: Option<&'a Digest>,
47 pub metadata: serde_json::Map<String, serde_json::Value>,
48 pub dpop_jkt: Option<&'a str>,
50}
51
52#[derive(Debug)]
54pub struct RotateRefreshTokenParams<'a> {
55 pub new_refresh_token_digest: &'a Digest,
56 pub new_access_token_digest: &'a Digest,
57 pub access_token_expires_at: DateTime<Utc>,
58 pub refresh_token_expires_at: DateTime<Utc>,
59 pub access_token_type: TokenType,
60 pub access_token_dpop_jkt: Option<&'a str>,
61 pub refresh_token_dpop_jkt: Option<&'a str>,
62}
63
64#[derive(Debug, Clone)]
66pub struct IssueTokensFromAuthCodeParams<'a> {
67 pub user_id: Uuid,
68 pub client_id: Uuid,
69 pub scopes: &'a [String],
70 pub access_token_digest: &'a Digest,
71 pub refresh_token_digest: &'a Digest,
72 pub access_token_expires_at: DateTime<Utc>,
73 pub refresh_token_expires_at: DateTime<Utc>,
74 pub access_token_type: TokenType,
75 pub access_token_dpop_jkt: Option<&'a str>,
76 pub refresh_token_dpop_jkt: Option<&'a str>,
77}
78
79impl OAuthRefreshTokens {
80 pub async fn insert(
81 conn: &mut PgConnection,
82 params: NewRefreshTokenParams<'_>,
83 ) -> ModelResult<()> {
84 sqlx::query!(
85 r#"
86 INSERT INTO oauth_refresh_tokens
87 (digest, user_id, client_id, scopes, audience, jti, expires_at, revoked, rotated_from, metadata, dpop_jkt)
88 VALUES
89 ($1, $2, $3, $4, $5, gen_random_uuid(), $6, false, $7, $8, $9)
90 "#,
91 params.digest.as_bytes(),
92 params.user_id,
93 params.client_id,
94 params.scopes,
95 params.audience,
96 params.expires_at,
97 params.rotated_from.map(|d| d.as_bytes() as &[u8]),
98 serde_json::Value::Object(params.metadata),
99 params.dpop_jkt
100 )
101 .execute(conn)
102 .await?;
103 Ok(())
104 }
105
106 pub async fn find_valid(
107 conn: &mut PgConnection,
108 digest: Digest,
109 ) -> ModelResult<OAuthRefreshTokens> {
110 let mut tx = conn.begin().await?;
111 let token = sqlx::query_as!(
112 OAuthRefreshTokens,
113 r#"
114 SELECT *
115 FROM oauth_refresh_tokens
116 WHERE digest = $1
117 AND expires_at > now()
118 AND revoked = false
119 "#,
120 digest.as_bytes()
121 )
122 .fetch_one(&mut *tx)
123 .await?;
124 tx.commit().await?;
125 Ok(token)
126 }
127
128 pub async fn find_valid_for_sender(
130 conn: &mut PgConnection,
131 digest: Digest,
132 presented_jkt: Option<&str>,
133 ) -> ModelResult<OAuthRefreshTokens> {
134 let t = Self::find_valid(conn, digest).await?;
135 if let Some(expected) = t.dpop_jkt.as_deref() {
136 let Some(presented) = presented_jkt else {
137 return Err(ModelError::new(
138 ModelErrorType::PreconditionFailed,
139 "refresh token requires DPoP but no JKT presented",
140 None::<anyhow::Error>,
141 ));
142 };
143 if expected != presented {
144 return Err(ModelError::new(
145 ModelErrorType::PreconditionFailed,
146 "DPoP JKT mismatch for refresh token",
147 None::<anyhow::Error>,
148 ));
149 }
150 }
151 Ok(t)
152 }
153
154 pub async fn revoke_by_digest(conn: &mut PgConnection, digest: Digest) -> ModelResult<()> {
155 let mut tx = conn.begin().await?;
156 sqlx::query!(
157 r#"
158 UPDATE oauth_refresh_tokens
159 SET revoked = true
160 WHERE digest = $1
161 "#,
162 digest.as_bytes()
163 )
164 .execute(&mut *tx)
165 .await?;
166 tx.commit().await?;
167 Ok(())
168 }
169
170 pub async fn revoke_all_by_user_client(
171 conn: &mut PgConnection,
172 user_id: Uuid,
173 client_id: Uuid,
174 ) -> ModelResult<()> {
175 let mut tx = conn.begin().await?;
176 sqlx::query!(
177 r#"
178 UPDATE oauth_refresh_tokens
179 SET revoked = true
180 WHERE user_id = $1 AND client_id = $2
181 "#,
182 user_id,
183 client_id
184 )
185 .execute(&mut *tx)
186 .await?;
187 tx.commit().await?;
188 Ok(())
189 }
190
191 pub async fn consume_in_transaction(
199 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
200 digest: Digest,
201 client_id: Uuid,
202 ) -> ModelResult<OAuthRefreshTokens> {
203 let row = sqlx::query_as!(
204 OAuthRefreshTokens,
205 r#"
206 UPDATE oauth_refresh_tokens
207 SET revoked = true
208 WHERE digest = $1
209 AND client_id = $2
210 AND revoked = false
211 AND expires_at > now()
212 RETURNING *
213 "#,
214 digest.as_bytes(),
215 client_id
216 )
217 .fetch_one(&mut **tx)
218 .await?;
219 Ok(row)
220 }
221
222 pub async fn complete_refresh_token_rotation_in_transaction(
230 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
231 old_token: &OAuthRefreshTokens,
232 params: RotateRefreshTokenParams<'_>,
233 ) -> ModelResult<()> {
234 sqlx::query!(
236 r#"
237 UPDATE oauth_refresh_tokens
238 SET revoked = true
239 WHERE user_id = $1 AND client_id = $2
240 "#,
241 old_token.user_id,
242 old_token.client_id
243 )
244 .execute(&mut **tx)
245 .await?;
246
247 sqlx::query!(
249 r#"
250 INSERT INTO oauth_refresh_tokens
251 (digest, user_id, client_id, scopes, audience, jti, expires_at, revoked, rotated_from, metadata, dpop_jkt)
252 VALUES
253 ($1, $2, $3, $4, $5, gen_random_uuid(), $6, false, $7, $8, $9)
254 "#,
255 params.new_refresh_token_digest.as_bytes(),
256 old_token.user_id,
257 old_token.client_id,
258 &old_token.scopes,
259 old_token.audience.as_deref(),
260 params.refresh_token_expires_at,
261 old_token.digest.as_bytes(),
262 serde_json::Value::Object(serde_json::Map::new()),
263 params.refresh_token_dpop_jkt
264 )
265 .execute(&mut **tx)
266 .await?;
267
268 OAuthAccessToken::insert(
270 tx,
271 NewAccessTokenParams {
272 digest: params.new_access_token_digest,
273 user_id: Some(old_token.user_id),
274 client_id: old_token.client_id,
275 scopes: &old_token.scopes,
276 audience: old_token.audience.as_deref(),
277 token_type: params.access_token_type,
278 dpop_jkt: params.access_token_dpop_jkt,
279 metadata: serde_json::Map::new(),
280 expires_at: params.access_token_expires_at,
281 },
282 )
283 .await?;
284
285 Ok(())
286 }
287
288 pub async fn issue_tokens_from_auth_code_in_transaction(
296 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
297 params: IssueTokensFromAuthCodeParams<'_>,
298 ) -> ModelResult<()> {
299 OAuthAccessToken::insert(
301 tx,
302 NewAccessTokenParams {
303 digest: params.access_token_digest,
304 user_id: Some(params.user_id),
305 client_id: params.client_id,
306 scopes: params.scopes,
307 audience: None,
308 token_type: params.access_token_type,
309 dpop_jkt: params.access_token_dpop_jkt,
310 metadata: serde_json::Map::new(),
311 expires_at: params.access_token_expires_at,
312 },
313 )
314 .await?;
315
316 sqlx::query!(
318 r#"
319 UPDATE oauth_refresh_tokens
320 SET revoked = true
321 WHERE user_id = $1 AND client_id = $2
322 "#,
323 params.user_id,
324 params.client_id
325 )
326 .execute(&mut **tx)
327 .await?;
328
329 sqlx::query!(
331 r#"
332 INSERT INTO oauth_refresh_tokens
333 (digest, user_id, client_id, scopes, audience, jti, expires_at, revoked, rotated_from, metadata, dpop_jkt)
334 VALUES
335 ($1, $2, $3, $4, $5, gen_random_uuid(), $6, false, NULL, $7, $8)
336 "#,
337 params.refresh_token_digest.as_bytes(),
338 params.user_id,
339 params.client_id,
340 params.scopes,
341 Option::<Vec<String>>::None as Option<Vec<String>>,
342 params.refresh_token_expires_at,
343 serde_json::Value::Object(serde_json::Map::new()),
344 params.refresh_token_dpop_jkt
345 )
346 .execute(&mut **tx)
347 .await?;
348
349 Ok(())
350 }
351}