| 9 |
9 |
|
use crate::error::Result;
|
| 10 |
10 |
|
|
| 11 |
11 |
|
/// Store a new OAuth authorization code, carrying the granted scope.
|
|
12 |
+ |
///
|
|
13 |
+ |
/// `code_hash` is the SHA-256 hex of the opaque code, never the plaintext —
|
|
14 |
+ |
/// the `code` column holds the hash, looked up by hash in [`peek_oauth_code`] /
|
|
15 |
+ |
/// [`consume_oauth_code`], so a DB read never exposes a live, redeemable code.
|
|
16 |
+ |
/// Same at-rest contract as `oauth_refresh_tokens.token_hash`.
|
| 12 |
17 |
|
#[allow(clippy::too_many_arguments)]
|
| 13 |
18 |
|
#[tracing::instrument(skip_all)]
|
| 14 |
19 |
|
pub async fn create_oauth_code(
|
| 15 |
20 |
|
pool: &PgPool,
|
| 16 |
|
- |
code: &str,
|
|
21 |
+ |
code_hash: &str,
|
| 17 |
22 |
|
app_id: SyncAppId,
|
| 18 |
23 |
|
user_id: UserId,
|
| 19 |
24 |
|
code_challenge: &str,
|
| 30 |
35 |
|
RETURNING *
|
| 31 |
36 |
|
"#,
|
| 32 |
37 |
|
)
|
| 33 |
|
- |
.bind(code)
|
|
38 |
+ |
.bind(code_hash)
|
| 34 |
39 |
|
.bind(app_id)
|
| 35 |
40 |
|
.bind(user_id)
|
| 36 |
41 |
|
.bind(code_challenge)
|
| 162 |
167 |
|
|
| 163 |
168 |
|
/// Fetch a still-valid authorization code WITHOUT consuming it.
|
| 164 |
169 |
|
///
|
| 165 |
|
- |
/// Lets the token handler validate client_id / redirect_uri / PKCE against the
|
| 166 |
|
- |
/// code's stored values before burning it, so a failed validation leaves the
|
| 167 |
|
- |
/// code usable for the legitimate client's retry (ultra-fuzz Run #1 Security
|
| 168 |
|
- |
/// LOW: the code was previously marked used before any of those checks). The
|
| 169 |
|
- |
/// atomic `consume_oauth_code` below is still what actually claims the code, so
|
| 170 |
|
- |
/// concurrent redemptions remain race-safe — this is only a pre-flight read.
|
|
170 |
+ |
/// `code_hash` is the SHA-256 hex of the presented code (the column stores the
|
|
171 |
+ |
/// hash, not the plaintext). Lets the token handler validate client_id /
|
|
172 |
+ |
/// redirect_uri / PKCE against the code's stored values before burning it, so a
|
|
173 |
+ |
/// failed validation leaves the code usable for the legitimate client's retry
|
|
174 |
+ |
/// (ultra-fuzz Run #1 Security LOW: the code was previously marked used before
|
|
175 |
+ |
/// any of those checks). The atomic `consume_oauth_code` below is still what
|
|
176 |
+ |
/// actually claims the code, so concurrent redemptions remain race-safe — this
|
|
177 |
+ |
/// is only a pre-flight read.
|
| 171 |
178 |
|
#[tracing::instrument(skip_all)]
|
| 172 |
|
- |
pub async fn peek_oauth_code(pool: &PgPool, code: &str) -> Result<Option<DbOAuthCode>> {
|
|
179 |
+ |
pub async fn peek_oauth_code(pool: &PgPool, code_hash: &str) -> Result<Option<DbOAuthCode>> {
|
| 173 |
180 |
|
let row = sqlx::query_as::<_, DbOAuthCode>(
|
| 174 |
181 |
|
r#"
|
| 175 |
182 |
|
SELECT * FROM oauth_authorization_codes
|
| 178 |
185 |
|
AND expires_at > NOW()
|
| 179 |
186 |
|
"#,
|
| 180 |
187 |
|
)
|
| 181 |
|
- |
.bind(code)
|
|
188 |
+ |
.bind(code_hash)
|
| 182 |
189 |
|
.fetch_optional(pool)
|
| 183 |
190 |
|
.await?;
|
| 184 |
191 |
|
|
| 187 |
194 |
|
|
| 188 |
195 |
|
/// Atomically consume an authorization code: mark it used and return it in one step.
|
| 189 |
196 |
|
///
|
| 190 |
|
- |
/// Returns `Some(code)` if the code was valid and successfully consumed,
|
| 191 |
|
- |
/// or `None` if the code was already used, expired, or does not exist.
|
| 192 |
|
- |
/// Because this is a single UPDATE with `used_at IS NULL` in the WHERE clause,
|
| 193 |
|
- |
/// concurrent requests for the same code will never both succeed.
|
|
197 |
+ |
/// `code_hash` is the SHA-256 hex of the presented code. Returns `Some(code)` if
|
|
198 |
+ |
/// it was valid and successfully consumed, or `None` if already used, expired, or
|
|
199 |
+ |
/// nonexistent. Because this is a single UPDATE with `used_at IS NULL` in the
|
|
200 |
+ |
/// WHERE clause, concurrent requests for the same code will never both succeed.
|
| 194 |
201 |
|
#[tracing::instrument(skip_all)]
|
| 195 |
|
- |
pub async fn consume_oauth_code(pool: &PgPool, code: &str) -> Result<Option<DbOAuthCode>> {
|
|
202 |
+ |
pub async fn consume_oauth_code(pool: &PgPool, code_hash: &str) -> Result<Option<DbOAuthCode>> {
|
| 196 |
203 |
|
let row = sqlx::query_as::<_, DbOAuthCode>(
|
| 197 |
204 |
|
r#"
|
| 198 |
205 |
|
UPDATE oauth_authorization_codes
|
| 203 |
210 |
|
RETURNING *
|
| 204 |
211 |
|
"#,
|
| 205 |
212 |
|
)
|
| 206 |
|
- |
.bind(code)
|
|
213 |
+ |
.bind(code_hash)
|
| 207 |
214 |
|
.fetch_optional(pool)
|
| 208 |
215 |
|
.await?;
|
| 209 |
216 |
|
|