1use crate::{library::oauth::Digest, prelude::*};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use sqlx::{FromRow, PgConnection};
5use uuid::Uuid;
6
7use crate::library::oauth::pkce::PkceMethod;
8
9#[derive(Debug, Serialize, Deserialize, FromRow)]
22pub struct OAuthAuthCode {
23 pub digest: Digest,
24 pub user_id: Uuid,
25 pub client_id: Uuid,
26 pub redirect_uri: String,
27 pub scopes: Vec<String>,
28 pub jti: Uuid,
29 pub nonce: Option<String>,
30
31 pub code_challenge: Option<String>,
32 pub code_challenge_method: Option<PkceMethod>,
33
34 pub dpop_jkt: Option<String>,
35
36 pub used: bool,
37 pub expires_at: DateTime<Utc>,
38 pub metadata: serde_json::Value,
39}
40
41#[derive(Debug, Clone)]
42pub struct NewAuthCodeParams<'a> {
43 pub digest: &'a Digest,
44 pub user_id: Uuid,
45 pub client_id: Uuid,
46 pub redirect_uri: &'a str,
47 pub scopes: &'a [String],
48 pub nonce: Option<&'a str>,
49
50 pub code_challenge: Option<&'a str>,
51 pub code_challenge_method: Option<PkceMethod>,
52
53 pub dpop_jkt: Option<&'a str>,
54
55 pub expires_at: DateTime<Utc>,
56 pub metadata: serde_json::Map<String, serde_json::Value>,
57}
58
59impl<'a> NewAuthCodeParams<'a> {
60 pub fn validate(&self) -> ModelResult<()> {
61 match (self.code_challenge, self.code_challenge_method) {
63 (Some(_), Some(_)) | (None, None) => {}
64 _ => {
65 return Err(ModelError::new(
66 ModelErrorType::InvalidRequest,
67 "PKCE: code_challenge and code_challenge_method must be provided together",
68 None::<anyhow::Error>,
69 ));
70 }
71 }
72 Ok(())
73 }
74}
75
76impl OAuthAuthCode {
77 pub async fn insert(conn: &mut PgConnection, params: NewAuthCodeParams<'_>) -> ModelResult<()> {
78 params.validate()?;
79
80 sqlx::query!(
81 r#"
82 INSERT INTO oauth_auth_codes (
83 digest,
84 user_id,
85 client_id,
86 redirect_uri,
87 scopes,
88 nonce,
89 code_challenge,
90 code_challenge_method,
91 dpop_jkt,
92 expires_at,
93 metadata
94 )
95 VALUES (
96 $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11
97 )
98 "#,
99 params.digest.as_bytes(),
100 params.user_id,
101 params.client_id,
102 params.redirect_uri,
103 params.scopes,
104 params.nonce,
105 params.code_challenge,
106 params.code_challenge_method as Option<PkceMethod>,
108 params.dpop_jkt,
109 params.expires_at,
110 serde_json::Value::Object(params.metadata)
111 )
112 .execute(conn)
113 .await?;
114
115 Ok(())
116 }
117
118 pub async fn consume_in_transaction(
126 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
127 digest: Digest,
128 client_id: Uuid,
129 ) -> ModelResult<OAuthAuthCode> {
130 let auth_code = sqlx::query_as!(
131 OAuthAuthCode,
132 r#"
133 UPDATE oauth_auth_codes
134 SET used = true
135 WHERE digest = $1
136 AND client_id = $2
137 AND used = false
138 AND expires_at > now()
139 RETURNING
140 digest as "digest: _",
141 user_id,
142 client_id,
143 redirect_uri,
144 scopes,
145 jti,
146 nonce,
147 code_challenge,
148 code_challenge_method as "code_challenge_method: PkceMethod",
149 dpop_jkt,
150 used,
151 expires_at,
152 metadata
153 "#,
154 digest.as_bytes(),
155 client_id
156 )
157 .fetch_one(&mut **tx)
158 .await?;
159
160 Ok(auth_code)
161 }
162
163 pub async fn consume_with_redirect_in_transaction(
171 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
172 digest: Digest,
173 client_id: Uuid,
174 redirect_uri: &str,
175 ) -> ModelResult<OAuthAuthCode> {
176 let auth_code = sqlx::query_as!(
177 OAuthAuthCode,
178 r#"
179 UPDATE oauth_auth_codes
180 SET used = true
181 WHERE digest = $1
182 AND client_id = $2
183 AND redirect_uri = $3
184 AND used = false
185 AND expires_at > now()
186 RETURNING
187 digest as "digest: _",
188 user_id,
189 client_id,
190 redirect_uri,
191 scopes,
192 jti,
193 nonce,
194 code_challenge,
195 code_challenge_method as "code_challenge_method: PkceMethod",
196 dpop_jkt,
197 used,
198 expires_at,
199 metadata
200 "#,
201 digest.as_bytes(),
202 client_id,
203 redirect_uri
204 )
205 .fetch_one(&mut **tx)
206 .await?;
207
208 Ok(auth_code)
209 }
210}