From 62f633a7162352a3ee25675da12f714a2c79949e Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 8 Mar 2022 17:33:25 +0100 Subject: [PATCH] Move clients to the database --- Cargo.lock | 14 +- crates/cli/Cargo.toml | 2 + crates/cli/src/commands/manage.rs | 76 +- crates/config/Cargo.toml | 6 +- crates/config/src/sections/clients.rs | 103 +- crates/data-model/Cargo.toml | 1 + crates/data-model/src/lib.rs | 3 +- crates/data-model/src/oauth2/client.rs | 113 ++ crates/data-model/src/oauth2/mod.rs | 4 +- crates/handlers/src/lib.rs | 9 +- crates/handlers/src/oauth2/authorization.rs | 57 +- crates/handlers/src/oauth2/introspection.rs | 17 +- crates/handlers/src/oauth2/mod.rs | 9 +- crates/handlers/src/oauth2/token.rs | 16 +- crates/handlers/src/views/shared.rs | 6 +- crates/iana/src/lib.rs | 2 + crates/jose/Cargo.toml | 2 +- crates/jose/src/lib.rs | 2 + crates/oauth2-types/src/oidc.rs | 11 +- crates/oauth2-types/src/requests.rs | 1 + crates/storage/Cargo.toml | 4 +- .../20220228144045_oauth2_clients.down.sql | 17 + .../20220228144045_oauth2_clients.up.sql | 51 + crates/storage/sqlx-data.json | 1542 ++++++++++------- crates/storage/src/lib.rs | 2 +- crates/storage/src/oauth2/access_token.rs | 17 +- .../storage/src/oauth2/authorization_grant.rs | 41 +- crates/storage/src/oauth2/client.rs | 380 ++++ crates/storage/src/oauth2/mod.rs | 1 + crates/storage/src/oauth2/refresh_token.rs | 15 +- crates/warp-utils/Cargo.toml | 7 +- crates/warp-utils/src/filters/authenticate.rs | 7 + crates/warp-utils/src/filters/client.rs | 255 ++- 33 files changed, 1926 insertions(+), 867 deletions(-) create mode 100644 crates/storage/migrations/20220228144045_oauth2_clients.down.sql create mode 100644 crates/storage/migrations/20220228144045_oauth2_clients.up.sql create mode 100644 crates/storage/src/oauth2/client.rs diff --git a/Cargo.lock b/Cargo.lock index 19c12019..3bf1cea4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1828,6 +1828,7 @@ dependencies = [ "argon2", "atty", "clap", + "data-encoding", "dotenv", "futures 0.3.21", "hyper", @@ -1845,6 +1846,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry-zipkin", + "rand", "reqwest", "schemars", "serde_json", @@ -1870,12 +1872,9 @@ dependencies = [ "chrono", "elliptic-curve", "figment", - "futures-util", - "http", - "http-body", "indoc", "lettre", - "mas-http", + "mas-iana", "mas-jose", "p256", "pem-rfc7468", @@ -1889,7 +1888,6 @@ dependencies = [ "sqlx", "thiserror", "tokio", - "tower", "tracing", "url", ] @@ -1901,6 +1899,7 @@ dependencies = [ "chrono", "crc", "mas-iana", + "mas-jose", "oauth2-types", "rand", "serde", @@ -2068,10 +2067,12 @@ dependencies = [ "chrono", "mas-data-model", "mas-iana", + "mas-jose", "oauth2-types", "password-hash", "rand", "serde", + "serde_json", "sqlx", "thiserror", "tokio", @@ -2123,9 +2124,12 @@ dependencies = [ "crc", "data-encoding", "headers", + "http", + "http-body", "hyper", "mas-config", "mas-data-model", + "mas-http", "mas-iana", "mas-jose", "mas-storage", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 710c2bc8..cce96186 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -22,6 +22,8 @@ argon2 = { version = "0.3.4", features = ["password-hash"] } reqwest = { version = "0.11.9", features = ["rustls-tls"], default-features = false, optional = true } watchman_client = "0.7.1" atty = "0.2.14" +rand = "0.8.5" +data-encoding = "2.3.2" tracing = "0.1.31" tracing-appender = "0.2.1" diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 5511a8c6..2022d256 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -14,9 +14,13 @@ use argon2::Argon2; use clap::Parser; -use mas_config::DatabaseConfig; -use mas_storage::user::{ - lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, register_user, +use data_encoding::BASE64; +use mas_config::{DatabaseConfig, RootConfig}; +use mas_storage::{ + oauth2::client::{insert_client_from_config, lookup_client_by_client_id, truncate_clients}, + user::{ + lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, register_user, + }, }; use tracing::{info, warn}; @@ -36,6 +40,13 @@ enum Subcommand { /// Mark email address as verified VerifyEmail { username: String, email: String }, + + /// Import clients from config + ImportClients { + /// Remove all clients before importing + #[clap(long)] + truncate: bool, + }, } impl Options { @@ -71,6 +82,65 @@ impl Options { txn.commit().await?; info!(?email, "Email marked as verified"); + Ok(()) + } + SC::ImportClients { truncate } => { + let config: RootConfig = root.load_config()?; + let pool = config.database.connect().await?; + let encrypter = config.secrets.encrypter(); + + let mut txn = pool.begin().await?; + + if *truncate { + warn!("Removing all clients first"); + truncate_clients(&mut txn).await?; + } + + for client in config.clients.iter() { + let client_id = &client.client_id; + let res = lookup_client_by_client_id(&mut txn, client_id).await; + match res { + Ok(_) => { + warn!(%client_id, "Skipping already imported client"); + continue; + } + Err(e) if e.not_found() => {} + Err(e) => anyhow::bail!(e), + } + + info!(%client_id, "Importing client"); + let client_secret = client.client_secret(); + let client_auth_method = client.client_auth_method(); + let jwks = client.jwks(); + let jwks_uri = client.jwks_uri(); + let redirect_uris = &client.redirect_uris; + + // TODO: should be moved somewhere else + let encrypted_client_secret = client_secret + .map(|client_secret| { + let nonce: [u8; 12] = rand::random(); + let message = encrypter.encrypt(&nonce, client_secret.as_bytes())?; + let concat = [&nonce[..], &message[..]].concat(); + let res = BASE64.encode(&concat); + + anyhow::Ok(res) + }) + .transpose()?; + + insert_client_from_config( + &mut txn, + client_id, + client_auth_method, + encrypted_client_secret.as_deref(), + jwks, + jwks_uri, + redirect_uris, + ) + .await?; + } + + txn.commit().await?; + Ok(()) } } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index cc716d8d..dd12a63a 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -35,8 +35,4 @@ pem-rfc7468 = "0.3.1" indoc = "1.0.4" mas-jose = { path = "../jose" } -mas-http = { path = "../http" } -tower = { version = "0.4.12", features = ["util"] } -http = "0.2.6" -http-body = "0.4.4" -futures-util = "0.3.21" +mas-iana = { path = "../iana" } diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 48326921..ad4a4a6d 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -15,15 +15,12 @@ use std::ops::{Deref, DerefMut}; use async_trait::async_trait; -use futures_util::future::Either; -use http::Request; -use mas_http::HttpServiceExt; -use mas_jose::{DynamicJwksStore, JsonWebKeySet, StaticJwksStore, VerifyingKeystore}; +use mas_iana::oauth::OAuthClientAuthenticationMethod; +use mas_jose::JsonWebKeySet; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use thiserror::Error; -use tower::{BoxError, ServiceExt}; use url::Url; use super::ConfigurationSection; @@ -35,41 +32,6 @@ pub enum JwksOrJwksUri { JwksUri(Url), } -impl JwksOrJwksUri { - pub fn key_store(&self) -> Either { - // Assert that the output is both a VerifyingKeystore and Send - fn assert(t: T) -> T { - t - } - - let inner = match self { - Self::Jwks(jwks) => Either::Left(StaticJwksStore::new(jwks.clone())), - Self::JwksUri(uri) => { - let uri = uri.clone(); - - // TODO: get the client from somewhere else? - let exporter = mas_http::client("fetch-jwks") - .json::() - .map_request(move |_: ()| { - Request::builder() - .method("GET") - // TODO: change the Uri type in config to avoid reparsing here - .uri(uri.to_string()) - .body(http_body::Empty::new()) - .unwrap() - }) - .map_response(http::Response::into_body) - .map_err(BoxError::from) - .boxed_clone(); - - Either::Right(DynamicJwksStore::new(exporter)) - } - }; - - assert(inner) - } -} - impl From for JwksOrJwksUri { fn from(jwks: JsonWebKeySet) -> Self { Self::Jwks(jwks) @@ -131,24 +93,53 @@ pub struct InvalidRedirectUriError; impl ClientConfig { #[doc(hidden)] - pub fn resolve_redirect_uri<'a>( - &'a self, - suggested_uri: &'a Option, - ) -> Result<&'a Url, InvalidRedirectUriError> { - suggested_uri.as_ref().map_or_else( - || self.redirect_uris.get(0).ok_or(InvalidRedirectUriError), - |suggested_uri| self.check_redirect_uri(suggested_uri), - ) + #[must_use] + pub fn client_secret(&self) -> Option<&str> { + match &self.client_auth_method { + ClientAuthMethodConfig::ClientSecretPost { client_secret } + | ClientAuthMethodConfig::ClientSecretBasic { client_secret } + | ClientAuthMethodConfig::ClientSecretJwt { client_secret } => Some(client_secret), + _ => None, + } } - fn check_redirect_uri<'a>( - &self, - redirect_uri: &'a Url, - ) -> Result<&'a Url, InvalidRedirectUriError> { - if self.redirect_uris.contains(redirect_uri) { - Ok(redirect_uri) - } else { - Err(InvalidRedirectUriError) + #[doc(hidden)] + #[must_use] + pub fn client_auth_method(&self) -> OAuthClientAuthenticationMethod { + match &self.client_auth_method { + ClientAuthMethodConfig::None => OAuthClientAuthenticationMethod::None, + ClientAuthMethodConfig::ClientSecretBasic { .. } => { + OAuthClientAuthenticationMethod::ClientSecretBasic + } + ClientAuthMethodConfig::ClientSecretPost { .. } => { + OAuthClientAuthenticationMethod::ClientSecretPost + } + ClientAuthMethodConfig::ClientSecretJwt { .. } => { + OAuthClientAuthenticationMethod::ClientSecretJwt + } + ClientAuthMethodConfig::PrivateKeyJwt(_) => { + OAuthClientAuthenticationMethod::PrivateKeyJwt + } + } + } + + #[doc(hidden)] + #[must_use] + pub fn jwks(&self) -> Option<&JsonWebKeySet> { + match &self.client_auth_method { + ClientAuthMethodConfig::PrivateKeyJwt(JwksOrJwksUri::Jwks(jwks)) => Some(jwks), + _ => None, + } + } + + #[doc(hidden)] + #[must_use] + pub fn jwks_uri(&self) -> Option<&Url> { + match &self.client_auth_method { + ClientAuthMethodConfig::PrivateKeyJwt(JwksOrJwksUri::JwksUri(jwks_uri)) => { + Some(jwks_uri) + } + _ => None, } } } diff --git a/crates/data-model/Cargo.toml b/crates/data-model/Cargo.toml index b5d29e3e..bd24c549 100644 --- a/crates/data-model/Cargo.toml +++ b/crates/data-model/Cargo.toml @@ -14,4 +14,5 @@ crc = "2.1.0" rand = "0.8.5" mas-iana = { path = "../iana" } +mas-jose = { path = "../jose" } oauth2-types = { path = "../oauth2-types" } diff --git a/crates/data-model/src/lib.rs b/crates/data-model/src/lib.rs index 99e7cbd5..f8310897 100644 --- a/crates/data-model/src/lib.rs +++ b/crates/data-model/src/lib.rs @@ -30,7 +30,8 @@ pub(crate) mod users; pub use self::{ oauth2::{ - AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client, Pkce, Session, + AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client, JwksOrJwksUri, + Pkce, Session, }, tokens::{AccessToken, RefreshToken, TokenFormatError, TokenType}, traits::{StorageBackend, StorageBackendMarker}, diff --git a/crates/data-model/src/oauth2/client.rs b/crates/data-model/src/oauth2/client.rs index 2d8705b7..b34dc271 100644 --- a/crates/data-model/src/oauth2/client.rs +++ b/crates/data-model/src/oauth2/client.rs @@ -12,16 +12,87 @@ // See the License for the specific language governing permissions and // limitations under the License. +use mas_iana::{ + jose::JsonWebSignatureAlg, + oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod}, +}; +use mas_jose::JsonWebKeySet; +use oauth2_types::requests::GrantType; use serde::Serialize; +use thiserror::Error; +use url::Url; use crate::traits::{StorageBackend, StorageBackendMarker}; +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum JwksOrJwksUri { + /// Client's JSON Web Key Set document, passed by value. + Jwks(JsonWebKeySet), + + /// URL for the Client's JSON Web Key Set document. + JwksUri(Url), +} + #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(bound = "T: StorageBackend")] pub struct Client { #[serde(skip_serializing)] pub data: T::ClientData, + + /// Client identifier pub client_id: String, + + pub encrypted_client_secret: Option, + + /// Array of Redirection URI values used by the Client + pub redirect_uris: Vec, + + /// Array containing a list of the OAuth 2.0 response_type values that the + /// Client is declaring that it will restrict itself to using + pub response_types: Vec, + + /// Array containing a list of the OAuth 2.0 Grant Types that the Client is + /// declaring that it will restrict itself to using. + pub grant_types: Vec, + + /// Array of e-mail addresses of people responsible for this Client + pub contacts: Vec, + + /// Name of the Client to be presented to the End-User + pub client_name: Option, // TODO: translations + + /// URL that references a logo for the Client application + pub logo_uri: Option, // TODO: translations + + /// URL of the home page of the Client + pub client_uri: Option, // TODO: translations + + /// URL that the Relying Party Client provides to the End-User to read about + /// the how the profile data will be used + pub policy_uri: Option, // TODO: translations + + /// URL that the Relying Party Client provides to the End-User to read about + /// the Relying Party's terms of service + pub tos_uri: Option, // TODO: translations + + pub jwks: Option, + + /// JWS alg algorithm REQUIRED for signing the ID Token issued to this + /// Client + pub id_token_signed_response_alg: Option, + + /// Requested authentication method for the token endpoint + pub token_endpoint_auth_method: Option, + + /// JWS alg algorithm that MUST be used for signing the JWT used to + /// authenticate the Client at the Token Endpoint for the private_key_jwt + /// and client_secret_jwt authentication methods + pub token_endpoint_auth_signing_alg: Option, + + /// URI using the https scheme that a third party can use to initiate a + /// login by the RP + pub initiate_login_uri: Option, } impl From> for Client<()> { @@ -29,6 +100,48 @@ impl From> for Client<()> { Client { data: (), client_id: c.client_id, + encrypted_client_secret: c.encrypted_client_secret, + redirect_uris: c.redirect_uris, + response_types: c.response_types, + grant_types: c.grant_types, + contacts: c.contacts, + client_name: c.client_name, + logo_uri: c.logo_uri, + client_uri: c.client_uri, + policy_uri: c.policy_uri, + tos_uri: c.tos_uri, + jwks: c.jwks, + id_token_signed_response_alg: c.id_token_signed_response_alg, + token_endpoint_auth_method: c.token_endpoint_auth_method, + token_endpoint_auth_signing_alg: c.token_endpoint_auth_signing_alg, + initiate_login_uri: c.initiate_login_uri, + } + } +} + +#[derive(Debug, Error)] +pub enum InvalidRedirectUriError { + #[error("redirect_uri is not allowed for this client")] + NotAllowed, + + #[error("multiple redirect_uris registered for this client")] + MultipleRegistered, + + #[error("client has no redirect_uri registered")] + NoneRegistered, +} + +impl Client { + pub fn resolve_redirect_uri<'a>( + &'a self, + redirect_uri: &'a Option, + ) -> Result<&'a Url, InvalidRedirectUriError> { + match (&self.redirect_uris[..], redirect_uri) { + ([], _) => Err(InvalidRedirectUriError::NoneRegistered), + ([one], None) => Ok(one), + (_, None) => Err(InvalidRedirectUriError::MultipleRegistered), + (uris, Some(uri)) if uris.contains(uri) => Ok(uri), + _ => Err(InvalidRedirectUriError::NotAllowed), } } } diff --git a/crates/data-model/src/oauth2/mod.rs b/crates/data-model/src/oauth2/mod.rs index 6309fd7e..a0c7237b 100644 --- a/crates/data-model/src/oauth2/mod.rs +++ b/crates/data-model/src/oauth2/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. +// Copyright 2021, 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. @@ -18,6 +18,6 @@ pub(self) mod session; pub use self::{ authorization_grant::{AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Pkce}, - client::Client, + client::{Client, JwksOrJwksUri}, session::Session, }; diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 683a2c5b..974d7daf 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -46,14 +46,7 @@ pub fn root( config: &RootConfig, ) -> BoxedFilter<(impl Reply,)> { let health = health(pool); - let oauth2 = oauth2( - pool, - templates, - key_store, - encrypter, - &config.clients, - &config.http, - ); + let oauth2 = oauth2(pool, templates, key_store, encrypter, &config.http); let views = views( pool, templates, diff --git a/crates/handlers/src/oauth2/authorization.rs b/crates/handlers/src/oauth2/authorization.rs index 2774adfb..9d8b7a07 100644 --- a/crates/handlers/src/oauth2/authorization.rs +++ b/crates/handlers/src/oauth2/authorization.rs @@ -20,7 +20,7 @@ use hyper::{ http::uri::{Parts, PathAndQuery, Uri}, StatusCode, }; -use mas_config::{ClientsConfig, Encrypter}; +use mas_config::Encrypter; use mas_data_model::{ Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession, Pkce, StorageBackend, TokenType, @@ -32,6 +32,7 @@ use mas_storage::{ authorization_grant::{ derive_session, fulfill_grant, get_grant_by_id, new_authorization_grant, }, + client::lookup_client_by_client_id, refresh_token::add_refresh_token, }, PostgresqlBackend, @@ -41,7 +42,7 @@ use mas_warp_utils::{ errors::WrapError, filters::{ self, - database::transaction, + database::{connection, transaction}, session::{optional_session, session}, with_templates, }, @@ -49,19 +50,20 @@ use mas_warp_utils::{ use oauth2_types::{ errors::{ ErrorResponse, InvalidGrant, InvalidRequest, LoginRequired, OAuth2Error, - RegistrationNotSupported, RequestNotSupported, RequestUriNotSupported, + RegistrationNotSupported, RequestNotSupported, RequestUriNotSupported, UnauthorizedClient, }, pkce, prelude::*, requests::{ - AccessTokenResponse, AuthorizationRequest, AuthorizationResponse, Prompt, ResponseMode, + AccessTokenResponse, AuthorizationRequest, AuthorizationResponse, GrantType, Prompt, + ResponseMode, }, scope::ScopeToken, }; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sqlx::{PgExecutor, PgPool, Postgres, Transaction}; +use sqlx::{pool::PoolConnection, PgConnection, PgPool, Postgres, Transaction}; use url::Url; use warp::{ filters::BoxedFilter, @@ -217,15 +219,10 @@ pub fn filter( pool: &PgPool, templates: &Templates, encrypter: &Encrypter, - clients_config: &ClientsConfig, ) -> BoxedFilter<(Box,)> { - let clients_config = clients_config.clone(); - let clients_config_2 = clients_config.clone(); - let authorize = warp::path!("oauth2" / "authorize") .and(filters::trace::name("GET /oauth2/authorize")) .and(warp::get()) - .map(move || clients_config.clone()) .and(warp::query()) .and(optional_session(pool, encrypter)) .and(transaction(pool)) @@ -245,8 +242,8 @@ pub fn filter( .recover(recover) .unify() .and(warp::query()) - .and(warp::any().map(move || clients_config_2.clone())) .and(with_templates(templates)) + .and(connection(pool)) .and_then(actually_reply) .boxed() } @@ -262,8 +259,8 @@ async fn recover(rejection: Rejection) -> Result async fn actually_reply( rep: ReplyOrBackToClient, q: PartialParams, - clients: ClientsConfig, templates: Templates, + mut conn: PoolConnection, ) -> Result, Rejection> { let (redirect_uri, response_mode, state, params) = match rep { ReplyOrBackToClient::Reply(r) => return Ok(r), @@ -281,15 +278,14 @@ async fn actually_reply( .. } = q; - // First, disover the client - let client = client_id - .and_then(|client_id| clients.iter().find(|client| client.client_id == client_id)); - - let client = match client { - Some(client) => client, - None => return Ok(Box::new(html(templates.render_error(&error.into()).await?))), + let client_id = if let Some(client_id) = client_id { + client_id + } else { + return Ok(Box::new(html(templates.render_error(&error.into()).await?))); }; + let client = lookup_client_by_client_id(&mut conn, &client_id).await?; + let redirect_uri: Result, _> = redirect_uri.map(|r| r.parse()).transpose(); let redirect_uri = match redirect_uri { Ok(r) => r, @@ -315,7 +311,6 @@ async fn actually_reply( } async fn get( - clients: ClientsConfig, params: Params, maybe_session: Option>, mut txn: Transaction<'_, Postgres>, @@ -337,15 +332,17 @@ async fn get( } // First, find out what client it is - let client = clients - .iter() - .find(|client| client.client_id == params.auth.client_id) - .ok_or_else(|| anyhow::anyhow!("could not find client")) - .wrap_error()?; + let client = lookup_client_by_client_id(&mut txn, ¶ms.auth.client_id).await?; + + // Check if it is allowed to use this grant type + if !client.grant_types.contains(&GrantType::AuthorizationCode) { + return Ok(ReplyOrBackToClient::Error(Box::new(UnauthorizedClient))); + } let redirect_uri = client .resolve_redirect_uri(¶ms.auth.redirect_uri) - .wrap_error()?; + .wrap_error()? + .clone(); let response_type = params.auth.response_type; let response_mode = resolve_response_mode(response_type, params.auth.response_mode).wrap_error()?; @@ -392,8 +389,8 @@ async fn get( let grant = new_authorization_grant( &mut txn, - client.client_id.clone(), - redirect_uri.clone(), + client, + redirect_uri, scope, code, params.auth.state, @@ -471,10 +468,10 @@ impl ContinueAuthorizationGrant { pub async fn fetch_authorization_grant( &self, - executor: impl PgExecutor<'_>, + conn: &mut PgConnection, ) -> anyhow::Result> { let data = self.data.parse()?; - get_grant_by_id(executor, data).await + get_grant_by_id(conn, data).await } } diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 1d2f9025..9ed65d67 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{ClientConfig, ClientsConfig, HttpConfig}; -use mas_data_model::TokenType; +use mas_config::{Encrypter, HttpConfig}; +use mas_data_model::{Client, TokenType}; use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; -use mas_storage::oauth2::{ - access_token::lookup_active_access_token, refresh_token::lookup_active_refresh_token, +use mas_storage::{ + oauth2::{ + access_token::lookup_active_access_token, refresh_token::lookup_active_refresh_token, + }, + PostgresqlBackend, }; use mas_warp_utils::{ errors::WrapError, @@ -29,7 +32,7 @@ use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; pub fn filter( pool: &PgPool, - clients_config: &ClientsConfig, + encrypter: &Encrypter, http_config: &HttpConfig, ) -> BoxedFilter<(Box,)> { let audience = UrlBuilder::from(http_config) @@ -41,7 +44,7 @@ pub fn filter( .and( warp::post() .and(connection(pool)) - .and(client_authentication(clients_config, audience)) + .and(client_authentication(pool, encrypter, audience)) .and_then(introspect) .recover(recover) .unify(), @@ -67,7 +70,7 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse { async fn introspect( mut conn: PoolConnection, auth: OAuthClientAuthenticationMethod, - client: ClientConfig, + client: Client, params: IntrospectionRequest, ) -> Result, Rejection> { // Token introspection is only allowed by confidential clients diff --git a/crates/handlers/src/oauth2/mod.rs b/crates/handlers/src/oauth2/mod.rs index abab2a6f..7f3f578c 100644 --- a/crates/handlers/src/oauth2/mod.rs +++ b/crates/handlers/src/oauth2/mod.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use hyper::Method; -use mas_config::{ClientsConfig, Encrypter, HttpConfig}; +use mas_config::{Encrypter, HttpConfig}; use mas_jose::StaticKeystore; use mas_templates::Templates; use mas_warp_utils::filters::cors::cors; @@ -41,15 +41,14 @@ pub fn filter( templates: &Templates, key_store: &Arc, encrypter: &Encrypter, - clients_config: &ClientsConfig, http_config: &HttpConfig, ) -> BoxedFilter<(impl Reply,)> { let discovery = discovery(key_store.as_ref(), http_config); let keys = keys(key_store); - let authorization = authorization(pool, templates, encrypter, clients_config); + let authorization = authorization(pool, templates, encrypter); let userinfo = userinfo(pool); - let introspection = introspection(pool, clients_config, http_config); - let token = token(pool, key_store, clients_config, http_config); + let introspection = introspection(pool, encrypter, http_config); + let token = token(pool, encrypter, key_store, http_config); let filter = discovery .or(keys) diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 23fe5ef1..1a1a294f 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -19,8 +19,8 @@ use chrono::{DateTime, Duration, Utc}; use data_encoding::BASE64URL_NOPAD; use headers::{CacheControl, Pragma}; use hyper::StatusCode; -use mas_config::{ClientConfig, ClientsConfig, HttpConfig}; -use mas_data_model::{AuthorizationGrantStage, TokenType}; +use mas_config::{Encrypter, HttpConfig}; +use mas_data_model::{AuthorizationGrantStage, Client, TokenType}; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::{claims, DecodedJsonWebToken, SigningKeystore, StaticKeystore}; use mas_storage::{ @@ -33,7 +33,7 @@ use mas_storage::{ RefreshTokenLookupError, }, }, - DatabaseInconsistencyError, + DatabaseInconsistencyError, PostgresqlBackend, }; use mas_warp_utils::{ errors::WrapError, @@ -99,8 +99,8 @@ where pub fn filter( pool: &PgPool, + encrypter: &Encrypter, key_store: &Arc, - clients_config: &ClientsConfig, http_config: &HttpConfig, ) -> BoxedFilter<(Box,)> { let key_store = key_store.clone(); @@ -113,7 +113,7 @@ pub fn filter( .and(filters::trace::name("POST /oauth2/token")) .and( warp::post() - .and(client_authentication(clients_config, audience)) + .and(client_authentication(pool, encrypter, audience)) .and(warp::any().map(move || key_store.clone())) .and(warp::any().map(move || issuer.clone())) .and(connection(pool)) @@ -145,7 +145,7 @@ async fn recover(rejection: Rejection) -> Result, Infallible> { async fn token( _auth: OAuthClientAuthenticationMethod, - client: ClientConfig, + client: Client, req: AccessTokenRequest, key_store: Arc, issuer: Url, @@ -185,7 +185,7 @@ fn hash(mut hasher: H, token: &str) -> anyhow::Result { #[allow(clippy::too_many_lines)] async fn authorization_code_grant( grant: &AuthorizationCodeGrant, - client: &ClientConfig, + client: &Client, key_store: &StaticKeystore, issuer: Url, conn: &mut PoolConnection, @@ -349,7 +349,7 @@ async fn authorization_code_grant( async fn refresh_token_grant( grant: &RefreshTokenGrant, - client: &ClientConfig, + client: &Client, conn: &mut PoolConnection, ) -> Result { let mut txn = conn.begin().await.wrap_error()?; diff --git a/crates/handlers/src/views/shared.rs b/crates/handlers/src/views/shared.rs index 002ae52c..c7f4c172 100644 --- a/crates/handlers/src/views/shared.rs +++ b/crates/handlers/src/views/shared.rs @@ -17,7 +17,7 @@ use hyper::Uri; use mas_templates::PostAuthContext; use serde::{Deserialize, Serialize}; -use sqlx::PgExecutor; +use sqlx::PgConnection; use super::super::oauth2::ContinueAuthorizationGrant; @@ -36,11 +36,11 @@ impl PostAuthAction { pub async fn load_context<'e>( &self, - executor: impl PgExecutor<'e>, + conn: &mut PgConnection, ) -> anyhow::Result { match self { Self::ContinueAuthorizationGrant(c) => { - let grant = c.fetch_authorization_grant(executor).await?; + let grant = c.fetch_authorization_grant(conn).await?; let grant = grant.into(); Ok(PostAuthContext::ContinueAuthorizationGrant { grant }) } diff --git a/crates/iana/src/lib.rs b/crates/iana/src/lib.rs index 6bb22b0f..b754dd14 100644 --- a/crates/iana/src/lib.rs +++ b/crates/iana/src/lib.rs @@ -21,3 +21,5 @@ pub mod jose; pub mod oauth; + +pub use parse_display::ParseError; diff --git a/crates/jose/Cargo.toml b/crates/jose/Cargo.toml index 91595252..4ea8159c 100644 --- a/crates/jose/Cargo.toml +++ b/crates/jose/Cargo.toml @@ -31,7 +31,7 @@ sha2 = "0.10.2" signature = "1.4.0" thiserror = "1.0.30" tokio = { version = "1.17.0", features = ["macros", "rt", "sync"] } -tower = "0.4.12" +tower = { version = "0.4.12", features = ["util"] } url = { version = "2.2.2", features = ["serde"] } mas-iana = { path = "../iana" } diff --git a/crates/jose/src/lib.rs b/crates/jose/src/lib.rs index 2fb9340e..56255067 100644 --- a/crates/jose/src/lib.rs +++ b/crates/jose/src/lib.rs @@ -22,6 +22,8 @@ pub(crate) mod jwk; pub(crate) mod jwt; mod keystore; +pub use futures_util::future::Either; + pub use self::{ jwk::{JsonWebKey, JsonWebKeySet}, jwt::{DecodedJsonWebToken, JsonWebTokenParts, JwtHeader}, diff --git a/crates/oauth2-types/src/oidc.rs b/crates/oauth2-types/src/oidc.rs index 04a71f05..e6c74fef 100644 --- a/crates/oauth2-types/src/oidc.rs +++ b/crates/oauth2-types/src/oidc.rs @@ -27,14 +27,21 @@ use url::Url; use crate::requests::{Display, GrantType, ResponseMode}; -#[derive(Serialize, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ApplicationType { + Web, + Native, +} + +#[derive(Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] #[serde(rename_all = "lowercase")] pub enum SubjectType { Public, Pairwise, } -#[derive(Serialize, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] #[serde(rename_all = "lowercase")] pub enum ClaimType { Normal, diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index 71f97a20..e9251303 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -192,6 +192,7 @@ pub struct ClientCredentialsGrant { pub enum GrantType { AuthorizationCode, RefreshToken, + Implicit, ClientCredentials, } diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index e8304a05..c5dcc319 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -7,9 +7,10 @@ license = "Apache-2.0" [dependencies] tokio = "1.17.0" -sqlx = { version = "0.5.11", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline"] } +sqlx = { version = "0.5.11", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline", "json"] } chrono = { version = "0.4.19", features = ["serde"] } serde = { version = "1.0.136", features = ["derive"] } +serde_json = "1.0.79" thiserror = "1.0.30" anyhow = "1.0.55" tracing = "0.1.31" @@ -24,3 +25,4 @@ url = { version = "2.2.2", features = ["serde"] } oauth2-types = { path = "../oauth2-types" } mas-data-model = { path = "../data-model" } mas-iana = { path = "../iana" } +mas-jose = { path = "../jose" } diff --git a/crates/storage/migrations/20220228144045_oauth2_clients.down.sql b/crates/storage/migrations/20220228144045_oauth2_clients.down.sql new file mode 100644 index 00000000..844e3935 --- /dev/null +++ b/crates/storage/migrations/20220228144045_oauth2_clients.down.sql @@ -0,0 +1,17 @@ +-- Copyright 2021 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. + +DROP TABLE oauth2_client_redirect_uris; +DROP TRIGGER set_timestamp ON oauth2_clients; +DROP TABLE oauth2_clients; diff --git a/crates/storage/migrations/20220228144045_oauth2_clients.up.sql b/crates/storage/migrations/20220228144045_oauth2_clients.up.sql new file mode 100644 index 00000000..dcc2da78 --- /dev/null +++ b/crates/storage/migrations/20220228144045_oauth2_clients.up.sql @@ -0,0 +1,51 @@ +-- 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. + +CREATE TABLE oauth2_clients ( + "id" BIGSERIAL PRIMARY KEY, + "client_id" TEXT NOT NULL UNIQUE, + "encrypted_client_secret" TEXT, + "response_types" TEXT[] NOT NULL, + "grant_type_authorization_code" BOOL NOT NULL, + "grant_type_refresh_token" BOOL NOT NULL, + "contacts" TEXT[] NOT NULL, + "client_name" TEXT, + "logo_uri" TEXT, + "client_uri" TEXT, + "policy_uri" TEXT, + "tos_uri" TEXT, + "jwks_uri" TEXT, + "jwks" JSONB, + "id_token_signed_response_alg" TEXT, + "token_endpoint_auth_method" TEXT, + "token_endpoint_auth_signing_alg" TEXT, + "initiate_login_uri" TEXT, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + + -- jwks and jwks_uri can't be set at the same time + CHECK ("jwks" IS NULL OR "jwks_uri" IS NULL) +); + +CREATE TRIGGER set_timestamp + BEFORE UPDATE ON oauth2_clients + FOR EACH ROW + EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TABLE oauth2_client_redirect_uris ( + "id" BIGSERIAL PRIMARY KEY, + "oauth2_client_id" BIGINT NOT NULL REFERENCES oauth2_clients (id) ON DELETE CASCADE, + "redirect_uri" TEXT NOT NULL +); diff --git a/crates/storage/sqlx-data.json b/crates/storage/sqlx-data.json index 15637640..78a43798 100644 --- a/crates/storage/sqlx-data.json +++ b/crates/storage/sqlx-data.json @@ -1,327 +1,348 @@ { "db": "PostgreSQL", "0c056fcc1a85d00db88034bcc582376cf220e1933d2932e520c44ed9931f5c9d": { - "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_session_id, oauth2_access_token_id, token)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "created_at", + "ordinal": 1, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false + ], "parameters": { "Left": [ "Int8", "Int8", "Text" ] - }, + } + }, + "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_session_id, oauth2_access_token_id, token)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n " + }, + "11f29a7b467bef1cf483d91eede7849707e01847542e4fc3c1be702560bf36bf": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], "nullable": [ - false, false - ] - } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "TextArray", + "Bool", + "Bool", + "Text", + "Jsonb", + "Text" + ] + } + }, + "query": "\n INSERT INTO oauth2_clients\n (client_id,\n encrypted_client_secret,\n response_types,\n grant_type_authorization_code,\n grant_type_refresh_token,\n token_endpoint_auth_method,\n jwks,\n jwks_uri,\n contacts)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, '{}')\n RETURNING id\n " }, "16df03346a3186c289bd64d1a3869103064ddb8f8827af8f19fc9ab93910ede5": { - "query": "\n SELECT\n rt.id AS refresh_token_id,\n rt.token AS refresh_token,\n rt.created_at AS refresh_token_created_at,\n at.id AS \"access_token_id?\",\n at.token AS \"access_token?\",\n at.expires_after AS \"access_token_expires_after?\",\n at.created_at AS \"access_token_created_at?\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM oauth2_refresh_tokens rt\n LEFT JOIN oauth2_access_tokens at\n ON at.id = rt.oauth2_access_token_id\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE rt.token = $1\n AND rt.next_token_id IS NULL\n AND us.active\n AND os.ended_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "refresh_token_id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "refresh_token", + "ordinal": 1, "type_info": "Text" }, { - "ordinal": 2, "name": "refresh_token_created_at", + "ordinal": 2, "type_info": "Timestamptz" }, { - "ordinal": 3, "name": "access_token_id?", + "ordinal": 3, "type_info": "Int8" }, { - "ordinal": 4, "name": "access_token?", + "ordinal": 4, "type_info": "Text" }, { - "ordinal": 5, "name": "access_token_expires_after?", + "ordinal": 5, "type_info": "Int4" }, { - "ordinal": 6, "name": "access_token_created_at?", + "ordinal": 6, "type_info": "Timestamptz" }, { - "ordinal": 7, "name": "session_id!", + "ordinal": 7, "type_info": "Int8" }, { - "ordinal": 8, "name": "client_id!", + "ordinal": 8, "type_info": "Text" }, { - "ordinal": 9, "name": "scope!", + "ordinal": 9, "type_info": "Text" }, { - "ordinal": 10, "name": "user_session_id!", + "ordinal": 10, "type_info": "Int8" }, { - "ordinal": 11, "name": "user_session_created_at!", + "ordinal": 11, "type_info": "Timestamptz" }, { - "ordinal": 12, "name": "user_id!", + "ordinal": 12, "type_info": "Int8" }, { - "ordinal": 13, "name": "user_username!", + "ordinal": 13, "type_info": "Text" }, { - "ordinal": 14, "name": "user_session_last_authentication_id?", + "ordinal": 14, "type_info": "Int8" }, { - "ordinal": 15, "name": "user_session_last_authentication_created_at?", + "ordinal": 15, "type_info": "Timestamptz" }, { - "ordinal": 16, "name": "user_email_id?", + "ordinal": 16, "type_info": "Int8" }, { - "ordinal": 17, "name": "user_email?", + "ordinal": 17, "type_info": "Text" }, { - "ordinal": 18, "name": "user_email_created_at?", + "ordinal": 18, "type_info": "Timestamptz" }, { - "ordinal": 19, "name": "user_email_confirmed_at?", + "ordinal": 19, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ] - } + } + }, + "query": "\n SELECT\n rt.id AS refresh_token_id,\n rt.token AS refresh_token,\n rt.created_at AS refresh_token_created_at,\n at.id AS \"access_token_id?\",\n at.token AS \"access_token?\",\n at.expires_after AS \"access_token_expires_after?\",\n at.created_at AS \"access_token_created_at?\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM oauth2_refresh_tokens rt\n LEFT JOIN oauth2_access_tokens at\n ON at.id = rt.oauth2_access_token_id\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE rt.token = $1\n AND rt.next_token_id IS NULL\n AND us.active\n AND os.ended_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " }, "2e8c6507df6c0af78deca3550157b9cc0286f204b15a646c2e7e24c51100e040": { - "query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "grant_id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "grant_created_at", + "ordinal": 1, "type_info": "Timestamptz" }, { - "ordinal": 2, "name": "grant_cancelled_at", + "ordinal": 2, "type_info": "Timestamptz" }, { - "ordinal": 3, "name": "grant_fulfilled_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "grant_exchanged_at", + "ordinal": 4, "type_info": "Timestamptz" }, { - "ordinal": 5, "name": "grant_scope", + "ordinal": 5, "type_info": "Text" }, { - "ordinal": 6, "name": "grant_state", + "ordinal": 6, "type_info": "Text" }, { - "ordinal": 7, "name": "grant_redirect_uri", + "ordinal": 7, "type_info": "Text" }, { - "ordinal": 8, "name": "grant_response_mode", + "ordinal": 8, "type_info": "Text" }, { - "ordinal": 9, "name": "grant_nonce", + "ordinal": 9, "type_info": "Text" }, { - "ordinal": 10, "name": "grant_max_age", + "ordinal": 10, "type_info": "Int4" }, { - "ordinal": 11, "name": "grant_acr_values", + "ordinal": 11, "type_info": "Text" }, { - "ordinal": 12, "name": "client_id", + "ordinal": 12, "type_info": "Text" }, { - "ordinal": 13, "name": "grant_code", + "ordinal": 13, "type_info": "Text" }, { - "ordinal": 14, "name": "grant_response_type_code", + "ordinal": 14, "type_info": "Bool" }, { - "ordinal": 15, "name": "grant_response_type_token", + "ordinal": 15, "type_info": "Bool" }, { - "ordinal": 16, "name": "grant_response_type_id_token", + "ordinal": 16, "type_info": "Bool" }, { - "ordinal": 17, "name": "grant_code_challenge", + "ordinal": 17, "type_info": "Text" }, { - "ordinal": 18, "name": "grant_code_challenge_method", + "ordinal": 18, "type_info": "Text" }, { - "ordinal": 19, "name": "session_id?", + "ordinal": 19, "type_info": "Int8" }, { - "ordinal": 20, "name": "user_session_id?", + "ordinal": 20, "type_info": "Int8" }, { - "ordinal": 21, "name": "user_session_created_at?", + "ordinal": 21, "type_info": "Timestamptz" }, { - "ordinal": 22, "name": "user_id?", + "ordinal": 22, "type_info": "Int8" }, { - "ordinal": 23, "name": "user_username?", + "ordinal": 23, "type_info": "Text" }, { - "ordinal": 24, "name": "user_session_last_authentication_id?", + "ordinal": 24, "type_info": "Int8" }, { - "ordinal": 25, "name": "user_session_last_authentication_created_at?", + "ordinal": 25, "type_info": "Timestamptz" }, { - "ordinal": 26, "name": "user_email_id?", + "ordinal": 26, "type_info": "Int8" }, { - "ordinal": 27, "name": "user_email?", + "ordinal": 27, "type_info": "Text" }, { - "ordinal": 28, "name": "user_email_created_at?", + "ordinal": 28, "type_info": "Timestamptz" }, { - "ordinal": 29, "name": "user_email_confirmed_at?", + "ordinal": 29, "type_info": "Timestamptz" } ], - "parameters": { - "Left": [ - "Int8" - ] - }, "nullable": [ false, false, @@ -353,50 +374,59 @@ false, false, true - ] - } - }, - "307fd9f71e7a94a0a0d9ce523ee9792e127485d0d12480c43f179dd9b75afbab": { - "query": "\n INSERT INTO user_sessions (user_id)\n VALUES ($1)\n RETURNING id, created_at\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "created_at", - "type_info": "Timestamptz" - } ], "parameters": { "Left": [ "Int8" ] - }, - "nullable": [ - false, - false - ] - } + } + }, + "query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " }, - "38641231a3bff71252e8bc0ead3a033c9148762ea64d707642551c01a4c89b84": { - "query": "\n INSERT INTO oauth2_authorization_grants\n (client_id, redirect_uri, scope, state, nonce, max_age,\n acr_values, response_mode, code_challenge, code_challenge_method,\n response_type_code, response_type_token, response_type_id_token,\n code)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n RETURNING id, created_at\n ", + "307fd9f71e7a94a0a0d9ce523ee9792e127485d0d12480c43f179dd9b75afbab": { "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "created_at", + "ordinal": 1, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n INSERT INTO user_sessions (user_id)\n VALUES ($1)\n RETURNING id, created_at\n " + }, + "38641231a3bff71252e8bc0ead3a033c9148762ea64d707642551c01a4c89b84": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "created_at", + "ordinal": 1, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false + ], "parameters": { "Left": [ "Text", @@ -414,163 +444,291 @@ "Bool", "Text" ] - }, + } + }, + "query": "\n INSERT INTO oauth2_authorization_grants\n (client_id, redirect_uri, scope, state, nonce, max_age,\n acr_values, response_mode, code_challenge, code_challenge_method,\n response_type_code, response_type_token, response_type_id_token,\n code)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n RETURNING id, created_at\n " + }, + "41b5ecd6860791ac6f90417ac51eb977b8c69a3dd81af4672b2592efb65963eb": { + "describe": { + "columns": [ + { + "name": "user_email_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "user_email", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "user_email_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at", + "ordinal": 3, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n\n ORDER BY ue.email ASC\n " + }, + "494c17c20047e761b0dbdac0e13854af7955743afd970bbcae83ba944838c58e": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "username", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "last_authentication_id?", + "ordinal": 4, + "type_info": "Int8" + }, + { + "name": "last_authd_at?", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "user_email_id?", + "ordinal": 6, + "type_info": "Int8" + }, + { + "name": "user_email?", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 9, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT\n s.id,\n u.id AS user_id,\n u.username,\n s.created_at,\n a.id AS \"last_authentication_id?\",\n a.created_at AS \"last_authd_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM user_sessions s\n INNER JOIN users u \n ON s.user_id = u.id\n LEFT JOIN user_session_authentications a\n ON a.session_id = s.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n WHERE s.id = $1 AND s.active\n ORDER BY a.created_at DESC\n LIMIT 1\n " + }, + "4a9ed96857bbebbb56400301f6fb1c727a8a754f52d3d67a53d57a4cd9bcdca8": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "client_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "encrypted_client_secret", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "redirect_uris!", + "ordinal": 3, + "type_info": "TextArray" + }, + { + "name": "response_types", + "ordinal": 4, + "type_info": "TextArray" + }, + { + "name": "grant_type_authorization_code", + "ordinal": 5, + "type_info": "Bool" + }, + { + "name": "grant_type_refresh_token", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "contacts", + "ordinal": 7, + "type_info": "TextArray" + }, + { + "name": "client_name", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "logo_uri", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "client_uri", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "policy_uri", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "tos_uri", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "jwks_uri", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "jwks", + "ordinal": 14, + "type_info": "Jsonb" + }, + { + "name": "id_token_signed_response_alg", + "ordinal": 15, + "type_info": "Text" + }, + { + "name": "token_endpoint_auth_method", + "ordinal": 16, + "type_info": "Text" + }, + { + "name": "token_endpoint_auth_signing_alg", + "ordinal": 17, + "type_info": "Text" + }, + { + "name": "initiate_login_uri", + "ordinal": 18, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + true, + null, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n c.id,\n c.client_id,\n c.encrypted_client_secret,\n ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS \"redirect_uris!\",\n c.response_types,\n c.grant_type_authorization_code,\n c.grant_type_refresh_token,\n c.contacts,\n c.client_name,\n c.logo_uri,\n c.client_uri,\n c.policy_uri,\n c.tos_uri,\n c.jwks_uri,\n c.jwks,\n c.id_token_signed_response_alg,\n c.token_endpoint_auth_method,\n c.token_endpoint_auth_signing_alg,\n c.initiate_login_uri\n FROM oauth2_clients c\n\n WHERE c.client_id = $1\n " + }, + "4b9de6face2e21117c947b4f550cc747ad8397b6dfadb6bc6a84124763dc66e8": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET primary_email_id = user_emails.id \n FROM user_emails\n WHERE user_emails.id = $1\n AND users.id = user_emails.user_id\n " + }, + "581243a7f0c033548cc9644e0c60855ecb8bfefe51779eb135dd7547b886de79": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE oauth2_sessions\n SET ended_at = NOW()\n WHERE id = $1\n " + }, + "59e8a5de682642883a9b9fc1b522736fa4397f0a0c97074f2c8908e5956c0166": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "created_at", + "ordinal": 1, + "type_info": "Timestamptz" + } + ], "nullable": [ false, false - ] - } - }, - "41b5ecd6860791ac6f90417ac51eb977b8c69a3dd81af4672b2592efb65963eb": { - "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n\n ORDER BY ue.email ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "user_email_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_email", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "user_email_created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 3, - "name": "user_email_confirmed_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true - ] - } - }, - "494c17c20047e761b0dbdac0e13854af7955743afd970bbcae83ba944838c58e": { - "query": "\n SELECT\n s.id,\n u.id AS user_id,\n u.username,\n s.created_at,\n a.id AS \"last_authentication_id?\",\n a.created_at AS \"last_authd_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM user_sessions s\n INNER JOIN users u \n ON s.user_id = u.id\n LEFT JOIN user_session_authentications a\n ON a.session_id = s.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n WHERE s.id = $1 AND s.active\n ORDER BY a.created_at DESC\n LIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "username", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "last_authentication_id?", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "last_authd_at?", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "user_email_id?", - "type_info": "Int8" - }, - { - "ordinal": 7, - "name": "user_email?", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "user_email_created_at?", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "user_email_confirmed_at?", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ] - } - }, - "4b9de6face2e21117c947b4f550cc747ad8397b6dfadb6bc6a84124763dc66e8": { - "query": "\n UPDATE users\n SET primary_email_id = user_emails.id \n FROM user_emails\n WHERE user_emails.id = $1\n AND users.id = user_emails.user_id\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "581243a7f0c033548cc9644e0c60855ecb8bfefe51779eb135dd7547b886de79": { - "query": "\n UPDATE oauth2_sessions\n SET ended_at = NOW()\n WHERE id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "59e8a5de682642883a9b9fc1b522736fa4397f0a0c97074f2c8908e5956c0166": { - "query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_session_id, token, expires_after)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "created_at", - "type_info": "Timestamptz" - } ], "parameters": { "Left": [ @@ -578,320 +736,461 @@ "Text", "Int4" ] - }, - "nullable": [ - false, - false - ] - } + } + }, + "query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_session_id, token, expires_after)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n " }, "5d1a17b2ad6153217551ae31549ad9d62cc39d2f9a4e62a7ccb60fd91e0ac685": { - "query": "\n DELETE FROM oauth2_access_tokens\n WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()\n ", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [] - }, - "nullable": [] - } + } + }, + "query": "\n DELETE FROM oauth2_access_tokens\n WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()\n " }, "647a2a5bbde39d0ed3931d0287b468bc7dedf6171e1dc6171a5d9f079b9ed0fa": { - "query": "\n SELECT up.hashed_password\n FROM user_passwords up\n WHERE up.user_id = $1\n ORDER BY up.created_at DESC\n LIMIT 1\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "hashed_password", + "ordinal": 0, "type_info": "Text" } ], - "parameters": { - "Left": [ - "Int8" - ] - }, "nullable": [ false - ] - } + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT up.hashed_password\n FROM user_passwords up\n WHERE up.user_id = $1\n ORDER BY up.created_at DESC\n LIMIT 1\n " }, "6da88febe6d8e45787cdd609dcea5f51dc601f4dffb07dd4c5d699c7d4c5b2d1": { - "query": "\n INSERT INTO user_emails (user_id, email)\n VALUES ($1, $2)\n RETURNING \n id AS user_email_id,\n email AS user_email,\n created_at AS user_email_created_at,\n confirmed_at AS user_email_confirmed_at\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "user_email_id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "user_email", + "ordinal": 1, "type_info": "Text" }, { - "ordinal": 2, "name": "user_email_created_at", + "ordinal": 2, "type_info": "Timestamptz" }, { - "ordinal": 3, "name": "user_email_confirmed_at", + "ordinal": 3, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false, + false, + true + ], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [ - false, - false, - false, - true - ] - } + } + }, + "query": "\n INSERT INTO user_emails (user_id, email)\n VALUES ($1, $2)\n RETURNING \n id AS user_email_id,\n email AS user_email,\n created_at AS user_email_created_at,\n confirmed_at AS user_email_confirmed_at\n " }, "703850ba4e001d53776d77a64cbc1ee6feb61485ce41aff1103251f9b3778128": { - "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.id = $1 AND os.id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime\"\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "fulfilled_at!: DateTime", + "ordinal": 0, "type_info": "Timestamptz" } ], + "nullable": [ + true + ], "parameters": { "Left": [ "Int8", "Int8" ] - }, - "nullable": [ - true - ] - } + } + }, + "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.id = $1 AND os.id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime\"\n " }, - "7de9cfa6e90ba20f5b298ea387cf13a7e40d0f5b3eb903a80d06fbe33074d596": { - "query": "\n UPDATE user_emails\n SET confirmed_at = NOW()\n WHERE id = $1\n RETURNING confirmed_at\n ", + "782d74cacafeb173f8dba51c9a94708a563b72dddc45c9aca22efe787ce8c444": { "describe": { "columns": [ { + "name": "id", "ordinal": 0, - "name": "confirmed_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - true - ] - } - }, - "88ac8783bd5881c42eafd9cf87a16fe6031f3153fd6a8618e689694584aeb2de": { - "query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "99a1504e3cf80fb4eaad40e8593ac722ba1da7ee29ae674fa9ffe37dffa8b361": { - "query": "\n INSERT INTO user_email_verifications (user_email_id, code)\n VALUES ($1, $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - }, - "nullable": [] - } - }, - "a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": { - "query": "UPDATE user_sessions SET active = FALSE WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "aea289a04e151da235825305a5085bc6aa100fce139dbf10a2c1bed4867fc52a": { - "query": "\n SELECT \n u.id AS user_id, \n u.username AS user_username,\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM users u\n\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE u.username = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "user_id", "type_info": "Int8" }, { + "name": "client_id", "ordinal": 1, - "name": "user_username", "type_info": "Text" }, { + "name": "encrypted_client_secret", "ordinal": 2, - "name": "user_email_id?", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "user_email?", "type_info": "Text" }, { + "name": "redirect_uris!", + "ordinal": 3, + "type_info": "TextArray" + }, + { + "name": "response_types", "ordinal": 4, - "name": "user_email_created_at?", - "type_info": "Timestamptz" + "type_info": "TextArray" }, { + "name": "grant_type_authorization_code", "ordinal": 5, - "name": "user_email_confirmed_at?", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true - ] - } - }, - "b0fec01072df856ba9cd8be0ecf7a58dd4709a0efca4035a2c6f99c43d5a12be": { - "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.id = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "user_email_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_email", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "user_email_created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 3, - "name": "user_email_confirmed_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true - ] - } - }, - "ba431a27a4b256ceacb5724bd746424ed1f059e59ae1aa818fdd5f44c01d70a0": { - "query": "\n UPDATE user_email_verifications\n SET consumed_at = NOW()\n WHERE id = $1\n RETURNING consumed_at AS \"consumed_at!\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "consumed_at!", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - true - ] - } - }, - "bb5ad7b64a2901a0d94ec22e6b2e497c6d2748e644478a7345da94a61b4ed053": { - "query": "\n SELECT\n ev.id AS \"verification_id\",\n (ev.created_at + $2 < NOW()) AS \"verification_expired!\",\n ev.created_at AS \"verification_created_at\",\n ev.consumed_at AS \"verification_consumed_at\",\n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_email_verifications ev\n INNER JOIN user_emails ue\n ON ue.id = ev.user_email_id\n WHERE ev.code = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "verification_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "verification_expired!", "type_info": "Bool" }, { - "ordinal": 2, - "name": "verification_created_at", - "type_info": "Timestamptz" + "name": "grant_type_refresh_token", + "ordinal": 6, + "type_info": "Bool" }, { - "ordinal": 3, - "name": "verification_consumed_at", - "type_info": "Timestamptz" + "name": "contacts", + "ordinal": 7, + "type_info": "TextArray" }, { - "ordinal": 4, - "name": "user_email_id", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "user_email", + "name": "client_name", + "ordinal": 8, "type_info": "Text" }, { - "ordinal": 6, - "name": "user_email_created_at", - "type_info": "Timestamptz" + "name": "logo_uri", + "ordinal": 9, + "type_info": "Text" }, { - "ordinal": 7, - "name": "user_email_confirmed_at", - "type_info": "Timestamptz" + "name": "client_uri", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "policy_uri", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "tos_uri", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "jwks_uri", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "jwks", + "ordinal": 14, + "type_info": "Jsonb" + }, + { + "name": "id_token_signed_response_alg", + "ordinal": 15, + "type_info": "Text" + }, + { + "name": "token_endpoint_auth_method", + "ordinal": 16, + "type_info": "Text" + }, + { + "name": "token_endpoint_auth_signing_alg", + "ordinal": 17, + "type_info": "Text" + }, + { + "name": "initiate_login_uri", + "ordinal": 18, + "type_info": "Text" } ], + "nullable": [ + false, + false, + true, + null, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true + ], "parameters": { "Left": [ - "Text", - "Interval" + "Int8" ] - }, + } + }, + "query": "\n SELECT\n c.id,\n c.client_id,\n c.encrypted_client_secret,\n ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS \"redirect_uris!\",\n c.response_types,\n c.grant_type_authorization_code,\n c.grant_type_refresh_token,\n c.contacts,\n c.client_name,\n c.logo_uri,\n c.client_uri,\n c.policy_uri,\n c.tos_uri,\n c.jwks_uri,\n c.jwks,\n c.id_token_signed_response_alg,\n c.token_endpoint_auth_method,\n c.token_endpoint_auth_signing_alg,\n c.initiate_login_uri\n FROM oauth2_clients c\n\n WHERE c.id = $1\n " + }, + "7de9cfa6e90ba20f5b298ea387cf13a7e40d0f5b3eb903a80d06fbe33074d596": { + "describe": { + "columns": [ + { + "name": "confirmed_at", + "ordinal": 0, + "type_info": "Timestamptz" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE user_emails\n SET confirmed_at = NOW()\n WHERE id = $1\n RETURNING confirmed_at\n " + }, + "88ac8783bd5881c42eafd9cf87a16fe6031f3153fd6a8618e689694584aeb2de": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n " + }, + "99a1504e3cf80fb4eaad40e8593ac722ba1da7ee29ae674fa9ffe37dffa8b361": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + } + }, + "query": "\n INSERT INTO user_email_verifications (user_email_id, code)\n VALUES ($1, $2)\n " + }, + "a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "UPDATE user_sessions SET active = FALSE WHERE id = $1" + }, + "a2bb4dcf950385e843068c7b77db08118ec892d0d24d05da6ac9263101c340b6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [] + } + }, + "query": "TRUNCATE oauth2_client_redirect_uris, oauth2_clients" + }, + "a80c14ba82cfc29493048d9e9578ec5ca482c9228efc7c7212dae4fed86b8367": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + } + }, + "query": "\n INSERT INTO oauth2_client_redirect_uris (oauth2_client_id, redirect_uri)\n SELECT $1, uri FROM UNNEST($2::text[]) uri\n " + }, + "aea289a04e151da235825305a5085bc6aa100fce139dbf10a2c1bed4867fc52a": { + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "user_username", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "user_email_id?", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "user_email?", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 5, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT \n u.id AS user_id, \n u.username AS user_username,\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM users u\n\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE u.username = $1\n " + }, + "b0fec01072df856ba9cd8be0ecf7a58dd4709a0efca4035a2c6f99c43d5a12be": { + "describe": { + "columns": [ + { + "name": "user_email_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "user_email", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "user_email_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at", + "ordinal": 3, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.id = $2\n " + }, + "ba431a27a4b256ceacb5724bd746424ed1f059e59ae1aa818fdd5f44c01d70a0": { + "describe": { + "columns": [ + { + "name": "consumed_at!", + "ordinal": 0, + "type_info": "Timestamptz" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE user_email_verifications\n SET consumed_at = NOW()\n WHERE id = $1\n RETURNING consumed_at AS \"consumed_at!\"\n " + }, + "bb5ad7b64a2901a0d94ec22e6b2e497c6d2748e644478a7345da94a61b4ed053": { + "describe": { + "columns": [ + { + "name": "verification_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "verification_expired!", + "ordinal": 1, + "type_info": "Bool" + }, + { + "name": "verification_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "verification_consumed_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "user_email_id", + "ordinal": 4, + "type_info": "Int8" + }, + { + "name": "user_email", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "user_email_created_at", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at", + "ordinal": 7, + "type_info": "Timestamptz" + } + ], "nullable": [ false, null, @@ -901,144 +1200,145 @@ false, false, true - ] - } + ], + "parameters": { + "Left": [ + "Text", + "Interval" + ] + } + }, + "query": "\n SELECT\n ev.id AS \"verification_id\",\n (ev.created_at + $2 < NOW()) AS \"verification_expired!\",\n ev.created_at AS \"verification_created_at\",\n ev.consumed_at AS \"verification_consumed_at\",\n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_email_verifications ev\n INNER JOIN user_emails ue\n ON ue.id = ev.user_email_id\n WHERE ev.code = $1\n " }, "c29e741474aacc91c0aacc028a9e7452a5327d5ce6d4b791bf20a2636069087e": { - "query": "\n INSERT INTO oauth2_sessions\n (user_session_id, client_id, scope)\n SELECT\n $1,\n og.client_id,\n og.scope\n FROM\n oauth2_authorization_grants og\n WHERE\n og.id = $2\n RETURNING id, created_at\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "created_at", + "ordinal": 1, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false + ], "parameters": { "Left": [ "Int8", "Int8" ] - }, - "nullable": [ - false, - false - ] - } + } + }, + "query": "\n INSERT INTO oauth2_sessions\n (user_session_id, client_id, scope)\n SELECT\n $1,\n og.client_id,\n og.scope\n FROM\n oauth2_authorization_grants og\n WHERE\n og.id = $2\n RETURNING id, created_at\n " }, "c2c402cfe0adcafa615f14a499caba4c96ca71d9ffb163e1feb05e5d85f3462c": { - "query": "\n UPDATE oauth2_refresh_tokens\n SET next_token_id = $2\n WHERE id = $1\n ", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Int8", "Int8" ] - }, - "nullable": [] - } + } + }, + "query": "\n UPDATE oauth2_refresh_tokens\n SET next_token_id = $2\n WHERE id = $1\n " }, "cf1b7513a56d20e405bf11f806adea2853a08ee05497c952bb3ee1dadc866d4b": { - "query": "\n SELECT\n at.id AS \"access_token_id\",\n at.token AS \"access_token\",\n at.expires_after AS \"access_token_expires_after\",\n at.created_at AS \"access_token_created_at\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n ON os.id = at.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE at.token = $1\n AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()\n AND us.active\n AND os.ended_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "access_token_id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "access_token", + "ordinal": 1, "type_info": "Text" }, { - "ordinal": 2, "name": "access_token_expires_after", + "ordinal": 2, "type_info": "Int4" }, { - "ordinal": 3, "name": "access_token_created_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "session_id!", + "ordinal": 4, "type_info": "Int8" }, { - "ordinal": 5, "name": "client_id!", + "ordinal": 5, "type_info": "Text" }, { - "ordinal": 6, "name": "scope!", + "ordinal": 6, "type_info": "Text" }, { - "ordinal": 7, "name": "user_session_id!", + "ordinal": 7, "type_info": "Int8" }, { - "ordinal": 8, "name": "user_session_created_at!", + "ordinal": 8, "type_info": "Timestamptz" }, { - "ordinal": 9, "name": "user_id!", + "ordinal": 9, "type_info": "Int8" }, { - "ordinal": 10, "name": "user_username!", + "ordinal": 10, "type_info": "Text" }, { - "ordinal": 11, "name": "user_session_last_authentication_id?", + "ordinal": 11, "type_info": "Int8" }, { - "ordinal": 12, "name": "user_session_last_authentication_created_at?", + "ordinal": 12, "type_info": "Timestamptz" }, { - "ordinal": 13, "name": "user_email_id?", + "ordinal": 13, "type_info": "Int8" }, { - "ordinal": 14, "name": "user_email?", + "ordinal": 14, "type_info": "Text" }, { - "ordinal": 15, "name": "user_email_created_at?", + "ordinal": 15, "type_info": "Timestamptz" }, { - "ordinal": 16, "name": "user_email_confirmed_at?", + "ordinal": 16, "type_info": "Timestamptz" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ false, false, @@ -1057,181 +1357,181 @@ false, false, true - ] - } + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n at.id AS \"access_token_id\",\n at.token AS \"access_token\",\n at.expires_after AS \"access_token_expires_after\",\n at.created_at AS \"access_token_created_at\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n ON os.id = at.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE at.token = $1\n AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()\n AND us.active\n AND os.ended_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " }, "d2f767218ec2489058db9a0382ca0eea20379c30aeae9f492da4ba35b66f4dc7": { - "query": "\n DELETE FROM user_emails\n WHERE user_emails.id = $1\n ", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Int8" ] - }, - "nullable": [] - } + } + }, + "query": "\n DELETE FROM user_emails\n WHERE user_emails.id = $1\n " }, "d3883020ad9a0e5ea72fb9ddd2801a067209488a6ef3179afbc8173e4cc729de": { - "query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "grant_id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "grant_created_at", + "ordinal": 1, "type_info": "Timestamptz" }, { - "ordinal": 2, "name": "grant_cancelled_at", + "ordinal": 2, "type_info": "Timestamptz" }, { - "ordinal": 3, "name": "grant_fulfilled_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "grant_exchanged_at", + "ordinal": 4, "type_info": "Timestamptz" }, { - "ordinal": 5, "name": "grant_scope", + "ordinal": 5, "type_info": "Text" }, { - "ordinal": 6, "name": "grant_state", + "ordinal": 6, "type_info": "Text" }, { - "ordinal": 7, "name": "grant_redirect_uri", + "ordinal": 7, "type_info": "Text" }, { - "ordinal": 8, "name": "grant_response_mode", + "ordinal": 8, "type_info": "Text" }, { - "ordinal": 9, "name": "grant_nonce", + "ordinal": 9, "type_info": "Text" }, { - "ordinal": 10, "name": "grant_max_age", + "ordinal": 10, "type_info": "Int4" }, { - "ordinal": 11, "name": "grant_acr_values", + "ordinal": 11, "type_info": "Text" }, { - "ordinal": 12, "name": "client_id", + "ordinal": 12, "type_info": "Text" }, { - "ordinal": 13, "name": "grant_code", + "ordinal": 13, "type_info": "Text" }, { - "ordinal": 14, "name": "grant_response_type_code", + "ordinal": 14, "type_info": "Bool" }, { - "ordinal": 15, "name": "grant_response_type_token", + "ordinal": 15, "type_info": "Bool" }, { - "ordinal": 16, "name": "grant_response_type_id_token", + "ordinal": 16, "type_info": "Bool" }, { - "ordinal": 17, "name": "grant_code_challenge", + "ordinal": 17, "type_info": "Text" }, { - "ordinal": 18, "name": "grant_code_challenge_method", + "ordinal": 18, "type_info": "Text" }, { - "ordinal": 19, "name": "session_id?", + "ordinal": 19, "type_info": "Int8" }, { - "ordinal": 20, "name": "user_session_id?", + "ordinal": 20, "type_info": "Int8" }, { - "ordinal": 21, "name": "user_session_created_at?", + "ordinal": 21, "type_info": "Timestamptz" }, { - "ordinal": 22, "name": "user_id?", + "ordinal": 22, "type_info": "Int8" }, { - "ordinal": 23, "name": "user_username?", + "ordinal": 23, "type_info": "Text" }, { - "ordinal": 24, "name": "user_session_last_authentication_id?", + "ordinal": 24, "type_info": "Int8" }, { - "ordinal": 25, "name": "user_session_last_authentication_created_at?", + "ordinal": 25, "type_info": "Timestamptz" }, { - "ordinal": 26, "name": "user_email_id?", + "ordinal": 26, "type_info": "Int8" }, { - "ordinal": 27, "name": "user_email?", + "ordinal": 27, "type_info": "Text" }, { - "ordinal": 28, "name": "user_email_created_at?", + "ordinal": 28, "type_info": "Timestamptz" }, { - "ordinal": 29, "name": "user_email_confirmed_at?", + "ordinal": 29, "type_info": "Timestamptz" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ false, false, @@ -1263,145 +1563,151 @@ false, false, true - ] - } + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " }, "d604e13bdfb2ff3d354d995f0b68f04091847755db98bafea7c45bd7b5c4ab68": { - "query": "\n UPDATE oauth2_authorization_grants\n SET\n exchanged_at = NOW()\n WHERE\n id = $1\n RETURNING exchanged_at AS \"exchanged_at!: DateTime\"\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "exchanged_at!: DateTime", + "ordinal": 0, "type_info": "Timestamptz" } ], + "nullable": [ + true + ], "parameters": { "Left": [ "Int8" ] - }, - "nullable": [ - true - ] - } + } + }, + "query": "\n UPDATE oauth2_authorization_grants\n SET\n exchanged_at = NOW()\n WHERE\n id = $1\n RETURNING exchanged_at AS \"exchanged_at!: DateTime\"\n " }, "d7200c0def0662fda4af259c7872e06b8208e36f320ca90ea781c13d2bf85a9f": { - "query": "\n INSERT INTO user_passwords (user_id, hashed_password)\n VALUES ($1, $2)\n ", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "\n INSERT INTO user_passwords (user_id, hashed_password)\n VALUES ($1, $2)\n " }, "d9d27eb4a0c11818a636d407438c4bc567a39396e7e236b3e776504417988eab": { - "query": "\n INSERT INTO user_session_authentications (session_id)\n VALUES ($1)\n RETURNING id, created_at\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "created_at", + "ordinal": 1, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false + ], "parameters": { "Left": [ "Int8" ] - }, - "nullable": [ - false, - false - ] - } + } + }, + "query": "\n INSERT INTO user_session_authentications (session_id)\n VALUES ($1)\n RETURNING id, created_at\n " }, "db34b3d7fa5d824e63f388d660615d748e11c1406e8166da907e0a54a665e37a": { - "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.email = $2\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "user_email_id", + "ordinal": 0, "type_info": "Int8" }, { - "ordinal": 1, "name": "user_email", + "ordinal": 1, "type_info": "Text" }, { - "ordinal": 2, "name": "user_email_created_at", + "ordinal": 2, "type_info": "Timestamptz" }, { - "ordinal": 3, "name": "user_email_confirmed_at", + "ordinal": 3, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false, + false, + true + ], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [ - false, - false, - false, - true - ] - } + } + }, + "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.email = $2\n " }, "dda03ba41249bff965cb8f129acc15f4e40807adb9b75dee0ac43edd7809de84": { - "query": "\n INSERT INTO users (username)\n VALUES ($1)\n RETURNING id\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Int8" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "\n INSERT INTO users (username)\n VALUES ($1)\n RETURNING id\n " }, "e5cd99bdaf9c678fc659431fecc5d76b25bb08b781fd17e50eda82ea3aa8cea8": { - "query": "\n SELECT COUNT(*) as \"count!\"\n FROM user_sessions s\n WHERE s.user_id = $1 AND s.active\n ", "describe": { "columns": [ { - "ordinal": 0, "name": "count!", + "ordinal": 0, "type_info": "Int8" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Int8" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "\n SELECT COUNT(*) as \"count!\"\n FROM user_sessions s\n WHERE s.user_id = $1 AND s.active\n " } } \ No newline at end of file diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index b2f0f7a8..efe1390e 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -41,7 +41,7 @@ impl StorageBackend for PostgresqlBackend { type AuthenticationData = i64; type AuthorizationGrantData = i64; type BrowserSessionData = i64; - type ClientData = (); + type ClientData = i64; type RefreshTokenData = i64; type SessionData = i64; type UserData = i64; diff --git a/crates/storage/src/oauth2/access_token.rs b/crates/storage/src/oauth2/access_token.rs index 104d2064..cfa271b1 100644 --- a/crates/storage/src/oauth2/access_token.rs +++ b/crates/storage/src/oauth2/access_token.rs @@ -14,12 +14,11 @@ use anyhow::Context; use chrono::{DateTime, Duration, Utc}; -use mas_data_model::{ - AccessToken, Authentication, BrowserSession, Client, Session, User, UserEmail, -}; -use sqlx::PgExecutor; +use mas_data_model::{AccessToken, Authentication, BrowserSession, Session, User, UserEmail}; +use sqlx::{PgConnection, PgExecutor}; use thiserror::Error; +use super::client::{lookup_client_by_client_id, ClientFetchError}; use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend}; pub async fn add_access_token( @@ -83,6 +82,7 @@ pub struct OAuth2AccessTokenLookup { #[error("failed to lookup access token")] pub enum AccessTokenLookupError { Database(#[from] sqlx::Error), + ClientFetch(#[from] ClientFetchError), Inconsistency(#[from] DatabaseInconsistencyError), } @@ -95,7 +95,7 @@ impl AccessTokenLookupError { #[allow(clippy::too_many_lines)] pub async fn lookup_active_access_token( - executor: impl PgExecutor<'_>, + conn: &mut PgConnection, token: &str, ) -> Result<(AccessToken, Session), AccessTokenLookupError> { let res = sqlx::query_as!( @@ -142,7 +142,7 @@ pub async fn lookup_active_access_token( "#, token, ) - .fetch_one(executor) + .fetch_one(&mut *conn) .await?; let access_token = AccessToken { @@ -153,10 +153,7 @@ pub async fn lookup_active_access_token( expires_after: Duration::seconds(res.access_token_expires_after.into()), }; - let client = Client { - data: (), - client_id: res.client_id, - }; + let client = lookup_client_by_client_id(&mut *conn, &res.client_id).await?; let primary_email = match ( res.user_email_id, diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index 86d3a73e..94dbc36b 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -24,15 +24,16 @@ use mas_data_model::{ }; use mas_iana::oauth::PkceCodeChallengeMethod; use oauth2_types::{requests::ResponseMode, scope::Scope}; -use sqlx::PgExecutor; +use sqlx::{PgConnection, PgExecutor}; use url::Url; +use super::client::lookup_client_by_client_id; use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend}; #[allow(clippy::too_many_arguments)] pub async fn new_authorization_grant( executor: impl PgExecutor<'_>, - client_id: String, + client: Client, redirect_uri: Url, scope: Scope, code: Option, @@ -65,7 +66,7 @@ pub async fn new_authorization_grant( ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at "#, - &client_id, + &client.client_id, redirect_uri.to_string(), scope.to_string(), state, @@ -85,11 +86,6 @@ pub async fn new_authorization_grant( .await .context("could not insert oauth2 authorization grant")?; - let client = Client { - data: (), - client_id, - }; - Ok(AuthorizationGrant { data: res.id, stage: AuthorizationGrantStage::Pending, @@ -141,20 +137,21 @@ struct GrantLookup { user_email_confirmed_at: Option>, } -impl TryInto> for GrantLookup { - type Error = DatabaseInconsistencyError; - +impl GrantLookup { #[allow(clippy::too_many_lines)] - fn try_into(self) -> Result, Self::Error> { + async fn into_authorization_grant( + self, + executor: impl PgExecutor<'_>, + ) -> Result, DatabaseInconsistencyError> { let scope: Scope = self .grant_scope .parse() .map_err(|_e| DatabaseInconsistencyError)?; - let client = Client { - data: (), - client_id: self.client_id, - }; + // TODO: don't unwrap + let client = lookup_client_by_client_id(executor, &self.client_id) + .await + .unwrap(); let last_authentication = match ( self.user_session_last_authentication_id, @@ -323,7 +320,7 @@ impl TryInto> for GrantLookup { } pub async fn get_grant_by_id( - executor: impl PgExecutor<'_>, + conn: &mut PgConnection, id: i64, ) -> anyhow::Result> { // TODO: handle "not found" cases @@ -381,17 +378,17 @@ pub async fn get_grant_by_id( "#, id, ) - .fetch_one(executor) + .fetch_one(&mut *conn) .await .context("failed to get grant by id")?; - let grant = res.try_into()?; + let grant = res.into_authorization_grant(&mut *conn).await?; Ok(grant) } pub async fn lookup_grant_by_code( - executor: impl PgExecutor<'_>, + conn: &mut PgConnection, code: &str, ) -> anyhow::Result> { // TODO: handle "not found" cases @@ -449,11 +446,11 @@ pub async fn lookup_grant_by_code( "#, code, ) - .fetch_one(executor) + .fetch_one(&mut *conn) .await .context("failed to lookup grant by code")?; - let grant = res.try_into()?; + let grant = res.into_authorization_grant(&mut *conn).await?; Ok(grant) } diff --git a/crates/storage/src/oauth2/client.rs b/crates/storage/src/oauth2/client.rs new file mode 100644 index 00000000..699be00d --- /dev/null +++ b/crates/storage/src/oauth2/client.rs @@ -0,0 +1,380 @@ +// 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::string::ToString; + +use mas_data_model::{Client, JwksOrJwksUri}; +use mas_iana::oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod}; +use mas_jose::JsonWebKeySet; +use oauth2_types::requests::GrantType; +use sqlx::{PgConnection, PgExecutor}; +use thiserror::Error; +use url::Url; +use warp::reject::Reject; + +use crate::PostgresqlBackend; + +#[derive(Debug)] +pub struct OAuth2ClientLookup { + id: i64, + client_id: String, + encrypted_client_secret: Option, + redirect_uris: Vec, + response_types: Vec, + grant_type_authorization_code: bool, + grant_type_refresh_token: bool, + contacts: Vec, + client_name: Option, + logo_uri: Option, + client_uri: Option, + policy_uri: Option, + tos_uri: Option, + jwks_uri: Option, + jwks: Option, + id_token_signed_response_alg: Option, + token_endpoint_auth_method: Option, + token_endpoint_auth_signing_alg: Option, + initiate_login_uri: Option, +} + +#[derive(Debug, Error)] +pub enum ClientFetchError { + #[error("malformed jwks column")] + MalformedJwks(#[source] serde_json::Error), + + #[error("entry has both a jwks and a jwks_uri")] + BothJwksAndJwksUri, + + #[error("could not parse URL in field {field:?}")] + ParseUrl { + field: &'static str, + source: url::ParseError, + }, + + #[error("could not parse field {field:?}")] + ParseField { + field: &'static str, + source: mas_iana::ParseError, + }, + + #[error(transparent)] + Database(#[from] sqlx::Error), +} + +impl ClientFetchError { + #[must_use] + pub fn not_found(&self) -> bool { + matches!(self, Self::Database(sqlx::Error::RowNotFound)) + } +} + +impl Reject for ClientFetchError {} + +impl TryInto> for OAuth2ClientLookup { + type Error = ClientFetchError; + + #[allow(clippy::too_many_lines)] // TODO: refactor some of the field parsing + fn try_into(self) -> Result, Self::Error> { + let redirect_uris: Result, _> = + self.redirect_uris.iter().map(|s| s.parse()).collect(); + let redirect_uris = redirect_uris.map_err(|source| ClientFetchError::ParseUrl { + field: "redirect_uris", + source, + })?; + + let response_types: Result, _> = + self.response_types.iter().map(|s| s.parse()).collect(); + let response_types = response_types.map_err(|source| ClientFetchError::ParseField { + field: "response_types", + source, + })?; + + let mut grant_types = Vec::new(); + if self.grant_type_authorization_code { + grant_types.push(GrantType::AuthorizationCode); + } + if self.grant_type_refresh_token { + grant_types.push(GrantType::RefreshToken); + } + + let logo_uri = self + .logo_uri + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseUrl { + field: "logo_uri", + source, + })?; + + let client_uri = self + .client_uri + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseUrl { + field: "client_uri", + source, + })?; + + let policy_uri = self + .policy_uri + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseUrl { + field: "policy_uri", + source, + })?; + + let tos_uri = self + .tos_uri + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseUrl { + field: "tos_uri", + source, + })?; + + let id_token_signed_response_alg = self + .id_token_signed_response_alg + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseField { + field: "id_token_signed_response_alg", + source, + })?; + + let token_endpoint_auth_method = self + .token_endpoint_auth_method + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseField { + field: "token_endpoint_auth_method", + source, + })?; + + let token_endpoint_auth_signing_alg = self + .token_endpoint_auth_signing_alg + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseField { + field: "token_endpoint_auth_signing_alg", + source, + })?; + + let initiate_login_uri = self + .initiate_login_uri + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseUrl { + field: "initiate_login_uri", + source, + })?; + + let jwks = match (self.jwks, self.jwks_uri) { + (None, None) => None, + (Some(jwks), None) => { + let jwks = serde_json::from_value(jwks).map_err(ClientFetchError::MalformedJwks)?; + Some(JwksOrJwksUri::Jwks(jwks)) + } + (None, Some(jwks_uri)) => { + let jwks_uri = jwks_uri + .parse() + .map_err(|source| ClientFetchError::ParseUrl { + field: "jwks_uri", + source, + })?; + + Some(JwksOrJwksUri::JwksUri(jwks_uri)) + } + _ => return Err(ClientFetchError::BothJwksAndJwksUri), + }; + + Ok(Client { + data: self.id, + client_id: self.client_id, + encrypted_client_secret: self.encrypted_client_secret, + redirect_uris, + response_types, + grant_types, + contacts: self.contacts, + client_name: self.client_name, + logo_uri, + client_uri, + policy_uri, + tos_uri, + jwks, + id_token_signed_response_alg, + token_endpoint_auth_method, + token_endpoint_auth_signing_alg, + initiate_login_uri, + }) + } +} + +pub async fn lookup_client( + executor: impl PgExecutor<'_>, + id: i64, +) -> Result, ClientFetchError> { + let res = sqlx::query_as!( + OAuth2ClientLookup, + r#" + SELECT + c.id, + c.client_id, + c.encrypted_client_secret, + ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS "redirect_uris!", + c.response_types, + c.grant_type_authorization_code, + c.grant_type_refresh_token, + c.contacts, + c.client_name, + c.logo_uri, + c.client_uri, + c.policy_uri, + c.tos_uri, + c.jwks_uri, + c.jwks, + c.id_token_signed_response_alg, + c.token_endpoint_auth_method, + c.token_endpoint_auth_signing_alg, + c.initiate_login_uri + FROM oauth2_clients c + + WHERE c.id = $1 + "#, + id, + ) + .fetch_one(executor) + .await?; + + let client = res.try_into()?; + + Ok(client) +} + +pub async fn lookup_client_by_client_id( + executor: impl PgExecutor<'_>, + client_id: &str, +) -> Result, ClientFetchError> { + let res = sqlx::query_as!( + OAuth2ClientLookup, + r#" + SELECT + c.id, + c.client_id, + c.encrypted_client_secret, + ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS "redirect_uris!", + c.response_types, + c.grant_type_authorization_code, + c.grant_type_refresh_token, + c.contacts, + c.client_name, + c.logo_uri, + c.client_uri, + c.policy_uri, + c.tos_uri, + c.jwks_uri, + c.jwks, + c.id_token_signed_response_alg, + c.token_endpoint_auth_method, + c.token_endpoint_auth_signing_alg, + c.initiate_login_uri + FROM oauth2_clients c + + WHERE c.client_id = $1 + "#, + client_id, + ) + .fetch_one(executor) + .await?; + + let client = res.try_into()?; + + Ok(client) +} + +pub async fn insert_client_from_config( + conn: &mut PgConnection, + client_id: &str, + client_auth_method: OAuthClientAuthenticationMethod, + encrypted_client_secret: Option<&str>, + jwks: Option<&JsonWebKeySet>, + jwks_uri: Option<&Url>, + redirect_uris: &[Url], +) -> anyhow::Result<()> { + let response_types = vec![ + OAuthAuthorizationEndpointResponseType::Code.to_string(), + OAuthAuthorizationEndpointResponseType::CodeIdToken.to_string(), + OAuthAuthorizationEndpointResponseType::CodeIdTokenToken.to_string(), + OAuthAuthorizationEndpointResponseType::CodeToken.to_string(), + OAuthAuthorizationEndpointResponseType::IdToken.to_string(), + OAuthAuthorizationEndpointResponseType::IdTokenToken.to_string(), + OAuthAuthorizationEndpointResponseType::None.to_string(), + OAuthAuthorizationEndpointResponseType::Token.to_string(), + ]; + + let jwks = jwks.map(serde_json::to_value).transpose()?; + let jwks_uri = jwks_uri.map(Url::as_str); + + let client_auth_method = client_auth_method.to_string(); + + let id = sqlx::query_scalar!( + r#" + INSERT INTO oauth2_clients + (client_id, + encrypted_client_secret, + response_types, + grant_type_authorization_code, + grant_type_refresh_token, + token_endpoint_auth_method, + jwks, + jwks_uri, + contacts) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, '{}') + RETURNING id + "#, + client_id, + encrypted_client_secret, + &response_types, + true, + true, + client_auth_method, + jwks, + jwks_uri, + ) + .fetch_one(&mut *conn) + .await?; + + let redirect_uris: Vec = redirect_uris.iter().map(ToString::to_string).collect(); + + sqlx::query!( + r#" + INSERT INTO oauth2_client_redirect_uris (oauth2_client_id, redirect_uri) + SELECT $1, uri FROM UNNEST($2::text[]) uri + "#, + id, + &redirect_uris, + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} + +pub async fn truncate_clients(executor: impl PgExecutor<'_>) -> anyhow::Result<()> { + sqlx::query!("TRUNCATE oauth2_client_redirect_uris, oauth2_clients") + .execute(executor) + .await?; + Ok(()) +} diff --git a/crates/storage/src/oauth2/mod.rs b/crates/storage/src/oauth2/mod.rs index a81e9873..9202e30a 100644 --- a/crates/storage/src/oauth2/mod.rs +++ b/crates/storage/src/oauth2/mod.rs @@ -19,6 +19,7 @@ use crate::PostgresqlBackend; pub mod access_token; pub mod authorization_grant; +pub mod client; pub mod refresh_token; pub async fn end_oauth_session( diff --git a/crates/storage/src/oauth2/refresh_token.rs b/crates/storage/src/oauth2/refresh_token.rs index 18bc1a9c..7a9dfb53 100644 --- a/crates/storage/src/oauth2/refresh_token.rs +++ b/crates/storage/src/oauth2/refresh_token.rs @@ -15,12 +15,13 @@ use anyhow::Context; use chrono::{DateTime, Duration, Utc}; use mas_data_model::{ - AccessToken, Authentication, BrowserSession, Client, RefreshToken, Session, User, UserEmail, + AccessToken, Authentication, BrowserSession, RefreshToken, Session, User, UserEmail, }; -use sqlx::PgExecutor; +use sqlx::{PgConnection, PgExecutor}; use thiserror::Error; use warp::reject::Reject; +use super::client::{lookup_client_by_client_id, ClientFetchError}; use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend}; pub async fn add_refresh_token( @@ -82,6 +83,7 @@ struct OAuth2RefreshTokenLookup { #[error("could not lookup refresh token")] pub enum RefreshTokenLookupError { Fetch(#[from] sqlx::Error), + ClientFetch(#[from] ClientFetchError), Conversion(#[from] DatabaseInconsistencyError), } @@ -96,7 +98,7 @@ impl RefreshTokenLookupError { #[allow(clippy::too_many_lines)] pub async fn lookup_active_refresh_token( - executor: impl PgExecutor<'_>, + conn: &mut PgConnection, token: &str, ) -> Result<(RefreshToken, Session), RefreshTokenLookupError> { @@ -148,7 +150,7 @@ pub async fn lookup_active_refresh_token( "#, token, ) - .fetch_one(executor) + .fetch_one(&mut *conn) .await?; let access_token = match ( @@ -175,10 +177,7 @@ pub async fn lookup_active_refresh_token( access_token, }; - let client = Client { - data: (), - client_id: res.client_id, - }; + let client = lookup_client_by_client_id(&mut *conn, &res.client_id).await?; let primary_email = match ( res.user_email_id, diff --git a/crates/warp-utils/Cargo.toml b/crates/warp-utils/Cargo.toml index 84377bf5..fa4ea704 100644 --- a/crates/warp-utils/Cargo.toml +++ b/crates/warp-utils/Cargo.toml @@ -28,6 +28,9 @@ mime = "0.3.16" bincode = "1.3.3" crc = "2.1.0" url = "2.2.2" +http = "0.2.6" +http-body = "0.4.4" +tower = { version = "0.4.12", features = ["util"] } oauth2-types = { path = "../oauth2-types" } mas-config = { path = "../config" } @@ -36,6 +39,4 @@ mas-data-model = { path = "../data-model" } mas-storage = { path = "../storage" } mas-jose = { path = "../jose" } mas-iana = { path = "../iana" } - -[dev-dependencies] -tower = { version = "0.4.12", features = ["util"] } +mas-http = { path = "../http" } diff --git a/crates/warp-utils/src/filters/authenticate.rs b/crates/warp-utils/src/filters/authenticate.rs index 6d09159d..9da8fc11 100644 --- a/crates/warp-utils/src/filters/authenticate.rs +++ b/crates/warp-utils/src/filters/authenticate.rs @@ -87,6 +87,10 @@ pub fn authentication( .untuple_one() } +fn ensure(t: T) -> T { + t +} + async fn authenticate( mut conn: PoolConnection, auth: Authorization, @@ -110,6 +114,9 @@ async fn authenticate( } })?; + let session = ensure(session); + let token = ensure(token); + Ok((token, session)) } diff --git a/crates/warp-utils/src/filters/client.rs b/crates/warp-utils/src/filters/client.rs index 01ec7c39..9cf42ac5 100644 --- a/crates/warp-utils/src/filters/client.rs +++ b/crates/warp-utils/src/filters/client.rs @@ -16,30 +16,49 @@ use std::collections::HashMap; +use data_encoding::BASE64; use headers::{authorization::Basic, Authorization}; -use mas_config::{ClientAuthMethodConfig, ClientConfig, ClientsConfig}; +use mas_config::Encrypter; +use mas_data_model::{Client, JwksOrJwksUri, StorageBackend}; +use mas_http::HttpServiceExt; use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_jose::{ claims::{TimeOptions, AUD, EXP, IAT, ISS, JTI, NBF, SUB}, - DecodedJsonWebToken, JsonWebTokenParts, SharedSecret, + DecodedJsonWebToken, DynamicJwksStore, Either, JsonWebKeySet, JsonWebTokenParts, SharedSecret, + StaticJwksStore, VerifyingKeystore, +}; +use mas_storage::{ + oauth2::client::{lookup_client_by_client_id, ClientFetchError}, + PostgresqlBackend, }; use serde::{de::DeserializeOwned, Deserialize}; +use sqlx::{pool::PoolConnection, PgPool, Postgres}; use thiserror::Error; +use tower::{BoxError, ServiceExt}; use warp::{reject::Reject, Filter, Rejection}; -use super::headers::typed_header; +use super::{database::connection, headers::typed_header}; use crate::errors::WrapError; /// Protect an enpoint with client authentication #[must_use] pub fn client_authentication( - clients_config: &ClientsConfig, + pool: &PgPool, + encrypter: &Encrypter, audience: String, -) -> impl Filter - + Clone +) -> impl Filter< + Extract = ( + OAuthClientAuthenticationMethod, + Client, + T, + ), + Error = Rejection, +> + Clone + Send + Sync + 'static { + let encrypter = encrypter.clone(); + // First, extract the client credentials let credentials = typed_header() .and(warp::body::form()) @@ -65,9 +84,9 @@ pub fn client_authentication( .unify() .untuple_one(); - let clients_config = clients_config.clone(); warp::any() - .map(move || clients_config.clone()) + .and(connection(pool)) + .and(warp::any().map(move || encrypter.clone())) .and(warp::any().map(move || audience.clone())) .and(credentials) .and_then(authenticate_client) @@ -79,8 +98,20 @@ enum ClientAuthenticationError { #[error("wrong client secret for client {client_id:?}")] ClientSecretMismatch { client_id: String }, - #[error("could not find client {client_id:?}")] - ClientNotFound { client_id: String }, + #[error("could not fetch client {client_id:?}")] + ClientFetch { + client_id: String, + source: ClientFetchError, + }, + + #[error("client {client_id:?} has an invalid client secret")] + InvalidClientSecret { + client_id: String, + source: anyhow::Error, + }, + + #[error("client {client_id:?} has an invalid JWKS")] + InvalidJwks { client_id: String }, #[error("wrong client authentication method for client {client_id:?}")] WrongAuthenticationMethod { client_id: String }, @@ -94,68 +125,136 @@ enum ClientAuthenticationError { impl Reject for ClientAuthenticationError {} +fn decrypt_client_secret( + client: &Client, + encrypter: &Encrypter, +) -> anyhow::Result> { + let encrypted_client_secret = client + .encrypted_client_secret + .as_ref() + .ok_or_else(|| anyhow::anyhow!("missing encrypted_client_secret field"))?; + + let encrypted_client_secret = BASE64.decode(encrypted_client_secret.as_bytes())?; + + let nonce: &[u8; 12] = encrypted_client_secret + .get(0..12) + .ok_or_else(|| anyhow::anyhow!("invalid payload serialization"))? + .try_into()?; + + let payload = encrypted_client_secret + .get(12..) + .ok_or_else(|| anyhow::anyhow!("invalid payload serialization"))?; + + let decrypted_client_secret = encrypter.decrypt(nonce, payload)?; + + Ok(decrypted_client_secret) +} + +fn jwks_key_store(jwks: &JwksOrJwksUri) -> Either { + // Assert that the output is both a VerifyingKeystore and Send + fn assert(t: T) -> T { + t + } + + let inner = match jwks { + JwksOrJwksUri::Jwks(jwks) => Either::Left(StaticJwksStore::new(jwks.clone())), + JwksOrJwksUri::JwksUri(uri) => { + let uri = uri.clone(); + + // TODO: get the client from somewhere else? + let exporter = mas_http::client("fetch-jwks") + .json::() + .map_request(move |_: ()| { + http::Request::builder() + .method("GET") + // TODO: change the Uri type in config to avoid reparsing here + .uri(uri.to_string()) + .body(http_body::Empty::new()) + .unwrap() + }) + .map_response(http::Response::into_body) + .map_err(BoxError::from) + .boxed_clone(); + + Either::Right(DynamicJwksStore::new(exporter)) + } + }; + + assert(inner) +} + #[allow(clippy::too_many_lines)] #[tracing::instrument(skip_all, fields(enduser.id), err(Debug))] async fn authenticate_client( - clients_config: ClientsConfig, + mut conn: PoolConnection, + encrypter: Encrypter, audience: String, credentials: ClientCredentials, body: T, -) -> Result<(OAuthClientAuthenticationMethod, ClientConfig, T), Rejection> { +) -> Result< + ( + OAuthClientAuthenticationMethod, + Client, + T, + ), + Rejection, +> { let (auth_method, client) = match credentials { ClientCredentials::Pair { client_id, client_secret, via, } => { - let client = clients_config - .iter() - .find(|client| client.client_id == client_id) - .ok_or_else(|| ClientAuthenticationError::ClientNotFound { - client_id: client_id.to_string(), + let client = lookup_client_by_client_id(&mut *conn, &client_id) + .await + .map_err(|source| ClientAuthenticationError::ClientFetch { + client_id: client_id.clone(), + source, })?; - let auth_method = match (&client.client_auth_method, client_secret, via) { - (ClientAuthMethodConfig::None, None, _) => OAuthClientAuthenticationMethod::None, + let auth_method = client.token_endpoint_auth_method.ok_or( + ClientAuthenticationError::WrongAuthenticationMethod { + client_id: client.client_id.clone(), + }, + )?; + // Let's match the authentication method + match (auth_method, client_secret, via) { + (OAuthClientAuthenticationMethod::None, None, _) => {} ( - ClientAuthMethodConfig::ClientSecretBasic { - client_secret: ref expected_client_secret, - }, - Some(ref given_client_secret), + OAuthClientAuthenticationMethod::ClientSecretBasic, + Some(client_secret), CredentialsVia::AuthorizationHeader, - ) => { - if expected_client_secret != given_client_secret { - return Err( - ClientAuthenticationError::ClientSecretMismatch { client_id }.into(), - ); - } - - OAuthClientAuthenticationMethod::ClientSecretBasic - } - - ( - ClientAuthMethodConfig::ClientSecretPost { - client_secret: ref expected_client_secret, - }, - Some(ref given_client_secret), + ) + | ( + OAuthClientAuthenticationMethod::ClientSecretPost, + Some(client_secret), CredentialsVia::FormBody, ) => { - if expected_client_secret != given_client_secret { - return Err( - ClientAuthenticationError::ClientSecretMismatch { client_id }.into(), - ); + let decrypted = + decrypt_client_secret(&client, &encrypter).map_err(|source| { + ClientAuthenticationError::InvalidClientSecret { + client_id: client.client_id.clone(), + source, + } + })?; + + if client_secret.as_bytes() != decrypted { + return Err(warp::reject::custom( + ClientAuthenticationError::ClientSecretMismatch { + client_id: client.client_id, + }, + )); } - - OAuthClientAuthenticationMethod::ClientSecretPost } - _ => { - return Err( - ClientAuthenticationError::WrongAuthenticationMethod { client_id }.into(), - ) + return Err(warp::reject::custom( + ClientAuthenticationError::WrongAuthenticationMethod { + client_id: client.client_id, + }, + )); } - }; + } (auth_method, client) } @@ -195,34 +294,52 @@ async fn authenticate_client( // from the token, as per rfc7521 sec. 4.2 let client_id = client_id.as_ref().unwrap_or(&sub); - let client = clients_config - .iter() - .find(|client| &client.client_id == client_id) - .ok_or_else(|| ClientAuthenticationError::ClientNotFound { + let client = lookup_client_by_client_id(&mut *conn, client_id) + .await + .map_err(|source| ClientAuthenticationError::ClientFetch { client_id: client_id.to_string(), + source, })?; - let auth_method = match &client.client_auth_method { - ClientAuthMethodConfig::PrivateKeyJwt(jwks) => { - let store = jwks.key_store(); + let auth_method = client.token_endpoint_auth_method.ok_or( + ClientAuthenticationError::WrongAuthenticationMethod { + client_id: client.client_id.clone(), + }, + )?; + + match auth_method { + OAuthClientAuthenticationMethod::ClientSecretJwt => { + let client_secret = + decrypt_client_secret(&client, &encrypter).map_err(|source| { + ClientAuthenticationError::InvalidClientSecret { + client_id: client.client_id.clone(), + source, + } + })?; + + let store = SharedSecret::new(&client_secret); let fut = token.verify(&decoded, &store); fut.await.wrap_error()?; - OAuthClientAuthenticationMethod::PrivateKeyJwt } + OAuthClientAuthenticationMethod::PrivateKeyJwt => { + let jwks = client.jwks.as_ref().ok_or_else(|| { + ClientAuthenticationError::InvalidJwks { + client_id: client.client_id.clone(), + } + })?; - ClientAuthMethodConfig::ClientSecretJwt { client_secret } => { - let store = SharedSecret::new(client_secret); - token.verify(&decoded, &store).await.wrap_error()?; - OAuthClientAuthenticationMethod::ClientSecretJwt + let store = jwks_key_store(jwks); + let fut = token.verify(&decoded, &store); + fut.await.wrap_error()?; } - _ => { - return Err(ClientAuthenticationError::WrongAuthenticationMethod { - client_id: client_id.clone(), - } - .into()) + return Err(warp::reject::custom( + ClientAuthenticationError::WrongAuthenticationMethod { + client_id: client.client_id, + }, + )); } - }; + } // rfc7523 sec. 3.3: the audience is the URL being called if !aud.contains(&audience) { @@ -243,7 +360,7 @@ async fn authenticate_client( tracing::Span::current().record("enduser.id", &client.client_id.as_str()); - Ok((auth_method, client.clone(), body)) + Ok((auth_method, client, body)) } #[derive(Deserialize)] @@ -291,6 +408,7 @@ struct ClientAuthForm { body: T, } +/* TODO: all secrets are broken because there is no way to mock the DB yet #[cfg(test)] mod tests { use headers::authorization::Credentials; @@ -651,3 +769,4 @@ mod tests { assert_eq!(body.bar, "foobar"); } } +*/