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 created_at: DateTime<Utc>,
39 pub updated_at: DateTime<Utc>,
40 pub metadata: serde_json::Value,
41 pub resource: Option<String>,
43}
44
45#[derive(Debug, Clone)]
46pub struct NewAuthCodeParams<'a> {
47 pub digest: &'a Digest,
48 pub user_id: Uuid,
49 pub client_id: Uuid,
50 pub redirect_uri: &'a str,
51 pub scopes: &'a [String],
52 pub nonce: Option<&'a str>,
53
54 pub code_challenge: Option<&'a str>,
55 pub code_challenge_method: Option<PkceMethod>,
56
57 pub dpop_jkt: Option<&'a str>,
58
59 pub expires_at: DateTime<Utc>,
60 pub metadata: serde_json::Map<String, serde_json::Value>,
61 pub resource: Option<&'a str>,
62}
63
64impl<'a> NewAuthCodeParams<'a> {
65 pub fn validate(&self) -> ModelResult<()> {
66 match (self.code_challenge, self.code_challenge_method) {
68 (Some(_), Some(_)) | (None, None) => {}
69 _ => {
70 return Err(ModelError::new(
71 ModelErrorType::InvalidRequest,
72 "PKCE: code_challenge and code_challenge_method must be provided together",
73 None::<anyhow::Error>,
74 ));
75 }
76 }
77 Ok(())
78 }
79}
80
81impl OAuthAuthCode {
82 pub async fn insert(conn: &mut PgConnection, params: NewAuthCodeParams<'_>) -> ModelResult<()> {
83 params.validate()?;
84
85 sqlx::query!(
86 r#"
87 INSERT INTO oauth_auth_codes (
88 digest,
89 user_id,
90 client_id,
91 redirect_uri,
92 scopes,
93 nonce,
94 code_challenge,
95 code_challenge_method,
96 dpop_jkt,
97 expires_at,
98 metadata,
99 resource
100 )
101 VALUES (
102 $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12
103 )
104 "#,
105 params.digest.as_bytes(),
106 params.user_id,
107 params.client_id,
108 params.redirect_uri,
109 params.scopes,
110 params.nonce,
111 params.code_challenge,
112 params.code_challenge_method as Option<PkceMethod>,
114 params.dpop_jkt,
115 params.expires_at,
116 serde_json::Value::Object(params.metadata),
117 params.resource
118 )
119 .execute(conn)
120 .await?;
121
122 Ok(())
123 }
124
125 pub async fn consume_in_transaction(
133 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
134 digest: Digest,
135 client_id: Uuid,
136 ) -> ModelResult<OAuthAuthCode> {
137 let auth_code = sqlx::query_as!(
138 OAuthAuthCode,
139 r#"
140 UPDATE oauth_auth_codes
141 SET used = true
142 WHERE digest = $1
143 AND client_id = $2
144 AND used = false
145 AND expires_at > now()
146 RETURNING *
147 "#,
148 digest.as_bytes(),
149 client_id
150 )
151 .fetch_one(&mut **tx)
152 .await?;
153
154 Ok(auth_code)
155 }
156
157 pub async fn consume_with_redirect_in_transaction(
165 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
166 digest: Digest,
167 client_id: Uuid,
168 redirect_uri: &str,
169 ) -> ModelResult<OAuthAuthCode> {
170 let auth_code = sqlx::query_as!(
171 OAuthAuthCode,
172 r#"
173 UPDATE oauth_auth_codes
174 SET used = true
175 WHERE digest = $1
176 AND client_id = $2
177 AND redirect_uri = $3
178 AND used = false
179 AND expires_at > now()
180 RETURNING *
181 "#,
182 digest.as_bytes(),
183 client_id,
184 redirect_uri
185 )
186 .fetch_one(&mut **tx)
187 .await?;
188
189 Ok(auth_code)
190 }
191}