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, RefreshTokenRow,
15 RegisteredClient,
16};
17
18#[path = "sqlite_assertions.rs"]
19mod sqlite_assertions;
20#[path = "sqlite_browser_sessions.rs"]
21mod sqlite_browser_sessions;
22#[path = "sqlite_migrations.rs"]
23mod sqlite_migrations;
24
25use sqlite_migrations::{add_column_if_missing, run_migrations};
26#[path = "sqlite_rows.rs"]
27mod sqlite_rows;
28#[path = "sqlite_upstream_oauth.rs"]
29mod sqlite_upstream_oauth;
30use sqlite_rows::{row_to_allowed_user, row_to_authorization_code, row_to_authorization_request};
31
32const SCHEMA_VERSION: i64 = 5;
35
36use crate::util::{
37 ensure_restrictive_permissions, fingerprint, now_unix, set_restrictive_permissions,
38};
39
40const SQLITE_BUSY_TIMEOUT_MS: u64 = 5_000;
41const SQLITE_POOL_SIZE: usize = 4;
42
43#[derive(Clone)]
44pub struct SqliteStore {
45 conns: Arc<Vec<Mutex<Connection>>>,
46 next_conn: Arc<AtomicUsize>,
47 path: Arc<PathBuf>,
48 enc_key: Option<Arc<TokenEncryptionKey>>,
50}
51
52impl std::fmt::Debug for SqliteStore {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("SqliteStore")
55 .field("path", &self.path)
56 .field("enc_key", &self.enc_key.as_ref().map(|_| "<redacted>"))
57 .finish_non_exhaustive()
58 }
59}
60
61impl SqliteStore {
62 pub async fn open(path: PathBuf) -> Result<Self, AuthError> {
63 Self::open_with_key(path, None).await
64 }
65
66 pub async fn open_with_key(
67 path: PathBuf,
68 enc_key: Option<TokenEncryptionKey>,
69 ) -> Result<Self, AuthError> {
70 let path_for_open = path.clone();
71 let conns = tokio::task::spawn_blocking(move || {
72 open_connections(path_for_open.as_path(), SQLITE_POOL_SIZE)
73 })
74 .await;
75 let store = match conns {
76 Ok(result) => result,
77 Err(error) => Err(AuthError::Storage(format!(
78 "sqlite open task failed: {error}"
79 ))),
80 }
81 .map(|conns| Self {
82 conns: Arc::new(conns.into_iter().map(Mutex::new).collect()),
83 next_conn: Arc::new(AtomicUsize::new(0)),
84 path: Arc::new(path),
85 enc_key: enc_key.map(Arc::new),
86 })?;
87
88 store.cleanup_expired().await?;
89 Ok(store)
90 }
91
92 pub async fn pragma(&self, name: &str) -> Result<String, AuthError> {
93 let pragma = match name {
94 "journal_mode" | "busy_timeout" | "foreign_keys" => name.to_string(),
95 other => {
96 return Err(AuthError::Config(format!(
97 "unsupported pragma query `{other}`"
98 )));
99 }
100 };
101
102 self.with_conn(move |conn| {
103 conn.query_row(&format!("PRAGMA {pragma};"), [], |row| {
104 row.get::<_, Value>(0)
105 })
106 .map(|value| match value {
107 Value::Text(text) => text,
108 Value::Integer(int) => int.to_string(),
109 other => format!("{other:?}"),
110 })
111 .map_err(sqlite_error)
112 })
113 .await
114 }
115
116 pub async fn register_client(&self, client: RegisteredClient) -> Result<(), AuthError> {
117 self.with_conn(move |conn| {
118 let redirect_uris = serde_json::to_string(&client.redirect_uris)
119 .map_err(|error| AuthError::Storage(format!("serialize redirect_uris: {error}")))?;
120 let jwks = client
121 .jwks
122 .as_ref()
123 .map(serde_json::to_string)
124 .transpose()
125 .map_err(|error| AuthError::Storage(format!("serialize client jwks: {error}")))?;
126 conn.execute(
127 "INSERT INTO registered_clients (
128 client_id, redirect_uris, created_at, token_endpoint_auth_method, jwks
129 ) VALUES (?1, ?2, ?3, ?4, ?5)
130 ON CONFLICT(client_id) DO UPDATE SET
131 redirect_uris = excluded.redirect_uris,
132 created_at = excluded.created_at,
133 token_endpoint_auth_method = excluded.token_endpoint_auth_method,
134 jwks = excluded.jwks",
135 params![
136 client.client_id,
137 redirect_uris,
138 client.created_at,
139 client.token_endpoint_auth_method,
140 jwks,
141 ],
142 )
143 .map_err(sqlite_error)?;
144 Ok(())
145 })
146 .await
147 }
148
149 pub async fn find_client(
150 &self,
151 client_id: &str,
152 ) -> Result<Option<RegisteredClient>, AuthError> {
153 let client_id = client_id.to_string();
154 self.with_conn(move |conn| {
155 conn.query_row(
156 "SELECT client_id, redirect_uris, created_at, token_endpoint_auth_method, jwks
157 FROM registered_clients
158 WHERE client_id = ?1",
159 params![client_id],
160 |row| {
161 let redirect_uris: String = row.get(1)?;
162 let redirect_uris = serde_json::from_str(&redirect_uris).map_err(|error| {
163 rusqlite::Error::FromSqlConversionFailure(
164 1,
165 rusqlite::types::Type::Text,
166 Box::new(error),
167 )
168 })?;
169 let jwks: Option<String> = row.get(4)?;
170 let jwks = jwks
171 .map(|value| serde_json::from_str(&value))
172 .transpose()
173 .map_err(|error| {
174 rusqlite::Error::FromSqlConversionFailure(
175 4,
176 rusqlite::types::Type::Text,
177 Box::new(error),
178 )
179 })?;
180 Ok(RegisteredClient {
181 client_id: row.get(0)?,
182 redirect_uris,
183 created_at: row.get(2)?,
184 token_endpoint_auth_method: row.get(3)?,
185 jwks,
186 })
187 },
188 )
189 .optional()
190 .map_err(sqlite_error)
191 })
192 .await
193 }
194
195 pub async fn insert_authorization_request(
196 &self,
197 request: AuthorizationRequestRow,
198 ) -> Result<(), AuthError> {
199 self.with_conn(move |conn| {
200 conn.execute(
201 "INSERT INTO authorization_requests (
202 state, client_id, redirect_uri, client_state, resource, scope, provider_code_verifier,
203 code_challenge, code_challenge_method, created_at, expires_at, provider,
204 token_endpoint_auth_method
205 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
206 params![
207 request.state,
208 request.client_id,
209 request.redirect_uri,
210 request.client_state,
211 request.resource,
212 request.scope,
213 request.provider_code_verifier,
214 request.code_challenge,
215 request.code_challenge_method,
216 request.created_at,
217 request.expires_at,
218 request.provider,
219 request.token_endpoint_auth_method,
220 ],
221 )
222 .map_err(sqlite_error)?;
223 Ok(())
224 })
225 .await
226 }
227
228 pub async fn take_authorization_request(
229 &self,
230 state: &str,
231 ) -> Result<AuthorizationRequestRow, AuthError> {
232 let state = state.to_string();
233 let now = now_unix();
234 self.with_conn(move |conn| {
235 conn.query_row(
236 "DELETE FROM authorization_requests
237 WHERE state = ?1
238 AND expires_at > ?2
239 RETURNING state, client_id, redirect_uri, client_state, scope, provider_code_verifier,
240 code_challenge, code_challenge_method, created_at, expires_at, resource, provider,
241 token_endpoint_auth_method",
242 params![state, now],
243 row_to_authorization_request,
244 )
245 .map_err(|error| match error {
246 rusqlite::Error::QueryReturnedNoRows => AuthError::InvalidGrant(
247 "authorization state is missing, expired, or already used".to_string(),
248 ),
249 other => sqlite_error(other),
250 })
251 })
252 .await
253 }
254
255 pub async fn insert_auth_code(&self, code: AuthorizationCodeRow) -> Result<(), AuthError> {
256 self.with_conn(move |conn| {
257 conn.execute(
258 "INSERT INTO authorization_codes (
259 code, client_id, subject, redirect_uri, resource, scope,
260 code_challenge, code_challenge_method, provider_refresh_token,
261 created_at, expires_at, provider, token_endpoint_auth_method
262 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
263 params![
264 code.code,
265 code.client_id,
266 code.subject,
267 code.redirect_uri,
268 code.resource,
269 code.scope,
270 code.code_challenge,
271 code.code_challenge_method,
272 code.provider_refresh_token,
273 code.created_at,
274 code.expires_at,
275 code.provider,
276 code.token_endpoint_auth_method,
277 ],
278 )
279 .map_err(sqlite_error)?;
280 Ok(())
281 })
282 .await
283 }
284
285 pub async fn redeem_auth_code(&self, code: &str) -> Result<AuthorizationCodeRow, AuthError> {
286 let code = code.to_string();
287 let now = now_unix();
288 self.with_conn(move |conn| {
289 conn.query_row(
290 "DELETE FROM authorization_codes
291 WHERE code = ?1
292 AND expires_at > ?2
293 RETURNING code, client_id, subject, redirect_uri, scope,
294 code_challenge, code_challenge_method, provider_refresh_token,
295 created_at, expires_at, resource, provider,
296 token_endpoint_auth_method",
297 params![code, now],
298 row_to_authorization_code,
299 )
300 .map_err(|error| match error {
301 rusqlite::Error::QueryReturnedNoRows => AuthError::InvalidGrant(
302 "authorization code is missing, expired, or already redeemed".to_string(),
303 ),
304 other => sqlite_error(other),
305 })
306 })
307 .await
308 }
309
310 pub async fn upsert_refresh_token(&self, token: RefreshTokenRow) -> Result<(), AuthError> {
318 let hash = hash_token(&token.refresh_token);
319 let encrypted_provider_rt = token
320 .provider_refresh_token
321 .as_deref()
322 .map(|raw| maybe_encrypt_bound(self.enc_key.as_deref(), raw, &refresh_token_aad(&hash)))
323 .transpose()?;
324 self.with_conn(move |conn| {
325 conn.execute(
326 "INSERT INTO refresh_tokens (
327 refresh_token_hash, client_id, subject, resource, scope,
328 provider_refresh_token, created_at, expires_at, provider,
329 token_endpoint_auth_method
330 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
331 ON CONFLICT(refresh_token_hash) DO UPDATE SET
332 client_id = excluded.client_id,
333 subject = excluded.subject,
334 resource = excluded.resource,
335 scope = excluded.scope,
336 provider_refresh_token = excluded.provider_refresh_token,
337 created_at = excluded.created_at,
338 expires_at = excluded.expires_at,
339 provider = excluded.provider,
340 token_endpoint_auth_method = excluded.token_endpoint_auth_method",
341 params![
342 hash,
343 token.client_id,
344 token.subject,
345 token.resource,
346 token.scope,
347 encrypted_provider_rt,
348 token.created_at,
349 token.expires_at,
350 token.provider,
351 token.token_endpoint_auth_method,
352 ],
353 )
354 .map_err(sqlite_error)?;
355 Ok(())
356 })
357 .await
358 }
359
360 pub async fn rotate_refresh_token(
372 &self,
373 old_token: &str,
374 new_token: RefreshTokenRow,
375 ) -> Result<Option<RefreshTokenRow>, AuthError> {
376 let old_hash = hash_token(old_token);
377 let new_hash = hash_token(&new_token.refresh_token);
378 let now = now_unix();
379 let encrypted_provider_rt = new_token
380 .provider_refresh_token
381 .as_deref()
382 .map(|raw| {
383 maybe_encrypt_bound(self.enc_key.as_deref(), raw, &refresh_token_aad(&new_hash))
384 })
385 .transpose()?;
386 self.with_conn(move |conn| {
387 conn.execute_batch("BEGIN").map_err(sqlite_error)?;
388
389 let delete_result = conn
390 .execute(
391 "DELETE FROM refresh_tokens
392 WHERE refresh_token_hash = ?1
393 AND expires_at > ?2",
394 params![old_hash, now],
395 )
396 .map_err(sqlite_error);
397
398 let deleted = match delete_result {
399 Ok(n) => n,
400 Err(e) => {
401 drop(conn.execute_batch("ROLLBACK"));
402 return Err(e);
403 }
404 };
405
406 if deleted == 0 {
407 drop(conn.execute_batch("ROLLBACK"));
409 return Ok(None);
410 }
411
412 let insert_result = conn
413 .execute(
414 "INSERT INTO refresh_tokens (
415 refresh_token_hash, client_id, subject, resource, scope,
416 provider_refresh_token, created_at, expires_at, provider,
417 token_endpoint_auth_method
418 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
419 params![
420 new_hash,
421 new_token.client_id,
422 new_token.subject,
423 new_token.resource,
424 new_token.scope,
425 encrypted_provider_rt,
426 new_token.created_at,
427 new_token.expires_at,
428 new_token.provider,
429 new_token.token_endpoint_auth_method,
430 ],
431 )
432 .map_err(sqlite_error);
433
434 match insert_result {
435 Ok(_) => {
436 conn.execute_batch("COMMIT").map_err(sqlite_error)?;
437 Ok(Some(new_token))
438 }
439 Err(e) => {
440 drop(conn.execute_batch("ROLLBACK"));
441 Err(e)
442 }
443 }
444 })
445 .await
446 }
447
448 pub async fn find_refresh_token(
449 &self,
450 refresh_token: &str,
451 ) -> Result<Option<RefreshTokenRow>, AuthError> {
452 let hash = hash_token(refresh_token);
453 let plaintext = refresh_token.to_string();
456 let now = now_unix();
457 let enc_key = self.enc_key.clone();
458 self.with_conn(move |conn| {
459 let row = conn
460 .query_row(
461 "SELECT client_id, subject, scope,
462 provider_refresh_token, created_at, expires_at, resource, provider,
463 token_endpoint_auth_method
464 FROM refresh_tokens
465 WHERE refresh_token_hash = ?1
466 AND expires_at > ?2",
467 params![hash, now],
468 |row| {
469 Ok(RefreshTokenRow {
470 refresh_token: plaintext.clone(),
471 client_id: row.get(0)?,
472 subject: row.get(1)?,
473 scope: row.get(2)?,
474 provider_refresh_token: row.get(3)?,
475 created_at: row.get(4)?,
476 expires_at: row.get(5)?,
477 resource: row.get(6).unwrap_or_default(),
478 provider: row.get(7)?,
479 token_endpoint_auth_method: row.get(8)?,
480 })
481 },
482 )
483 .optional()
484 .map_err(sqlite_error)?;
485
486 match row {
493 Some(mut r) => {
494 if let Some(raw) = r.provider_refresh_token.as_deref() {
495 r.provider_refresh_token = Some(maybe_decrypt_bound(
496 enc_key.as_deref(),
497 raw,
498 &refresh_token_aad(&hash),
499 )?);
500 }
501 Ok(Some(r))
502 }
503 None => Ok(None),
504 }
505 })
506 .await
507 }
508
509 pub async fn auth_code_client_auth_method(
516 &self,
517 code: &str,
518 ) -> Result<Option<String>, AuthError> {
519 self.client_auth_method(
520 "SELECT token_endpoint_auth_method FROM authorization_codes WHERE code = ?1",
521 code.to_string(),
522 )
523 .await
524 }
525
526 pub async fn refresh_token_client_auth_method(
530 &self,
531 refresh_token: &str,
532 ) -> Result<Option<String>, AuthError> {
533 self.client_auth_method(
534 "SELECT token_endpoint_auth_method FROM refresh_tokens \
535 WHERE refresh_token_hash = ?1",
536 hash_token(refresh_token),
537 )
538 .await
539 }
540
541 async fn client_auth_method(
545 &self,
546 sql: &'static str,
547 key: String,
548 ) -> Result<Option<String>, AuthError> {
549 self.with_conn(move |conn| {
550 conn.query_row(sql, params![key], |row| row.get::<_, Option<String>>(0))
551 .optional()
552 .map(Option::flatten)
553 .map_err(sqlite_error)
554 })
555 .await
556 }
557
558 pub async fn has_any_refresh_token(&self) -> Result<bool, AuthError> {
573 let now = now_unix();
574 self.with_conn(move |conn| {
575 conn.query_row(
576 "SELECT EXISTS(SELECT 1 FROM refresh_tokens WHERE expires_at > ?1)",
577 params![now],
578 |row| row.get::<_, i64>(0),
579 )
580 .map(|count| count != 0)
581 .map_err(sqlite_error)
582 })
583 .await
584 }
585
586 pub async fn has_any_refresh_token_for_provider(
595 &self,
596 provider: &str,
597 ) -> Result<bool, AuthError> {
598 let provider = provider.to_string();
599 let now = now_unix();
600 self.with_conn(move |conn| {
601 conn.query_row(
602 "SELECT EXISTS(SELECT 1 FROM refresh_tokens WHERE provider = ?1 AND expires_at > ?2)",
603 params![provider, now],
604 |row| row.get::<_, i64>(0),
605 )
606 .map(|count| count != 0)
607 .map_err(sqlite_error)
608 })
609 .await
610 }
611
612 #[cfg(any(test, debug_assertions))]
619 pub async fn execute_test_statement(&self, sql: &str) -> Result<(), AuthError> {
620 let sql = sql.to_string();
621 self.with_conn(move |conn| conn.execute_batch(&sql).map_err(sqlite_error))
622 .await
623 }
624
625 pub async fn count_pending_oauth_states(&self) -> Result<usize, AuthError> {
626 let now = now_unix();
627 self.with_conn(move |conn| {
628 let authorization_requests: i64 = conn
629 .query_row(
630 "SELECT COUNT(*) FROM authorization_requests WHERE expires_at > ?1",
631 params![now],
632 |row| row.get(0),
633 )
634 .map_err(sqlite_error)?;
635 let browser_login_states: i64 = conn
636 .query_row(
637 "SELECT COUNT(*) FROM browser_login_states WHERE expires_at > ?1",
638 params![now],
639 |row| row.get(0),
640 )
641 .map_err(sqlite_error)?;
642 let native_authorization_results: i64 = conn
643 .query_row(
644 "SELECT COUNT(*) FROM native_authorization_results WHERE expires_at > ?1",
645 params![now],
646 |row| row.get(0),
647 )
648 .map_err(sqlite_error)?;
649 Ok(
650 (authorization_requests + browser_login_states + native_authorization_results)
651 as usize,
652 )
653 })
654 .await
655 }
656
657 pub async fn cleanup_expired(&self) -> Result<u64, AuthError> {
661 let now = now_unix();
662 self.with_conn(move |conn| {
663 let mut total: u64 = 0;
664 for table in [
665 "authorization_requests",
666 "authorization_codes",
667 "refresh_tokens",
668 "browser_sessions",
669 "browser_login_states",
670 "native_authorization_results",
671 ] {
672 let deleted = conn
673 .execute(
674 &format!("DELETE FROM {table} WHERE expires_at <= ?1"),
675 params![now],
676 )
677 .map_err(sqlite_error)?;
678 total += deleted as u64;
679 }
680 let deleted = conn
681 .execute(
682 "DELETE FROM upstream_oauth_state WHERE expires_at <= ?1",
683 params![now],
684 )
685 .map_err(sqlite_error)?;
686 total += deleted as u64;
687 let deleted = conn
688 .execute(
689 "DELETE FROM upstream_oauth_credentials
690 WHERE access_token_expires_at <= ?1 AND refresh_token_present = 0",
691 params![now],
692 )
693 .map_err(sqlite_error)?;
694 total += deleted as u64;
695 Ok(total)
696 })
697 .await
698 }
699
700 pub async fn add_allowed_user(
705 &self,
706 email: &str,
707 added_by: &str,
708 created_at: i64,
709 ) -> Result<(), AuthError> {
710 let email = email.to_lowercase();
711 let fp = fingerprint(&email);
712 let added_by = added_by.to_string();
713 self.with_conn(move |conn| {
714 let changed = conn
715 .execute(
716 "INSERT INTO allowed_users (email, added_by, created_at)
717 VALUES (?1, ?2, ?3)",
718 params![email, added_by, created_at],
719 )
720 .map_err(|error| match error {
721 rusqlite::Error::SqliteFailure(ref e, _)
722 if e.code == rusqlite::ErrorCode::ConstraintViolation =>
723 {
724 AuthError::Validation(format!(
725 "email fingerprint {fp} is already in the allowlist"
726 ))
727 }
728 other => sqlite_error(other),
729 })?;
730 debug_assert_eq!(changed, 1);
731 Ok(())
732 })
733 .await
734 }
735
736 pub async fn remove_allowed_user(&self, email: &str) -> Result<(), AuthError> {
740 let email = email.to_lowercase();
741 self.with_conn(move |conn| {
742 conn.execute("DELETE FROM allowed_users WHERE email = ?1", params![email])
743 .map_err(sqlite_error)?;
744 Ok(())
745 })
746 .await
747 }
748
749 pub async fn list_allowed_users(&self) -> Result<Vec<AllowedUserRow>, AuthError> {
751 self.with_conn(move |conn| {
752 let mut stmt = conn
753 .prepare(
754 "SELECT email, added_by, created_at
755 FROM allowed_users
756 ORDER BY created_at ASC",
757 )
758 .map_err(sqlite_error)?;
759 let rows = stmt
760 .query_map([], row_to_allowed_user)
761 .map_err(sqlite_error)?
762 .collect::<rusqlite::Result<Vec<_>>>()
763 .map_err(sqlite_error)?;
764 Ok(rows)
765 })
766 .await
767 }
768
769 async fn with_conn<T, F>(&self, op: F) -> Result<T, AuthError>
770 where
771 T: Send + 'static,
772 F: FnOnce(&Connection) -> Result<T, AuthError> + Send + 'static,
773 {
774 let conns = Arc::clone(&self.conns);
775 let path = Arc::clone(&self.path);
776 let len = conns.len();
777 let idx = self.next_conn.fetch_add(1, Ordering::Relaxed) % len;
778 tokio::task::spawn_blocking(move || {
779 let mut guard = conns[idx]
780 .lock()
781 .map_err(|_| AuthError::Storage("sqlite mutex poisoned".to_string()))?;
782 validate_or_reopen_connection(&mut guard, path.as_ref())?;
783 op(&guard)
784 })
785 .await
786 .map_err(|error| AuthError::Storage(format!("sqlite task failed: {error}")))?
787 }
788
789 #[cfg(test)]
790 fn connection_count(&self) -> usize {
791 self.conns.len()
792 }
793}
794
795fn open_connections(path: &Path, count: usize) -> Result<Vec<Connection>, AuthError> {
796 (0..count).map(|_| open_connection(path)).collect()
797}
798
799#[allow(clippy::too_many_lines)]
800fn open_connection(path: &Path) -> Result<Connection, AuthError> {
801 if let Some(parent) = path.parent() {
802 std::fs::create_dir_all(parent).map_err(|error| {
803 AuthError::Storage(format!(
804 "create auth database directory `{}`: {error}",
805 parent.display()
806 ))
807 })?;
808 }
809
810 let existed = path.exists();
811 if existed {
812 ensure_restrictive_permissions(path)?;
813 }
814
815 let conn = Connection::open(path).map_err(sqlite_error)?;
816 conn.busy_timeout(std::time::Duration::from_millis(SQLITE_BUSY_TIMEOUT_MS))
817 .map_err(sqlite_error)?;
818 conn.pragma_update(None, "journal_mode", "WAL")
819 .map_err(sqlite_error)?;
820 conn.pragma_update(None, "foreign_keys", "ON")
821 .map_err(sqlite_error)?;
822 conn.execute_batch(
823 "CREATE TABLE IF NOT EXISTS registered_clients (
824 client_id TEXT PRIMARY KEY,
825 redirect_uris TEXT NOT NULL,
826 created_at INTEGER NOT NULL,
827 token_endpoint_auth_method TEXT NOT NULL DEFAULT 'none',
828 jwks TEXT
829 );
830 CREATE TABLE IF NOT EXISTS authorization_requests (
831 state TEXT PRIMARY KEY,
832 client_id TEXT NOT NULL,
833 redirect_uri TEXT NOT NULL,
834 client_state TEXT NOT NULL,
835 resource TEXT NOT NULL DEFAULT '',
836 scope TEXT NOT NULL,
837 provider TEXT NOT NULL DEFAULT 'google',
838 provider_code_verifier TEXT NOT NULL,
839 code_challenge TEXT NOT NULL,
840 code_challenge_method TEXT NOT NULL,
841 created_at INTEGER NOT NULL,
842 expires_at INTEGER NOT NULL,
843 token_endpoint_auth_method TEXT
844 );
845 CREATE TABLE IF NOT EXISTS authorization_codes (
846 code TEXT PRIMARY KEY,
847 client_id TEXT NOT NULL,
848 subject TEXT NOT NULL,
849 redirect_uri TEXT NOT NULL,
850 resource TEXT NOT NULL DEFAULT '',
851 scope TEXT NOT NULL,
852 provider TEXT NOT NULL DEFAULT 'google',
853 code_challenge TEXT NOT NULL,
854 code_challenge_method TEXT NOT NULL,
855 provider_refresh_token TEXT,
856 created_at INTEGER NOT NULL,
857 expires_at INTEGER NOT NULL,
858 token_endpoint_auth_method TEXT
859 );
860 CREATE TABLE IF NOT EXISTS refresh_tokens (
861 refresh_token_hash TEXT PRIMARY KEY,
862 client_id TEXT NOT NULL,
863 subject TEXT NOT NULL,
864 resource TEXT NOT NULL DEFAULT '',
865 scope TEXT NOT NULL,
866 provider TEXT NOT NULL DEFAULT 'google',
867 provider_refresh_token TEXT,
868 created_at INTEGER NOT NULL,
869 expires_at INTEGER NOT NULL,
870 token_endpoint_auth_method TEXT
871 );
872 CREATE TABLE IF NOT EXISTS browser_sessions (
873 session_id TEXT PRIMARY KEY,
874 subject TEXT NOT NULL,
875 email TEXT,
876 csrf_token TEXT NOT NULL,
877 created_at INTEGER NOT NULL,
878 expires_at INTEGER NOT NULL
879 );
880 CREATE TABLE IF NOT EXISTS browser_login_states (
881 state TEXT PRIMARY KEY,
882 return_to TEXT NOT NULL,
883 provider TEXT NOT NULL DEFAULT 'google',
884 provider_code_verifier TEXT NOT NULL,
885 created_at INTEGER NOT NULL,
886 expires_at INTEGER NOT NULL
887 );
888 CREATE TABLE IF NOT EXISTS native_authorization_results (
889 state TEXT PRIMARY KEY,
890 code TEXT NOT NULL,
891 created_at INTEGER NOT NULL,
892 expires_at INTEGER NOT NULL
893 );
894 CREATE TABLE IF NOT EXISTS upstream_oauth_credentials (
895 upstream_name TEXT NOT NULL,
896 subject TEXT NOT NULL,
897 issuer TEXT NOT NULL DEFAULT '',
898 client_id TEXT NOT NULL,
899 granted_scopes_json TEXT NOT NULL,
900 token_blob BLOB NOT NULL,
901 token_blob_nonce BLOB NOT NULL,
902 token_received_at INTEGER NOT NULL,
903 access_token_expires_at INTEGER NOT NULL,
904 refresh_token_present INTEGER NOT NULL,
905 PRIMARY KEY (upstream_name, subject)
906 ) WITHOUT ROWID;
907 CREATE TABLE IF NOT EXISTS upstream_oauth_state (
908 upstream_name TEXT NOT NULL,
909 subject TEXT NOT NULL,
910 csrf_token TEXT NOT NULL,
911 pkce_verifier TEXT NOT NULL,
912 expected_issuer TEXT,
913 require_issuer INTEGER NOT NULL DEFAULT 0,
914 requested_scopes_json TEXT NOT NULL DEFAULT '[]',
915 created_at INTEGER NOT NULL,
916 expires_at INTEGER NOT NULL,
917 PRIMARY KEY (upstream_name, subject, csrf_token)
918 ) WITHOUT ROWID;
919 CREATE TABLE IF NOT EXISTS upstream_oauth_dynamic_clients (
920 upstream_name TEXT NOT NULL,
921 subject TEXT NOT NULL,
922 client_id TEXT NOT NULL,
923 issuer TEXT NOT NULL DEFAULT '',
924 created_at INTEGER NOT NULL,
925 PRIMARY KEY (upstream_name, subject)
926 ) WITHOUT ROWID;
927 CREATE TABLE IF NOT EXISTS allowed_users (
928 email TEXT PRIMARY KEY NOT NULL,
929 added_by TEXT NOT NULL,
930 created_at INTEGER NOT NULL
931 );
932 CREATE TABLE IF NOT EXISTS assertion_jtis (
933 issuer TEXT NOT NULL,
934 jti TEXT NOT NULL,
935 expires_at INTEGER NOT NULL,
936 PRIMARY KEY (issuer, jti)
937 );
938 CREATE INDEX IF NOT EXISTS idx_assertion_jtis_expiry
939 ON assertion_jtis(expires_at);",
940 )
941 .map_err(sqlite_error)?;
942 add_column_if_missing(
943 &conn,
944 "registered_clients",
945 "token_endpoint_auth_method",
946 "TEXT NOT NULL DEFAULT 'none'",
947 )?;
948 add_column_if_missing(&conn, "registered_clients", "jwks", "TEXT")?;
949 add_column_if_missing(
950 &conn,
951 "authorization_requests",
952 "resource",
953 "TEXT NOT NULL DEFAULT ''",
954 )?;
955 add_column_if_missing(
956 &conn,
957 "authorization_codes",
958 "resource",
959 "TEXT NOT NULL DEFAULT ''",
960 )?;
961 add_column_if_missing(
962 &conn,
963 "refresh_tokens",
964 "resource",
965 "TEXT NOT NULL DEFAULT ''",
966 )?;
967 add_column_if_missing(
968 &conn,
969 "authorization_requests",
970 "provider",
971 "TEXT NOT NULL DEFAULT 'google'",
972 )?;
973 add_column_if_missing(
974 &conn,
975 "authorization_codes",
976 "provider",
977 "TEXT NOT NULL DEFAULT 'google'",
978 )?;
979 add_column_if_missing(
980 &conn,
981 "refresh_tokens",
982 "provider",
983 "TEXT NOT NULL DEFAULT 'google'",
984 )?;
985 add_column_if_missing(
986 &conn,
987 "browser_login_states",
988 "provider",
989 "TEXT NOT NULL DEFAULT 'google'",
990 )?;
991
992 if !existed {
993 set_restrictive_permissions(path)?;
994 }
995 ensure_restrictive_permissions(path)?;
996
997 run_migrations(&conn)?;
998
999 Ok(conn)
1000}
1001
1002fn refresh_token_aad(refresh_token_hash: &str) -> Vec<u8> {
1010 format!("refresh_token_hash={refresh_token_hash}").into_bytes()
1011}
1012
1013fn hash_token(token: &str) -> String {
1014 let digest = Sha256::digest(token.as_bytes());
1015 let mut hex = String::with_capacity(64);
1016 for byte in &digest {
1017 let _ = write!(&mut hex, "{byte:02x}");
1018 }
1019 hex
1020}
1021
1022fn validate_or_reopen_connection(conn: &mut Connection, path: &Path) -> Result<(), AuthError> {
1023 let Err(error) = conn.query_row("SELECT 1", [], |row| row.get::<_, i64>(0)) else {
1024 return Ok(());
1025 };
1026 warn!(
1027 path = %path.display(),
1028 error = %error,
1029 "stale sqlite connection detected, reopening"
1030 );
1031
1032 *conn = open_connection(path)?;
1033 conn.query_row("SELECT 1", [], |row| row.get::<_, i64>(0))
1034 .map(|_| ())
1035 .map_err(sqlite_error)
1036}
1037
1038#[allow(clippy::needless_pass_by_value)]
1039fn sqlite_error(error: rusqlite::Error) -> AuthError {
1040 AuthError::Storage(format!("sqlite error: {error}"))
1041}
1042
1043#[cfg(test)]
1044#[path = "sqlite_tests.rs"]
1045mod tests;
1046
1047#[cfg(test)]
1048#[path = "sqlite_migration_tests.rs"]
1049mod migration_tests;