1use crate::library::oauth::Digest;
2use crate::prelude::*;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sqlx::{self, FromRow, PgConnection, Type};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
9#[sqlx(type_name = "token_type")]
10pub enum TokenType {
11 Bearer,
12 DPoP,
13}
14
15#[derive(Debug, Serialize, Deserialize, FromRow)]
28pub struct OAuthAccessToken {
29 pub digest: Digest,
30 pub user_id: Option<Uuid>,
31 pub client_id: Uuid,
32 pub scopes: Vec<String>,
33 pub audience: Option<Vec<String>>,
34 pub jti: Uuid,
35
36 pub dpop_jkt: Option<String>,
38
39 pub token_type: TokenType,
40
41 pub metadata: serde_json::Value,
42 pub expires_at: DateTime<Utc>,
43 pub created_at: DateTime<Utc>,
44}
45
46#[derive(Debug, Clone)]
47pub struct NewAccessTokenParams<'a> {
48 pub digest: &'a Digest,
49 pub user_id: Option<Uuid>,
50 pub client_id: Uuid,
51 pub scopes: &'a [String],
52 pub audience: Option<&'a [String]>,
53
54 pub token_type: TokenType,
57 pub dpop_jkt: Option<&'a str>,
58
59 pub metadata: serde_json::Map<String, serde_json::Value>,
60 pub expires_at: DateTime<Utc>,
61}
62
63impl OAuthAccessToken {
64 pub async fn insert(
70 conn: &mut PgConnection,
71 params: NewAccessTokenParams<'_>,
72 ) -> ModelResult<()> {
73 match (params.token_type, params.dpop_jkt) {
74 (TokenType::Bearer, None) => {}
75 (TokenType::Bearer, Some(_)) => {
76 return Err(ModelError::new(
77 ModelErrorType::InvalidRequest,
78 "Bearer tokens must not include dpop_jkt",
79 None::<anyhow::Error>,
80 ));
81 }
82 (TokenType::DPoP, Some(_)) => {}
83 (TokenType::DPoP, None) => {
84 return Err(ModelError::new(
85 ModelErrorType::InvalidRequest,
86 "DPoP tokens must include dpop_jkt",
87 None::<anyhow::Error>,
88 ));
89 }
90 }
91
92 sqlx::query!(
93 r#"
94 INSERT INTO oauth_access_tokens
95 (digest, user_id, client_id, scopes, audience, token_type, dpop_jkt, metadata, expires_at)
96 VALUES
97 ($1, $2, $3, $4, $5, $6, $7, $8, $9)
98 "#,
99 params.digest.as_bytes(),
100 params.user_id,
101 params.client_id,
102 params.scopes,
103 params.audience,
104 params.token_type as TokenType,
105 params.dpop_jkt,
106 serde_json::Value::Object(params.metadata),
107 params.expires_at
108 )
109 .execute(conn)
110 .await?;
111 Ok(())
112 }
113
114 pub async fn find_valid(
116 conn: &mut PgConnection,
117 digest: Digest,
118 ) -> ModelResult<OAuthAccessToken> {
119 let token = sqlx::query_as!(
120 OAuthAccessToken,
121 r#"
122 SELECT
123 digest as "digest: _",
124 user_id,
125 client_id,
126 scopes,
127 audience,
128 jti,
129 dpop_jkt,
130 token_type as "token_type: TokenType",
131 metadata,
132 expires_at,
133 created_at
134 FROM oauth_access_tokens
135 WHERE digest = $1 AND expires_at > now()
136 "#,
137 digest.as_bytes()
138 )
139 .fetch_one(conn)
140 .await?;
141 Ok(token)
142 }
143
144 pub async fn find_valid_for_sender(
148 conn: &mut PgConnection,
149 digest: Digest,
150 sender_jkt: Option<&str>,
151 ) -> ModelResult<OAuthAccessToken> {
152 let t = Self::find_valid(conn, digest).await?;
153
154 match t.token_type {
155 TokenType::Bearer => Ok(t),
156 TokenType::DPoP => {
157 let Some(expected) = t.dpop_jkt.as_deref() else {
158 return Err(ModelError::new(
159 ModelErrorType::PreconditionFailed,
160 "token missing dpop_jkt",
161 None::<anyhow::Error>,
162 ));
163 };
164 let Some(presented) = sender_jkt else {
165 return Err(ModelError::new(
166 ModelErrorType::PreconditionFailed,
167 "DPoP proof missing JKT",
168 None::<anyhow::Error>,
169 ));
170 };
171 if expected != presented {
172 return Err(ModelError::new(
173 ModelErrorType::PreconditionFailed,
174 "DPoP JKT mismatch",
175 None::<anyhow::Error>,
176 ));
177 }
178 Ok(t)
179 }
180 }
181 }
182
183 pub async fn delete_all_by_user_client(
184 conn: &mut PgConnection,
185 user_id: Uuid,
186 client_id: Uuid,
187 ) -> ModelResult<()> {
188 let mut tx = conn.begin().await?;
189 sqlx::query!(
190 r#"
191 DELETE FROM oauth_access_tokens
192 WHERE user_id = $1 AND client_id = $2
193 "#,
194 user_id,
195 client_id
196 )
197 .execute(&mut *tx)
198 .await?;
199 tx.commit().await?;
200 Ok(())
201 }
202
203 pub async fn revoke_by_digest(conn: &mut PgConnection, digest: Digest) -> ModelResult<()> {
208 sqlx::query!(
209 r#"
210 DELETE FROM oauth_access_tokens
211 WHERE digest = $1
212 "#,
213 digest.as_bytes()
214 )
215 .execute(conn)
216 .await?;
217 Ok(())
218 }
219}