From 488a666a8dcf6b9678f5f45d34410384d41536da Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 12 Jan 2023 18:26:04 +0100 Subject: [PATCH] storage: remaining oauth2 repositories - authorization grants - access tokens - refresh tokens --- crates/axum-utils/src/user_authorization.rs | 6 +- .../src/oauth2/authorization_grant.rs | 26 +- .../src/oauth2/authorization/complete.rs | 20 +- .../handlers/src/oauth2/authorization/mod.rs | 36 +- crates/handlers/src/oauth2/consent.rs | 34 +- crates/handlers/src/oauth2/introspection.rs | 13 +- crates/handlers/src/oauth2/token.rs | 84 +- crates/handlers/src/views/shared.rs | 6 +- crates/storage/sqlx-data.json | 1077 +++++++++-------- crates/storage/src/oauth2/access_token.rs | 362 +++--- .../storage/src/oauth2/authorization_grant.rs | 781 ++++++------ crates/storage/src/oauth2/client.rs | 113 +- crates/storage/src/oauth2/consent.rs | 110 -- crates/storage/src/oauth2/mod.rs | 10 +- crates/storage/src/oauth2/refresh_token.rs | 329 +++-- crates/storage/src/repository.rs | 50 +- crates/tasks/src/database.rs | 9 +- 17 files changed, 1700 insertions(+), 1366 deletions(-) delete mode 100644 crates/storage/src/oauth2/consent.rs diff --git a/crates/axum-utils/src/user_authorization.rs b/crates/axum-utils/src/user_authorization.rs index a76e1e9a..ec60103d 100644 --- a/crates/axum-utils/src/user_authorization.rs +++ b/crates/axum-utils/src/user_authorization.rs @@ -29,7 +29,7 @@ use headers::{authorization::Bearer, Authorization, Header, HeaderMapExt, Header use http::{header::WWW_AUTHENTICATE, HeaderMap, HeaderValue, Request, StatusCode}; use mas_data_model::Session; use mas_storage::{ - oauth2::{access_token::find_access_token, OAuth2SessionRepository}, + oauth2::{OAuth2AccessTokenRepository, OAuth2SessionRepository}, DatabaseError, Repository, }; use serde::{de::DeserializeOwned, Deserialize}; @@ -62,7 +62,9 @@ impl AccessToken { AccessToken::None => return Err(AuthorizationVerificationError::MissingToken), }; - let token = find_access_token(conn, token.as_str()) + let token = conn + .oauth2_access_token() + .find_by_token(token.as_str()) .await? .ok_or(AuthorizationVerificationError::InvalidToken)?; diff --git a/crates/data-model/src/oauth2/authorization_grant.rs b/crates/data-model/src/oauth2/authorization_grant.rs index 10f619c7..76572f48 100644 --- a/crates/data-model/src/oauth2/authorization_grant.rs +++ b/crates/data-model/src/oauth2/authorization_grant.rs @@ -78,7 +78,7 @@ impl AuthorizationGrantStage { Self::Pending } - pub fn fulfill( + fn fulfill( self, fulfilled_at: DateTime, session: &Session, @@ -92,7 +92,7 @@ impl AuthorizationGrantStage { } } - pub fn exchange(self, exchanged_at: DateTime) -> Result { + fn exchange(self, exchanged_at: DateTime) -> Result { match self { Self::Fulfilled { fulfilled_at, @@ -106,7 +106,7 @@ impl AuthorizationGrantStage { } } - pub fn cancel(self, cancelled_at: DateTime) -> Result { + fn cancel(self, cancelled_at: DateTime) -> Result { match self { Self::Pending => Ok(Self::Cancelled { cancelled_at }), _ => Err(InvalidTransitionError), @@ -146,4 +146,24 @@ impl AuthorizationGrant { let max_age: Option = self.max_age.map(|x| x.get().into()); self.created_at - Duration::seconds(max_age.unwrap_or(3600 * 24 * 365)) } + + pub fn exchange(mut self, exchanged_at: DateTime) -> Result { + self.stage = self.stage.exchange(exchanged_at)?; + Ok(self) + } + + pub fn fulfill( + mut self, + fulfilled_at: DateTime, + session: &Session, + ) -> Result { + self.stage = self.stage.fulfill(fulfilled_at, session)?; + Ok(self) + } + + // TODO: this is not used? + pub fn cancel(mut self, canceld_at: DateTime) -> Result { + self.stage = self.stage.cancel(canceld_at)?; + Ok(self) + } } diff --git a/crates/handlers/src/oauth2/authorization/complete.rs b/crates/handlers/src/oauth2/authorization/complete.rs index b5cfd6ff..9f462c50 100644 --- a/crates/handlers/src/oauth2/authorization/complete.rs +++ b/crates/handlers/src/oauth2/authorization/complete.rs @@ -26,11 +26,7 @@ use mas_keystore::Encrypter; use mas_policy::PolicyFactory; use mas_router::{PostAuthAction, Route}; use mas_storage::{ - oauth2::{ - authorization_grant::{fulfill_grant, get_grant_by_id}, - consent::fetch_client_consent, - OAuth2ClientRepository, OAuth2SessionRepository, - }, + oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository}, Repository, }; use mas_templates::Templates; @@ -94,7 +90,9 @@ pub(crate) async fn get( let maybe_session = session_info.load_session(&mut txn).await?; - let grant = get_grant_by_id(&mut txn, grant_id) + let grant = txn + .oauth2_authorization_grant() + .lookup(grant_id) .await? .ok_or(RouteError::NotFound)?; @@ -192,7 +190,10 @@ pub(crate) async fn complete( .await? .ok_or(GrantCompletionError::NoSuchClient)?; - let current_consent = fetch_client_consent(&mut txn, &browser_session.user, &client).await?; + let current_consent = txn + .oauth2_client() + .get_consent_for_user(&client, &browser_session.user) + .await?; let lacks_consent = grant .scope @@ -211,7 +212,10 @@ pub(crate) async fn complete( .create_from_grant(&mut rng, &clock, &grant, &browser_session) .await?; - let grant = fulfill_grant(&mut txn, grant, session.clone()).await?; + let grant = txn + .oauth2_authorization_grant() + .fulfill(&clock, &session, grant) + .await?; // Yep! Let's complete the auth now let mut params = AuthorizationResponse::default(); diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index faf7015d..b33b6912 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -26,7 +26,7 @@ use mas_keystore::Encrypter; use mas_policy::PolicyFactory; use mas_router::{PostAuthAction, Route}; use mas_storage::{ - oauth2::{authorization_grant::new_authorization_grant, OAuth2ClientRepository}, + oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository}, Repository, }; use mas_templates::Templates; @@ -275,23 +275,23 @@ pub(crate) async fn get( let requires_consent = prompt.contains(&Prompt::Consent); - let grant = new_authorization_grant( - &mut txn, - &mut rng, - &clock, - client, - redirect_uri.clone(), - params.auth.scope, - code, - params.auth.state.clone(), - params.auth.nonce, - params.auth.max_age, - None, - response_mode, - response_type.has_id_token(), - requires_consent, - ) - .await?; + let grant = txn + .oauth2_authorization_grant() + .add( + &mut rng, + &clock, + &client, + redirect_uri.clone(), + params.auth.scope, + code, + params.auth.state.clone(), + params.auth.nonce, + params.auth.max_age, + response_mode, + response_type.has_id_token(), + requires_consent, + ) + .await?; let continue_grant = PostAuthAction::continue_grant(grant.id); let res = match maybe_session { diff --git a/crates/handlers/src/oauth2/consent.rs b/crates/handlers/src/oauth2/consent.rs index 64b72d58..94bf1346 100644 --- a/crates/handlers/src/oauth2/consent.rs +++ b/crates/handlers/src/oauth2/consent.rs @@ -29,11 +29,7 @@ use mas_keystore::Encrypter; use mas_policy::PolicyFactory; use mas_router::{PostAuthAction, Route}; use mas_storage::{ - oauth2::{ - authorization_grant::{get_grant_by_id, give_consent_to_grant}, - consent::insert_client_consent, - OAuth2ClientRepository, - }, + oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository}, Repository, }; use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates}; @@ -91,7 +87,9 @@ pub(crate) async fn get( let maybe_session = session_info.load_session(&mut conn).await?; - let grant = get_grant_by_id(&mut conn, grant_id) + let grant = conn + .oauth2_authorization_grant() + .lookup(grant_id) .await? .ok_or(RouteError::GrantNotFound)?; @@ -146,7 +144,9 @@ pub(crate) async fn post( let maybe_session = session_info.load_session(&mut txn).await?; - let grant = get_grant_by_id(&mut txn, grant_id) + let grant = txn + .oauth2_authorization_grant() + .lookup(grant_id) .await? .ok_or(RouteError::GrantNotFound)?; let next = PostAuthAction::continue_grant(grant_id); @@ -180,17 +180,17 @@ pub(crate) async fn post( .filter(|s| !s.starts_with("urn:matrix:org.matrix.msc2967.client:device:")) .cloned() .collect(); - insert_client_consent( - &mut txn, - &mut rng, - &clock, - &session.user, - &client, - &scope_without_device, - ) - .await?; + txn.oauth2_client() + .give_consent_for_user( + &mut rng, + &clock, + &client, + &session.user, + &scope_without_device, + ) + .await?; - let _grant = give_consent_to_grant(&mut txn, grant).await?; + txn.oauth2_authorization_grant().give_consent(grant).await?; txn.commit().await?; diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index ef6ba5b2..d032695a 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -23,10 +23,7 @@ use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; use mas_keystore::Encrypter; use mas_storage::{ compat::{CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository}, - oauth2::{ - access_token::find_access_token, refresh_token::lookup_refresh_token, - OAuth2SessionRepository, - }, + oauth2::{OAuth2AccessTokenRepository, OAuth2RefreshTokenRepository, OAuth2SessionRepository}, user::{BrowserSessionRepository, UserRepository}, Clock, Repository, }; @@ -169,7 +166,9 @@ pub(crate) async fn post( let reply = match token_type { TokenType::AccessToken => { - let token = find_access_token(&mut conn, token) + let token = conn + .oauth2_access_token() + .find_by_token(token) .await? .filter(|t| t.is_valid(clock.now())) .ok_or(RouteError::UnknownToken)?; @@ -206,7 +205,9 @@ pub(crate) async fn post( } TokenType::RefreshToken => { - let token = lookup_refresh_token(&mut conn, token) + let token = conn + .oauth2_refresh_token() + .find_by_token(token) .await? .filter(|t| t.is_valid()) .ok_or(RouteError::UnknownToken)?; diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 75ddb4a6..97f249c2 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -1,4 +1,4 @@ -// Copyright 2021, 2022 The Matrix.org Foundation C.I.C. +// Copyright 2021-2023 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,10 +33,8 @@ use mas_keystore::{Encrypter, Keystore}; use mas_router::UrlBuilder; use mas_storage::{ oauth2::{ - access_token::{add_access_token, lookup_access_token, revoke_access_token}, - authorization_grant::{exchange_grant, lookup_grant_by_code}, - refresh_token::{add_refresh_token, consume_refresh_token, lookup_refresh_token}, - OAuth2SessionRepository, + OAuth2AccessTokenRepository, OAuth2AuthorizationGrantRepository, + OAuth2RefreshTokenRepository, OAuth2SessionRepository, }, user::BrowserSessionRepository, Repository, @@ -217,9 +215,9 @@ async fn authorization_code_grant( ) -> Result { let (clock, mut rng) = crate::clock_and_rng(); - // TODO: there is a bunch of unnecessary cloning here - // TODO: handle "not found" cases - let authz_grant = lookup_grant_by_code(&mut txn, &grant.code) + let authz_grant = txn + .oauth2_authorization_grant() + .find_by_code(&grant.code) .await? .ok_or(RouteError::GrantNotFound)?; @@ -301,18 +299,15 @@ async fn authorization_code_grant( let access_token_str = TokenType::AccessToken.generate(&mut rng); let refresh_token_str = TokenType::RefreshToken.generate(&mut rng); - let access_token = - add_access_token(&mut txn, &mut rng, &clock, &session, access_token_str, ttl).await?; + let access_token = txn + .oauth2_access_token() + .add(&mut rng, &clock, &session, access_token_str, ttl) + .await?; - let refresh_token = add_refresh_token( - &mut txn, - &mut rng, - &clock, - &session, - &access_token, - refresh_token_str, - ) - .await?; + let refresh_token = txn + .oauth2_refresh_token() + .add(&mut rng, &clock, &session, &access_token, refresh_token_str) + .await?; let id_token = if session.scope.contains(&scope::OPENID) { let mut claims = HashMap::new(); @@ -360,7 +355,9 @@ async fn authorization_code_grant( params = params.with_id_token(id_token); } - exchange_grant(&mut txn, &clock, authz_grant).await?; + txn.oauth2_authorization_grant() + .exchange(&clock, authz_grant) + .await?; txn.commit().await?; @@ -374,7 +371,9 @@ async fn refresh_token_grant( ) -> Result { let (clock, mut rng) = crate::clock_and_rng(); - let refresh_token = lookup_refresh_token(&mut txn, &grant.refresh_token) + let refresh_token = txn + .oauth2_refresh_token() + .find_by_token(&grant.refresh_token) .await? .ok_or(RouteError::InvalidGrant)?; @@ -397,31 +396,32 @@ async fn refresh_token_grant( let access_token_str = TokenType::AccessToken.generate(&mut rng); let refresh_token_str = TokenType::RefreshToken.generate(&mut rng); - let new_access_token = add_access_token( - &mut txn, - &mut rng, - &clock, - &session, - access_token_str.clone(), - ttl, - ) - .await?; + let new_access_token = txn + .oauth2_access_token() + .add(&mut rng, &clock, &session, access_token_str.clone(), ttl) + .await?; - let new_refresh_token = add_refresh_token( - &mut txn, - &mut rng, - &clock, - &session, - &new_access_token, - refresh_token_str, - ) - .await?; + let new_refresh_token = txn + .oauth2_refresh_token() + .add( + &mut rng, + &clock, + &session, + &new_access_token, + refresh_token_str, + ) + .await?; - let refresh_token = consume_refresh_token(&mut txn, &clock, refresh_token).await?; + let refresh_token = txn + .oauth2_refresh_token() + .consume(&clock, refresh_token) + .await?; if let Some(access_token_id) = refresh_token.access_token_id { - if let Some(access_token) = lookup_access_token(&mut txn, access_token_id).await? { - revoke_access_token(&mut txn, &clock, access_token).await?; + if let Some(access_token) = txn.oauth2_access_token().lookup(access_token_id).await? { + txn.oauth2_access_token() + .revoke(&clock, access_token) + .await?; } } diff --git a/crates/handlers/src/views/shared.rs b/crates/handlers/src/views/shared.rs index 3872588f..57d53762 100644 --- a/crates/handlers/src/views/shared.rs +++ b/crates/handlers/src/views/shared.rs @@ -15,7 +15,7 @@ use anyhow::Context; use mas_router::{PostAuthAction, Route}; use mas_storage::{ - compat::CompatSsoLoginRepository, oauth2::authorization_grant::get_grant_by_id, + compat::CompatSsoLoginRepository, oauth2::OAuth2AuthorizationGrantRepository, upstream_oauth2::UpstreamOAuthProviderRepository, Repository, UpstreamOAuthLinkRepository, }; use mas_templates::{PostAuthContext, PostAuthContextInner}; @@ -46,7 +46,9 @@ impl OptionalPostAuthAction { let Some(action) = self.post_auth_action.clone() else { return Ok(None) }; let ctx = match action { PostAuthAction::ContinueAuthorizationGrant { id } => { - let grant = get_grant_by_id(conn, id) + let grant = conn + .oauth2_authorization_grant() + .lookup(id) .await? .context("Failed to load authorization grant")?; let grant = Box::new(grant); diff --git a/crates/storage/sqlx-data.json b/crates/storage/sqlx-data.json index 6e7082dd..5dd18250 100644 --- a/crates/storage/sqlx-data.json +++ b/crates/storage/sqlx-data.json @@ -1,5 +1,19 @@ { "db": "PostgreSQL", + "015f7ad7c8d5403ce4dfb71d598fd9af472689d5aef7c1c4b1c594ca57c02237": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz", + "Uuid" + ] + } + }, + "query": "\n UPDATE oauth2_authorization_grants\n SET fulfilled_at = $2\n , oauth2_session_id = $3\n WHERE oauth2_authorization_grant_id = $1\n " + }, "08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35": { "describe": { "columns": [ @@ -113,6 +127,18 @@ }, "query": "\n INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at)\n VALUES ($1, $2, $3, $4)\n " }, + "1a8701f5672de052bb766933f60b93249acc7237b996e8b93cd61b9f69c902ff": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Timestamptz" + ] + } + }, + "query": "\n DELETE FROM oauth2_access_tokens\n WHERE expires_at < $1\n " + }, "1d372f36c382ab16264cea54537af3544ea6d6d75d10b432b07dbd0dadd2fa4e": { "describe": { "columns": [ @@ -183,7 +209,7 @@ }, "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n client_id,\n encrypted_client_secret,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n " }, - "262bee715889dc3e608639549600a131e641951ff979634e7c97afc74bbc1605": { + "2564bf6366eb59268c41fb25bb40d0e4e9e1fd1f9ea53b7a359c9025d7304223": { "describe": { "columns": [], "nullable": [], @@ -194,7 +220,7 @@ ] } }, - "query": "\n UPDATE oauth2_authorization_grants\n SET exchanged_at = $2\n WHERE oauth2_authorization_grant_id = $1\n " + "query": "\n UPDATE oauth2_access_tokens\n SET revoked_at = $2\n WHERE oauth2_access_token_id = $1\n " }, "3d66f3121b11ce923b9c60609b510a8ca899640e78cc8f5b03168622928ffe94": { "describe": { @@ -360,22 +386,6 @@ }, "query": "\n SELECT compat_refresh_token_id\n , refresh_token\n , created_at\n , consumed_at\n , compat_session_id\n , compat_access_token_id\n\n FROM compat_refresh_tokens\n\n WHERE refresh_token = $1\n " }, - "43a5cafbdc8037e9fb779812a0793cf0859902aa0dc8d25d4c33d231d3d1118b": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Text", - "Timestamptz", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_access_token_id, oauth2_session_id, access_token, created_at, expires_at)\n VALUES\n ($1, $2, $3, $4, $5)\n " - }, "446a8d7bd8532a751810401adfab924dc20785c91770ed43d62df2e590e8da71": { "describe": { "columns": [ @@ -420,26 +430,55 @@ }, "query": "\n SELECT up.user_password_id\n , up.hashed_password\n , up.version\n , up.upgraded_from_id\n , up.created_at\n FROM user_passwords up\n WHERE up.user_id = $1\n ORDER BY up.created_at DESC\n LIMIT 1\n " }, - "46c5ae7052504bfd7b94f20e61b9cf92570779a794bccda23dd654fb8523f340": { + "477f79556e5777b38feb85013b4f04dbb8230e4b0b0bcc45f669d7b8d0b91db4": { "describe": { "columns": [ { - "name": "fulfilled_at!: DateTime", + "name": "oauth2_access_token_id", "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "access_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, "type_info": "Timestamptz" + }, + { + "name": "expires_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "revoked_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_session_id", + "ordinal": 5, + "type_info": "Uuid" } ], "nullable": [ - true + false, + false, + false, + false, + true, + false ], "parameters": { "Left": [ - "Uuid", - "Uuid" + "Text" ] } }, - "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.oauth2_session_id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.oauth2_authorization_grant_id = $1\n AND os.oauth2_session_id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime\"\n " + "query": "\n SELECT oauth2_access_token_id\n , access_token\n , created_at\n , expires_at\n , revoked_at\n , oauth2_session_id\n\n FROM oauth2_access_tokens\n\n WHERE access_token = $1\n " }, "478f0ad710da8bfd803c6cddd982bc504d1b6bd0f5283de53c8c7b1b4b7dafd4": { "describe": { @@ -497,6 +536,134 @@ }, "query": "\n SELECT compat_sso_login_id\n , login_token\n , redirect_uri\n , created_at\n , fulfilled_at\n , exchanged_at\n , compat_session_id\n\n FROM compat_sso_logins\n WHERE login_token = $1\n " }, + "496813daf6f8486353e7f509a64362626daebb0121c3c9420b96e2d8157f1e07": { + "describe": { + "columns": [ + { + "name": "oauth2_authorization_grant_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "created_at", + "ordinal": 1, + "type_info": "Timestamptz" + }, + { + "name": "cancelled_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "fulfilled_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "exchanged_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "scope", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "state", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "redirect_uri", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "response_mode", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "nonce", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "max_age", + "ordinal": 10, + "type_info": "Int4" + }, + { + "name": "oauth2_client_id", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "authorization_code", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "response_type_code", + "ordinal": 13, + "type_info": "Bool" + }, + { + "name": "response_type_id_token", + "ordinal": 14, + "type_info": "Bool" + }, + { + "name": "code_challenge", + "ordinal": 15, + "type_info": "Text" + }, + { + "name": "code_challenge_method", + "ordinal": 16, + "type_info": "Text" + }, + { + "name": "requires_consent", + "ordinal": 17, + "type_info": "Bool" + }, + { + "name": "oauth2_session_id", + "ordinal": 18, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + false, + true, + false, + false, + true, + true, + false, + true, + false, + false, + true, + true, + false, + true + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , max_age\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , requires_consent\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n " + }, "4d79ce892e4595edb8b801e94fb0cbef28facdfd2e45d1c72c57f47418fbe24b": { "describe": { "columns": [], @@ -511,27 +678,6 @@ }, "query": "\n UPDATE compat_sso_logins\n SET\n compat_session_id = $2,\n fulfilled_at = $3\n WHERE\n compat_sso_login_id = $1\n " }, - "51158bfcaa1a8d8e051bffe7c5ba0369bf53fb162f7622626054e89e68fc07bd": { - "describe": { - "columns": [ - { - "name": "scope_token", - "ordinal": 0, - "type_info": "Text" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Uuid", - "Uuid" - ] - } - }, - "query": "\n SELECT scope_token\n FROM oauth2_consents\n WHERE user_id = $1 AND oauth2_client_id = $2\n " - }, "53ad718642644b47a2d49f768d81bd993088526923769a9147281686c2d47591": { "describe": { "columns": [ @@ -592,18 +738,6 @@ }, "query": "\n INSERT INTO oauth2_sessions\n ( oauth2_session_id\n , user_session_id\n , oauth2_client_id\n , scope\n , created_at\n )\n VALUES ($1, $2, $3, $4, $5)\n " }, - "5b5d5c82da37c6f2d8affacfb02119965c04d1f2a9cc53dbf5bd4c12584969a0": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Timestamptz" - ] - } - }, - "query": "\n DELETE FROM oauth2_access_tokens\n WHERE expires_at < $1\n " - }, "5f6b7e38ef9bc3b39deabba277d0255fb8cfb2adaa65f47b78a8fac11d8c91c3": { "describe": { "columns": [], @@ -619,22 +753,6 @@ }, "query": "\n INSERT INTO upstream_oauth_links (\n upstream_oauth_link_id,\n upstream_oauth_provider_id,\n user_id,\n subject,\n created_at\n ) VALUES ($1, $2, NULL, $3, $4)\n " }, - "62d05e8e4317bdb180298737d422e64d161c5ad3813913a6f7d67a53569ea76a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "UuidArray", - "Uuid", - "Uuid", - "TextArray", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO oauth2_consents\n (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at)\n SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token)\n ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5\n " - }, "64e6ea47c2e877c1ebe4338d64d9ad8a6c1c777d1daea024b8ca2e7f0dd75b0f": { "describe": { "columns": [], @@ -739,18 +857,133 @@ }, "query": "\n UPDATE upstream_oauth_authorization_sessions\n SET consumed_at = $1\n WHERE upstream_oauth_authorization_session_id = $2\n " }, - "6bf0da5ba3dd07b499193a2e0ddeea6e712f9df8f7f28874ff56a952a9f10e54": { + "6a3b543ec53ce242866d1e84de26728e6dd275cae745f9c646e3824d859c5384": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "oauth2_authorization_grant_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "created_at", + "ordinal": 1, + "type_info": "Timestamptz" + }, + { + "name": "cancelled_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "fulfilled_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "exchanged_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "scope", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "state", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "redirect_uri", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "response_mode", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "nonce", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "max_age", + "ordinal": 10, + "type_info": "Int4" + }, + { + "name": "oauth2_client_id", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "authorization_code", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "response_type_code", + "ordinal": 13, + "type_info": "Bool" + }, + { + "name": "response_type_id_token", + "ordinal": 14, + "type_info": "Bool" + }, + { + "name": "code_challenge", + "ordinal": 15, + "type_info": "Text" + }, + { + "name": "code_challenge_method", + "ordinal": 16, + "type_info": "Text" + }, + { + "name": "requires_consent", + "ordinal": 17, + "type_info": "Bool" + }, + { + "name": "oauth2_session_id", + "ordinal": 18, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + true, + true, + true, + false, + true, + false, + false, + true, + true, + false, + true, + false, + false, + true, + true, + false, + true + ], "parameters": { "Left": [ - "Uuid", - "Timestamptz" + "Text" ] } }, - "query": "\n UPDATE oauth2_access_tokens\n SET revoked_at = $2\n WHERE oauth2_access_token_id = $1\n " + "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , max_age\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , requires_consent\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n " }, "6e21e7d816f806da9bb5176931bdb550dee05c44c9d93f53df95fe3b4a840347": { "describe": { @@ -1091,18 +1324,26 @@ }, "query": "\n SELECT oauth2_client_id\n , encrypted_client_secret\n , ARRAY(\n SELECT redirect_uri\n FROM oauth2_client_redirect_uris r\n WHERE r.oauth2_client_id = c.oauth2_client_id\n ) AS \"redirect_uris!\"\n , grant_type_authorization_code\n , grant_type_refresh_token\n , client_name\n , logo_uri\n , client_uri\n , policy_uri\n , tos_uri\n , jwks_uri\n , jwks\n , id_token_signed_response_alg\n , userinfo_signed_response_alg\n , token_endpoint_auth_method\n , token_endpoint_auth_signing_alg\n , initiate_login_uri\n FROM oauth2_clients c\n\n WHERE oauth2_client_id = ANY($1::uuid[])\n " }, - "874e677f82c221c5bb621c12f293bcef4e70c68c87ec003fcd475bcb994b5a4c": { + "8b7297c263336d70c2b647212b16f7ae39bc5cb1572e3a2dcfcd67f196a1fa39": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "scope_token", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], "parameters": { "Left": [ "Uuid", - "Timestamptz" + "Uuid" ] } }, - "query": "\n UPDATE oauth2_refresh_tokens\n SET consumed_at = $2\n WHERE oauth2_refresh_token_id = $1\n " + "query": "\n SELECT scope_token\n FROM oauth2_consents\n WHERE user_id = $1 AND oauth2_client_id = $2\n " }, "8f7a9fb1f24c24f8dbc3c193df2a742c9ac730ab958587b67297de2d4b843863": { "describe": { @@ -1240,6 +1481,22 @@ }, "query": "\n SELECT EXISTS(\n SELECT 1 FROM users WHERE username = $1\n ) AS \"exists!\"\n " }, + "9a6c197ff4ad80217262d48f8792ce7e16bc5df0677c7cd4ecb4fdbc5ee86395": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "UuidArray", + "Uuid", + "Uuid", + "TextArray", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO oauth2_consents\n (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at)\n SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token)\n ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5\n " + }, "9f7bdc034c618e47e49c467d0d7f5b8c297d055abe248cc876dbc12c5a7dc920": { "describe": { "columns": [], @@ -1256,6 +1513,22 @@ }, "query": "\n INSERT INTO compat_refresh_tokens\n (compat_refresh_token_id, compat_session_id,\n compat_access_token_id, refresh_token, created_at)\n VALUES ($1, $2, $3, $4, $5)\n " }, + "a2f7433f06fb4f6a7ad5ac6c1db18705276bce41e9b19d5d7e910ad4b767fb5e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id,\n refresh_token, created_at)\n VALUES\n ($1, $2, $3, $4, $5)\n " + }, "a300fe99c95679c5664646a6a525c0491829e97db45f3234483872ed38436322": { "describe": { "columns": [ @@ -1300,17 +1573,55 @@ }, "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n , confirmed_at\n FROM user_emails\n\n WHERE user_id = $1\n\n ORDER BY email ASC\n " }, - "a5a7dad633396e087239d5629092e4a305908ffce9c2610db07372f719070546": { + "a6fa7811d0a7c62c7cccff96dc82db5b25462fa7669fde1941ccab4712585b20": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "oauth2_refresh_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "refresh_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "consumed_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_id", + "ordinal": 4, + "type_info": "Uuid" + }, + { + "name": "oauth2_session_id", + "ordinal": 5, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + false, + true, + true, + false + ], "parameters": { "Left": [ "Uuid" ] } }, - "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.oauth2_authorization_grant_id = $1\n " + "query": "\n SELECT oauth2_refresh_token_id\n , refresh_token\n , created_at\n , consumed_at\n , oauth2_access_token_id\n , oauth2_session_id\n FROM oauth2_refresh_tokens\n\n WHERE oauth2_refresh_token_id = $1\n " }, "a7f780528882a2ae66c45435215763eed0582264861436eab3f862e3eb12cab1": { "describe": { @@ -1328,134 +1639,6 @@ }, "query": "\n INSERT INTO compat_access_tokens\n (compat_access_token_id, compat_session_id, access_token, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n " }, - "aa2fd69c595f94d8598715766a79671dba8f87b9d7af6ac30e3fa1fbc8cce28a": { - "describe": { - "columns": [ - { - "name": "oauth2_authorization_grant_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "oauth2_authorization_grant_created_at", - "ordinal": 1, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_cancelled_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_fulfilled_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_exchanged_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_scope", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_state", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_redirect_uri", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_response_mode", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_nonce", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_max_age", - "ordinal": 10, - "type_info": "Int4" - }, - { - "name": "oauth2_client_id", - "ordinal": 11, - "type_info": "Uuid" - }, - { - "name": "oauth2_authorization_grant_code", - "ordinal": 12, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_response_type_code", - "ordinal": 13, - "type_info": "Bool" - }, - { - "name": "oauth2_authorization_grant_response_type_id_token", - "ordinal": 14, - "type_info": "Bool" - }, - { - "name": "oauth2_authorization_grant_code_challenge", - "ordinal": 15, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_code_challenge_method", - "ordinal": 16, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_requires_consent", - "ordinal": 17, - "type_info": "Bool" - }, - { - "name": "oauth2_session_id?", - "ordinal": 18, - "type_info": "Uuid" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - false, - true, - false, - false, - true, - true, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT oauth2_authorization_grant_id\n , created_at AS oauth2_authorization_grant_created_at\n , cancelled_at AS oauth2_authorization_grant_cancelled_at\n , fulfilled_at AS oauth2_authorization_grant_fulfilled_at\n , exchanged_at AS oauth2_authorization_grant_exchanged_at\n , scope AS oauth2_authorization_grant_scope\n , state AS oauth2_authorization_grant_state\n , redirect_uri AS oauth2_authorization_grant_redirect_uri\n , response_mode AS oauth2_authorization_grant_response_mode\n , nonce AS oauth2_authorization_grant_nonce\n , max_age AS oauth2_authorization_grant_max_age\n , oauth2_client_id AS oauth2_client_id\n , authorization_code AS oauth2_authorization_grant_code\n , response_type_code AS oauth2_authorization_grant_response_type_code\n , response_type_id_token AS oauth2_authorization_grant_response_type_id_token\n , code_challenge AS oauth2_authorization_grant_code_challenge\n , code_challenge_method AS oauth2_authorization_grant_code_challenge_method\n , requires_consent AS oauth2_authorization_grant_requires_consent\n , oauth2_session_id AS \"oauth2_session_id?\"\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n " - }, "ab34912b42a48a8b5c8d63e271b99b7d0b690a2471873c6654b1b6cf2079b95c": { "describe": { "columns": [], @@ -1469,6 +1652,22 @@ }, "query": "\n UPDATE compat_sessions cs\n SET finished_at = $2\n WHERE compat_session_id = $1\n " }, + "afa86e79e3de2a83265cb0db8549d378a2f11b2a27bbd86d60558318c87eb698": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_access_token_id, oauth2_session_id, access_token, created_at, expires_at)\n VALUES\n ($1, $2, $3, $4, $5)\n " + }, "aff08a8caabeb62f4929e6e901e7ca7c55e284c18c5c1d1e78821dd9bc961412": { "describe": { "columns": [ @@ -1514,184 +1713,6 @@ }, "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n , confirmed_at\n FROM user_emails\n\n WHERE user_id = $1 AND email = $2\n " }, - "b12f7ba71ad522261f54ffbb6739a7a06214b4f01e3ed6f7fdaa2033d249f3fb": { - "describe": { - "columns": [ - { - "name": "oauth2_authorization_grant_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "oauth2_authorization_grant_created_at", - "ordinal": 1, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_cancelled_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_fulfilled_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_exchanged_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_authorization_grant_scope", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_state", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_redirect_uri", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_response_mode", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_nonce", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_max_age", - "ordinal": 10, - "type_info": "Int4" - }, - { - "name": "oauth2_client_id", - "ordinal": 11, - "type_info": "Uuid" - }, - { - "name": "oauth2_authorization_grant_code", - "ordinal": 12, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_response_type_code", - "ordinal": 13, - "type_info": "Bool" - }, - { - "name": "oauth2_authorization_grant_response_type_id_token", - "ordinal": 14, - "type_info": "Bool" - }, - { - "name": "oauth2_authorization_grant_code_challenge", - "ordinal": 15, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_code_challenge_method", - "ordinal": 16, - "type_info": "Text" - }, - { - "name": "oauth2_authorization_grant_requires_consent", - "ordinal": 17, - "type_info": "Bool" - }, - { - "name": "oauth2_session_id?", - "ordinal": 18, - "type_info": "Uuid" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - false, - true, - false, - false, - true, - true, - false, - true - ], - "parameters": { - "Left": [ - "Uuid" - ] - } - }, - "query": "\n SELECT oauth2_authorization_grant_id\n , created_at AS oauth2_authorization_grant_created_at\n , cancelled_at AS oauth2_authorization_grant_cancelled_at\n , fulfilled_at AS oauth2_authorization_grant_fulfilled_at\n , exchanged_at AS oauth2_authorization_grant_exchanged_at\n , scope AS oauth2_authorization_grant_scope\n , state AS oauth2_authorization_grant_state\n , redirect_uri AS oauth2_authorization_grant_redirect_uri\n , response_mode AS oauth2_authorization_grant_response_mode\n , nonce AS oauth2_authorization_grant_nonce\n , max_age AS oauth2_authorization_grant_max_age\n , oauth2_client_id AS oauth2_client_id\n , authorization_code AS oauth2_authorization_grant_code\n , response_type_code AS oauth2_authorization_grant_response_type_code\n , response_type_id_token AS oauth2_authorization_grant_response_type_id_token\n , code_challenge AS oauth2_authorization_grant_code_challenge\n , code_challenge_method AS oauth2_authorization_grant_code_challenge_method\n , requires_consent AS oauth2_authorization_grant_requires_consent\n , oauth2_session_id AS \"oauth2_session_id?\"\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n " - }, - "b20e846843cf88810fbc0f4b0fa3159117f035841758d682d90c614c374f6059": { - "describe": { - "columns": [ - { - "name": "oauth2_access_token_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "access_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "expires_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "revoked_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_session_id", - "ordinal": 5, - "type_info": "Uuid" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - false - ], - "parameters": { - "Left": [ - "Uuid" - ] - } - }, - "query": "\n SELECT oauth2_access_token_id\n , access_token\n , created_at\n , expires_at\n , revoked_at\n , oauth2_session_id\n\n FROM oauth2_access_tokens\n\n WHERE oauth2_access_token_id = $1\n " - }, "b26ae7dd28f8a756b55a76e80cdedd7be9ba26435ea4a914421483f8ed832537": { "describe": { "columns": [], @@ -1722,6 +1743,19 @@ }, "query": "\n INSERT INTO user_email_confirmation_codes\n (user_email_confirmation_code_id, user_email_id, code, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n " }, + "b6a6f5386dc89e4bc2ce56d578a29341848fce336d339b6bbf425956f5ed5032": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE oauth2_refresh_tokens\n SET consumed_at = $2\n WHERE oauth2_refresh_token_id = $1\n " + }, "b700dc3f7d0f86f4904725d8357e34b7e457f857ed37c467c314142877fd5367": { "describe": { "columns": [], @@ -1793,21 +1827,7 @@ }, "query": "\n INSERT INTO oauth2_clients\n ( oauth2_client_id\n , encrypted_client_secret\n , grant_type_authorization_code\n , grant_type_refresh_token\n , token_endpoint_auth_method\n , jwks\n , jwks_uri\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (oauth2_client_id)\n DO\n UPDATE SET encrypted_client_secret = EXCLUDED.encrypted_client_secret\n , grant_type_authorization_code = EXCLUDED.grant_type_authorization_code\n , grant_type_refresh_token = EXCLUDED.grant_type_refresh_token\n , token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method\n , jwks = EXCLUDED.jwks\n , jwks_uri = EXCLUDED.jwks_uri\n " }, - "c1d90a7f2287ec779c81a521fab19e5ede3fa95484033e0312c30d9b6ecc03f0": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO user_sessions (user_session_id, user_id, created_at)\n VALUES ($1, $2, $3)\n " - }, - "c88376abdba124ff0487a9a69d2345c7d69d7394f355111ec369cfa6d45fb40f": { + "c0ed9d70e496433d8686a499055d8a8376459109b6154a2c0c13b28462afa523": { "describe": { "columns": [], "nullable": [], @@ -1831,7 +1851,34 @@ ] } }, - "query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n max_age,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n requires_consent,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\n " + "query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n max_age,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n requires_consent,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\n " + }, + "c1d90a7f2287ec779c81a521fab19e5ede3fa95484033e0312c30d9b6ecc03f0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_sessions (user_session_id, user_id, created_at)\n VALUES ($1, $2, $3)\n " + }, + "c5e7dbb22488aca427b85b3415bd1f1a1766ff865f2e08a5daa095d2a1ccbd56": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE oauth2_authorization_grants\n SET exchanged_at = $2\n WHERE oauth2_authorization_grant_id = $1\n " }, "d0b403e9c843ef19fa5ad60bec32ebf14a1ba0d01681c3836366d3f55e7851f4": { "describe": { @@ -1866,121 +1913,17 @@ }, "query": "\n SELECT COUNT(*)\n FROM user_emails\n WHERE user_id = $1\n " }, - "d1f1aac41bb2f0d194f9b3d846663c267865d0d22dd5fa8a668daf29dca88d36": { - "describe": { - "columns": [ - { - "name": "oauth2_refresh_token_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "refresh_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "consumed_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_access_token_id", - "ordinal": 4, - "type_info": "Uuid" - }, - { - "name": "oauth2_session_id", - "ordinal": 5, - "type_info": "Uuid" - } - ], - "nullable": [ - false, - false, - false, - true, - true, - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT oauth2_refresh_token_id\n , refresh_token\n , created_at\n , consumed_at\n , oauth2_access_token_id\n , oauth2_session_id\n FROM oauth2_refresh_tokens\n\n WHERE refresh_token = $1\n " - }, - "d2b1af24f88b2f05eb219f7cbdcfa9680bafe9f77fa1772097875b3718bd1aff": { - "describe": { - "columns": [ - { - "name": "oauth2_access_token_id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "access_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "expires_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "revoked_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "oauth2_session_id", - "ordinal": 5, - "type_info": "Uuid" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT oauth2_access_token_id\n , access_token\n , created_at\n , expires_at\n , revoked_at\n , oauth2_session_id\n\n FROM oauth2_access_tokens\n\n WHERE access_token = $1\n " - }, - "d8677b3b6ee594c230fad98c1aa1c6e3d983375bf5b701c7b52468e7f906abf9": { + "d83421d4a16f4ad084dd0db5abb56d3688851c36a48a50aa6104e8291e73630d": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Uuid", - "Uuid", - "Uuid", - "Text", - "Timestamptz" + "Uuid" ] } }, - "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id,\n refresh_token, created_at)\n VALUES\n ($1, $2, $3, $4, $5)\n " + "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.oauth2_authorization_grant_id = $1\n " }, "db90cbc406a399f5447bd2c1d8018464f83b927dec620353516c0285b76fcf24": { "describe": { @@ -2111,6 +2054,56 @@ }, "query": "\n UPDATE user_sessions\n SET finished_at = $1\n WHERE user_session_id = $2\n " }, + "dd16942318bf38d9a245b2c86fedd3cbd6b65e7a13465552d79cd3c022122fd4": { + "describe": { + "columns": [ + { + "name": "oauth2_access_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "access_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "expires_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "revoked_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_session_id", + "ordinal": 5, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + false, + false, + true, + false + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT oauth2_access_token_id\n , access_token\n , created_at\n , expires_at\n , revoked_at\n , oauth2_session_id\n\n FROM oauth2_access_tokens\n\n WHERE oauth2_access_token_id = $1\n " + }, "ddb22dd9ae9367af65a607e1fdc48b3d9581d67deea0c168f24e02090082bb82": { "describe": { "columns": [ @@ -2262,6 +2255,56 @@ }, "query": "\n SELECT\n upstream_oauth_link_id,\n upstream_oauth_provider_id,\n user_id,\n subject,\n created_at\n FROM upstream_oauth_links\n WHERE upstream_oauth_provider_id = $1\n AND subject = $2\n " }, + "e709869c062ac50248b1f9f8f808cc2f5e7bef58a6c2e42a7bb0c1cb8f508671": { + "describe": { + "columns": [ + { + "name": "oauth2_refresh_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "refresh_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "consumed_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_id", + "ordinal": 4, + "type_info": "Uuid" + }, + { + "name": "oauth2_session_id", + "ordinal": 5, + "type_info": "Uuid" + } + ], + "nullable": [ + false, + false, + false, + true, + true, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT oauth2_refresh_token_id\n , refresh_token\n , created_at\n , consumed_at\n , oauth2_access_token_id\n , oauth2_session_id\n FROM oauth2_refresh_tokens\n\n WHERE refresh_token = $1\n " + }, "f0ace1af3775192a555c4ebb59b81183f359771f9f77e5fad759d38d872541d1": { "describe": { "columns": [ diff --git a/crates/storage/src/oauth2/access_token.rs b/crates/storage/src/oauth2/access_token.rs index 8389dff4..db10ed72 100644 --- a/crates/storage/src/oauth2/access_token.rs +++ b/crates/storage/src/oauth2/access_token.rs @@ -1,4 +1,4 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. +// Copyright 2021-2023 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,67 +12,61 @@ // See the License for the specific language governing permissions and // limitations under the License. +use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; use mas_data_model::{AccessToken, AccessTokenState, Session}; -use rand::Rng; -use sqlx::{PgConnection, PgExecutor}; +use rand::RngCore; +use sqlx::PgConnection; use ulid::Ulid; use uuid::Uuid; -use crate::{Clock, DatabaseError, LookupResultExt}; +use crate::{tracing::ExecuteExt, Clock, DatabaseError, LookupResultExt}; -#[tracing::instrument( - skip_all, - fields( - %session.id, - user_session.id = %session.user_session_id, - client.id = %session.client_id, - access_token.id, - ), - err, -)] -pub async fn add_access_token( - executor: impl PgExecutor<'_>, - mut rng: impl Rng + Send, - clock: &Clock, - session: &Session, - access_token: String, - expires_after: Duration, -) -> Result { - let created_at = clock.now(); - let expires_at = created_at + expires_after; - let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); +#[async_trait] +pub trait OAuth2AccessTokenRepository: Send + Sync { + type Error; - tracing::Span::current().record("access_token.id", tracing::field::display(id)); + /// Lookup an access token by its ID + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; - sqlx::query!( - r#" - INSERT INTO oauth2_access_tokens - (oauth2_access_token_id, oauth2_session_id, access_token, created_at, expires_at) - VALUES - ($1, $2, $3, $4, $5) - "#, - Uuid::from(id), - Uuid::from(session.id), - &access_token, - created_at, - expires_at, - ) - .execute(executor) - .await?; + /// Find an access token by its token + async fn find_by_token( + &mut self, + access_token: &str, + ) -> Result, Self::Error>; - Ok(AccessToken { - id, - state: AccessTokenState::default(), - access_token, - session_id: session.id, - created_at, - expires_at, - }) + /// Add a new access token to the database + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + session: &Session, + access_token: String, + expires_after: Duration, + ) -> Result; + + /// Revoke an access token + async fn revoke( + &mut self, + clock: &Clock, + access_token: AccessToken, + ) -> Result; + + /// Cleanup expired access tokens + async fn cleanup_expired(&mut self, clock: &Clock) -> Result; } -#[derive(Debug)] -pub struct OAuth2AccessTokenLookup { +pub struct PgOAuth2AccessTokenRepository<'c> { + conn: &'c mut PgConnection, +} + +impl<'c> PgOAuth2AccessTokenRepository<'c> { + pub fn new(conn: &'c mut PgConnection) -> Self { + Self { conn } + } +} + +struct OAuth2AccessTokenLookup { oauth2_access_token_id: Uuid, oauth2_session_id: Uuid, access_token: String, @@ -99,118 +93,164 @@ impl From for AccessToken { } } -#[tracing::instrument(skip_all, err)] -pub async fn find_access_token( - conn: &mut PgConnection, - token: &str, -) -> Result, DatabaseError> { - let res = sqlx::query_as!( - OAuth2AccessTokenLookup, - r#" - SELECT oauth2_access_token_id - , access_token - , created_at - , expires_at - , revoked_at - , oauth2_session_id +#[async_trait] +impl<'c> OAuth2AccessTokenRepository for PgOAuth2AccessTokenRepository<'c> { + type Error = DatabaseError; - FROM oauth2_access_tokens + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error> { + let res = sqlx::query_as!( + OAuth2AccessTokenLookup, + r#" + SELECT oauth2_access_token_id + , access_token + , created_at + , expires_at + , revoked_at + , oauth2_session_id - WHERE access_token = $1 - "#, - token, - ) - .fetch_one(&mut *conn) - .await - .to_option()?; + FROM oauth2_access_tokens - let Some(res) = res else { return Ok(None) }; + WHERE oauth2_access_token_id = $1 + "#, + Uuid::from(id), + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; - Ok(Some(res.into())) -} - -#[tracing::instrument( - skip_all, - fields(access_token.id = %access_token_id), - err, -)] -pub async fn lookup_access_token( - conn: &mut PgConnection, - access_token_id: Ulid, -) -> Result, DatabaseError> { - let res = sqlx::query_as!( - OAuth2AccessTokenLookup, - r#" - SELECT oauth2_access_token_id - , access_token - , created_at - , expires_at - , revoked_at - , oauth2_session_id - - FROM oauth2_access_tokens - - WHERE oauth2_access_token_id = $1 - "#, - Uuid::from(access_token_id), - ) - .fetch_one(&mut *conn) - .await - .to_option()?; - - let Some(res) = res else { return Ok(None) }; - - Ok(Some(res.into())) -} - -#[tracing::instrument( - skip_all, - fields( - %access_token.id, - session.id = %access_token.session_id, - ), - err, -)] -pub async fn revoke_access_token( - executor: impl PgExecutor<'_>, - clock: &Clock, - access_token: AccessToken, -) -> Result { - let revoked_at = clock.now(); - let res = sqlx::query!( - r#" - UPDATE oauth2_access_tokens - SET revoked_at = $2 - WHERE oauth2_access_token_id = $1 - "#, - Uuid::from(access_token.id), - revoked_at, - ) - .execute(executor) - .await?; - - DatabaseError::ensure_affected_rows(&res, 1)?; - - access_token - .revoke(revoked_at) - .map_err(DatabaseError::to_invalid_operation) -} - -pub async fn cleanup_expired( - executor: impl PgExecutor<'_>, - clock: &Clock, -) -> Result { - // Cleanup token which expired more than 15 minutes ago - let threshold = clock.now() - Duration::minutes(15); - let res = sqlx::query!( - r#" - DELETE FROM oauth2_access_tokens - WHERE expires_at < $1 - "#, - threshold, - ) - .execute(executor) - .await?; - - Ok(res.rows_affected()) + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + name = "db.oauth2_access_token.find_by_token", + skip_all, + fields( + db.statement, + ), + err, + )] + async fn find_by_token( + &mut self, + access_token: &str, + ) -> Result, Self::Error> { + let res = sqlx::query_as!( + OAuth2AccessTokenLookup, + r#" + SELECT oauth2_access_token_id + , access_token + , created_at + , expires_at + , revoked_at + , oauth2_session_id + + FROM oauth2_access_tokens + + WHERE access_token = $1 + "#, + access_token, + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + name = "db.oauth2_access_token.add", + skip_all, + fields( + db.statement, + %session.id, + user_session.id = %session.user_session_id, + client.id = %session.client_id, + access_token.id, + ), + err, + )] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + session: &Session, + access_token: String, + expires_after: Duration, + ) -> Result { + let created_at = clock.now(); + let expires_at = created_at + expires_after; + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + + tracing::Span::current().record("access_token.id", tracing::field::display(id)); + + sqlx::query!( + r#" + INSERT INTO oauth2_access_tokens + (oauth2_access_token_id, oauth2_session_id, access_token, created_at, expires_at) + VALUES + ($1, $2, $3, $4, $5) + "#, + Uuid::from(id), + Uuid::from(session.id), + &access_token, + created_at, + expires_at, + ) + .traced() + .execute(&mut *self.conn) + .await?; + + Ok(AccessToken { + id, + state: AccessTokenState::default(), + access_token, + session_id: session.id, + created_at, + expires_at, + }) + } + + async fn revoke( + &mut self, + clock: &Clock, + access_token: AccessToken, + ) -> Result { + let revoked_at = clock.now(); + let res = sqlx::query!( + r#" + UPDATE oauth2_access_tokens + SET revoked_at = $2 + WHERE oauth2_access_token_id = $1 + "#, + Uuid::from(access_token.id), + revoked_at, + ) + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + access_token + .revoke(revoked_at) + .map_err(DatabaseError::to_invalid_operation) + } + + async fn cleanup_expired(&mut self, clock: &Clock) -> Result { + // Cleanup token which expired more than 15 minutes ago + let threshold = clock.now() - Duration::minutes(15); + let res = sqlx::query!( + r#" + DELETE FROM oauth2_access_tokens + WHERE expires_at < $1 + "#, + threshold, + ) + .execute(&mut *self.conn) + .await?; + + Ok(res.rows_affected().try_into().unwrap_or(usize::MAX)) + } } diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index c5d96976..91df9313 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -14,138 +14,97 @@ use std::num::NonZeroU32; +use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{ AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client, Pkce, Session, }; use mas_iana::oauth::PkceCodeChallengeMethod; use oauth2_types::{requests::ResponseMode, scope::Scope}; -use rand::Rng; -use sqlx::{PgConnection, PgExecutor}; +use rand::RngCore; +use sqlx::PgConnection; use ulid::Ulid; use url::Url; use uuid::Uuid; -use crate::{Clock, DatabaseError, DatabaseInconsistencyError, LookupResultExt}; +use crate::{ + tracing::ExecuteExt, Clock, DatabaseError, DatabaseInconsistencyError, LookupResultExt, +}; -#[tracing::instrument( - skip_all, - fields( - %client.id, - grant.id, - ), - err, -)] -#[allow(clippy::too_many_arguments)] -pub async fn new_authorization_grant( - executor: impl PgExecutor<'_>, - mut rng: impl Rng + Send, - clock: &Clock, - client: Client, - redirect_uri: Url, - scope: Scope, - code: Option, - state: Option, - nonce: Option, - max_age: Option, - _acr_values: Option, - response_mode: ResponseMode, - response_type_id_token: bool, - requires_consent: bool, -) -> Result { - let code_challenge = code - .as_ref() - .and_then(|c| c.pkce.as_ref()) - .map(|p| &p.challenge); - let code_challenge_method = code - .as_ref() - .and_then(|c| c.pkce.as_ref()) - .map(|p| p.challenge_method.to_string()); - // TODO: this conversion is a bit ugly - let max_age_i32 = max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX)); - let code_str = code.as_ref().map(|c| &c.code); +#[async_trait] +pub trait OAuth2AuthorizationGrantRepository { + type Error; - let created_at = clock.now(); - let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); - tracing::Span::current().record("grant.id", tracing::field::display(id)); + #[allow(clippy::too_many_arguments)] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + client: &Client, + redirect_uri: Url, + scope: Scope, + code: Option, + state: Option, + nonce: Option, + max_age: Option, + response_mode: ResponseMode, + response_type_id_token: bool, + requires_consent: bool, + ) -> Result; - sqlx::query!( - r#" - INSERT INTO oauth2_authorization_grants ( - oauth2_authorization_grant_id, - oauth2_client_id, - redirect_uri, - scope, - state, - nonce, - max_age, - response_mode, - code_challenge, - code_challenge_method, - response_type_code, - response_type_id_token, - authorization_code, - requires_consent, - created_at - ) - VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) - "#, - Uuid::from(id), - Uuid::from(client.id), - redirect_uri.to_string(), - scope.to_string(), - state, - nonce, - max_age_i32, - response_mode.to_string(), - code_challenge, - code_challenge_method, - code.is_some(), - response_type_id_token, - code_str, - requires_consent, - created_at, - ) - .execute(executor) - .await?; + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; - Ok(AuthorizationGrant { - id, - stage: AuthorizationGrantStage::Pending, - code, - redirect_uri, - client_id: client.id, - scope, - state, - nonce, - max_age, - response_mode, - created_at, - response_type_id_token, - requires_consent, - }) + async fn find_by_code(&mut self, code: &str) + -> Result, Self::Error>; + + async fn fulfill( + &mut self, + clock: &Clock, + session: &Session, + authorization_grant: AuthorizationGrant, + ) -> Result; + + async fn exchange( + &mut self, + clock: &Clock, + authorization_grant: AuthorizationGrant, + ) -> Result; + + async fn give_consent( + &mut self, + authorization_grant: AuthorizationGrant, + ) -> Result; +} + +pub struct PgOAuth2AuthorizationGrantRepository<'c> { + conn: &'c mut PgConnection, +} + +impl<'c> PgOAuth2AuthorizationGrantRepository<'c> { + pub fn new(conn: &'c mut PgConnection) -> Self { + Self { conn } + } } #[allow(clippy::struct_excessive_bools)] struct GrantLookup { oauth2_authorization_grant_id: Uuid, - oauth2_authorization_grant_created_at: DateTime, - oauth2_authorization_grant_cancelled_at: Option>, - oauth2_authorization_grant_fulfilled_at: Option>, - oauth2_authorization_grant_exchanged_at: Option>, - oauth2_authorization_grant_scope: String, - oauth2_authorization_grant_state: Option, - oauth2_authorization_grant_nonce: Option, - oauth2_authorization_grant_redirect_uri: String, - oauth2_authorization_grant_response_mode: String, - oauth2_authorization_grant_max_age: Option, - oauth2_authorization_grant_response_type_code: bool, - oauth2_authorization_grant_response_type_id_token: bool, - oauth2_authorization_grant_code: Option, - oauth2_authorization_grant_code_challenge: Option, - oauth2_authorization_grant_code_challenge_method: Option, - oauth2_authorization_grant_requires_consent: bool, + created_at: DateTime, + cancelled_at: Option>, + fulfilled_at: Option>, + exchanged_at: Option>, + scope: String, + state: Option, + nonce: Option, + redirect_uri: String, + response_mode: String, + max_age: Option, + response_type_code: bool, + response_type_id_token: bool, + authorization_code: Option, + code_challenge: Option, + code_challenge_method: Option, + requires_consent: bool, oauth2_client_id: Uuid, oauth2_session_id: Option, } @@ -156,20 +115,17 @@ impl TryFrom for AuthorizationGrant { #[allow(clippy::too_many_lines)] fn try_from(value: GrantLookup) -> Result { let id = value.oauth2_authorization_grant_id.into(); - let scope: Scope = value - .oauth2_authorization_grant_scope - .parse() - .map_err(|e| { - DatabaseInconsistencyError::on("oauth2_authorization_grants") - .column("scope") - .row(id) - .source(e) - })?; + let scope: Scope = value.scope.parse().map_err(|e| { + DatabaseInconsistencyError::on("oauth2_authorization_grants") + .column("scope") + .row(id) + .source(e) + })?; let stage = match ( - value.oauth2_authorization_grant_fulfilled_at, - value.oauth2_authorization_grant_exchanged_at, - value.oauth2_authorization_grant_cancelled_at, + value.fulfilled_at, + value.exchanged_at, + value.cancelled_at, value.oauth2_session_id, ) { (None, None, None, None) => AuthorizationGrantStage::Pending, @@ -198,10 +154,7 @@ impl TryFrom for AuthorizationGrant { } }; - let pkce = match ( - value.oauth2_authorization_grant_code_challenge, - value.oauth2_authorization_grant_code_challenge_method, - ) { + let pkce = match (value.code_challenge, value.code_challenge_method) { (Some(challenge), Some(challenge_method)) if challenge_method == "plain" => { Some(Pkce { challenge_method: PkceCodeChallengeMethod::Plain, @@ -222,44 +175,35 @@ impl TryFrom for AuthorizationGrant { } }; - let code: Option = match ( - value.oauth2_authorization_grant_response_type_code, - value.oauth2_authorization_grant_code, - pkce, - ) { - (false, None, None) => None, - (true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }), - _ => { - return Err( - DatabaseInconsistencyError::on("oauth2_authorization_grants") - .column("authorization_code") - .row(id), - ); - } - }; + let code: Option = + match (value.response_type_code, value.authorization_code, pkce) { + (false, None, None) => None, + (true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }), + _ => { + return Err( + DatabaseInconsistencyError::on("oauth2_authorization_grants") + .column("authorization_code") + .row(id), + ); + } + }; - let redirect_uri = value - .oauth2_authorization_grant_redirect_uri - .parse() - .map_err(|e| { - DatabaseInconsistencyError::on("oauth2_authorization_grants") - .column("redirect_uri") - .row(id) - .source(e) - })?; + let redirect_uri = value.redirect_uri.parse().map_err(|e| { + DatabaseInconsistencyError::on("oauth2_authorization_grants") + .column("redirect_uri") + .row(id) + .source(e) + })?; - let response_mode = value - .oauth2_authorization_grant_response_mode - .parse() - .map_err(|e| { - DatabaseInconsistencyError::on("oauth2_authorization_grants") - .column("response_mode") - .row(id) - .source(e) - })?; + let response_mode = value.response_mode.parse().map_err(|e| { + DatabaseInconsistencyError::on("oauth2_authorization_grants") + .column("response_mode") + .row(id) + .source(e) + })?; let max_age = value - .oauth2_authorization_grant_max_age + .max_age .map(u32::try_from) .transpose() .map_err(|e| { @@ -283,209 +227,330 @@ impl TryFrom for AuthorizationGrant { client_id: value.oauth2_client_id.into(), code, scope, - state: value.oauth2_authorization_grant_state, - nonce: value.oauth2_authorization_grant_nonce, + state: value.state, + nonce: value.nonce, max_age, response_mode, redirect_uri, - created_at: value.oauth2_authorization_grant_created_at, - response_type_id_token: value.oauth2_authorization_grant_response_type_id_token, - requires_consent: value.oauth2_authorization_grant_requires_consent, + created_at: value.created_at, + response_type_id_token: value.response_type_id_token, + requires_consent: value.requires_consent, }) } } -#[tracing::instrument( - skip_all, - fields(grant.id = %id), - err, -)] -pub async fn get_grant_by_id( - conn: &mut PgConnection, - id: Ulid, -) -> Result, DatabaseError> { - let res = sqlx::query_as!( - GrantLookup, - r#" - SELECT oauth2_authorization_grant_id - , created_at AS oauth2_authorization_grant_created_at - , cancelled_at AS oauth2_authorization_grant_cancelled_at - , fulfilled_at AS oauth2_authorization_grant_fulfilled_at - , exchanged_at AS oauth2_authorization_grant_exchanged_at - , scope AS oauth2_authorization_grant_scope - , state AS oauth2_authorization_grant_state - , redirect_uri AS oauth2_authorization_grant_redirect_uri - , response_mode AS oauth2_authorization_grant_response_mode - , nonce AS oauth2_authorization_grant_nonce - , max_age AS oauth2_authorization_grant_max_age - , oauth2_client_id AS oauth2_client_id - , authorization_code AS oauth2_authorization_grant_code - , response_type_code AS oauth2_authorization_grant_response_type_code - , response_type_id_token AS oauth2_authorization_grant_response_type_id_token - , code_challenge AS oauth2_authorization_grant_code_challenge - , code_challenge_method AS oauth2_authorization_grant_code_challenge_method - , requires_consent AS oauth2_authorization_grant_requires_consent - , oauth2_session_id AS "oauth2_session_id?" - FROM - oauth2_authorization_grants +#[async_trait] +impl<'c> OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository<'c> { + type Error = DatabaseError; - WHERE oauth2_authorization_grant_id = $1 - "#, - Uuid::from(id), - ) - .fetch_one(&mut *conn) - .await - .to_option()?; + #[tracing::instrument( + name = "db.oauth2_authorization_grant.add", + skip_all, + fields( + db.statement, + grant.id, + grant.scope = %scope, + %client.id, + ), + err, + )] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + client: &Client, + redirect_uri: Url, + scope: Scope, + code: Option, + state: Option, + nonce: Option, + max_age: Option, + response_mode: ResponseMode, + response_type_id_token: bool, + requires_consent: bool, + ) -> Result { + let code_challenge = code + .as_ref() + .and_then(|c| c.pkce.as_ref()) + .map(|p| &p.challenge); + let code_challenge_method = code + .as_ref() + .and_then(|c| c.pkce.as_ref()) + .map(|p| p.challenge_method.to_string()); + // TODO: this conversion is a bit ugly + let max_age_i32 = max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX)); + let code_str = code.as_ref().map(|c| &c.code); - let Some(res) = res else { return Ok(None) }; + let created_at = clock.now(); + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + tracing::Span::current().record("grant.id", tracing::field::display(id)); - Ok(Some(res.try_into()?)) -} - -#[tracing::instrument(skip_all, err)] -pub async fn lookup_grant_by_code( - conn: &mut PgConnection, - code: &str, -) -> Result, DatabaseError> { - let res = sqlx::query_as!( - GrantLookup, - r#" - SELECT oauth2_authorization_grant_id - , created_at AS oauth2_authorization_grant_created_at - , cancelled_at AS oauth2_authorization_grant_cancelled_at - , fulfilled_at AS oauth2_authorization_grant_fulfilled_at - , exchanged_at AS oauth2_authorization_grant_exchanged_at - , scope AS oauth2_authorization_grant_scope - , state AS oauth2_authorization_grant_state - , redirect_uri AS oauth2_authorization_grant_redirect_uri - , response_mode AS oauth2_authorization_grant_response_mode - , nonce AS oauth2_authorization_grant_nonce - , max_age AS oauth2_authorization_grant_max_age - , oauth2_client_id AS oauth2_client_id - , authorization_code AS oauth2_authorization_grant_code - , response_type_code AS oauth2_authorization_grant_response_type_code - , response_type_id_token AS oauth2_authorization_grant_response_type_id_token - , code_challenge AS oauth2_authorization_grant_code_challenge - , code_challenge_method AS oauth2_authorization_grant_code_challenge_method - , requires_consent AS oauth2_authorization_grant_requires_consent - , oauth2_session_id AS "oauth2_session_id?" - FROM - oauth2_authorization_grants - - WHERE authorization_code = $1 - "#, - code, - ) - .fetch_one(&mut *conn) - .await - .to_option()?; - - let Some(res) = res else { return Ok(None) }; - - Ok(Some(res.try_into()?)) -} - -#[tracing::instrument( - skip_all, - fields( - %grant.id, - client.id = %grant.client_id, - %session.id, - user_session.id = %session.user_session_id, - ), - err, -)] -pub async fn fulfill_grant( - executor: impl PgExecutor<'_>, - mut grant: AuthorizationGrant, - session: Session, -) -> Result { - let fulfilled_at = sqlx::query_scalar!( - r#" - UPDATE oauth2_authorization_grants AS og - SET - oauth2_session_id = os.oauth2_session_id, - fulfilled_at = os.created_at - FROM oauth2_sessions os - WHERE - og.oauth2_authorization_grant_id = $1 - AND os.oauth2_session_id = $2 - RETURNING fulfilled_at AS "fulfilled_at!: DateTime" - "#, - Uuid::from(grant.id), - Uuid::from(session.id), - ) - .fetch_one(executor) - .await?; - - grant.stage = grant - .stage - .fulfill(fulfilled_at, &session) - .map_err(DatabaseError::to_invalid_operation)?; - - Ok(grant) -} - -#[tracing::instrument( - skip_all, - fields( - %grant.id, - client.id = %grant.client_id, - ), - err, -)] -pub async fn give_consent_to_grant( - executor: impl PgExecutor<'_>, - mut grant: AuthorizationGrant, -) -> Result { - sqlx::query!( - r#" - UPDATE oauth2_authorization_grants AS og - SET - requires_consent = 'f' - WHERE - og.oauth2_authorization_grant_id = $1 - "#, - Uuid::from(grant.id), - ) - .execute(executor) - .await?; - - grant.requires_consent = false; - - Ok(grant) -} - -#[tracing::instrument( - skip_all, - fields( - %grant.id, - client.id = %grant.client_id, - ), - err, -)] -pub async fn exchange_grant( - executor: impl PgExecutor<'_>, - clock: &Clock, - mut grant: AuthorizationGrant, -) -> Result { - let exchanged_at = clock.now(); - sqlx::query!( - r#" - UPDATE oauth2_authorization_grants - SET exchanged_at = $2 - WHERE oauth2_authorization_grant_id = $1 - "#, - Uuid::from(grant.id), - exchanged_at, - ) - .execute(executor) - .await?; - - grant.stage = grant - .stage - .exchange(exchanged_at) - .map_err(DatabaseError::to_invalid_operation)?; - - Ok(grant) + sqlx::query!( + r#" + INSERT INTO oauth2_authorization_grants ( + oauth2_authorization_grant_id, + oauth2_client_id, + redirect_uri, + scope, + state, + nonce, + max_age, + response_mode, + code_challenge, + code_challenge_method, + response_type_code, + response_type_id_token, + authorization_code, + requires_consent, + created_at + ) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + "#, + Uuid::from(id), + Uuid::from(client.id), + redirect_uri.to_string(), + scope.to_string(), + state, + nonce, + max_age_i32, + response_mode.to_string(), + code_challenge, + code_challenge_method, + code.is_some(), + response_type_id_token, + code_str, + requires_consent, + created_at, + ) + .execute(&mut *self.conn) + .await?; + + Ok(AuthorizationGrant { + id, + stage: AuthorizationGrantStage::Pending, + code, + redirect_uri, + client_id: client.id, + scope, + state, + nonce, + max_age, + response_mode, + created_at, + response_type_id_token, + requires_consent, + }) + } + + #[tracing::instrument( + name = "db.oauth2_authorization_grant.lookup", + skip_all, + fields( + db.statement, + grant.id = %id, + ), + err, + )] + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error> { + let res = sqlx::query_as!( + GrantLookup, + r#" + SELECT oauth2_authorization_grant_id + , created_at + , cancelled_at + , fulfilled_at + , exchanged_at + , scope + , state + , redirect_uri + , response_mode + , nonce + , max_age + , oauth2_client_id + , authorization_code + , response_type_code + , response_type_id_token + , code_challenge + , code_challenge_method + , requires_consent + , oauth2_session_id + FROM + oauth2_authorization_grants + + WHERE oauth2_authorization_grant_id = $1 + "#, + Uuid::from(id), + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.try_into()?)) + } + + #[tracing::instrument( + name = "db.oauth2_authorization_grant.find_by_code", + skip_all, + fields( + db.statement, + ), + err, + )] + async fn find_by_code( + &mut self, + code: &str, + ) -> Result, Self::Error> { + let res = sqlx::query_as!( + GrantLookup, + r#" + SELECT oauth2_authorization_grant_id + , created_at + , cancelled_at + , fulfilled_at + , exchanged_at + , scope + , state + , redirect_uri + , response_mode + , nonce + , max_age + , oauth2_client_id + , authorization_code + , response_type_code + , response_type_id_token + , code_challenge + , code_challenge_method + , requires_consent + , oauth2_session_id + FROM + oauth2_authorization_grants + + WHERE authorization_code = $1 + "#, + code, + ) + .traced() + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.try_into()?)) + } + + #[tracing::instrument( + name = "db.oauth2_authorization_grant.fulfill", + skip_all, + fields( + db.statement, + %grant.id, + client.id = %grant.client_id, + %session.id, + user_session.id = %session.user_session_id, + ), + err, + )] + async fn fulfill( + &mut self, + clock: &Clock, + session: &Session, + grant: AuthorizationGrant, + ) -> Result { + let fulfilled_at = clock.now(); + let res = sqlx::query!( + r#" + UPDATE oauth2_authorization_grants + SET fulfilled_at = $2 + , oauth2_session_id = $3 + WHERE oauth2_authorization_grant_id = $1 + "#, + Uuid::from(grant.id), + fulfilled_at, + Uuid::from(session.id), + ) + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + // XXX: check affected rows & new methods + let grant = grant + .fulfill(fulfilled_at, session) + .map_err(DatabaseError::to_invalid_operation)?; + + Ok(grant) + } + + #[tracing::instrument( + name = "db.oauth2_authorization_grant.exchange", + skip_all, + fields( + db.statement, + %grant.id, + client.id = %grant.client_id, + ), + err, + )] + async fn exchange( + &mut self, + clock: &Clock, + grant: AuthorizationGrant, + ) -> Result { + let exchanged_at = clock.now(); + let res = sqlx::query!( + r#" + UPDATE oauth2_authorization_grants + SET exchanged_at = $2 + WHERE oauth2_authorization_grant_id = $1 + "#, + Uuid::from(grant.id), + exchanged_at, + ) + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + let grant = grant + .exchange(exchanged_at) + .map_err(DatabaseError::to_invalid_operation)?; + + Ok(grant) + } + + #[tracing::instrument( + name = "db.oauth2_authorization_grant.give_consent", + skip_all, + fields( + db.statement, + %grant.id, + client.id = %grant.client_id, + ), + err, + )] + async fn give_consent( + &mut self, + mut grant: AuthorizationGrant, + ) -> Result { + sqlx::query!( + r#" + UPDATE oauth2_authorization_grants AS og + SET + requires_consent = 'f' + WHERE + og.oauth2_authorization_grant_id = $1 + "#, + Uuid::from(grant.id), + ) + .execute(&mut *self.conn) + .await?; + + grant.requires_consent = false; + + Ok(grant) + } } diff --git a/crates/storage/src/oauth2/client.rs b/crates/storage/src/oauth2/client.rs index afe789db..756017b8 100644 --- a/crates/storage/src/oauth2/client.rs +++ b/crates/storage/src/oauth2/client.rs @@ -14,17 +14,21 @@ use std::{ collections::{BTreeMap, BTreeSet}, + str::FromStr, string::ToString, }; use async_trait::async_trait; -use mas_data_model::{Client, JwksOrJwksUri}; +use mas_data_model::{Client, JwksOrJwksUri, User}; use mas_iana::{ jose::JsonWebSignatureAlg, oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod}, }; use mas_jose::jwk::PublicJsonWebKeySet; -use oauth2_types::requests::GrantType; +use oauth2_types::{ + requests::GrantType, + scope::{Scope, ScopeToken}, +}; use rand::{Rng, RngCore}; use sqlx::PgConnection; use tracing::{info_span, Instrument}; @@ -87,6 +91,21 @@ pub trait OAuth2ClientRepository: Send + Sync { jwks_uri: Option, redirect_uris: Vec, ) -> Result; + + async fn get_consent_for_user( + &mut self, + client: &Client, + user: &User, + ) -> Result; + + async fn give_consent_for_user( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + client: &Client, + user: &User, + scope: &Scope, + ) -> Result<(), Self::Error>; } pub struct PgOAuth2ClientRepository<'c> { @@ -702,4 +721,94 @@ impl<'c> OAuth2ClientRepository for PgOAuth2ClientRepository<'c> { initiate_login_uri: None, }) } + + #[tracing::instrument( + name = "db.oauth2_client.get_consent_for_user", + skip_all, + fields( + db.statement, + %user.id, + %client.id, + ), + err, + )] + async fn get_consent_for_user( + &mut self, + client: &Client, + user: &User, + ) -> Result { + let scope_tokens: Vec = sqlx::query_scalar!( + r#" + SELECT scope_token + FROM oauth2_consents + WHERE user_id = $1 AND oauth2_client_id = $2 + "#, + Uuid::from(user.id), + Uuid::from(client.id), + ) + .fetch_all(&mut *self.conn) + .await?; + + let scope: Result = scope_tokens + .into_iter() + .map(|s| ScopeToken::from_str(&s)) + .collect(); + + let scope = scope.map_err(|e| { + DatabaseInconsistencyError::on("oauth2_consents") + .column("scope_token") + .source(e) + })?; + + Ok(scope) + } + + #[tracing::instrument( + skip_all, + fields( + db.statement, + %user.id, + %client.id, + %scope, + ), + err, + )] + async fn give_consent_for_user( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + client: &Client, + user: &User, + scope: &Scope, + ) -> Result<(), Self::Error> { + let now = clock.now(); + let (tokens, ids): (Vec, Vec) = scope + .iter() + .map(|token| { + ( + token.to_string(), + Uuid::from(Ulid::from_datetime_with_source(now.into(), rng)), + ) + }) + .unzip(); + + sqlx::query!( + r#" + INSERT INTO oauth2_consents + (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at) + SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token) + ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5 + "#, + &ids, + Uuid::from(user.id), + Uuid::from(client.id), + &tokens, + now, + ) + .traced() + .execute(&mut *self.conn) + .await?; + + Ok(()) + } } diff --git a/crates/storage/src/oauth2/consent.rs b/crates/storage/src/oauth2/consent.rs deleted file mode 100644 index c1a5080d..00000000 --- a/crates/storage/src/oauth2/consent.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2022 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::str::FromStr; - -use mas_data_model::{Client, User}; -use oauth2_types::scope::{Scope, ScopeToken}; -use rand::Rng; -use sqlx::PgExecutor; -use ulid::Ulid; -use uuid::Uuid; - -use crate::{Clock, DatabaseError, DatabaseInconsistencyError}; - -#[tracing::instrument( - skip_all, - fields( - %user.id, - %client.id, - ), - err, -)] -pub async fn fetch_client_consent( - executor: impl PgExecutor<'_>, - user: &User, - client: &Client, -) -> Result { - let scope_tokens: Vec = sqlx::query_scalar!( - r#" - SELECT scope_token - FROM oauth2_consents - WHERE user_id = $1 AND oauth2_client_id = $2 - "#, - Uuid::from(user.id), - Uuid::from(client.id), - ) - .fetch_all(executor) - .await?; - - let scope: Result = scope_tokens - .into_iter() - .map(|s| ScopeToken::from_str(&s)) - .collect(); - - let scope = scope.map_err(|e| { - DatabaseInconsistencyError::on("oauth2_consents") - .column("scope_token") - .source(e) - })?; - - Ok(scope) -} - -#[tracing::instrument( - skip_all, - fields( - %user.id, - %client.id, - %scope, - ), - err, -)] -pub async fn insert_client_consent( - executor: impl PgExecutor<'_>, - mut rng: impl Rng + Send, - clock: &Clock, - user: &User, - client: &Client, - scope: &Scope, -) -> Result<(), sqlx::Error> { - let now = clock.now(); - let (tokens, ids): (Vec, Vec) = scope - .iter() - .map(|token| { - ( - token.to_string(), - Uuid::from(Ulid::from_datetime_with_source(now.into(), &mut rng)), - ) - }) - .unzip(); - - sqlx::query!( - r#" - INSERT INTO oauth2_consents - (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at) - SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token) - ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5 - "#, - &ids, - Uuid::from(user.id), - Uuid::from(client.id), - &tokens, - now, - ) - .execute(executor) - .await?; - - Ok(()) -} diff --git a/crates/storage/src/oauth2/mod.rs b/crates/storage/src/oauth2/mod.rs index b02216a6..480c4515 100644 --- a/crates/storage/src/oauth2/mod.rs +++ b/crates/storage/src/oauth2/mod.rs @@ -12,14 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod access_token; +mod access_token; pub mod authorization_grant; mod client; -pub mod consent; -pub mod refresh_token; +mod refresh_token; mod session; pub use self::{ + access_token::{OAuth2AccessTokenRepository, PgOAuth2AccessTokenRepository}, + authorization_grant::{ + OAuth2AuthorizationGrantRepository, PgOAuth2AuthorizationGrantRepository, + }, client::{OAuth2ClientRepository, PgOAuth2ClientRepository}, + refresh_token::{OAuth2RefreshTokenRepository, PgOAuth2RefreshTokenRepository}, session::{OAuth2SessionRepository, PgOAuth2SessionRepository}, }; diff --git a/crates/storage/src/oauth2/refresh_token.rs b/crates/storage/src/oauth2/refresh_token.rs index 29f6ab34..5d3bb013 100644 --- a/crates/storage/src/oauth2/refresh_token.rs +++ b/crates/storage/src/oauth2/refresh_token.rs @@ -12,62 +12,55 @@ // See the License for the specific language governing permissions and // limitations under the License. +use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{AccessToken, RefreshToken, RefreshTokenState, Session}; -use rand::Rng; -use sqlx::{PgConnection, PgExecutor}; +use rand::RngCore; +use sqlx::PgConnection; use ulid::Ulid; use uuid::Uuid; -use crate::{Clock, DatabaseError}; +use crate::{tracing::ExecuteExt, Clock, DatabaseError, LookupResultExt}; -#[tracing::instrument( - skip_all, - fields( - %session.id, - user_session.id = %session.user_session_id, - client.id = %session.client_id, - refresh_token.id, - ), - err, -)] -pub async fn add_refresh_token( - executor: impl PgExecutor<'_>, - mut rng: impl Rng + Send, - clock: &Clock, - session: &Session, - access_token: &AccessToken, - refresh_token: String, -) -> Result { - let created_at = clock.now(); - let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); - tracing::Span::current().record("refresh_token.id", tracing::field::display(id)); +#[async_trait] +pub trait OAuth2RefreshTokenRepository: Send + Sync { + type Error; - sqlx::query!( - r#" - INSERT INTO oauth2_refresh_tokens - (oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id, - refresh_token, created_at) - VALUES - ($1, $2, $3, $4, $5) - "#, - Uuid::from(id), - Uuid::from(session.id), - Uuid::from(access_token.id), - refresh_token, - created_at, - ) - .execute(executor) - .await?; + /// Lookup a refresh token by its ID + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; - Ok(RefreshToken { - id, - state: RefreshTokenState::default(), - session_id: session.id, - refresh_token, - access_token_id: Some(access_token.id), - created_at, - }) + /// Find a refresh token by its token + async fn find_by_token( + &mut self, + refresh_token: &str, + ) -> Result, Self::Error>; + + /// Add a new refresh token to the database + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + session: &Session, + access_token: &AccessToken, + refresh_token: String, + ) -> Result; + + /// Consume a refresh token + async fn consume( + &mut self, + clock: &Clock, + refresh_token: RefreshToken, + ) -> Result; +} + +pub struct PgOAuth2RefreshTokenRepository<'c> { + conn: &'c mut PgConnection, +} + +impl<'c> PgOAuth2RefreshTokenRepository<'c> { + pub fn new(conn: &'c mut PgConnection) -> Self { + Self { conn } + } } struct OAuth2RefreshTokenLookup { @@ -79,75 +72,183 @@ struct OAuth2RefreshTokenLookup { oauth2_session_id: Uuid, } -#[tracing::instrument(skip_all, err)] -#[allow(clippy::too_many_lines)] -pub async fn lookup_refresh_token( - conn: &mut PgConnection, - token: &str, -) -> Result, DatabaseError> { - let res = sqlx::query_as!( - OAuth2RefreshTokenLookup, - r#" - SELECT oauth2_refresh_token_id - , refresh_token - , created_at - , consumed_at - , oauth2_access_token_id - , oauth2_session_id - FROM oauth2_refresh_tokens +impl From for RefreshToken { + fn from(value: OAuth2RefreshTokenLookup) -> Self { + let state = match value.consumed_at { + None => RefreshTokenState::Valid, + Some(consumed_at) => RefreshTokenState::Consumed { consumed_at }, + }; - WHERE refresh_token = $1 - "#, - token, - ) - .fetch_one(&mut *conn) - .await?; - - let state = match res.consumed_at { - None => RefreshTokenState::Valid, - Some(consumed_at) => RefreshTokenState::Consumed { consumed_at }, - }; - - let refresh_token = RefreshToken { - id: res.oauth2_refresh_token_id.into(), - state, - session_id: res.oauth2_session_id.into(), - refresh_token: res.refresh_token, - created_at: res.created_at, - access_token_id: res.oauth2_access_token_id.map(Ulid::from), - }; - - Ok(Some(refresh_token)) + RefreshToken { + id: value.oauth2_refresh_token_id.into(), + state, + session_id: value.oauth2_session_id.into(), + refresh_token: value.refresh_token, + created_at: value.created_at, + access_token_id: value.oauth2_access_token_id.map(Ulid::from), + } + } } -#[tracing::instrument( - skip_all, - fields( - %refresh_token.id, - ), - err, -)] -pub async fn consume_refresh_token( - executor: impl PgExecutor<'_>, - clock: &Clock, - refresh_token: RefreshToken, -) -> Result { - let consumed_at = clock.now(); - let res = sqlx::query!( - r#" - UPDATE oauth2_refresh_tokens - SET consumed_at = $2 - WHERE oauth2_refresh_token_id = $1 - "#, - Uuid::from(refresh_token.id), - consumed_at, - ) - .execute(executor) - .await?; +#[async_trait] +impl<'c> OAuth2RefreshTokenRepository for PgOAuth2RefreshTokenRepository<'c> { + type Error = DatabaseError; - DatabaseError::ensure_affected_rows(&res, 1)?; + #[tracing::instrument( + name = "db.oauth2_refresh_token.lookup", + skip_all, + fields( + db.statement, + refresh_token.id = %id, + ), + err, + )] + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error> { + let res = sqlx::query_as!( + OAuth2RefreshTokenLookup, + r#" + SELECT oauth2_refresh_token_id + , refresh_token + , created_at + , consumed_at + , oauth2_access_token_id + , oauth2_session_id + FROM oauth2_refresh_tokens - refresh_token - .consume(consumed_at) - .map_err(DatabaseError::to_invalid_operation) + WHERE oauth2_refresh_token_id = $1 + "#, + Uuid::from(id), + ) + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + name = "db.oauth2_refresh_token.find_by_token", + skip_all, + fields( + db.statement, + ), + err, + )] + async fn find_by_token( + &mut self, + refresh_token: &str, + ) -> Result, Self::Error> { + let res = sqlx::query_as!( + OAuth2RefreshTokenLookup, + r#" + SELECT oauth2_refresh_token_id + , refresh_token + , created_at + , consumed_at + , oauth2_access_token_id + , oauth2_session_id + FROM oauth2_refresh_tokens + + WHERE refresh_token = $1 + "#, + refresh_token, + ) + .traced() + .fetch_one(&mut *self.conn) + .await + .to_option()?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + name = "db.oauth2_refresh_token.add", + skip_all, + fields( + db.statement, + %session.id, + user_session.id = %session.user_session_id, + client.id = %session.client_id, + refresh_token.id, + ), + err, + )] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &Clock, + session: &Session, + access_token: &AccessToken, + refresh_token: String, + ) -> Result { + let created_at = clock.now(); + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + tracing::Span::current().record("refresh_token.id", tracing::field::display(id)); + + sqlx::query!( + r#" + INSERT INTO oauth2_refresh_tokens + (oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id, + refresh_token, created_at) + VALUES + ($1, $2, $3, $4, $5) + "#, + Uuid::from(id), + Uuid::from(session.id), + Uuid::from(access_token.id), + refresh_token, + created_at, + ) + .traced() + .execute(&mut *self.conn) + .await?; + + Ok(RefreshToken { + id, + state: RefreshTokenState::default(), + session_id: session.id, + refresh_token, + access_token_id: Some(access_token.id), + created_at, + }) + } + + #[tracing::instrument( + name = "db.oauth2_refresh_token.consume", + skip_all, + fields( + db.statement, + %refresh_token.id, + session.id = %refresh_token.session_id, + ), + err, + )] + async fn consume( + &mut self, + clock: &Clock, + refresh_token: RefreshToken, + ) -> Result { + let consumed_at = clock.now(); + let res = sqlx::query!( + r#" + UPDATE oauth2_refresh_tokens + SET consumed_at = $2 + WHERE oauth2_refresh_token_id = $1 + "#, + Uuid::from(refresh_token.id), + consumed_at, + ) + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + refresh_token + .consume(consumed_at) + .map_err(DatabaseError::to_invalid_operation) + } } diff --git a/crates/storage/src/repository.rs b/crates/storage/src/repository.rs index ddd6e1ea..b9bf5683 100644 --- a/crates/storage/src/repository.rs +++ b/crates/storage/src/repository.rs @@ -19,7 +19,10 @@ use crate::{ PgCompatAccessTokenRepository, PgCompatRefreshTokenRepository, PgCompatSessionRepository, PgCompatSsoLoginRepository, }, - oauth2::{PgOAuth2ClientRepository, PgOAuth2SessionRepository}, + oauth2::{ + PgOAuth2AccessTokenRepository, PgOAuth2AuthorizationGrantRepository, + PgOAuth2ClientRepository, PgOAuth2RefreshTokenRepository, PgOAuth2SessionRepository, + }, upstream_oauth2::{ PgUpstreamOAuthLinkRepository, PgUpstreamOAuthProviderRepository, PgUpstreamOAuthSessionRepository, @@ -63,10 +66,22 @@ pub trait Repository { where Self: 'c; + type OAuth2AuthorizationGrantRepository<'c> + where + Self: 'c; + type OAuth2SessionRepository<'c> where Self: 'c; + type OAuth2AccessTokenRepository<'c> + where + Self: 'c; + + type OAuth2RefreshTokenRepository<'c> + where + Self: 'c; + type CompatSessionRepository<'c> where Self: 'c; @@ -91,7 +106,10 @@ pub trait Repository { fn user_password(&mut self) -> Self::UserPasswordRepository<'_>; fn browser_session(&mut self) -> Self::BrowserSessionRepository<'_>; fn oauth2_client(&mut self) -> Self::OAuth2ClientRepository<'_>; + fn oauth2_authorization_grant(&mut self) -> Self::OAuth2AuthorizationGrantRepository<'_>; fn oauth2_session(&mut self) -> Self::OAuth2SessionRepository<'_>; + fn oauth2_access_token(&mut self) -> Self::OAuth2AccessTokenRepository<'_>; + fn oauth2_refresh_token(&mut self) -> Self::OAuth2RefreshTokenRepository<'_>; fn compat_session(&mut self) -> Self::CompatSessionRepository<'_>; fn compat_sso_login(&mut self) -> Self::CompatSsoLoginRepository<'_>; fn compat_access_token(&mut self) -> Self::CompatAccessTokenRepository<'_>; @@ -107,7 +125,10 @@ impl Repository for PgConnection { type UserPasswordRepository<'c> = PgUserPasswordRepository<'c> where Self: 'c; type BrowserSessionRepository<'c> = PgBrowserSessionRepository<'c> where Self: 'c; type OAuth2ClientRepository<'c> = PgOAuth2ClientRepository<'c> where Self: 'c; + type OAuth2AuthorizationGrantRepository<'c> = PgOAuth2AuthorizationGrantRepository<'c> where Self: 'c; type OAuth2SessionRepository<'c> = PgOAuth2SessionRepository<'c> where Self: 'c; + type OAuth2AccessTokenRepository<'c> = PgOAuth2AccessTokenRepository<'c> where Self: 'c; + type OAuth2RefreshTokenRepository<'c> = PgOAuth2RefreshTokenRepository<'c> where Self: 'c; type CompatSessionRepository<'c> = PgCompatSessionRepository<'c> where Self: 'c; type CompatSsoLoginRepository<'c> = PgCompatSsoLoginRepository<'c> where Self: 'c; type CompatAccessTokenRepository<'c> = PgCompatAccessTokenRepository<'c> where Self: 'c; @@ -145,10 +166,22 @@ impl Repository for PgConnection { PgOAuth2ClientRepository::new(self) } + fn oauth2_authorization_grant(&mut self) -> Self::OAuth2AuthorizationGrantRepository<'_> { + PgOAuth2AuthorizationGrantRepository::new(self) + } + fn oauth2_session(&mut self) -> Self::OAuth2SessionRepository<'_> { PgOAuth2SessionRepository::new(self) } + fn oauth2_access_token(&mut self) -> Self::OAuth2AccessTokenRepository<'_> { + PgOAuth2AccessTokenRepository::new(self) + } + + fn oauth2_refresh_token(&mut self) -> Self::OAuth2RefreshTokenRepository<'_> { + PgOAuth2RefreshTokenRepository::new(self) + } + fn compat_session(&mut self) -> Self::CompatSessionRepository<'_> { PgCompatSessionRepository::new(self) } @@ -175,7 +208,10 @@ impl<'t> Repository for Transaction<'t, Postgres> { type UserPasswordRepository<'c> = PgUserPasswordRepository<'c> where Self: 'c; type BrowserSessionRepository<'c> = PgBrowserSessionRepository<'c> where Self: 'c; type OAuth2ClientRepository<'c> = PgOAuth2ClientRepository<'c> where Self: 'c; + type OAuth2AuthorizationGrantRepository<'c> = PgOAuth2AuthorizationGrantRepository<'c> where Self: 'c; type OAuth2SessionRepository<'c> = PgOAuth2SessionRepository<'c> where Self: 'c; + type OAuth2AccessTokenRepository<'c> = PgOAuth2AccessTokenRepository<'c> where Self: 'c; + type OAuth2RefreshTokenRepository<'c> = PgOAuth2RefreshTokenRepository<'c> where Self: 'c; type CompatSessionRepository<'c> = PgCompatSessionRepository<'c> where Self: 'c; type CompatSsoLoginRepository<'c> = PgCompatSsoLoginRepository<'c> where Self: 'c; type CompatAccessTokenRepository<'c> = PgCompatAccessTokenRepository<'c> where Self: 'c; @@ -213,10 +249,22 @@ impl<'t> Repository for Transaction<'t, Postgres> { PgOAuth2ClientRepository::new(self) } + fn oauth2_authorization_grant(&mut self) -> Self::OAuth2AuthorizationGrantRepository<'_> { + PgOAuth2AuthorizationGrantRepository::new(self) + } + fn oauth2_session(&mut self) -> Self::OAuth2SessionRepository<'_> { PgOAuth2SessionRepository::new(self) } + fn oauth2_access_token(&mut self) -> Self::OAuth2AccessTokenRepository<'_> { + PgOAuth2AccessTokenRepository::new(self) + } + + fn oauth2_refresh_token(&mut self) -> Self::OAuth2RefreshTokenRepository<'_> { + PgOAuth2RefreshTokenRepository::new(self) + } + fn compat_session(&mut self) -> Self::CompatSessionRepository<'_> { PgCompatSessionRepository::new(self) } diff --git a/crates/tasks/src/database.rs b/crates/tasks/src/database.rs index 5e72141e..f4d11c6a 100644 --- a/crates/tasks/src/database.rs +++ b/crates/tasks/src/database.rs @@ -14,7 +14,7 @@ //! Database-related tasks -use mas_storage::Clock; +use mas_storage::{oauth2::OAuth2AccessTokenRepository, Clock, Repository}; use sqlx::{Pool, Postgres}; use tracing::{debug, error, info}; @@ -32,7 +32,12 @@ impl std::fmt::Debug for CleanupExpired { #[async_trait::async_trait] impl Task for CleanupExpired { async fn run(&self) { - let res = mas_storage::oauth2::access_token::cleanup_expired(&self.0, &self.1).await; + let res = async move { + let mut conn = self.0.acquire().await?; + conn.oauth2_access_token().cleanup_expired(&self.1).await + } + .await; + match res { Ok(0) => { debug!("no token to clean up");