1use std::fmt::Write as _;
2use std::path::{Path, PathBuf};
3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::{Arc, Mutex};
5
6use rusqlite::types::Value;
7use rusqlite::{Connection, OptionalExtension, params};
8use sha2::{Digest, Sha256};
9use tracing::warn;
10
11use crate::at_rest::{TokenEncryptionKey, maybe_decrypt_bound, maybe_encrypt_bound};
12use crate::error::AuthError;
13use crate::types::{
14 AllowedUserRow, AuthorizationCodeRow, AuthorizationRequestRow, BrowserLoginStateRow,
15 BrowserSessionRow, NativeAuthorizationResultRow, RefreshTokenRow, RegisteredClient,
16 UpstreamOauthCredentialRow, UpstreamOauthStateRow,
17};
18
19#[path = "sqlite_rows.rs"]
20mod sqlite_rows;
21use sqlite_rows::{
22 row_to_allowed_user, row_to_authorization_code, row_to_authorization_request,
23 row_to_browser_login_state, row_to_browser_session, row_to_native_authorization_result,
24 row_to_upstream_oauth_credentials, row_to_upstream_oauth_state,
25};
26
27const UPSTREAM_OAUTH_STATE_MAX_TTL_SECS: i64 = 600;
28const SCHEMA_VERSION: i64 = 2;
31
32use crate::util::{
33 ensure_restrictive_permissions, fingerprint, now_unix, set_restrictive_permissions,
34};
35
36const SQLITE_BUSY_TIMEOUT_MS: u64 = 5_000;
37const SQLITE_POOL_SIZE: usize = 4;
38
39#[derive(Clone)]
40pub struct SqliteStore {
41 conns: Arc<Vec<Mutex<Connection>>>,
42 next_conn: Arc<AtomicUsize>,
43 path: Arc<PathBuf>,
44 enc_key: Option<Arc<TokenEncryptionKey>>,
46}
47
48impl std::fmt::Debug for SqliteStore {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("SqliteStore")
51 .field("path", &self.path)
52 .field("enc_key", &self.enc_key.as_ref().map(|_| "<redacted>"))
53 .finish_non_exhaustive()
54 }
55}
56
57impl SqliteStore {
58 pub async fn open(path: PathBuf) -> Result<Self, AuthError> {
59 Self::open_with_key(path, None).await
60 }
61
62 pub async fn open_with_key(
63 path: PathBuf,
64 enc_key: Option<TokenEncryptionKey>,
65 ) -> Result<Self, AuthError> {
66 let path_for_open = path.clone();
67 let conns = tokio::task::spawn_blocking(move || {
68 open_connections(path_for_open.as_path(), SQLITE_POOL_SIZE)
69 })
70 .await;
71 let store = match conns {
72 Ok(result) => result,
73 Err(error) => Err(AuthError::Storage(format!(
74 "sqlite open task failed: {error}"
75 ))),
76 }
77 .map(|conns| Self {
78 conns: Arc::new(conns.into_iter().map(Mutex::new).collect()),
79 next_conn: Arc::new(AtomicUsize::new(0)),
80 path: Arc::new(path),
81 enc_key: enc_key.map(Arc::new),
82 })?;
83
84 store.cleanup_expired().await?;
85 Ok(store)
86 }
87
88 pub async fn pragma(&self, name: &str) -> Result<String, AuthError> {
89 let pragma = match name {
90 "journal_mode" | "busy_timeout" | "foreign_keys" => name.to_string(),
91 other => {
92 return Err(AuthError::Config(format!(
93 "unsupported pragma query `{other}`"
94 )));
95 }
96 };
97
98 self.with_conn(move |conn| {
99 conn.query_row(&format!("PRAGMA {pragma};"), [], |row| {
100 row.get::<_, Value>(0)
101 })
102 .map(|value| match value {
103 Value::Text(text) => text,
104 Value::Integer(int) => int.to_string(),
105 other => format!("{other:?}"),
106 })
107 .map_err(sqlite_error)
108 })
109 .await
110 }
111
112 pub async fn register_client(&self, client: RegisteredClient) -> Result<(), AuthError> {
113 self.with_conn(move |conn| {
114 let redirect_uris = serde_json::to_string(&client.redirect_uris)
115 .map_err(|error| AuthError::Storage(format!("serialize redirect_uris: {error}")))?;
116 conn.execute(
117 "INSERT INTO registered_clients (client_id, redirect_uris, created_at)
118 VALUES (?1, ?2, ?3)
119 ON CONFLICT(client_id) DO UPDATE SET
120 redirect_uris = excluded.redirect_uris,
121 created_at = excluded.created_at",
122 params![client.client_id, redirect_uris, client.created_at],
123 )
124 .map_err(sqlite_error)?;
125 Ok(())
126 })
127 .await
128 }
129
130 pub async fn find_client(
131 &self,
132 client_id: &str,
133 ) -> Result<Option<RegisteredClient>, AuthError> {
134 let client_id = client_id.to_string();
135 self.with_conn(move |conn| {
136 conn.query_row(
137 "SELECT client_id, redirect_uris, created_at
138 FROM registered_clients
139 WHERE client_id = ?1",
140 params![client_id],
141 |row| {
142 let redirect_uris: String = row.get(1)?;
143 let redirect_uris = serde_json::from_str(&redirect_uris).map_err(|error| {
144 rusqlite::Error::FromSqlConversionFailure(
145 1,
146 rusqlite::types::Type::Text,
147 Box::new(error),
148 )
149 })?;
150 Ok(RegisteredClient {
151 client_id: row.get(0)?,
152 redirect_uris,
153 created_at: row.get(2)?,
154 })
155 },
156 )
157 .optional()
158 .map_err(sqlite_error)
159 })
160 .await
161 }
162
163 pub async fn insert_authorization_request(
164 &self,
165 request: AuthorizationRequestRow,
166 ) -> Result<(), AuthError> {
167 self.with_conn(move |conn| {
168 conn.execute(
169 "INSERT INTO authorization_requests (
170 state, client_id, redirect_uri, client_state, resource, scope, provider_code_verifier,
171 code_challenge, code_challenge_method, created_at, expires_at, provider
172 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
173 params![
174 request.state,
175 request.client_id,
176 request.redirect_uri,
177 request.client_state,
178 request.resource,
179 request.scope,
180 request.provider_code_verifier,
181 request.code_challenge,
182 request.code_challenge_method,
183 request.created_at,
184 request.expires_at,
185 request.provider,
186 ],
187 )
188 .map_err(sqlite_error)?;
189 Ok(())
190 })
191 .await
192 }
193
194 pub async fn take_authorization_request(
195 &self,
196 state: &str,
197 ) -> Result<AuthorizationRequestRow, AuthError> {
198 let state = state.to_string();
199 let now = now_unix();
200 self.with_conn(move |conn| {
201 conn.query_row(
202 "DELETE FROM authorization_requests
203 WHERE state = ?1
204 AND expires_at > ?2
205 RETURNING state, client_id, redirect_uri, client_state, scope, provider_code_verifier,
206 code_challenge, code_challenge_method, created_at, expires_at, resource, provider",
207 params![state, now],
208 row_to_authorization_request,
209 )
210 .map_err(|error| match error {
211 rusqlite::Error::QueryReturnedNoRows => AuthError::InvalidGrant(
212 "authorization state is missing, expired, or already used".to_string(),
213 ),
214 other => sqlite_error(other),
215 })
216 })
217 .await
218 }
219
220 pub async fn insert_auth_code(&self, code: AuthorizationCodeRow) -> Result<(), AuthError> {
221 self.with_conn(move |conn| {
222 conn.execute(
223 "INSERT INTO authorization_codes (
224 code, client_id, subject, redirect_uri, resource, scope,
225 code_challenge, code_challenge_method, provider_refresh_token,
226 created_at, expires_at, provider
227 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
228 params![
229 code.code,
230 code.client_id,
231 code.subject,
232 code.redirect_uri,
233 code.resource,
234 code.scope,
235 code.code_challenge,
236 code.code_challenge_method,
237 code.provider_refresh_token,
238 code.created_at,
239 code.expires_at,
240 code.provider,
241 ],
242 )
243 .map_err(sqlite_error)?;
244 Ok(())
245 })
246 .await
247 }
248
249 pub async fn redeem_auth_code(&self, code: &str) -> Result<AuthorizationCodeRow, AuthError> {
250 let code = code.to_string();
251 let now = now_unix();
252 self.with_conn(move |conn| {
253 conn.query_row(
254 "DELETE FROM authorization_codes
255 WHERE code = ?1
256 AND expires_at > ?2
257 RETURNING code, client_id, subject, redirect_uri, scope,
258 code_challenge, code_challenge_method, provider_refresh_token,
259 created_at, expires_at, resource, provider",
260 params![code, now],
261 row_to_authorization_code,
262 )
263 .map_err(|error| match error {
264 rusqlite::Error::QueryReturnedNoRows => AuthError::InvalidGrant(
265 "authorization code is missing, expired, or already redeemed".to_string(),
266 ),
267 other => sqlite_error(other),
268 })
269 })
270 .await
271 }
272
273 pub async fn upsert_refresh_token(&self, token: RefreshTokenRow) -> Result<(), AuthError> {
281 let hash = hash_token(&token.refresh_token);
282 let encrypted_provider_rt = token
283 .provider_refresh_token
284 .as_deref()
285 .map(|raw| maybe_encrypt_bound(self.enc_key.as_deref(), raw, &refresh_token_aad(&hash)))
286 .transpose()?;
287 self.with_conn(move |conn| {
288 conn.execute(
289 "INSERT INTO refresh_tokens (
290 refresh_token_hash, client_id, subject, resource, scope,
291 provider_refresh_token, created_at, expires_at, provider
292 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
293 ON CONFLICT(refresh_token_hash) DO UPDATE SET
294 client_id = excluded.client_id,
295 subject = excluded.subject,
296 resource = excluded.resource,
297 scope = excluded.scope,
298 provider_refresh_token = excluded.provider_refresh_token,
299 created_at = excluded.created_at,
300 expires_at = excluded.expires_at,
301 provider = excluded.provider",
302 params![
303 hash,
304 token.client_id,
305 token.subject,
306 token.resource,
307 token.scope,
308 encrypted_provider_rt,
309 token.created_at,
310 token.expires_at,
311 token.provider,
312 ],
313 )
314 .map_err(sqlite_error)?;
315 Ok(())
316 })
317 .await
318 }
319
320 pub async fn rotate_refresh_token(
332 &self,
333 old_token: &str,
334 new_token: RefreshTokenRow,
335 ) -> Result<Option<RefreshTokenRow>, AuthError> {
336 let old_hash = hash_token(old_token);
337 let new_hash = hash_token(&new_token.refresh_token);
338 let now = now_unix();
339 let encrypted_provider_rt = new_token
340 .provider_refresh_token
341 .as_deref()
342 .map(|raw| {
343 maybe_encrypt_bound(self.enc_key.as_deref(), raw, &refresh_token_aad(&new_hash))
344 })
345 .transpose()?;
346 self.with_conn(move |conn| {
347 conn.execute_batch("BEGIN").map_err(sqlite_error)?;
348
349 let delete_result = conn
350 .execute(
351 "DELETE FROM refresh_tokens
352 WHERE refresh_token_hash = ?1
353 AND expires_at > ?2",
354 params![old_hash, now],
355 )
356 .map_err(sqlite_error);
357
358 let deleted = match delete_result {
359 Ok(n) => n,
360 Err(e) => {
361 drop(conn.execute_batch("ROLLBACK"));
362 return Err(e);
363 }
364 };
365
366 if deleted == 0 {
367 drop(conn.execute_batch("ROLLBACK"));
369 return Ok(None);
370 }
371
372 let insert_result = conn
373 .execute(
374 "INSERT INTO refresh_tokens (
375 refresh_token_hash, client_id, subject, resource, scope,
376 provider_refresh_token, created_at, expires_at, provider
377 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
378 params![
379 new_hash,
380 new_token.client_id,
381 new_token.subject,
382 new_token.resource,
383 new_token.scope,
384 encrypted_provider_rt,
385 new_token.created_at,
386 new_token.expires_at,
387 new_token.provider,
388 ],
389 )
390 .map_err(sqlite_error);
391
392 match insert_result {
393 Ok(_) => {
394 conn.execute_batch("COMMIT").map_err(sqlite_error)?;
395 Ok(Some(new_token))
396 }
397 Err(e) => {
398 drop(conn.execute_batch("ROLLBACK"));
399 Err(e)
400 }
401 }
402 })
403 .await
404 }
405
406 pub async fn find_refresh_token(
407 &self,
408 refresh_token: &str,
409 ) -> Result<Option<RefreshTokenRow>, AuthError> {
410 let hash = hash_token(refresh_token);
411 let plaintext = refresh_token.to_string();
414 let now = now_unix();
415 let enc_key = self.enc_key.clone();
416 self.with_conn(move |conn| {
417 let row = conn
418 .query_row(
419 "SELECT client_id, subject, scope,
420 provider_refresh_token, created_at, expires_at, resource, provider
421 FROM refresh_tokens
422 WHERE refresh_token_hash = ?1
423 AND expires_at > ?2",
424 params![hash, now],
425 |row| {
426 Ok(RefreshTokenRow {
427 refresh_token: plaintext.clone(),
428 client_id: row.get(0)?,
429 subject: row.get(1)?,
430 scope: row.get(2)?,
431 provider_refresh_token: row.get(3)?,
432 created_at: row.get(4)?,
433 expires_at: row.get(5)?,
434 resource: row.get(6).unwrap_or_default(),
435 provider: row.get(7)?,
436 })
437 },
438 )
439 .optional()
440 .map_err(sqlite_error)?;
441
442 match row {
449 Some(mut r) => {
450 if let Some(raw) = r.provider_refresh_token.as_deref() {
451 r.provider_refresh_token = Some(maybe_decrypt_bound(
452 enc_key.as_deref(),
453 raw,
454 &refresh_token_aad(&hash),
455 )?);
456 }
457 Ok(Some(r))
458 }
459 None => Ok(None),
460 }
461 })
462 .await
463 }
464
465 pub async fn has_any_refresh_token(&self) -> Result<bool, AuthError> {
480 let now = now_unix();
481 self.with_conn(move |conn| {
482 conn.query_row(
483 "SELECT EXISTS(SELECT 1 FROM refresh_tokens WHERE expires_at > ?1)",
484 params![now],
485 |row| row.get::<_, i64>(0),
486 )
487 .map(|count| count != 0)
488 .map_err(sqlite_error)
489 })
490 .await
491 }
492
493 pub async fn has_any_refresh_token_for_provider(
502 &self,
503 provider: &str,
504 ) -> Result<bool, AuthError> {
505 let provider = provider.to_string();
506 let now = now_unix();
507 self.with_conn(move |conn| {
508 conn.query_row(
509 "SELECT EXISTS(SELECT 1 FROM refresh_tokens WHERE provider = ?1 AND expires_at > ?2)",
510 params![provider, now],
511 |row| row.get::<_, i64>(0),
512 )
513 .map(|count| count != 0)
514 .map_err(sqlite_error)
515 })
516 .await
517 }
518
519 pub async fn upsert_browser_session(
520 &self,
521 session: BrowserSessionRow,
522 ) -> Result<(), AuthError> {
523 self.with_conn(move |conn| {
524 conn.execute(
525 "INSERT INTO browser_sessions (
526 session_id, subject, email, csrf_token, created_at, expires_at
527 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
528 ON CONFLICT(session_id) DO UPDATE SET
529 subject = excluded.subject,
530 email = excluded.email,
531 csrf_token = excluded.csrf_token,
532 created_at = excluded.created_at,
533 expires_at = excluded.expires_at",
534 params![
535 session.session_id,
536 session.subject,
537 session.email,
538 session.csrf_token,
539 session.created_at,
540 session.expires_at,
541 ],
542 )
543 .map_err(sqlite_error)?;
544 Ok(())
545 })
546 .await
547 }
548
549 pub async fn find_browser_session(
550 &self,
551 session_id: &str,
552 ) -> Result<Option<BrowserSessionRow>, AuthError> {
553 let session_id = session_id.to_string();
554 let now = now_unix();
555 self.with_conn(move |conn| {
556 conn.query_row(
557 "SELECT session_id, subject, email, csrf_token, created_at, expires_at
558 FROM browser_sessions
559 WHERE session_id = ?1
560 AND expires_at > ?2",
561 params![session_id, now],
562 row_to_browser_session,
563 )
564 .optional()
565 .map_err(sqlite_error)
566 })
567 .await
568 }
569
570 pub async fn revoke_browser_session(&self, session_id: &str) -> Result<(), AuthError> {
571 let session_id = session_id.to_string();
572 self.with_conn(move |conn| {
573 conn.execute(
574 "DELETE FROM browser_sessions WHERE session_id = ?1",
575 params![session_id],
576 )
577 .map_err(sqlite_error)?;
578 Ok(())
579 })
580 .await
581 }
582
583 #[cfg(any(test, debug_assertions))]
590 pub async fn execute_test_statement(&self, sql: &str) -> Result<(), AuthError> {
591 let sql = sql.to_string();
592 self.with_conn(move |conn| conn.execute_batch(&sql).map_err(sqlite_error))
593 .await
594 }
595
596 pub async fn insert_browser_login_state(
597 &self,
598 login: BrowserLoginStateRow,
599 ) -> Result<(), AuthError> {
600 self.with_conn(move |conn| {
601 conn.execute(
602 "INSERT INTO browser_login_states (
603 state, return_to, provider_code_verifier, created_at, expires_at, provider
604 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
605 params![
606 login.state,
607 login.return_to,
608 login.provider_code_verifier,
609 login.created_at,
610 login.expires_at,
611 login.provider,
612 ],
613 )
614 .map_err(sqlite_error)?;
615 Ok(())
616 })
617 .await
618 }
619
620 pub async fn count_pending_oauth_states(&self) -> Result<usize, AuthError> {
621 let now = now_unix();
622 self.with_conn(move |conn| {
623 let authorization_requests: i64 = conn
624 .query_row(
625 "SELECT COUNT(*) FROM authorization_requests WHERE expires_at > ?1",
626 params![now],
627 |row| row.get(0),
628 )
629 .map_err(sqlite_error)?;
630 let browser_login_states: i64 = conn
631 .query_row(
632 "SELECT COUNT(*) FROM browser_login_states WHERE expires_at > ?1",
633 params![now],
634 |row| row.get(0),
635 )
636 .map_err(sqlite_error)?;
637 let native_authorization_results: i64 = conn
638 .query_row(
639 "SELECT COUNT(*) FROM native_authorization_results WHERE expires_at > ?1",
640 params![now],
641 |row| row.get(0),
642 )
643 .map_err(sqlite_error)?;
644 Ok(
645 (authorization_requests + browser_login_states + native_authorization_results)
646 as usize,
647 )
648 })
649 .await
650 }
651
652 pub async fn take_browser_login_state(
653 &self,
654 state: &str,
655 ) -> Result<Option<BrowserLoginStateRow>, AuthError> {
656 let state = state.to_string();
657 let now = now_unix();
658 self.with_conn(move |conn| {
659 conn.query_row(
660 "DELETE FROM browser_login_states
661 WHERE state = ?1
662 AND expires_at > ?2
663 RETURNING state, return_to, provider_code_verifier, created_at, expires_at, provider",
664 params![state, now],
665 row_to_browser_login_state,
666 )
667 .optional()
668 .map_err(sqlite_error)
669 })
670 .await
671 }
672
673 pub async fn insert_native_authorization_result(
683 &self,
684 result: NativeAuthorizationResultRow,
685 ) -> Result<(), AuthError> {
686 self.with_conn(move |conn| {
687 conn.execute(
688 "INSERT INTO native_authorization_results (state, code, created_at, expires_at)
689 VALUES (?1, ?2, ?3, ?4)
690 ON CONFLICT(state) DO UPDATE SET
691 code = excluded.code,
692 created_at = excluded.created_at,
693 expires_at = excluded.expires_at",
694 params![
695 result.state,
696 result.code,
697 result.created_at,
698 result.expires_at,
699 ],
700 )
701 .map_err(sqlite_error)?;
702 Ok(())
703 })
704 .await
705 }
706
707 pub async fn take_native_authorization_result(
709 &self,
710 state: &str,
711 ) -> Result<Option<NativeAuthorizationResultRow>, AuthError> {
712 let state = state.to_string();
713 let now = now_unix();
714 self.with_conn(move |conn| {
715 conn.query_row(
716 "DELETE FROM native_authorization_results
717 WHERE state = ?1
718 AND expires_at > ?2
719 RETURNING state, code, created_at, expires_at",
720 params![state, now],
721 row_to_native_authorization_result,
722 )
723 .optional()
724 .map_err(sqlite_error)
725 })
726 .await
727 }
728
729 pub async fn cleanup_expired(&self) -> Result<u64, AuthError> {
733 let now = now_unix();
734 self.with_conn(move |conn| {
735 let mut total: u64 = 0;
736 for table in [
737 "authorization_requests",
738 "authorization_codes",
739 "refresh_tokens",
740 "browser_sessions",
741 "browser_login_states",
742 "native_authorization_results",
743 ] {
744 let deleted = conn
745 .execute(
746 &format!("DELETE FROM {table} WHERE expires_at <= ?1"),
747 params![now],
748 )
749 .map_err(sqlite_error)?;
750 total += deleted as u64;
751 }
752 let deleted = conn
753 .execute(
754 "DELETE FROM upstream_oauth_state WHERE expires_at <= ?1",
755 params![now],
756 )
757 .map_err(sqlite_error)?;
758 total += deleted as u64;
759 let deleted = conn
760 .execute(
761 "DELETE FROM upstream_oauth_credentials
762 WHERE access_token_expires_at <= ?1 AND refresh_token_present = 0",
763 params![now],
764 )
765 .map_err(sqlite_error)?;
766 total += deleted as u64;
767 Ok(total)
768 })
769 .await
770 }
771
772 pub async fn upsert_upstream_oauth_credentials(
773 &self,
774 row: UpstreamOauthCredentialRow,
775 ) -> Result<(), AuthError> {
776 self.with_conn(move |conn| {
777 conn.execute(
778 "INSERT INTO upstream_oauth_credentials (
779 upstream_name, subject, client_id, granted_scopes_json,
780 token_blob, token_blob_nonce, token_received_at,
781 access_token_expires_at, refresh_token_present
782 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
783 ON CONFLICT(upstream_name, subject) DO UPDATE SET
784 client_id = excluded.client_id,
785 granted_scopes_json = excluded.granted_scopes_json,
786 token_blob = excluded.token_blob,
787 token_blob_nonce = excluded.token_blob_nonce,
788 token_received_at = excluded.token_received_at,
789 access_token_expires_at = excluded.access_token_expires_at,
790 refresh_token_present = excluded.refresh_token_present",
791 params![
792 row.upstream_name,
793 row.subject,
794 row.client_id,
795 row.granted_scopes_json,
796 row.token_blob,
797 row.token_blob_nonce,
798 row.token_received_at,
799 row.access_token_expires_at,
800 i64::from(row.refresh_token_present),
801 ],
802 )
803 .map_err(sqlite_error)?;
804 Ok(())
805 })
806 .await
807 }
808
809 pub async fn find_upstream_oauth_credentials(
810 &self,
811 upstream_name: &str,
812 subject: &str,
813 ) -> Result<Option<UpstreamOauthCredentialRow>, AuthError> {
814 let upstream_name = upstream_name.to_string();
815 let subject = subject.to_string();
816 self.with_conn(move |conn| {
817 conn.query_row(
818 "SELECT upstream_name, subject, client_id, granted_scopes_json,
819 token_blob, token_blob_nonce, token_received_at,
820 access_token_expires_at, refresh_token_present
821 FROM upstream_oauth_credentials
822 WHERE upstream_name = ?1 AND subject = ?2",
823 params![upstream_name, subject],
824 row_to_upstream_oauth_credentials,
825 )
826 .optional()
827 .map_err(sqlite_error)
828 })
829 .await
830 }
831
832 pub async fn delete_upstream_oauth_credentials(
833 &self,
834 upstream_name: &str,
835 subject: &str,
836 ) -> Result<(), AuthError> {
837 let upstream_name = upstream_name.to_string();
838 let subject = subject.to_string();
839 self.with_conn(move |conn| {
840 conn.execute(
841 "DELETE FROM upstream_oauth_credentials
842 WHERE upstream_name = ?1 AND subject = ?2",
843 params![upstream_name, subject],
844 )
845 .map_err(sqlite_error)?;
846 Ok(())
847 })
848 .await
849 }
850
851 pub async fn save_upstream_oauth_state(
852 &self,
853 row: UpstreamOauthStateRow,
854 ) -> Result<(), AuthError> {
855 if row.expires_at <= row.created_at
856 || row.expires_at - row.created_at > UPSTREAM_OAUTH_STATE_MAX_TTL_SECS
857 {
858 return Err(AuthError::InvalidGrant(
859 "state TTL exceeds 600s".to_string(),
860 ));
861 }
862 self.with_conn(move |conn| {
863 conn.execute(
864 "INSERT INTO upstream_oauth_state (
865 upstream_name, subject, csrf_token, pkce_verifier, created_at, expires_at
866 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
867 params![
868 row.upstream_name,
869 row.subject,
870 row.csrf_token,
871 row.pkce_verifier,
872 row.created_at,
873 row.expires_at,
874 ],
875 )
876 .map_err(sqlite_error)?;
877 Ok(())
878 })
879 .await
880 }
881
882 pub async fn find_upstream_oauth_state_subject(
883 &self,
884 upstream_name: &str,
885 csrf_token: &str,
886 now: i64,
887 ) -> Result<Option<String>, AuthError> {
888 let upstream_name = upstream_name.to_string();
889 let csrf_token = csrf_token.to_string();
890 self.with_conn(move |conn| {
891 conn.query_row(
892 "SELECT subject
893 FROM upstream_oauth_state
894 WHERE upstream_name = ?1
895 AND csrf_token = ?2
896 AND expires_at > ?3",
897 params![upstream_name, csrf_token, now],
898 |row| row.get(0),
899 )
900 .optional()
901 .map_err(sqlite_error)
902 })
903 .await
904 }
905
906 pub async fn find_upstream_oauth_state_owner(
911 &self,
912 csrf_token: &str,
913 now: i64,
914 ) -> Result<Option<(String, String)>, AuthError> {
915 let csrf_token = csrf_token.to_string();
916 self.with_conn(move |conn| {
917 conn.query_row(
918 "SELECT upstream_name, subject
919 FROM upstream_oauth_state
920 WHERE csrf_token = ?1
921 AND expires_at > ?2",
922 params![csrf_token, now],
923 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
924 )
925 .optional()
926 .map_err(sqlite_error)
927 })
928 .await
929 }
930
931 pub async fn delete_upstream_oauth_state_by_csrf(
933 &self,
934 csrf_token: &str,
935 now: i64,
936 ) -> Result<(), AuthError> {
937 let csrf_token = csrf_token.to_string();
938 self.with_conn(move |conn| {
939 conn.execute(
940 "DELETE FROM upstream_oauth_state
941 WHERE csrf_token = ?1
942 AND expires_at > ?2",
943 params![csrf_token, now],
944 )
945 .map_err(sqlite_error)?;
946 Ok(())
947 })
948 .await
949 }
950
951 pub async fn set_upstream_oauth_state_client_id(
957 &self,
958 upstream_name: &str,
959 csrf_token: &str,
960 client_id: &str,
961 ) -> Result<(), AuthError> {
962 let upstream_name = upstream_name.to_string();
963 let csrf_token = csrf_token.to_string();
964 let client_id = client_id.to_string();
965 self.with_conn(move |conn| {
966 conn.execute(
967 "UPDATE upstream_oauth_state
968 SET dynamic_client_id = ?1
969 WHERE upstream_name = ?2
970 AND csrf_token = ?3",
971 params![client_id, upstream_name, csrf_token],
972 )
973 .map_err(sqlite_error)?;
974 Ok(())
975 })
976 .await
977 }
978
979 pub async fn get_upstream_oauth_state_client_id(
985 &self,
986 upstream_name: &str,
987 csrf_token: &str,
988 now: i64,
989 ) -> Result<Option<String>, AuthError> {
990 let upstream_name = upstream_name.to_string();
991 let csrf_token = csrf_token.to_string();
992 self.with_conn(move |conn| {
993 conn.query_row(
994 "SELECT dynamic_client_id
995 FROM upstream_oauth_state
996 WHERE upstream_name = ?1
997 AND csrf_token = ?2
998 AND expires_at > ?3",
999 params![upstream_name, csrf_token, now],
1000 |row| row.get::<_, Option<String>>(0),
1001 )
1002 .optional()
1003 .map(|opt| opt.flatten())
1004 .map_err(sqlite_error)
1005 })
1006 .await
1007 }
1008
1009 pub async fn take_upstream_oauth_state(
1011 &self,
1012 upstream_name: &str,
1013 subject: &str,
1014 csrf_token: &str,
1015 now: i64,
1016 ) -> Result<Option<UpstreamOauthStateRow>, AuthError> {
1017 let upstream_name = upstream_name.to_string();
1018 let subject = subject.to_string();
1019 let csrf_token = csrf_token.to_string();
1020 self.with_conn(move |conn| {
1021 conn.query_row(
1022 "DELETE FROM upstream_oauth_state
1023 WHERE upstream_name = ?1
1024 AND subject = ?2
1025 AND csrf_token = ?3
1026 AND expires_at > ?4
1027 RETURNING upstream_name, subject, csrf_token, pkce_verifier, created_at, expires_at",
1028 params![upstream_name, subject, csrf_token, now],
1029 row_to_upstream_oauth_state,
1030 )
1031 .optional()
1032 .map_err(sqlite_error)
1033 })
1034 .await
1035 }
1036
1037 pub async fn save_dynamic_client_registration(
1038 &self,
1039 upstream_name: &str,
1040 subject: &str,
1041 client_id: &str,
1042 ) -> Result<(), AuthError> {
1043 let upstream_name = upstream_name.to_string();
1044 let subject = subject.to_string();
1045 let client_id = client_id.to_string();
1046 let now = now_unix();
1047 self.with_conn(move |conn| {
1048 conn.execute(
1049 "INSERT INTO upstream_oauth_dynamic_clients (upstream_name, subject, client_id, created_at)
1050 VALUES (?1, ?2, ?3, ?4)
1051 ON CONFLICT(upstream_name, subject) DO UPDATE SET
1052 client_id = excluded.client_id,
1053 created_at = excluded.created_at",
1054 params![upstream_name, subject, client_id, now],
1055 )
1056 .map_err(sqlite_error)?;
1057 Ok(())
1058 })
1059 .await
1060 }
1061
1062 pub async fn find_dynamic_client_registration(
1063 &self,
1064 upstream_name: &str,
1065 subject: &str,
1066 ) -> Result<Option<String>, AuthError> {
1067 let upstream_name = upstream_name.to_string();
1068 let subject = subject.to_string();
1069 self.with_conn(move |conn| {
1070 conn.query_row(
1071 "SELECT client_id
1072 FROM upstream_oauth_dynamic_clients
1073 WHERE upstream_name = ?1 AND subject = ?2",
1074 params![upstream_name, subject],
1075 |row| row.get(0),
1076 )
1077 .optional()
1078 .map_err(sqlite_error)
1079 })
1080 .await
1081 }
1082
1083 pub async fn delete_dynamic_client_registration(
1084 &self,
1085 upstream_name: &str,
1086 subject: &str,
1087 ) -> Result<(), AuthError> {
1088 let upstream_name = upstream_name.to_string();
1089 let subject = subject.to_string();
1090 self.with_conn(move |conn| {
1091 conn.execute(
1092 "DELETE FROM upstream_oauth_dynamic_clients
1093 WHERE upstream_name = ?1 AND subject = ?2",
1094 params![upstream_name, subject],
1095 )
1096 .map_err(sqlite_error)?;
1097 Ok(())
1098 })
1099 .await
1100 }
1101
1102 pub async fn add_allowed_user(
1107 &self,
1108 email: &str,
1109 added_by: &str,
1110 created_at: i64,
1111 ) -> Result<(), AuthError> {
1112 let email = email.to_lowercase();
1113 let fp = fingerprint(&email);
1114 let added_by = added_by.to_string();
1115 self.with_conn(move |conn| {
1116 let changed = conn
1117 .execute(
1118 "INSERT INTO allowed_users (email, added_by, created_at)
1119 VALUES (?1, ?2, ?3)",
1120 params![email, added_by, created_at],
1121 )
1122 .map_err(|error| match error {
1123 rusqlite::Error::SqliteFailure(ref e, _)
1124 if e.code == rusqlite::ErrorCode::ConstraintViolation =>
1125 {
1126 AuthError::Validation(format!(
1127 "email fingerprint {fp} is already in the allowlist"
1128 ))
1129 }
1130 other => sqlite_error(other),
1131 })?;
1132 debug_assert_eq!(changed, 1);
1133 Ok(())
1134 })
1135 .await
1136 }
1137
1138 pub async fn remove_allowed_user(&self, email: &str) -> Result<(), AuthError> {
1142 let email = email.to_lowercase();
1143 self.with_conn(move |conn| {
1144 conn.execute("DELETE FROM allowed_users WHERE email = ?1", params![email])
1145 .map_err(sqlite_error)?;
1146 Ok(())
1147 })
1148 .await
1149 }
1150
1151 pub async fn list_allowed_users(&self) -> Result<Vec<AllowedUserRow>, AuthError> {
1153 self.with_conn(move |conn| {
1154 let mut stmt = conn
1155 .prepare(
1156 "SELECT email, added_by, created_at
1157 FROM allowed_users
1158 ORDER BY created_at ASC",
1159 )
1160 .map_err(sqlite_error)?;
1161 let rows = stmt
1162 .query_map([], row_to_allowed_user)
1163 .map_err(sqlite_error)?
1164 .collect::<rusqlite::Result<Vec<_>>>()
1165 .map_err(sqlite_error)?;
1166 Ok(rows)
1167 })
1168 .await
1169 }
1170
1171 async fn with_conn<T, F>(&self, op: F) -> Result<T, AuthError>
1172 where
1173 T: Send + 'static,
1174 F: FnOnce(&Connection) -> Result<T, AuthError> + Send + 'static,
1175 {
1176 let conns = Arc::clone(&self.conns);
1177 let path = Arc::clone(&self.path);
1178 let len = conns.len();
1179 let idx = self.next_conn.fetch_add(1, Ordering::Relaxed) % len;
1180 tokio::task::spawn_blocking(move || {
1181 let mut guard = conns[idx]
1182 .lock()
1183 .map_err(|_| AuthError::Storage("sqlite mutex poisoned".to_string()))?;
1184 validate_or_reopen_connection(&mut guard, path.as_ref())?;
1185 op(&guard)
1186 })
1187 .await
1188 .map_err(|error| AuthError::Storage(format!("sqlite task failed: {error}")))?
1189 }
1190
1191 #[cfg(test)]
1192 fn connection_count(&self) -> usize {
1193 self.conns.len()
1194 }
1195}
1196
1197fn open_connections(path: &Path, count: usize) -> Result<Vec<Connection>, AuthError> {
1198 (0..count).map(|_| open_connection(path)).collect()
1199}
1200
1201#[allow(clippy::too_many_lines)]
1202fn open_connection(path: &Path) -> Result<Connection, AuthError> {
1203 if let Some(parent) = path.parent() {
1204 std::fs::create_dir_all(parent).map_err(|error| {
1205 AuthError::Storage(format!(
1206 "create auth database directory `{}`: {error}",
1207 parent.display()
1208 ))
1209 })?;
1210 }
1211
1212 let existed = path.exists();
1213 if existed {
1214 ensure_restrictive_permissions(path)?;
1215 }
1216
1217 let conn = Connection::open(path).map_err(sqlite_error)?;
1218 conn.busy_timeout(std::time::Duration::from_millis(SQLITE_BUSY_TIMEOUT_MS))
1219 .map_err(sqlite_error)?;
1220 conn.pragma_update(None, "journal_mode", "WAL")
1221 .map_err(sqlite_error)?;
1222 conn.pragma_update(None, "foreign_keys", "ON")
1223 .map_err(sqlite_error)?;
1224 conn.execute_batch(
1225 "CREATE TABLE IF NOT EXISTS registered_clients (
1226 client_id TEXT PRIMARY KEY,
1227 redirect_uris TEXT NOT NULL,
1228 created_at INTEGER NOT NULL
1229 );
1230 CREATE TABLE IF NOT EXISTS authorization_requests (
1231 state TEXT PRIMARY KEY,
1232 client_id TEXT NOT NULL,
1233 redirect_uri TEXT NOT NULL,
1234 client_state TEXT NOT NULL,
1235 resource TEXT NOT NULL DEFAULT '',
1236 scope TEXT NOT NULL,
1237 provider TEXT NOT NULL DEFAULT 'google',
1238 provider_code_verifier TEXT NOT NULL,
1239 code_challenge TEXT NOT NULL,
1240 code_challenge_method TEXT NOT NULL,
1241 created_at INTEGER NOT NULL,
1242 expires_at INTEGER NOT NULL
1243 );
1244 CREATE TABLE IF NOT EXISTS authorization_codes (
1245 code TEXT PRIMARY KEY,
1246 client_id TEXT NOT NULL,
1247 subject TEXT NOT NULL,
1248 redirect_uri TEXT NOT NULL,
1249 resource TEXT NOT NULL DEFAULT '',
1250 scope TEXT NOT NULL,
1251 provider TEXT NOT NULL DEFAULT 'google',
1252 code_challenge TEXT NOT NULL,
1253 code_challenge_method TEXT NOT NULL,
1254 provider_refresh_token TEXT,
1255 created_at INTEGER NOT NULL,
1256 expires_at INTEGER NOT NULL
1257 );
1258 CREATE TABLE IF NOT EXISTS refresh_tokens (
1259 refresh_token_hash TEXT PRIMARY KEY,
1260 client_id TEXT NOT NULL,
1261 subject TEXT NOT NULL,
1262 resource TEXT NOT NULL DEFAULT '',
1263 scope TEXT NOT NULL,
1264 provider TEXT NOT NULL DEFAULT 'google',
1265 provider_refresh_token TEXT,
1266 created_at INTEGER NOT NULL,
1267 expires_at INTEGER NOT NULL
1268 );
1269 CREATE TABLE IF NOT EXISTS browser_sessions (
1270 session_id TEXT PRIMARY KEY,
1271 subject TEXT NOT NULL,
1272 email TEXT,
1273 csrf_token TEXT NOT NULL,
1274 created_at INTEGER NOT NULL,
1275 expires_at INTEGER NOT NULL
1276 );
1277 CREATE TABLE IF NOT EXISTS browser_login_states (
1278 state TEXT PRIMARY KEY,
1279 return_to TEXT NOT NULL,
1280 provider TEXT NOT NULL DEFAULT 'google',
1281 provider_code_verifier TEXT NOT NULL,
1282 created_at INTEGER NOT NULL,
1283 expires_at INTEGER NOT NULL
1284 );
1285 CREATE TABLE IF NOT EXISTS native_authorization_results (
1286 state TEXT PRIMARY KEY,
1287 code TEXT NOT NULL,
1288 created_at INTEGER NOT NULL,
1289 expires_at INTEGER NOT NULL
1290 );
1291 CREATE TABLE IF NOT EXISTS upstream_oauth_credentials (
1292 upstream_name TEXT NOT NULL,
1293 subject TEXT NOT NULL,
1294 client_id TEXT NOT NULL,
1295 granted_scopes_json TEXT NOT NULL,
1296 token_blob BLOB NOT NULL,
1297 token_blob_nonce BLOB NOT NULL,
1298 token_received_at INTEGER NOT NULL,
1299 access_token_expires_at INTEGER NOT NULL,
1300 refresh_token_present INTEGER NOT NULL,
1301 PRIMARY KEY (upstream_name, subject)
1302 ) WITHOUT ROWID;
1303 CREATE TABLE IF NOT EXISTS upstream_oauth_state (
1304 upstream_name TEXT NOT NULL,
1305 subject TEXT NOT NULL,
1306 csrf_token TEXT NOT NULL,
1307 pkce_verifier TEXT NOT NULL,
1308 created_at INTEGER NOT NULL,
1309 expires_at INTEGER NOT NULL,
1310 PRIMARY KEY (upstream_name, subject, csrf_token)
1311 ) WITHOUT ROWID;
1312 CREATE TABLE IF NOT EXISTS upstream_oauth_dynamic_clients (
1313 upstream_name TEXT NOT NULL,
1314 subject TEXT NOT NULL,
1315 client_id TEXT NOT NULL,
1316 created_at INTEGER NOT NULL,
1317 PRIMARY KEY (upstream_name, subject)
1318 ) WITHOUT ROWID;
1319 CREATE TABLE IF NOT EXISTS allowed_users (
1320 email TEXT PRIMARY KEY NOT NULL,
1321 added_by TEXT NOT NULL,
1322 created_at INTEGER NOT NULL
1323 );",
1324 )
1325 .map_err(sqlite_error)?;
1326 add_column_if_missing(
1327 &conn,
1328 "authorization_requests",
1329 "resource",
1330 "TEXT NOT NULL DEFAULT ''",
1331 )?;
1332 add_column_if_missing(
1333 &conn,
1334 "authorization_codes",
1335 "resource",
1336 "TEXT NOT NULL DEFAULT ''",
1337 )?;
1338 add_column_if_missing(
1339 &conn,
1340 "refresh_tokens",
1341 "resource",
1342 "TEXT NOT NULL DEFAULT ''",
1343 )?;
1344 add_column_if_missing(
1345 &conn,
1346 "authorization_requests",
1347 "provider",
1348 "TEXT NOT NULL DEFAULT 'google'",
1349 )?;
1350 add_column_if_missing(
1351 &conn,
1352 "authorization_codes",
1353 "provider",
1354 "TEXT NOT NULL DEFAULT 'google'",
1355 )?;
1356 add_column_if_missing(
1357 &conn,
1358 "refresh_tokens",
1359 "provider",
1360 "TEXT NOT NULL DEFAULT 'google'",
1361 )?;
1362 add_column_if_missing(
1363 &conn,
1364 "browser_login_states",
1365 "provider",
1366 "TEXT NOT NULL DEFAULT 'google'",
1367 )?;
1368
1369 if !existed {
1370 set_restrictive_permissions(path)?;
1371 }
1372 ensure_restrictive_permissions(path)?;
1373
1374 run_migrations(&conn)?;
1375
1376 Ok(conn)
1377}
1378
1379fn run_migrations(conn: &Connection) -> Result<(), AuthError> {
1388 let current_version: i64 = conn
1389 .query_row("PRAGMA user_version;", [], |row| row.get(0))
1390 .map_err(sqlite_error)?;
1391
1392 if current_version < 1 {
1393 let cols: Vec<String> = {
1396 let mut stmt = conn
1397 .prepare("PRAGMA table_info(refresh_tokens);")
1398 .map_err(sqlite_error)?;
1399 stmt.query_map([], |row| row.get::<_, String>(1))
1400 .map_err(sqlite_error)?
1401 .collect::<rusqlite::Result<Vec<_>>>()
1402 .map_err(sqlite_error)?
1403 };
1404
1405 if !cols.iter().any(|c| c == "refresh_token_hash") {
1406 conn.execute_batch("ALTER TABLE refresh_tokens ADD COLUMN refresh_token_hash TEXT;")
1408 .map_err(sqlite_error)?;
1409
1410 let rows: Vec<(String,)> = {
1415 let mut stmt = conn
1416 .prepare("SELECT refresh_token FROM refresh_tokens WHERE refresh_token_hash IS NULL;")
1417 .map_err(sqlite_error)?;
1418 stmt.query_map([], |row| Ok((row.get::<_, String>(0)?,)))
1419 .map_err(sqlite_error)?
1420 .collect::<rusqlite::Result<Vec<_>>>()
1421 .map_err(sqlite_error)?
1422 };
1423 for (plaintext,) in rows {
1424 let hash = hash_token(&plaintext);
1425 conn.execute(
1426 "UPDATE refresh_tokens SET refresh_token_hash = ?1 WHERE refresh_token = ?2 AND refresh_token_hash IS NULL;",
1427 params![hash, plaintext],
1428 )
1429 .map_err(sqlite_error)?;
1430 }
1431
1432 warn!(
1433 "migration v1: added refresh_token_hash column and backfilled existing rows — old plaintext tokens invalidated on next rotation"
1434 );
1435 }
1436
1437 conn.execute_batch(
1443 "CREATE UNIQUE INDEX IF NOT EXISTS idx_refresh_tokens_hash \
1444 ON refresh_tokens(refresh_token_hash);",
1445 )
1446 .map_err(sqlite_error)?;
1447
1448 conn.execute_batch("PRAGMA user_version = 1;")
1449 .map_err(sqlite_error)?;
1450 }
1451
1452 if current_version < 2 {
1453 add_column_if_missing(conn, "upstream_oauth_state", "dynamic_client_id", "TEXT")?;
1459
1460 conn.execute_batch(&format!("PRAGMA user_version = {SCHEMA_VERSION};"))
1461 .map_err(sqlite_error)?;
1462 }
1463
1464 Ok(())
1465}
1466
1467fn refresh_token_aad(refresh_token_hash: &str) -> Vec<u8> {
1480 format!("refresh_token_hash={refresh_token_hash}").into_bytes()
1481}
1482
1483fn hash_token(token: &str) -> String {
1484 let digest = Sha256::digest(token.as_bytes());
1485 let mut hex = String::with_capacity(64);
1486 for byte in &digest {
1487 let _ = write!(&mut hex, "{byte:02x}");
1488 }
1489 hex
1490}
1491
1492fn validate_or_reopen_connection(conn: &mut Connection, path: &Path) -> Result<(), AuthError> {
1493 let Err(error) = conn.query_row("SELECT 1", [], |row| row.get::<_, i64>(0)) else {
1494 return Ok(());
1495 };
1496 warn!(
1497 path = %path.display(),
1498 error = %error,
1499 "stale sqlite connection detected, reopening"
1500 );
1501
1502 *conn = open_connection(path)?;
1503 conn.query_row("SELECT 1", [], |row| row.get::<_, i64>(0))
1504 .map(|_| ())
1505 .map_err(sqlite_error)
1506}
1507
1508#[allow(clippy::needless_pass_by_value)]
1509fn sqlite_error(error: rusqlite::Error) -> AuthError {
1510 AuthError::Storage(format!("sqlite error: {error}"))
1511}
1512
1513fn add_column_if_missing(
1514 conn: &Connection,
1515 table: &str,
1516 column: &str,
1517 definition: &str,
1518) -> Result<(), AuthError> {
1519 let mut stmt = conn
1520 .prepare(&format!("PRAGMA table_info({table})"))
1521 .map_err(sqlite_error)?;
1522 let exists = stmt
1523 .query_map([], |row| row.get::<_, String>(1))
1524 .map_err(sqlite_error)?
1525 .collect::<rusqlite::Result<Vec<_>>>()
1526 .map_err(sqlite_error)?
1527 .iter()
1528 .any(|name| name == column);
1529 if !exists {
1530 conn.execute(
1531 &format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"),
1532 [],
1533 )
1534 .map_err(sqlite_error)?;
1535 }
1536 Ok(())
1537}
1538
1539#[cfg(test)]
1540#[path = "sqlite_tests.rs"]
1541mod tests;
1542
1543#[cfg(test)]
1544#[path = "sqlite_migration_tests.rs"]
1545mod migration_tests;