1use std::collections::BTreeMap;
23use std::sync::Arc;
24
25use anyhow::{Context, Result};
26use async_trait::async_trait;
27
28use super::types::{
29 AuthorizationCode, ConsentGrant, OidcClient, OidcSession, RefreshToken, TokenAuthMethod,
30 UpstreamLoginState, UpstreamProvider,
31};
32
33#[async_trait]
34pub trait OidcClientStore: Send + Sync + 'static {
35 async fn create(&self, client: &OidcClient) -> Result<()>;
36 async fn get(&self, client_id: &str) -> Result<Option<OidcClient>>;
37 async fn list(&self) -> Result<Vec<OidcClient>>;
38 async fn update(&self, client: &OidcClient) -> Result<()>;
39 async fn delete(&self, client_id: &str) -> Result<bool>;
40 async fn rotate_secret_hash(&self, client_id: &str, new_hash: &str) -> Result<bool>;
42}
43
44#[async_trait]
45pub trait OidcUpstreamStore: Send + Sync + 'static {
46 async fn upsert(&self, provider: &UpstreamProvider) -> Result<()>;
47 async fn get(&self, slug: &str) -> Result<Option<UpstreamProvider>>;
48 async fn list(&self) -> Result<Vec<UpstreamProvider>>;
49 async fn delete(&self, slug: &str) -> Result<bool>;
50}
51
52#[async_trait]
53pub trait OidcCodeStore: Send + Sync + 'static {
54 async fn create(&self, code: &AuthorizationCode) -> Result<()>;
55 async fn consume(&self, code: &str) -> Result<Option<AuthorizationCode>>;
60}
61
62#[async_trait]
63pub trait OidcRefreshStore: Send + Sync + 'static {
64 async fn create(&self, token: &RefreshToken) -> Result<()>;
65 async fn get(&self, token_hash: &str) -> Result<Option<RefreshToken>>;
66 async fn revoke(&self, token_hash: &str) -> Result<bool>;
67 async fn revoke_for_user(&self, user_id: &str) -> Result<u64>;
70}
71
72#[async_trait]
73pub trait OidcSessionStore: Send + Sync + 'static {
74 async fn create(&self, session: &OidcSession) -> Result<()>;
75 async fn get(&self, sid: &str) -> Result<Option<OidcSession>>;
76 async fn list_by_assay_session(&self, assay_session_id: &str) -> Result<Vec<OidcSession>>;
79 async fn delete(&self, sid: &str) -> Result<bool>;
80 async fn delete_by_assay_session(&self, assay_session_id: &str) -> Result<u64>;
81}
82
83#[async_trait]
84pub trait OidcConsentStore: Send + Sync + 'static {
85 async fn upsert(&self, grant: &ConsentGrant) -> Result<()>;
86 async fn get(&self, user_id: &str, client_id: &str) -> Result<Option<ConsentGrant>>;
87 async fn delete(&self, user_id: &str, client_id: &str) -> Result<bool>;
88}
89
90#[async_trait]
91pub trait OidcUpstreamStateStore: Send + Sync + 'static {
92 async fn create(&self, state: &UpstreamLoginState) -> Result<()>;
93 async fn take(&self, state: &str) -> Result<Option<UpstreamLoginState>>;
95}
96
97#[cfg(feature = "backend-postgres")]
102mod pg {
103 use super::*;
104 use sqlx::{PgPool, Row};
105
106 fn parse_json_array(s: &str) -> Vec<String> {
107 serde_json::from_str(s).unwrap_or_default()
108 }
109
110 fn encode_json_array(v: &[String]) -> String {
111 serde_json::to_string(v).unwrap_or_else(|_| "[]".to_string())
112 }
113
114 fn map_client_row(row: sqlx::postgres::PgRow) -> OidcClient {
115 let auth_method: String = row.get("token_endpoint_auth_method");
116 OidcClient {
117 client_id: row.get("client_id"),
118 client_secret_hash: row.get("client_secret_hash"),
119 redirect_uris: parse_json_array(&row.get::<String, _>("redirect_uris")),
120 name: row.get("name"),
121 logo_url: row.get("logo_url"),
122 token_endpoint_auth_method: TokenAuthMethod::parse(&auth_method)
123 .unwrap_or(TokenAuthMethod::ClientSecretBasic),
124 default_scopes: parse_json_array(&row.get::<String, _>("default_scopes")),
125 require_consent: row.get("require_consent"),
126 grant_types: parse_json_array(&row.get::<String, _>("grant_types")),
127 response_types: parse_json_array(&row.get::<String, _>("response_types")),
128 pkce_required: row.get("pkce_required"),
129 backchannel_logout_uri: row.get("backchannel_logout_uri"),
130 created_at: row.get("created_at"),
131 }
132 }
133
134 #[derive(Clone)]
136 pub struct PostgresOidcClientStore {
137 pool: PgPool,
138 }
139
140 impl PostgresOidcClientStore {
141 pub fn new(pool: PgPool) -> Self {
142 Self { pool }
143 }
144 pub fn into_dyn(self) -> Arc<dyn OidcClientStore> {
145 Arc::new(self)
146 }
147 }
148
149 #[async_trait]
150 impl OidcClientStore for PostgresOidcClientStore {
151 async fn create(&self, c: &OidcClient) -> Result<()> {
152 sqlx::query(
153 "INSERT INTO auth.oidc_clients
154 (client_id, client_secret_hash, redirect_uris, name, logo_url,
155 token_endpoint_auth_method, default_scopes, require_consent,
156 grant_types, response_types, pkce_required,
157 backchannel_logout_uri, created_at)
158 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
159 )
160 .bind(&c.client_id)
161 .bind(&c.client_secret_hash)
162 .bind(encode_json_array(&c.redirect_uris))
163 .bind(&c.name)
164 .bind(&c.logo_url)
165 .bind(c.token_endpoint_auth_method.as_str())
166 .bind(encode_json_array(&c.default_scopes))
167 .bind(c.require_consent)
168 .bind(encode_json_array(&c.grant_types))
169 .bind(encode_json_array(&c.response_types))
170 .bind(c.pkce_required)
171 .bind(&c.backchannel_logout_uri)
172 .bind(c.created_at)
173 .execute(&self.pool)
174 .await
175 .context("auth.oidc_clients insert")?;
176 Ok(())
177 }
178
179 async fn get(&self, client_id: &str) -> Result<Option<OidcClient>> {
180 let row = sqlx::query("SELECT * FROM auth.oidc_clients WHERE client_id = $1")
181 .bind(client_id)
182 .fetch_optional(&self.pool)
183 .await
184 .context("auth.oidc_clients select")?;
185 Ok(row.map(map_client_row))
186 }
187
188 async fn list(&self) -> Result<Vec<OidcClient>> {
189 let rows = sqlx::query("SELECT * FROM auth.oidc_clients ORDER BY created_at")
190 .fetch_all(&self.pool)
191 .await
192 .context("auth.oidc_clients list")?;
193 Ok(rows.into_iter().map(map_client_row).collect())
194 }
195
196 async fn update(&self, c: &OidcClient) -> Result<()> {
197 sqlx::query(
198 "UPDATE auth.oidc_clients SET
199 client_secret_hash = $2,
200 redirect_uris = $3,
201 name = $4,
202 logo_url = $5,
203 token_endpoint_auth_method = $6,
204 default_scopes = $7,
205 require_consent = $8,
206 grant_types = $9,
207 response_types = $10,
208 pkce_required = $11,
209 backchannel_logout_uri = $12
210 WHERE client_id = $1",
211 )
212 .bind(&c.client_id)
213 .bind(&c.client_secret_hash)
214 .bind(encode_json_array(&c.redirect_uris))
215 .bind(&c.name)
216 .bind(&c.logo_url)
217 .bind(c.token_endpoint_auth_method.as_str())
218 .bind(encode_json_array(&c.default_scopes))
219 .bind(c.require_consent)
220 .bind(encode_json_array(&c.grant_types))
221 .bind(encode_json_array(&c.response_types))
222 .bind(c.pkce_required)
223 .bind(&c.backchannel_logout_uri)
224 .execute(&self.pool)
225 .await
226 .context("auth.oidc_clients update")?;
227 Ok(())
228 }
229
230 async fn delete(&self, client_id: &str) -> Result<bool> {
231 let r = sqlx::query("DELETE FROM auth.oidc_clients WHERE client_id = $1")
232 .bind(client_id)
233 .execute(&self.pool)
234 .await
235 .context("auth.oidc_clients delete")?;
236 Ok(r.rows_affected() > 0)
237 }
238
239 async fn rotate_secret_hash(&self, client_id: &str, new_hash: &str) -> Result<bool> {
240 let r = sqlx::query(
241 "UPDATE auth.oidc_clients SET client_secret_hash = $2 WHERE client_id = $1",
242 )
243 .bind(client_id)
244 .bind(new_hash)
245 .execute(&self.pool)
246 .await
247 .context("auth.oidc_clients rotate_secret_hash")?;
248 Ok(r.rows_affected() > 0)
249 }
250 }
251
252 fn map_upstream_row(row: sqlx::postgres::PgRow) -> UpstreamProvider {
253 let scopes_str: String = row.try_get("scopes").unwrap_or_default();
254 let auth_params_str: String = row.try_get("auth_params").unwrap_or_default();
255 UpstreamProvider {
256 slug: row.get("slug"),
257 issuer: row.get("issuer"),
258 client_id: row.get("client_id"),
259 client_secret: row.get("client_secret"),
260 display_name: row.get("display_name"),
261 icon_url: row.get("icon_url"),
262 enabled: row.get("enabled"),
263 scopes: parse_json_array(&scopes_str),
264 auth_params: parse_auth_params(&auth_params_str),
265 }
266 }
267
268 fn parse_auth_params(s: &str) -> BTreeMap<String, String> {
269 if s.is_empty() {
270 return BTreeMap::new();
271 }
272 serde_json::from_str(s).unwrap_or_default()
273 }
274
275 fn encode_auth_params(map: &BTreeMap<String, String>) -> String {
276 serde_json::to_string(map).unwrap_or_else(|_| "{}".to_string())
277 }
278
279 #[derive(Clone)]
280 pub struct PostgresOidcUpstreamStore {
281 pool: PgPool,
282 }
283
284 impl PostgresOidcUpstreamStore {
285 pub fn new(pool: PgPool) -> Self {
286 Self { pool }
287 }
288 pub fn into_dyn(self) -> Arc<dyn OidcUpstreamStore> {
289 Arc::new(self)
290 }
291 }
292
293 #[async_trait]
294 impl OidcUpstreamStore for PostgresOidcUpstreamStore {
295 async fn upsert(&self, p: &UpstreamProvider) -> Result<()> {
296 sqlx::query(
297 "INSERT INTO auth.upstream_providers
298 (slug, issuer, client_id, client_secret, display_name, icon_url,
299 enabled, scopes, auth_params)
300 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
301 ON CONFLICT (slug) DO UPDATE SET
302 issuer = EXCLUDED.issuer,
303 client_id = EXCLUDED.client_id,
304 client_secret = EXCLUDED.client_secret,
305 display_name = EXCLUDED.display_name,
306 icon_url = EXCLUDED.icon_url,
307 enabled = EXCLUDED.enabled,
308 scopes = EXCLUDED.scopes,
309 auth_params = EXCLUDED.auth_params",
310 )
311 .bind(&p.slug)
312 .bind(&p.issuer)
313 .bind(&p.client_id)
314 .bind(&p.client_secret)
315 .bind(&p.display_name)
316 .bind(&p.icon_url)
317 .bind(p.enabled)
318 .bind(encode_json_array(&p.scopes))
319 .bind(encode_auth_params(&p.auth_params))
320 .execute(&self.pool)
321 .await
322 .context("auth.upstream_providers upsert")?;
323 Ok(())
324 }
325
326 async fn get(&self, slug: &str) -> Result<Option<UpstreamProvider>> {
327 let row = sqlx::query("SELECT * FROM auth.upstream_providers WHERE slug = $1")
328 .bind(slug)
329 .fetch_optional(&self.pool)
330 .await
331 .context("auth.upstream_providers select")?;
332 Ok(row.map(map_upstream_row))
333 }
334
335 async fn list(&self) -> Result<Vec<UpstreamProvider>> {
336 let rows = sqlx::query("SELECT * FROM auth.upstream_providers ORDER BY slug")
337 .fetch_all(&self.pool)
338 .await
339 .context("auth.upstream_providers list")?;
340 Ok(rows.into_iter().map(map_upstream_row).collect())
341 }
342
343 async fn delete(&self, slug: &str) -> Result<bool> {
344 let r = sqlx::query("DELETE FROM auth.upstream_providers WHERE slug = $1")
345 .bind(slug)
346 .execute(&self.pool)
347 .await
348 .context("auth.upstream_providers delete")?;
349 Ok(r.rows_affected() > 0)
350 }
351 }
352
353 fn map_code_row(row: sqlx::postgres::PgRow) -> AuthorizationCode {
354 AuthorizationCode {
355 code: row.get("code"),
356 client_id: row.get("client_id"),
357 user_id: row.get("user_id"),
358 redirect_uri: row.get("redirect_uri"),
359 scopes: parse_json_array(&row.get::<String, _>("scopes")),
360 code_challenge: row.get("code_challenge"),
361 code_challenge_method: row.get("code_challenge_method"),
362 nonce: row.get("nonce"),
363 state: row.get("state"),
364 issued_at: row.get("issued_at"),
365 expires_at: row.get("expires_at"),
366 consumed: row.get("consumed"),
367 }
368 }
369
370 #[derive(Clone)]
371 pub struct PostgresOidcCodeStore {
372 pool: PgPool,
373 }
374 impl PostgresOidcCodeStore {
375 pub fn new(pool: PgPool) -> Self {
376 Self { pool }
377 }
378 pub fn into_dyn(self) -> Arc<dyn OidcCodeStore> {
379 Arc::new(self)
380 }
381 }
382
383 #[async_trait]
384 impl OidcCodeStore for PostgresOidcCodeStore {
385 async fn create(&self, c: &AuthorizationCode) -> Result<()> {
386 sqlx::query(
387 "INSERT INTO auth.oidc_authorization_codes
388 (code, client_id, user_id, redirect_uri, scopes,
389 code_challenge, code_challenge_method, nonce, state,
390 issued_at, expires_at, consumed)
391 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
392 )
393 .bind(&c.code)
394 .bind(&c.client_id)
395 .bind(&c.user_id)
396 .bind(&c.redirect_uri)
397 .bind(encode_json_array(&c.scopes))
398 .bind(&c.code_challenge)
399 .bind(&c.code_challenge_method)
400 .bind(&c.nonce)
401 .bind(&c.state)
402 .bind(c.issued_at)
403 .bind(c.expires_at)
404 .bind(c.consumed)
405 .execute(&self.pool)
406 .await
407 .context("auth.oidc_authorization_codes insert")?;
408 Ok(())
409 }
410
411 async fn consume(&self, code: &str) -> Result<Option<AuthorizationCode>> {
412 let row = sqlx::query(
416 "UPDATE auth.oidc_authorization_codes
417 SET consumed = TRUE
418 WHERE code = $1 AND consumed = FALSE
419 RETURNING *",
420 )
421 .bind(code)
422 .fetch_optional(&self.pool)
423 .await
424 .context("auth.oidc_authorization_codes consume")?;
425 Ok(row.map(map_code_row))
426 }
427 }
428
429 fn map_refresh_row(row: sqlx::postgres::PgRow) -> RefreshToken {
430 RefreshToken {
431 token_hash: row.get("token_hash"),
432 client_id: row.get("client_id"),
433 user_id: row.get("user_id"),
434 scopes: parse_json_array(&row.get::<String, _>("scopes")),
435 issued_at: row.get("issued_at"),
436 expires_at: row.get("expires_at"),
437 revoked: row.get("revoked"),
438 }
439 }
440
441 #[derive(Clone)]
442 pub struct PostgresOidcRefreshStore {
443 pool: PgPool,
444 }
445 impl PostgresOidcRefreshStore {
446 pub fn new(pool: PgPool) -> Self {
447 Self { pool }
448 }
449 pub fn into_dyn(self) -> Arc<dyn OidcRefreshStore> {
450 Arc::new(self)
451 }
452 }
453
454 #[async_trait]
455 impl OidcRefreshStore for PostgresOidcRefreshStore {
456 async fn create(&self, t: &RefreshToken) -> Result<()> {
457 sqlx::query(
458 "INSERT INTO auth.oidc_refresh_tokens
459 (token_hash, client_id, user_id, scopes,
460 issued_at, expires_at, revoked)
461 VALUES ($1, $2, $3, $4, $5, $6, $7)",
462 )
463 .bind(&t.token_hash)
464 .bind(&t.client_id)
465 .bind(&t.user_id)
466 .bind(encode_json_array(&t.scopes))
467 .bind(t.issued_at)
468 .bind(t.expires_at)
469 .bind(t.revoked)
470 .execute(&self.pool)
471 .await
472 .context("auth.oidc_refresh_tokens insert")?;
473 Ok(())
474 }
475
476 async fn get(&self, token_hash: &str) -> Result<Option<RefreshToken>> {
477 let row = sqlx::query("SELECT * FROM auth.oidc_refresh_tokens WHERE token_hash = $1")
478 .bind(token_hash)
479 .fetch_optional(&self.pool)
480 .await
481 .context("auth.oidc_refresh_tokens select")?;
482 Ok(row.map(map_refresh_row))
483 }
484
485 async fn revoke(&self, token_hash: &str) -> Result<bool> {
486 let r = sqlx::query(
487 "UPDATE auth.oidc_refresh_tokens SET revoked = TRUE WHERE token_hash = $1",
488 )
489 .bind(token_hash)
490 .execute(&self.pool)
491 .await
492 .context("auth.oidc_refresh_tokens revoke")?;
493 Ok(r.rows_affected() > 0)
494 }
495
496 async fn revoke_for_user(&self, user_id: &str) -> Result<u64> {
497 let r = sqlx::query(
498 "UPDATE auth.oidc_refresh_tokens SET revoked = TRUE WHERE user_id = $1",
499 )
500 .bind(user_id)
501 .execute(&self.pool)
502 .await
503 .context("auth.oidc_refresh_tokens revoke_for_user")?;
504 Ok(r.rows_affected())
505 }
506 }
507
508 fn map_session_row(row: sqlx::postgres::PgRow) -> OidcSession {
509 OidcSession {
510 sid: row.get("sid"),
511 user_id: row.get("user_id"),
512 client_id: row.get("client_id"),
513 assay_session_id: row.get("assay_session_id"),
514 issued_at: row.get("issued_at"),
515 backchannel_logout_uri: row.get("backchannel_logout_uri"),
516 }
517 }
518
519 #[derive(Clone)]
520 pub struct PostgresOidcSessionStore {
521 pool: PgPool,
522 }
523 impl PostgresOidcSessionStore {
524 pub fn new(pool: PgPool) -> Self {
525 Self { pool }
526 }
527 pub fn into_dyn(self) -> Arc<dyn OidcSessionStore> {
528 Arc::new(self)
529 }
530 }
531
532 #[async_trait]
533 impl OidcSessionStore for PostgresOidcSessionStore {
534 async fn create(&self, s: &OidcSession) -> Result<()> {
535 sqlx::query(
536 "INSERT INTO auth.oidc_sessions
537 (sid, user_id, client_id, assay_session_id, issued_at, backchannel_logout_uri)
538 VALUES ($1, $2, $3, $4, $5, $6)",
539 )
540 .bind(&s.sid)
541 .bind(&s.user_id)
542 .bind(&s.client_id)
543 .bind(&s.assay_session_id)
544 .bind(s.issued_at)
545 .bind(&s.backchannel_logout_uri)
546 .execute(&self.pool)
547 .await
548 .context("auth.oidc_sessions insert")?;
549 Ok(())
550 }
551
552 async fn get(&self, sid: &str) -> Result<Option<OidcSession>> {
553 let row = sqlx::query("SELECT * FROM auth.oidc_sessions WHERE sid = $1")
554 .bind(sid)
555 .fetch_optional(&self.pool)
556 .await
557 .context("auth.oidc_sessions get")?;
558 Ok(row.map(map_session_row))
559 }
560
561 async fn list_by_assay_session(&self, assay_session_id: &str) -> Result<Vec<OidcSession>> {
562 let rows = sqlx::query("SELECT * FROM auth.oidc_sessions WHERE assay_session_id = $1")
563 .bind(assay_session_id)
564 .fetch_all(&self.pool)
565 .await
566 .context("auth.oidc_sessions list_by_assay_session")?;
567 Ok(rows.into_iter().map(map_session_row).collect())
568 }
569
570 async fn delete(&self, sid: &str) -> Result<bool> {
571 let r = sqlx::query("DELETE FROM auth.oidc_sessions WHERE sid = $1")
572 .bind(sid)
573 .execute(&self.pool)
574 .await
575 .context("auth.oidc_sessions delete")?;
576 Ok(r.rows_affected() > 0)
577 }
578
579 async fn delete_by_assay_session(&self, assay_session_id: &str) -> Result<u64> {
580 let r = sqlx::query("DELETE FROM auth.oidc_sessions WHERE assay_session_id = $1")
581 .bind(assay_session_id)
582 .execute(&self.pool)
583 .await
584 .context("auth.oidc_sessions delete_by_assay_session")?;
585 Ok(r.rows_affected())
586 }
587 }
588
589 fn map_consent_row(row: sqlx::postgres::PgRow) -> ConsentGrant {
590 ConsentGrant {
591 user_id: row.get("user_id"),
592 client_id: row.get("client_id"),
593 scopes: parse_json_array(&row.get::<String, _>("scopes")),
594 granted_at: row.get("granted_at"),
595 }
596 }
597
598 #[derive(Clone)]
599 pub struct PostgresOidcConsentStore {
600 pool: PgPool,
601 }
602 impl PostgresOidcConsentStore {
603 pub fn new(pool: PgPool) -> Self {
604 Self { pool }
605 }
606 pub fn into_dyn(self) -> Arc<dyn OidcConsentStore> {
607 Arc::new(self)
608 }
609 }
610
611 #[async_trait]
612 impl OidcConsentStore for PostgresOidcConsentStore {
613 async fn upsert(&self, g: &ConsentGrant) -> Result<()> {
614 sqlx::query(
615 "INSERT INTO auth.oidc_consents (user_id, client_id, scopes, granted_at)
616 VALUES ($1, $2, $3, $4)
617 ON CONFLICT (user_id, client_id) DO UPDATE
618 SET scopes = EXCLUDED.scopes,
619 granted_at = EXCLUDED.granted_at",
620 )
621 .bind(&g.user_id)
622 .bind(&g.client_id)
623 .bind(encode_json_array(&g.scopes))
624 .bind(g.granted_at)
625 .execute(&self.pool)
626 .await
627 .context("auth.oidc_consents upsert")?;
628 Ok(())
629 }
630
631 async fn get(&self, user_id: &str, client_id: &str) -> Result<Option<ConsentGrant>> {
632 let row = sqlx::query(
633 "SELECT * FROM auth.oidc_consents WHERE user_id = $1 AND client_id = $2",
634 )
635 .bind(user_id)
636 .bind(client_id)
637 .fetch_optional(&self.pool)
638 .await
639 .context("auth.oidc_consents get")?;
640 Ok(row.map(map_consent_row))
641 }
642
643 async fn delete(&self, user_id: &str, client_id: &str) -> Result<bool> {
644 let r =
645 sqlx::query("DELETE FROM auth.oidc_consents WHERE user_id = $1 AND client_id = $2")
646 .bind(user_id)
647 .bind(client_id)
648 .execute(&self.pool)
649 .await
650 .context("auth.oidc_consents delete")?;
651 Ok(r.rows_affected() > 0)
652 }
653 }
654
655 fn map_upstream_state_row(row: sqlx::postgres::PgRow) -> UpstreamLoginState {
656 UpstreamLoginState {
657 state: row.get("state"),
658 provider_slug: row.get("provider_slug"),
659 nonce: row.get("nonce"),
660 pkce_verifier: row.get("pkce_verifier"),
661 return_to: row.get("return_to"),
662 created_at: row.get("created_at"),
663 expires_at: row.get("expires_at"),
664 binding_hash: row.try_get("binding_hash").unwrap_or_default(),
665 }
666 }
667
668 #[derive(Clone)]
669 pub struct PostgresOidcUpstreamStateStore {
670 pool: PgPool,
671 }
672 impl PostgresOidcUpstreamStateStore {
673 pub fn new(pool: PgPool) -> Self {
674 Self { pool }
675 }
676 pub fn into_dyn(self) -> Arc<dyn OidcUpstreamStateStore> {
677 Arc::new(self)
678 }
679 }
680
681 #[async_trait]
682 impl OidcUpstreamStateStore for PostgresOidcUpstreamStateStore {
683 async fn create(&self, s: &UpstreamLoginState) -> Result<()> {
684 sqlx::query(
685 "INSERT INTO auth.oidc_upstream_states
686 (state, provider_slug, nonce, pkce_verifier, return_to,
687 created_at, expires_at, binding_hash)
688 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
689 )
690 .bind(&s.state)
691 .bind(&s.provider_slug)
692 .bind(&s.nonce)
693 .bind(&s.pkce_verifier)
694 .bind(&s.return_to)
695 .bind(s.created_at)
696 .bind(s.expires_at)
697 .bind(&s.binding_hash)
698 .execute(&self.pool)
699 .await
700 .context("auth.oidc_upstream_states insert")?;
701 Ok(())
702 }
703
704 async fn take(&self, state: &str) -> Result<Option<UpstreamLoginState>> {
705 let row =
707 sqlx::query("DELETE FROM auth.oidc_upstream_states WHERE state = $1 RETURNING *")
708 .bind(state)
709 .fetch_optional(&self.pool)
710 .await
711 .context("auth.oidc_upstream_states take")?;
712 Ok(row.map(map_upstream_state_row))
713 }
714 }
715}
716
717#[cfg(feature = "backend-postgres")]
718pub use pg::{
719 PostgresOidcClientStore, PostgresOidcCodeStore, PostgresOidcConsentStore,
720 PostgresOidcRefreshStore, PostgresOidcSessionStore, PostgresOidcUpstreamStateStore,
721 PostgresOidcUpstreamStore,
722};
723
724#[cfg(feature = "backend-sqlite")]
729mod sqlite_impl {
730 use super::*;
731 use sqlx::{Row, SqlitePool};
732
733 fn parse_json_array(s: &str) -> Vec<String> {
734 serde_json::from_str(s).unwrap_or_default()
735 }
736 fn encode_json_array(v: &[String]) -> String {
737 serde_json::to_string(v).unwrap_or_else(|_| "[]".to_string())
738 }
739 fn b(v: bool) -> i64 {
740 if v { 1 } else { 0 }
741 }
742 fn ub(v: i64) -> bool {
743 v != 0
744 }
745
746 fn map_client_row(row: sqlx::sqlite::SqliteRow) -> OidcClient {
747 let auth_method: String = row.get("token_endpoint_auth_method");
748 OidcClient {
749 client_id: row.get("client_id"),
750 client_secret_hash: row.get("client_secret_hash"),
751 redirect_uris: parse_json_array(&row.get::<String, _>("redirect_uris")),
752 name: row.get("name"),
753 logo_url: row.get("logo_url"),
754 token_endpoint_auth_method: TokenAuthMethod::parse(&auth_method)
755 .unwrap_or(TokenAuthMethod::ClientSecretBasic),
756 default_scopes: parse_json_array(&row.get::<String, _>("default_scopes")),
757 require_consent: ub(row.get("require_consent")),
758 grant_types: parse_json_array(&row.get::<String, _>("grant_types")),
759 response_types: parse_json_array(&row.get::<String, _>("response_types")),
760 pkce_required: ub(row.get("pkce_required")),
761 backchannel_logout_uri: row.get("backchannel_logout_uri"),
762 created_at: row.get("created_at"),
763 }
764 }
765
766 #[derive(Clone)]
767 pub struct SqliteOidcClientStore {
768 pool: SqlitePool,
769 }
770 impl SqliteOidcClientStore {
771 pub fn new(pool: SqlitePool) -> Self {
772 Self { pool }
773 }
774 pub fn into_dyn(self) -> Arc<dyn OidcClientStore> {
775 Arc::new(self)
776 }
777 }
778
779 #[async_trait]
780 impl OidcClientStore for SqliteOidcClientStore {
781 async fn create(&self, c: &OidcClient) -> Result<()> {
782 sqlx::query(
783 "INSERT INTO auth.oidc_clients
784 (client_id, client_secret_hash, redirect_uris, name, logo_url,
785 token_endpoint_auth_method, default_scopes, require_consent,
786 grant_types, response_types, pkce_required,
787 backchannel_logout_uri, created_at)
788 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
789 )
790 .bind(&c.client_id)
791 .bind(&c.client_secret_hash)
792 .bind(encode_json_array(&c.redirect_uris))
793 .bind(&c.name)
794 .bind(&c.logo_url)
795 .bind(c.token_endpoint_auth_method.as_str())
796 .bind(encode_json_array(&c.default_scopes))
797 .bind(b(c.require_consent))
798 .bind(encode_json_array(&c.grant_types))
799 .bind(encode_json_array(&c.response_types))
800 .bind(b(c.pkce_required))
801 .bind(&c.backchannel_logout_uri)
802 .bind(c.created_at)
803 .execute(&self.pool)
804 .await
805 .context("auth.oidc_clients insert")?;
806 Ok(())
807 }
808
809 async fn get(&self, client_id: &str) -> Result<Option<OidcClient>> {
810 let row = sqlx::query("SELECT * FROM auth.oidc_clients WHERE client_id = ?")
811 .bind(client_id)
812 .fetch_optional(&self.pool)
813 .await
814 .context("auth.oidc_clients get")?;
815 Ok(row.map(map_client_row))
816 }
817
818 async fn list(&self) -> Result<Vec<OidcClient>> {
819 let rows = sqlx::query("SELECT * FROM auth.oidc_clients ORDER BY created_at")
820 .fetch_all(&self.pool)
821 .await
822 .context("auth.oidc_clients list")?;
823 Ok(rows.into_iter().map(map_client_row).collect())
824 }
825
826 async fn update(&self, c: &OidcClient) -> Result<()> {
827 sqlx::query(
828 "UPDATE auth.oidc_clients SET
829 client_secret_hash = ?,
830 redirect_uris = ?,
831 name = ?,
832 logo_url = ?,
833 token_endpoint_auth_method = ?,
834 default_scopes = ?,
835 require_consent = ?,
836 grant_types = ?,
837 response_types = ?,
838 pkce_required = ?,
839 backchannel_logout_uri = ?
840 WHERE client_id = ?",
841 )
842 .bind(&c.client_secret_hash)
843 .bind(encode_json_array(&c.redirect_uris))
844 .bind(&c.name)
845 .bind(&c.logo_url)
846 .bind(c.token_endpoint_auth_method.as_str())
847 .bind(encode_json_array(&c.default_scopes))
848 .bind(b(c.require_consent))
849 .bind(encode_json_array(&c.grant_types))
850 .bind(encode_json_array(&c.response_types))
851 .bind(b(c.pkce_required))
852 .bind(&c.backchannel_logout_uri)
853 .bind(&c.client_id)
854 .execute(&self.pool)
855 .await
856 .context("auth.oidc_clients update")?;
857 Ok(())
858 }
859
860 async fn delete(&self, client_id: &str) -> Result<bool> {
861 let r = sqlx::query("DELETE FROM auth.oidc_clients WHERE client_id = ?")
862 .bind(client_id)
863 .execute(&self.pool)
864 .await
865 .context("auth.oidc_clients delete")?;
866 Ok(r.rows_affected() > 0)
867 }
868
869 async fn rotate_secret_hash(&self, client_id: &str, new_hash: &str) -> Result<bool> {
870 let r = sqlx::query(
871 "UPDATE auth.oidc_clients SET client_secret_hash = ? WHERE client_id = ?",
872 )
873 .bind(new_hash)
874 .bind(client_id)
875 .execute(&self.pool)
876 .await
877 .context("auth.oidc_clients rotate_secret_hash")?;
878 Ok(r.rows_affected() > 0)
879 }
880 }
881
882 fn map_upstream_row(row: sqlx::sqlite::SqliteRow) -> UpstreamProvider {
883 let scopes_str: String = row.try_get("scopes").unwrap_or_default();
884 let auth_params_str: String = row.try_get("auth_params").unwrap_or_default();
885 UpstreamProvider {
886 slug: row.get("slug"),
887 issuer: row.get("issuer"),
888 client_id: row.get("client_id"),
889 client_secret: row.get("client_secret"),
890 display_name: row.get("display_name"),
891 icon_url: row.get("icon_url"),
892 enabled: ub(row.get("enabled")),
893 scopes: parse_json_array(&scopes_str),
894 auth_params: parse_auth_params(&auth_params_str),
895 }
896 }
897
898 fn parse_auth_params(s: &str) -> BTreeMap<String, String> {
899 if s.is_empty() {
900 return BTreeMap::new();
901 }
902 serde_json::from_str(s).unwrap_or_default()
903 }
904
905 fn encode_auth_params(map: &BTreeMap<String, String>) -> String {
906 serde_json::to_string(map).unwrap_or_else(|_| "{}".to_string())
907 }
908
909 #[derive(Clone)]
910 pub struct SqliteOidcUpstreamStore {
911 pool: SqlitePool,
912 }
913 impl SqliteOidcUpstreamStore {
914 pub fn new(pool: SqlitePool) -> Self {
915 Self { pool }
916 }
917 pub fn into_dyn(self) -> Arc<dyn OidcUpstreamStore> {
918 Arc::new(self)
919 }
920 }
921
922 #[async_trait]
923 impl OidcUpstreamStore for SqliteOidcUpstreamStore {
924 async fn upsert(&self, p: &UpstreamProvider) -> Result<()> {
925 sqlx::query(
926 "INSERT INTO auth.upstream_providers
927 (slug, issuer, client_id, client_secret, display_name, icon_url,
928 enabled, scopes, auth_params)
929 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
930 ON CONFLICT (slug) DO UPDATE SET
931 issuer = excluded.issuer,
932 client_id = excluded.client_id,
933 client_secret = excluded.client_secret,
934 display_name = excluded.display_name,
935 icon_url = excluded.icon_url,
936 enabled = excluded.enabled,
937 scopes = excluded.scopes,
938 auth_params = excluded.auth_params",
939 )
940 .bind(&p.slug)
941 .bind(&p.issuer)
942 .bind(&p.client_id)
943 .bind(&p.client_secret)
944 .bind(&p.display_name)
945 .bind(&p.icon_url)
946 .bind(b(p.enabled))
947 .bind(encode_json_array(&p.scopes))
948 .bind(encode_auth_params(&p.auth_params))
949 .execute(&self.pool)
950 .await
951 .context("auth.upstream_providers upsert")?;
952 Ok(())
953 }
954
955 async fn get(&self, slug: &str) -> Result<Option<UpstreamProvider>> {
956 let row = sqlx::query("SELECT * FROM auth.upstream_providers WHERE slug = ?")
957 .bind(slug)
958 .fetch_optional(&self.pool)
959 .await
960 .context("auth.upstream_providers get")?;
961 Ok(row.map(map_upstream_row))
962 }
963
964 async fn list(&self) -> Result<Vec<UpstreamProvider>> {
965 let rows = sqlx::query("SELECT * FROM auth.upstream_providers ORDER BY slug")
966 .fetch_all(&self.pool)
967 .await
968 .context("auth.upstream_providers list")?;
969 Ok(rows.into_iter().map(map_upstream_row).collect())
970 }
971
972 async fn delete(&self, slug: &str) -> Result<bool> {
973 let r = sqlx::query("DELETE FROM auth.upstream_providers WHERE slug = ?")
974 .bind(slug)
975 .execute(&self.pool)
976 .await
977 .context("auth.upstream_providers delete")?;
978 Ok(r.rows_affected() > 0)
979 }
980 }
981
982 fn map_code_row(row: sqlx::sqlite::SqliteRow) -> AuthorizationCode {
983 AuthorizationCode {
984 code: row.get("code"),
985 client_id: row.get("client_id"),
986 user_id: row.get("user_id"),
987 redirect_uri: row.get("redirect_uri"),
988 scopes: parse_json_array(&row.get::<String, _>("scopes")),
989 code_challenge: row.get("code_challenge"),
990 code_challenge_method: row.get("code_challenge_method"),
991 nonce: row.get("nonce"),
992 state: row.get("state"),
993 issued_at: row.get("issued_at"),
994 expires_at: row.get("expires_at"),
995 consumed: ub(row.get("consumed")),
996 }
997 }
998
999 #[derive(Clone)]
1000 pub struct SqliteOidcCodeStore {
1001 pool: SqlitePool,
1002 }
1003 impl SqliteOidcCodeStore {
1004 pub fn new(pool: SqlitePool) -> Self {
1005 Self { pool }
1006 }
1007 pub fn into_dyn(self) -> Arc<dyn OidcCodeStore> {
1008 Arc::new(self)
1009 }
1010 }
1011
1012 #[async_trait]
1013 impl OidcCodeStore for SqliteOidcCodeStore {
1014 async fn create(&self, c: &AuthorizationCode) -> Result<()> {
1015 sqlx::query(
1016 "INSERT INTO auth.oidc_authorization_codes
1017 (code, client_id, user_id, redirect_uri, scopes,
1018 code_challenge, code_challenge_method, nonce, state,
1019 issued_at, expires_at, consumed)
1020 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1021 )
1022 .bind(&c.code)
1023 .bind(&c.client_id)
1024 .bind(&c.user_id)
1025 .bind(&c.redirect_uri)
1026 .bind(encode_json_array(&c.scopes))
1027 .bind(&c.code_challenge)
1028 .bind(&c.code_challenge_method)
1029 .bind(&c.nonce)
1030 .bind(&c.state)
1031 .bind(c.issued_at)
1032 .bind(c.expires_at)
1033 .bind(b(c.consumed))
1034 .execute(&self.pool)
1035 .await
1036 .context("auth.oidc_authorization_codes insert")?;
1037 Ok(())
1038 }
1039
1040 async fn consume(&self, code: &str) -> Result<Option<AuthorizationCode>> {
1041 let mut tx = self.pool.begin().await.context("begin consume tx")?;
1045 let row = sqlx::query(
1046 "SELECT * FROM auth.oidc_authorization_codes
1047 WHERE code = ? AND consumed = 0",
1048 )
1049 .bind(code)
1050 .fetch_optional(&mut *tx)
1051 .await
1052 .context("auth.oidc_authorization_codes consume select")?;
1053 let Some(row) = row else {
1054 tx.rollback().await.ok();
1055 return Ok(None);
1056 };
1057 let result = sqlx::query(
1058 "UPDATE auth.oidc_authorization_codes SET consumed = 1 \
1059 WHERE code = ? AND consumed = 0",
1060 )
1061 .bind(code)
1062 .execute(&mut *tx)
1063 .await
1064 .context("auth.oidc_authorization_codes consume update")?;
1065 if result.rows_affected() == 0 {
1066 tx.rollback().await.ok();
1067 return Ok(None);
1068 }
1069 tx.commit().await.context("commit consume tx")?;
1070 Ok(Some(map_code_row(row)))
1071 }
1072 }
1073
1074 fn map_refresh_row(row: sqlx::sqlite::SqliteRow) -> RefreshToken {
1075 RefreshToken {
1076 token_hash: row.get("token_hash"),
1077 client_id: row.get("client_id"),
1078 user_id: row.get("user_id"),
1079 scopes: parse_json_array(&row.get::<String, _>("scopes")),
1080 issued_at: row.get("issued_at"),
1081 expires_at: row.get("expires_at"),
1082 revoked: ub(row.get("revoked")),
1083 }
1084 }
1085
1086 #[derive(Clone)]
1087 pub struct SqliteOidcRefreshStore {
1088 pool: SqlitePool,
1089 }
1090 impl SqliteOidcRefreshStore {
1091 pub fn new(pool: SqlitePool) -> Self {
1092 Self { pool }
1093 }
1094 pub fn into_dyn(self) -> Arc<dyn OidcRefreshStore> {
1095 Arc::new(self)
1096 }
1097 }
1098
1099 #[async_trait]
1100 impl OidcRefreshStore for SqliteOidcRefreshStore {
1101 async fn create(&self, t: &RefreshToken) -> Result<()> {
1102 sqlx::query(
1103 "INSERT INTO auth.oidc_refresh_tokens
1104 (token_hash, client_id, user_id, scopes,
1105 issued_at, expires_at, revoked)
1106 VALUES (?, ?, ?, ?, ?, ?, ?)",
1107 )
1108 .bind(&t.token_hash)
1109 .bind(&t.client_id)
1110 .bind(&t.user_id)
1111 .bind(encode_json_array(&t.scopes))
1112 .bind(t.issued_at)
1113 .bind(t.expires_at)
1114 .bind(b(t.revoked))
1115 .execute(&self.pool)
1116 .await
1117 .context("auth.oidc_refresh_tokens insert")?;
1118 Ok(())
1119 }
1120
1121 async fn get(&self, token_hash: &str) -> Result<Option<RefreshToken>> {
1122 let row = sqlx::query("SELECT * FROM auth.oidc_refresh_tokens WHERE token_hash = ?")
1123 .bind(token_hash)
1124 .fetch_optional(&self.pool)
1125 .await
1126 .context("auth.oidc_refresh_tokens get")?;
1127 Ok(row.map(map_refresh_row))
1128 }
1129
1130 async fn revoke(&self, token_hash: &str) -> Result<bool> {
1131 let r =
1132 sqlx::query("UPDATE auth.oidc_refresh_tokens SET revoked = 1 WHERE token_hash = ?")
1133 .bind(token_hash)
1134 .execute(&self.pool)
1135 .await
1136 .context("auth.oidc_refresh_tokens revoke")?;
1137 Ok(r.rows_affected() > 0)
1138 }
1139
1140 async fn revoke_for_user(&self, user_id: &str) -> Result<u64> {
1141 let r =
1142 sqlx::query("UPDATE auth.oidc_refresh_tokens SET revoked = 1 WHERE user_id = ?")
1143 .bind(user_id)
1144 .execute(&self.pool)
1145 .await
1146 .context("auth.oidc_refresh_tokens revoke_for_user")?;
1147 Ok(r.rows_affected())
1148 }
1149 }
1150
1151 fn map_session_row(row: sqlx::sqlite::SqliteRow) -> OidcSession {
1152 OidcSession {
1153 sid: row.get("sid"),
1154 user_id: row.get("user_id"),
1155 client_id: row.get("client_id"),
1156 assay_session_id: row.get("assay_session_id"),
1157 issued_at: row.get("issued_at"),
1158 backchannel_logout_uri: row.get("backchannel_logout_uri"),
1159 }
1160 }
1161
1162 #[derive(Clone)]
1163 pub struct SqliteOidcSessionStore {
1164 pool: SqlitePool,
1165 }
1166 impl SqliteOidcSessionStore {
1167 pub fn new(pool: SqlitePool) -> Self {
1168 Self { pool }
1169 }
1170 pub fn into_dyn(self) -> Arc<dyn OidcSessionStore> {
1171 Arc::new(self)
1172 }
1173 }
1174
1175 #[async_trait]
1176 impl OidcSessionStore for SqliteOidcSessionStore {
1177 async fn create(&self, s: &OidcSession) -> Result<()> {
1178 sqlx::query(
1179 "INSERT INTO auth.oidc_sessions
1180 (sid, user_id, client_id, assay_session_id, issued_at, backchannel_logout_uri)
1181 VALUES (?, ?, ?, ?, ?, ?)",
1182 )
1183 .bind(&s.sid)
1184 .bind(&s.user_id)
1185 .bind(&s.client_id)
1186 .bind(&s.assay_session_id)
1187 .bind(s.issued_at)
1188 .bind(&s.backchannel_logout_uri)
1189 .execute(&self.pool)
1190 .await
1191 .context("auth.oidc_sessions insert")?;
1192 Ok(())
1193 }
1194
1195 async fn get(&self, sid: &str) -> Result<Option<OidcSession>> {
1196 let row = sqlx::query("SELECT * FROM auth.oidc_sessions WHERE sid = ?")
1197 .bind(sid)
1198 .fetch_optional(&self.pool)
1199 .await
1200 .context("auth.oidc_sessions get")?;
1201 Ok(row.map(map_session_row))
1202 }
1203
1204 async fn list_by_assay_session(&self, assay_session_id: &str) -> Result<Vec<OidcSession>> {
1205 let rows = sqlx::query("SELECT * FROM auth.oidc_sessions WHERE assay_session_id = ?")
1206 .bind(assay_session_id)
1207 .fetch_all(&self.pool)
1208 .await
1209 .context("auth.oidc_sessions list_by_assay_session")?;
1210 Ok(rows.into_iter().map(map_session_row).collect())
1211 }
1212
1213 async fn delete(&self, sid: &str) -> Result<bool> {
1214 let r = sqlx::query("DELETE FROM auth.oidc_sessions WHERE sid = ?")
1215 .bind(sid)
1216 .execute(&self.pool)
1217 .await
1218 .context("auth.oidc_sessions delete")?;
1219 Ok(r.rows_affected() > 0)
1220 }
1221
1222 async fn delete_by_assay_session(&self, assay_session_id: &str) -> Result<u64> {
1223 let r = sqlx::query("DELETE FROM auth.oidc_sessions WHERE assay_session_id = ?")
1224 .bind(assay_session_id)
1225 .execute(&self.pool)
1226 .await
1227 .context("auth.oidc_sessions delete_by_assay_session")?;
1228 Ok(r.rows_affected())
1229 }
1230 }
1231
1232 fn map_consent_row(row: sqlx::sqlite::SqliteRow) -> ConsentGrant {
1233 ConsentGrant {
1234 user_id: row.get("user_id"),
1235 client_id: row.get("client_id"),
1236 scopes: parse_json_array(&row.get::<String, _>("scopes")),
1237 granted_at: row.get("granted_at"),
1238 }
1239 }
1240
1241 #[derive(Clone)]
1242 pub struct SqliteOidcConsentStore {
1243 pool: SqlitePool,
1244 }
1245 impl SqliteOidcConsentStore {
1246 pub fn new(pool: SqlitePool) -> Self {
1247 Self { pool }
1248 }
1249 pub fn into_dyn(self) -> Arc<dyn OidcConsentStore> {
1250 Arc::new(self)
1251 }
1252 }
1253
1254 #[async_trait]
1255 impl OidcConsentStore for SqliteOidcConsentStore {
1256 async fn upsert(&self, g: &ConsentGrant) -> Result<()> {
1257 sqlx::query(
1258 "INSERT INTO auth.oidc_consents (user_id, client_id, scopes, granted_at)
1259 VALUES (?, ?, ?, ?)
1260 ON CONFLICT (user_id, client_id) DO UPDATE
1261 SET scopes = excluded.scopes,
1262 granted_at = excluded.granted_at",
1263 )
1264 .bind(&g.user_id)
1265 .bind(&g.client_id)
1266 .bind(encode_json_array(&g.scopes))
1267 .bind(g.granted_at)
1268 .execute(&self.pool)
1269 .await
1270 .context("auth.oidc_consents upsert")?;
1271 Ok(())
1272 }
1273
1274 async fn get(&self, user_id: &str, client_id: &str) -> Result<Option<ConsentGrant>> {
1275 let row =
1276 sqlx::query("SELECT * FROM auth.oidc_consents WHERE user_id = ? AND client_id = ?")
1277 .bind(user_id)
1278 .bind(client_id)
1279 .fetch_optional(&self.pool)
1280 .await
1281 .context("auth.oidc_consents get")?;
1282 Ok(row.map(map_consent_row))
1283 }
1284
1285 async fn delete(&self, user_id: &str, client_id: &str) -> Result<bool> {
1286 let r =
1287 sqlx::query("DELETE FROM auth.oidc_consents WHERE user_id = ? AND client_id = ?")
1288 .bind(user_id)
1289 .bind(client_id)
1290 .execute(&self.pool)
1291 .await
1292 .context("auth.oidc_consents delete")?;
1293 Ok(r.rows_affected() > 0)
1294 }
1295 }
1296
1297 fn map_upstream_state_row(row: sqlx::sqlite::SqliteRow) -> UpstreamLoginState {
1298 UpstreamLoginState {
1299 state: row.get("state"),
1300 provider_slug: row.get("provider_slug"),
1301 nonce: row.get("nonce"),
1302 pkce_verifier: row.get("pkce_verifier"),
1303 return_to: row.get("return_to"),
1304 created_at: row.get("created_at"),
1305 expires_at: row.get("expires_at"),
1306 binding_hash: row.try_get("binding_hash").unwrap_or_default(),
1307 }
1308 }
1309
1310 #[derive(Clone)]
1311 pub struct SqliteOidcUpstreamStateStore {
1312 pool: SqlitePool,
1313 }
1314 impl SqliteOidcUpstreamStateStore {
1315 pub fn new(pool: SqlitePool) -> Self {
1316 Self { pool }
1317 }
1318 pub fn into_dyn(self) -> Arc<dyn OidcUpstreamStateStore> {
1319 Arc::new(self)
1320 }
1321 }
1322
1323 #[async_trait]
1324 impl OidcUpstreamStateStore for SqliteOidcUpstreamStateStore {
1325 async fn create(&self, s: &UpstreamLoginState) -> Result<()> {
1326 sqlx::query(
1327 "INSERT INTO auth.oidc_upstream_states
1328 (state, provider_slug, nonce, pkce_verifier, return_to,
1329 created_at, expires_at, binding_hash)
1330 VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1331 )
1332 .bind(&s.state)
1333 .bind(&s.provider_slug)
1334 .bind(&s.nonce)
1335 .bind(&s.pkce_verifier)
1336 .bind(&s.return_to)
1337 .bind(s.created_at)
1338 .bind(s.expires_at)
1339 .bind(&s.binding_hash)
1340 .execute(&self.pool)
1341 .await
1342 .context("auth.oidc_upstream_states insert")?;
1343 Ok(())
1344 }
1345
1346 async fn take(&self, state: &str) -> Result<Option<UpstreamLoginState>> {
1347 let mut tx = self.pool.begin().await.context("begin take tx")?;
1350 let row = sqlx::query("SELECT * FROM auth.oidc_upstream_states WHERE state = ?")
1351 .bind(state)
1352 .fetch_optional(&mut *tx)
1353 .await
1354 .context("auth.oidc_upstream_states take select")?;
1355 let Some(row) = row else {
1356 tx.rollback().await.ok();
1357 return Ok(None);
1358 };
1359 sqlx::query("DELETE FROM auth.oidc_upstream_states WHERE state = ?")
1360 .bind(state)
1361 .execute(&mut *tx)
1362 .await
1363 .context("auth.oidc_upstream_states take delete")?;
1364 tx.commit().await.context("commit take tx")?;
1365 Ok(Some(map_upstream_state_row(row)))
1366 }
1367 }
1368}
1369
1370#[cfg(feature = "backend-sqlite")]
1371pub use sqlite_impl::{
1372 SqliteOidcClientStore, SqliteOidcCodeStore, SqliteOidcConsentStore, SqliteOidcRefreshStore,
1373 SqliteOidcSessionStore, SqliteOidcUpstreamStateStore, SqliteOidcUpstreamStore,
1374};