You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-28 11:02:02 +03:00
Database refactoring
This commit is contained in:
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -2430,6 +2430,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2505,6 +2506,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2575,6 +2577,7 @@ dependencies = [
|
|||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2744,6 +2747,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2781,7 +2785,9 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2813,6 +2819,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4525,6 +4532,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
@ -5153,6 +5161,17 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
|
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ulid"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd"
|
||||||
|
dependencies = [
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uncased"
|
name = "uncased"
|
||||||
version = "0.9.7"
|
version = "0.9.7"
|
||||||
|
@ -4,3 +4,6 @@ members = ["crates/*"]
|
|||||||
|
|
||||||
[profile.dev.package.num-bigint-dig]
|
[profile.dev.package.num-bigint-dig]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.dev.package.sqlx-macros]
|
||||||
|
opt-level = 3
|
||||||
|
@ -28,6 +28,7 @@ tokio = "1.21.2"
|
|||||||
tower = { version = "0.4.13", features = ["util"] }
|
tower = { version = "0.4.13", features = ["util"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
|
ulid = { version = "1.0.0", features = ["serde"] }
|
||||||
|
|
||||||
mas-data-model = { path = "../data-model" }
|
mas-data-model = { path = "../data-model" }
|
||||||
mas-http = { path = "../http", features = ["client"] }
|
mas-http = { path = "../http", features = ["client"] }
|
||||||
|
@ -23,9 +23,11 @@ pub struct FancyError {
|
|||||||
context: ErrorContext,
|
context: ErrorContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: std::fmt::Display> From<E> for FancyError {
|
impl<E: std::fmt::Debug + std::fmt::Display> From<E> for FancyError {
|
||||||
fn from(err: E) -> Self {
|
fn from(err: E) -> Self {
|
||||||
let context = ErrorContext::new().with_description(err.to_string());
|
let context = ErrorContext::new()
|
||||||
|
.with_description(format!("{err}"))
|
||||||
|
.with_details(format!("{err:?}"));
|
||||||
FancyError { context }
|
FancyError { context }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,14 @@ use mas_storage::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Executor, Postgres};
|
use sqlx::{Executor, Postgres};
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::CookieExt;
|
use crate::CookieExt;
|
||||||
|
|
||||||
/// An encrypted cookie to save the session ID
|
/// An encrypted cookie to save the session ID
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
pub struct SessionInfo {
|
pub struct SessionInfo {
|
||||||
current: Option<i64>,
|
current: Option<Ulid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionInfo {
|
impl SessionInfo {
|
||||||
|
@ -16,7 +16,7 @@ use argon2::Argon2;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use mas_config::{DatabaseConfig, RootConfig};
|
use mas_config::{DatabaseConfig, RootConfig};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
oauth2::client::{insert_client_from_config, lookup_client_by_client_id, truncate_clients},
|
oauth2::client::{insert_client_from_config, lookup_client, truncate_clients},
|
||||||
user::{
|
user::{
|
||||||
lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, register_user,
|
lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, register_user,
|
||||||
},
|
},
|
||||||
@ -96,8 +96,8 @@ impl Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for client in config.clients.iter() {
|
for client in config.clients.iter() {
|
||||||
let client_id = &client.client_id;
|
let client_id = client.client_id;
|
||||||
let res = lookup_client_by_client_id(&mut txn, client_id).await;
|
let res = lookup_client(&mut txn, client_id).await;
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
warn!(%client_id, "Skipping already imported client");
|
warn!(%client_id, "Skipping already imported client");
|
||||||
|
@ -17,6 +17,7 @@ schemars = { version = "0.8.11", features = ["url", "chrono"] }
|
|||||||
figment = { version = "0.10.8", features = ["env", "yaml", "test"] }
|
figment = { version = "0.10.8", features = ["env", "yaml", "test"] }
|
||||||
chrono = { version = "0.4.22", features = ["serde"] }
|
chrono = { version = "0.4.22", features = ["serde"] }
|
||||||
url = { version = "2.3.1", features = ["serde"] }
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
|
ulid = { version = "1.0.0", features = ["serde"] }
|
||||||
|
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
serde_with = { version = "2.0.1", features = ["hex", "chrono"] }
|
serde_with = { version = "2.0.1", features = ["hex", "chrono"] }
|
||||||
|
@ -21,6 +21,7 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::ConfigurationSection;
|
use super::ConfigurationSection;
|
||||||
@ -76,7 +77,8 @@ pub enum ClientAuthMethodConfig {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct ClientConfig {
|
pub struct ClientConfig {
|
||||||
/// The client ID
|
/// The client ID
|
||||||
pub client_id: String,
|
#[schemars(with = "String")]
|
||||||
|
pub client_id: Ulid,
|
||||||
|
|
||||||
/// Authentication method used for this client
|
/// Authentication method used for this client
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@ -181,6 +183,8 @@ impl ConfigurationSection<'_> for ClientsConfig {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use figment::Jail;
|
use figment::Jail;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -192,24 +196,24 @@ mod tests {
|
|||||||
"config.yaml",
|
"config.yaml",
|
||||||
r#"
|
r#"
|
||||||
clients:
|
clients:
|
||||||
- client_id: public
|
- client_id: 01GFWR28C4KNE04WG3HKXB7C9R
|
||||||
client_auth_method: none
|
client_auth_method: none
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
- https://exemple.fr/callback
|
- https://exemple.fr/callback
|
||||||
|
|
||||||
- client_id: secret-basic
|
- client_id: 01GFWR32NCQ12B8Z0J8CPXRRB6
|
||||||
client_auth_method: client_secret_basic
|
client_auth_method: client_secret_basic
|
||||||
client_secret: hello
|
client_secret: hello
|
||||||
|
|
||||||
- client_id: secret-post
|
- client_id: 01GFWR3WHR93Y5HK389H28VHZ9
|
||||||
client_auth_method: client_secret_post
|
client_auth_method: client_secret_post
|
||||||
client_secret: hello
|
client_secret: hello
|
||||||
|
|
||||||
- client_id: secret-jwk
|
- client_id: 01GFWR43R2ZZ8HX9CVBNW9TJWG
|
||||||
client_auth_method: client_secret_jwt
|
client_auth_method: client_secret_jwt
|
||||||
client_secret: hello
|
client_secret: hello
|
||||||
|
|
||||||
- client_id: jwks
|
- client_id: 01GFWR4BNFDCC4QDG6AMSP1VRR
|
||||||
client_auth_method: private_key_jwt
|
client_auth_method: private_key_jwt
|
||||||
jwks:
|
jwks:
|
||||||
keys:
|
keys:
|
||||||
@ -233,13 +237,19 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(config.0.len(), 5);
|
assert_eq!(config.0.len(), 5);
|
||||||
|
|
||||||
assert_eq!(config.0[0].client_id, "public");
|
assert_eq!(
|
||||||
|
config.0[0].client_id,
|
||||||
|
Ulid::from_str("01GFWR28C4KNE04WG3HKXB7C9R").unwrap()
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.0[0].redirect_uris,
|
config.0[0].redirect_uris,
|
||||||
vec!["https://exemple.fr/callback".parse().unwrap()]
|
vec!["https://exemple.fr/callback".parse().unwrap()]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(config.0[1].client_id, "secret-basic");
|
assert_eq!(
|
||||||
|
config.0[1].client_id,
|
||||||
|
Ulid::from_str("01GFWR32NCQ12B8Z0J8CPXRRB6").unwrap()
|
||||||
|
);
|
||||||
assert_eq!(config.0[1].redirect_uris, Vec::new());
|
assert_eq!(config.0[1].redirect_uris, Vec::new());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -89,7 +89,7 @@ pub struct CompatSession<T: StorageBackend> {
|
|||||||
pub user: User<T>,
|
pub user: User<T>,
|
||||||
pub device: Device,
|
pub device: Device,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub deleted_at: Option<DateTime<Utc>>,
|
pub finished_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: StorageBackendMarker> From<CompatSession<S>> for CompatSession<()> {
|
impl<S: StorageBackendMarker> From<CompatSession<S>> for CompatSession<()> {
|
||||||
@ -99,7 +99,7 @@ impl<S: StorageBackendMarker> From<CompatSession<S>> for CompatSession<()> {
|
|||||||
user: t.user.into(),
|
user: t.user.into(),
|
||||||
device: t.device,
|
device: t.device,
|
||||||
created_at: t.created_at,
|
created_at: t.created_at,
|
||||||
deleted_at: t.deleted_at,
|
finished_at: t.finished_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,12 +144,12 @@ impl<S: StorageBackendMarker> From<CompatRefreshToken<S>> for CompatRefreshToken
|
|||||||
#[serde(bound = "T: StorageBackend")]
|
#[serde(bound = "T: StorageBackend")]
|
||||||
pub enum CompatSsoLoginState<T: StorageBackend> {
|
pub enum CompatSsoLoginState<T: StorageBackend> {
|
||||||
Pending,
|
Pending,
|
||||||
Fullfilled {
|
Fulfilled {
|
||||||
fullfilled_at: DateTime<Utc>,
|
fulfilled_at: DateTime<Utc>,
|
||||||
session: CompatSession<T>,
|
session: CompatSession<T>,
|
||||||
},
|
},
|
||||||
Exchanged {
|
Exchanged {
|
||||||
fullfilled_at: DateTime<Utc>,
|
fulfilled_at: DateTime<Utc>,
|
||||||
exchanged_at: DateTime<Utc>,
|
exchanged_at: DateTime<Utc>,
|
||||||
session: CompatSession<T>,
|
session: CompatSession<T>,
|
||||||
},
|
},
|
||||||
@ -159,19 +159,19 @@ impl<S: StorageBackendMarker> From<CompatSsoLoginState<S>> for CompatSsoLoginSta
|
|||||||
fn from(t: CompatSsoLoginState<S>) -> Self {
|
fn from(t: CompatSsoLoginState<S>) -> Self {
|
||||||
match t {
|
match t {
|
||||||
CompatSsoLoginState::Pending => Self::Pending,
|
CompatSsoLoginState::Pending => Self::Pending,
|
||||||
CompatSsoLoginState::Fullfilled {
|
CompatSsoLoginState::Fulfilled {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
session,
|
session,
|
||||||
} => Self::Fullfilled {
|
} => Self::Fulfilled {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
session: session.into(),
|
session: session.into(),
|
||||||
},
|
},
|
||||||
CompatSsoLoginState::Exchanged {
|
CompatSsoLoginState::Exchanged {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
exchanged_at,
|
exchanged_at,
|
||||||
session,
|
session,
|
||||||
} => Self::Exchanged {
|
} => Self::Exchanged {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
exchanged_at,
|
exchanged_at,
|
||||||
session: session.into(),
|
session: session.into(),
|
||||||
},
|
},
|
||||||
@ -185,7 +185,7 @@ pub struct CompatSsoLogin<T: StorageBackend> {
|
|||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub data: T::CompatSsoLoginData,
|
pub data: T::CompatSsoLoginData,
|
||||||
pub redirect_uri: Url,
|
pub redirect_uri: Url,
|
||||||
pub token: String,
|
pub login_token: String,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub state: CompatSsoLoginState<T>,
|
pub state: CompatSsoLoginState<T>,
|
||||||
}
|
}
|
||||||
@ -195,7 +195,7 @@ impl<S: StorageBackendMarker> From<CompatSsoLogin<S>> for CompatSsoLogin<()> {
|
|||||||
Self {
|
Self {
|
||||||
data: (),
|
data: (),
|
||||||
redirect_uri: t.redirect_uri,
|
redirect_uri: t.redirect_uri,
|
||||||
token: t.token,
|
login_token: t.login_token,
|
||||||
created_at: t.created_at,
|
created_at: t.created_at,
|
||||||
state: t.state.into(),
|
state: t.state.into(),
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,6 @@ pub struct AuthorizationGrant<T: StorageBackend> {
|
|||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
pub nonce: Option<String>,
|
pub nonce: Option<String>,
|
||||||
pub max_age: Option<NonZeroU32>,
|
pub max_age: Option<NonZeroU32>,
|
||||||
pub acr_values: Option<String>,
|
|
||||||
pub response_mode: ResponseMode,
|
pub response_mode: ResponseMode,
|
||||||
pub response_type_id_token: bool,
|
pub response_type_id_token: bool,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
@ -190,7 +189,6 @@ impl<S: StorageBackendMarker> From<AuthorizationGrant<S>> for AuthorizationGrant
|
|||||||
state: g.state,
|
state: g.state,
|
||||||
nonce: g.nonce,
|
nonce: g.nonce,
|
||||||
max_age: g.max_age,
|
max_age: g.max_age,
|
||||||
acr_values: g.acr_values,
|
|
||||||
response_mode: g.response_mode,
|
response_mode: g.response_mode,
|
||||||
response_type_id_token: g.response_type_id_token,
|
response_type_id_token: g.response_type_id_token,
|
||||||
created_at: g.created_at,
|
created_at: g.created_at,
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use crc::{Crc, CRC_32_ISO_HDLC};
|
use crc::{Crc, CRC_32_ISO_HDLC};
|
||||||
use mas_iana::oauth::OAuthTokenTypeHint;
|
use mas_iana::oauth::OAuthTokenTypeHint;
|
||||||
use rand::{distributions::Alphanumeric, Rng};
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
@ -24,9 +24,9 @@ use crate::traits::{StorageBackend, StorageBackendMarker};
|
|||||||
pub struct AccessToken<T: StorageBackend> {
|
pub struct AccessToken<T: StorageBackend> {
|
||||||
pub data: T::AccessTokenData,
|
pub data: T::AccessTokenData,
|
||||||
pub jti: String,
|
pub jti: String,
|
||||||
pub token: String,
|
pub access_token: String,
|
||||||
pub expires_after: Duration,
|
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: StorageBackendMarker> From<AccessToken<S>> for AccessToken<()> {
|
impl<S: StorageBackendMarker> From<AccessToken<S>> for AccessToken<()> {
|
||||||
@ -34,23 +34,24 @@ impl<S: StorageBackendMarker> From<AccessToken<S>> for AccessToken<()> {
|
|||||||
AccessToken {
|
AccessToken {
|
||||||
data: (),
|
data: (),
|
||||||
jti: t.jti,
|
jti: t.jti,
|
||||||
token: t.token,
|
access_token: t.access_token,
|
||||||
expires_after: t.expires_after,
|
expires_at: t.expires_at,
|
||||||
created_at: t.created_at,
|
created_at: t.created_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: StorageBackend> AccessToken<T> {
|
impl<T: StorageBackend> AccessToken<T> {
|
||||||
|
// XXX
|
||||||
pub fn exp(&self) -> DateTime<Utc> {
|
pub fn exp(&self) -> DateTime<Utc> {
|
||||||
self.created_at + self.expires_after
|
self.expires_at
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct RefreshToken<T: StorageBackend> {
|
pub struct RefreshToken<T: StorageBackend> {
|
||||||
pub data: T::RefreshTokenData,
|
pub data: T::RefreshTokenData,
|
||||||
pub token: String,
|
pub refresh_token: String,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub access_token: Option<AccessToken<T>>,
|
pub access_token: Option<AccessToken<T>>,
|
||||||
}
|
}
|
||||||
@ -59,7 +60,7 @@ impl<S: StorageBackendMarker> From<RefreshToken<S>> for RefreshToken<()> {
|
|||||||
fn from(t: RefreshToken<S>) -> Self {
|
fn from(t: RefreshToken<S>) -> Self {
|
||||||
RefreshToken {
|
RefreshToken {
|
||||||
data: (),
|
data: (),
|
||||||
token: t.token,
|
refresh_token: t.refresh_token,
|
||||||
created_at: t.created_at,
|
created_at: t.created_at,
|
||||||
access_token: t.access_token.map(Into::into),
|
access_token: t.access_token.map(Into::into),
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ where
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub enum UserEmailVerificationState {
|
pub enum UserEmailVerificationState {
|
||||||
AlreadyUsed { when: DateTime<Utc> },
|
AlreadyUsed { when: DateTime<Utc> },
|
||||||
Expired,
|
Expired { when: DateTime<Utc> },
|
||||||
Valid,
|
Valid,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +200,9 @@ where
|
|||||||
UserEmailVerificationState::AlreadyUsed {
|
UserEmailVerificationState::AlreadyUsed {
|
||||||
when: Utc::now() - Duration::minutes(5),
|
when: Utc::now() - Duration::minutes(5),
|
||||||
},
|
},
|
||||||
UserEmailVerificationState::Expired,
|
UserEmailVerificationState::Expired {
|
||||||
|
when: Utc::now() - Duration::hours(5),
|
||||||
|
},
|
||||||
UserEmailVerificationState::Valid,
|
UserEmailVerificationState::Valid,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ url = { version = "2.3.1", features = ["serde"] }
|
|||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
headers = "0.3.8"
|
headers = "0.3.8"
|
||||||
|
ulid = "1.0.0"
|
||||||
|
|
||||||
oauth2-types = { path = "../oauth2-types" }
|
oauth2-types = { path = "../oauth2-types" }
|
||||||
mas-axum-utils = { path = "../axum-utils", default-features = false }
|
mas-axum-utils = { path = "../axum-utils", default-features = false }
|
||||||
|
@ -259,12 +259,15 @@ async fn token_login(
|
|||||||
match login.state {
|
match login.state {
|
||||||
CompatSsoLoginState::Pending => {
|
CompatSsoLoginState::Pending => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
login.data,
|
compat_sso_login.id = %login.data,
|
||||||
"Exchanged a token for a login that was not fullfilled yet"
|
"Exchanged a token for a login that was not fullfilled yet"
|
||||||
);
|
);
|
||||||
return Err(RouteError::InvalidLoginToken);
|
return Err(RouteError::InvalidLoginToken);
|
||||||
}
|
}
|
||||||
CompatSsoLoginState::Fullfilled { fullfilled_at, .. } => {
|
CompatSsoLoginState::Fulfilled {
|
||||||
|
fulfilled_at: fullfilled_at,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
if now > fullfilled_at + Duration::seconds(30) {
|
if now > fullfilled_at + Duration::seconds(30) {
|
||||||
return Err(RouteError::LoginTookTooLong);
|
return Err(RouteError::LoginTookTooLong);
|
||||||
}
|
}
|
||||||
@ -273,7 +276,7 @@ async fn token_login(
|
|||||||
if now > exchanged_at + Duration::seconds(30) {
|
if now > exchanged_at + Duration::seconds(30) {
|
||||||
// TODO: log that session out
|
// TODO: log that session out
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
login.data,
|
compat_sso_login.id = %login.data,
|
||||||
"Login token exchanged a second time more than 30s after"
|
"Login token exchanged a second time more than 30s after"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates};
|
|||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct AllParams<'s> {
|
struct AllParams<'s> {
|
||||||
@ -52,7 +53,7 @@ pub async fn get(
|
|||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<Ulid>,
|
||||||
Query(params): Query<Params>,
|
Query(params): Query<Params>,
|
||||||
) -> Result<Response, FancyError> {
|
) -> Result<Response, FancyError> {
|
||||||
let mut conn = pool.acquire().await?;
|
let mut conn = pool.acquire().await?;
|
||||||
@ -116,7 +117,7 @@ pub async fn post(
|
|||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<Ulid>,
|
||||||
Query(params): Query<Params>,
|
Query(params): Query<Params>,
|
||||||
Form(form): Form<ProtectedForm<()>>,
|
Form(form): Form<ProtectedForm<()>>,
|
||||||
) -> Result<Response, FancyError> {
|
) -> Result<Response, FancyError> {
|
||||||
@ -178,7 +179,7 @@ pub async fn post(
|
|||||||
|
|
||||||
let params = AllParams {
|
let params = AllParams {
|
||||||
existing_params,
|
existing_params,
|
||||||
login_token: &login.token,
|
login_token: &login.login_token,
|
||||||
};
|
};
|
||||||
let query = serde_urlencoded::to_string(¶ms)?;
|
let query = serde_urlencoded::to_string(¶ms)?;
|
||||||
redirect_uri.set_query(Some(&query));
|
redirect_uri.set_query(Some(&query));
|
||||||
|
@ -17,9 +17,8 @@ use chrono::Duration;
|
|||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_data_model::{TokenFormatError, TokenType};
|
use mas_data_model::{TokenFormatError, TokenType};
|
||||||
use mas_storage::compat::{
|
use mas_storage::compat::{
|
||||||
add_compat_access_token, add_compat_refresh_token, expire_compat_access_token,
|
add_compat_access_token, add_compat_refresh_token, consume_compat_refresh_token,
|
||||||
lookup_active_compat_refresh_token, replace_compat_refresh_token,
|
expire_compat_access_token, lookup_active_compat_refresh_token, CompatRefreshTokenLookupError,
|
||||||
CompatRefreshTokenLookupError,
|
|
||||||
};
|
};
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -125,7 +124,7 @@ pub(crate) async fn post(
|
|||||||
add_compat_refresh_token(&mut txn, &session, &new_access_token, new_refresh_token_str)
|
add_compat_refresh_token(&mut txn, &session, &new_access_token, new_refresh_token_str)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
replace_compat_refresh_token(&mut txn, &refresh_token, &new_refresh_token).await?;
|
consume_compat_refresh_token(&mut txn, refresh_token).await?;
|
||||||
expire_compat_access_token(&mut txn, access_token).await?;
|
expire_compat_access_token(&mut txn, access_token).await?;
|
||||||
|
|
||||||
txn.commit().await?;
|
txn.commit().await?;
|
||||||
|
@ -38,6 +38,7 @@ use mas_templates::Templates;
|
|||||||
use oauth2_types::requests::{AccessTokenResponse, AuthorizationResponse};
|
use oauth2_types::requests::{AccessTokenResponse, AuthorizationResponse};
|
||||||
use sqlx::{PgPool, Postgres, Transaction};
|
use sqlx::{PgPool, Postgres, Transaction};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
use super::callback::{
|
use super::callback::{
|
||||||
CallbackDestination, CallbackDestinationError, IntoCallbackDestinationError,
|
CallbackDestination, CallbackDestinationError, IntoCallbackDestinationError,
|
||||||
@ -109,7 +110,7 @@ pub(crate) async fn get(
|
|||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
Path(grant_id): Path<i64>,
|
Path(grant_id): Path<Ulid>,
|
||||||
) -> Result<Response, RouteError> {
|
) -> Result<Response, RouteError> {
|
||||||
let mut txn = pool.begin().await?;
|
let mut txn = pool.begin().await?;
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ use mas_storage::oauth2::{
|
|||||||
use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
|
use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum RouteError {
|
pub enum RouteError {
|
||||||
@ -54,7 +55,7 @@ pub(crate) async fn get(
|
|||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
Path(grant_id): Path<i64>,
|
Path(grant_id): Path<Ulid>,
|
||||||
) -> Result<Response, RouteError> {
|
) -> Result<Response, RouteError> {
|
||||||
let mut conn = pool
|
let mut conn = pool
|
||||||
.acquire()
|
.acquire()
|
||||||
@ -115,7 +116,7 @@ pub(crate) async fn post(
|
|||||||
State(policy_factory): State<Arc<PolicyFactory>>,
|
State(policy_factory): State<Arc<PolicyFactory>>,
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
Path(grant_id): Path<i64>,
|
Path(grant_id): Path<Ulid>,
|
||||||
Form(form): Form<ProtectedForm<()>>,
|
Form(form): Form<ProtectedForm<()>>,
|
||||||
) -> Result<Response, RouteError> {
|
) -> Result<Response, RouteError> {
|
||||||
let mut txn = pool
|
let mut txn = pool
|
||||||
|
@ -24,10 +24,10 @@ use oauth2_types::{
|
|||||||
ClientMetadata, ClientMetadataVerificationError, ClientRegistrationResponse, Localized,
|
ClientMetadata, ClientMetadataVerificationError, ClientRegistrationResponse, Localized,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub(crate) enum RouteError {
|
pub(crate) enum RouteError {
|
||||||
@ -127,18 +127,14 @@ pub(crate) async fn post(
|
|||||||
let mut txn = pool.begin().await?;
|
let mut txn = pool.begin().await?;
|
||||||
|
|
||||||
// Let's generate a random client ID
|
// Let's generate a random client ID
|
||||||
let client_id: String = thread_rng()
|
let client_id = Ulid::new();
|
||||||
.sample_iter(&Alphanumeric)
|
|
||||||
.take(10)
|
|
||||||
.map(char::from)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
insert_client(
|
insert_client(
|
||||||
&mut txn,
|
&mut txn,
|
||||||
&client_id,
|
client_id,
|
||||||
metadata.redirect_uris(),
|
metadata.redirect_uris(),
|
||||||
None,
|
None,
|
||||||
&metadata.response_types(),
|
//&metadata.response_types(),
|
||||||
metadata.grant_types(),
|
metadata.grant_types(),
|
||||||
contacts,
|
contacts,
|
||||||
metadata
|
metadata
|
||||||
@ -162,7 +158,7 @@ pub(crate) async fn post(
|
|||||||
txn.commit().await?;
|
txn.commit().await?;
|
||||||
|
|
||||||
let response = ClientRegistrationResponse {
|
let response = ClientRegistrationResponse {
|
||||||
client_id,
|
client_id: client_id.to_string(),
|
||||||
client_secret: None,
|
client_secret: None,
|
||||||
client_id_issued_at: None,
|
client_id_issued_at: None,
|
||||||
client_secret_expires_at: None,
|
client_secret_expires_at: None,
|
||||||
|
@ -36,7 +36,7 @@ use mas_storage::{
|
|||||||
client::ClientFetchError,
|
client::ClientFetchError,
|
||||||
end_oauth_session,
|
end_oauth_session,
|
||||||
refresh_token::{
|
refresh_token::{
|
||||||
add_refresh_token, lookup_active_refresh_token, replace_refresh_token,
|
add_refresh_token, consume_refresh_token, lookup_active_refresh_token,
|
||||||
RefreshTokenLookupError,
|
RefreshTokenLookupError,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -311,10 +311,10 @@ async fn authorization_code_grant(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let access_token = add_access_token(&mut txn, session, &access_token_str, ttl).await?;
|
let access_token = add_access_token(&mut txn, session, access_token_str.clone(), ttl).await?;
|
||||||
|
|
||||||
let _refresh_token =
|
let _refresh_token =
|
||||||
add_refresh_token(&mut txn, session, access_token, &refresh_token_str).await?;
|
add_refresh_token(&mut txn, session, access_token, refresh_token_str.clone()).await?;
|
||||||
|
|
||||||
let id_token = if session.scope.contains(&scope::OPENID) {
|
let id_token = if session.scope.contains(&scope::OPENID) {
|
||||||
let mut claims = HashMap::new();
|
let mut claims = HashMap::new();
|
||||||
@ -391,20 +391,21 @@ async fn refresh_token_grant(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_access_token = add_access_token(&mut txn, &session, &access_token_str, ttl).await?;
|
let new_access_token =
|
||||||
|
add_access_token(&mut txn, &session, access_token_str.clone(), ttl).await?;
|
||||||
|
|
||||||
let new_refresh_token =
|
let new_refresh_token =
|
||||||
add_refresh_token(&mut txn, &session, new_access_token, &refresh_token_str).await?;
|
add_refresh_token(&mut txn, &session, new_access_token, refresh_token_str).await?;
|
||||||
|
|
||||||
replace_refresh_token(&mut txn, &refresh_token, &new_refresh_token).await?;
|
consume_refresh_token(&mut txn, &refresh_token).await?;
|
||||||
|
|
||||||
if let Some(access_token) = refresh_token.access_token {
|
if let Some(access_token) = refresh_token.access_token {
|
||||||
revoke_access_token(&mut txn, &access_token).await?;
|
revoke_access_token(&mut txn, access_token).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = AccessTokenResponse::new(access_token_str)
|
let params = AccessTokenResponse::new(access_token_str)
|
||||||
.with_expires_in(ttl)
|
.with_expires_in(ttl)
|
||||||
.with_refresh_token(refresh_token_str)
|
.with_refresh_token(new_refresh_token.refresh_token)
|
||||||
.with_scope(session.scope);
|
.with_scope(session.scope);
|
||||||
|
|
||||||
txn.commit().await?;
|
txn.commit().await?;
|
||||||
|
@ -86,7 +86,7 @@ pub(crate) async fn post(
|
|||||||
return Ok((cookie_jar, login.go()).into_response());
|
return Ok((cookie_jar, login.go()).into_response());
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_email = add_user_email(&mut txn, &session.user, &form.email).await?;
|
let user_email = add_user_email(&mut txn, &session.user, form.email).await?;
|
||||||
let next = mas_router::AccountVerifyEmail::new(user_email.data);
|
let next = mas_router::AccountVerifyEmail::new(user_email.data);
|
||||||
let next = if let Some(action) = query.post_auth_action {
|
let next = if let Some(action) = query.post_auth_action {
|
||||||
next.and_then(action)
|
next.and_then(action)
|
||||||
|
@ -17,6 +17,7 @@ use axum::{
|
|||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::PrivateCookieJar;
|
use axum_extra::extract::PrivateCookieJar;
|
||||||
|
use chrono::Duration;
|
||||||
use lettre::{message::Mailbox, Address};
|
use lettre::{message::Mailbox, Address};
|
||||||
use mas_axum_utils::{
|
use mas_axum_utils::{
|
||||||
csrf::{CsrfExt, ProtectedForm},
|
csrf::{CsrfExt, ProtectedForm},
|
||||||
@ -101,7 +102,8 @@ async fn start_email_verification(
|
|||||||
|
|
||||||
let address: Address = user_email.email.parse()?;
|
let address: Address = user_email.email.parse()?;
|
||||||
|
|
||||||
let verification = add_user_email_verification_code(executor, user_email, code).await?;
|
let verification =
|
||||||
|
add_user_email_verification_code(executor, user_email, Duration::hours(8), code).await?;
|
||||||
|
|
||||||
// And send the verification email
|
// And send the verification email
|
||||||
let mailbox = Mailbox::new(Some(user.username.clone()), address);
|
let mailbox = Mailbox::new(Some(user.username.clone()), address);
|
||||||
@ -111,7 +113,7 @@ async fn start_email_verification(
|
|||||||
mailer.send_verification_email(mailbox, &context).await?;
|
mailer.send_verification_email(mailbox, &context).await?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
email.id = verification.email.data,
|
email.id = %verification.email.data,
|
||||||
"Verification email sent"
|
"Verification email sent"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -141,7 +143,7 @@ pub(crate) async fn post(
|
|||||||
|
|
||||||
match form {
|
match form {
|
||||||
ManagementForm::Add { email } => {
|
ManagementForm::Add { email } => {
|
||||||
let user_email = add_user_email(&mut txn, &session.user, &email).await?;
|
let user_email = add_user_email(&mut txn, &session.user, email).await?;
|
||||||
let next = mas_router::AccountVerifyEmail::new(user_email.data);
|
let next = mas_router::AccountVerifyEmail::new(user_email.data);
|
||||||
start_email_verification(&mailer, &mut txn, &session.user, user_email).await?;
|
start_email_verification(&mailer, &mut txn, &session.user, user_email).await?;
|
||||||
txn.commit().await?;
|
txn.commit().await?;
|
||||||
|
@ -17,7 +17,6 @@ use axum::{
|
|||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::PrivateCookieJar;
|
use axum_extra::extract::PrivateCookieJar;
|
||||||
use chrono::Duration;
|
|
||||||
use mas_axum_utils::{
|
use mas_axum_utils::{
|
||||||
csrf::{CsrfExt, ProtectedForm},
|
csrf::{CsrfExt, ProtectedForm},
|
||||||
FancyError, SessionInfoExt,
|
FancyError, SessionInfoExt,
|
||||||
@ -31,6 +30,7 @@ use mas_storage::user::{
|
|||||||
use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates};
|
use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::views::shared::OptionalPostAuthAction;
|
use crate::views::shared::OptionalPostAuthAction;
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ pub(crate) async fn get(
|
|||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
Query(query): Query<OptionalPostAuthAction>,
|
Query(query): Query<OptionalPostAuthAction>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<Ulid>,
|
||||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
) -> Result<Response, FancyError> {
|
) -> Result<Response, FancyError> {
|
||||||
let mut conn = pool.acquire().await?;
|
let mut conn = pool.acquire().await?;
|
||||||
@ -81,7 +81,7 @@ pub(crate) async fn post(
|
|||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
Query(query): Query<OptionalPostAuthAction>,
|
Query(query): Query<OptionalPostAuthAction>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<Ulid>,
|
||||||
Form(form): Form<ProtectedForm<CodeForm>>,
|
Form(form): Form<ProtectedForm<CodeForm>>,
|
||||||
) -> Result<Response, FancyError> {
|
) -> Result<Response, FancyError> {
|
||||||
let mut txn = pool.begin().await?;
|
let mut txn = pool.begin().await?;
|
||||||
@ -105,9 +105,7 @@ pub(crate) async fn post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make those 8 hours configurable
|
// TODO: make those 8 hours configurable
|
||||||
let verification =
|
let verification = lookup_user_email_verification_code(&mut txn, email, &form.code).await?;
|
||||||
lookup_user_email_verification_code(&mut txn, email, &form.code, Duration::hours(8))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: display nice errors if the code was already consumed or expired
|
// TODO: display nice errors if the code was already consumed or expired
|
||||||
let verification = consume_email_verification(&mut txn, verification).await?;
|
let verification = consume_email_verification(&mut txn, verification).await?;
|
||||||
|
@ -22,6 +22,7 @@ use axum::{
|
|||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::PrivateCookieJar;
|
use axum_extra::extract::PrivateCookieJar;
|
||||||
|
use chrono::Duration;
|
||||||
use lettre::{message::Mailbox, Address};
|
use lettre::{message::Mailbox, Address};
|
||||||
use mas_axum_utils::{
|
use mas_axum_utils::{
|
||||||
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
||||||
@ -181,7 +182,7 @@ pub(crate) async fn post(
|
|||||||
let pfh = Argon2::default();
|
let pfh = Argon2::default();
|
||||||
let user = register_user(&mut txn, pfh, &form.username, &form.password).await?;
|
let user = register_user(&mut txn, pfh, &form.username, &form.password).await?;
|
||||||
|
|
||||||
let user_email = add_user_email(&mut txn, &user, &form.email).await?;
|
let user_email = add_user_email(&mut txn, &user, form.email).await?;
|
||||||
|
|
||||||
// First, generate a code
|
// First, generate a code
|
||||||
let range = Uniform::<u32>::from(0..1_000_000);
|
let range = Uniform::<u32>::from(0..1_000_000);
|
||||||
@ -189,7 +190,8 @@ pub(crate) async fn post(
|
|||||||
|
|
||||||
let address: Address = user_email.email.parse()?;
|
let address: Address = user_email.email.parse()?;
|
||||||
|
|
||||||
let verification = add_user_email_verification_code(&mut txn, user_email, code).await?;
|
let verification =
|
||||||
|
add_user_email_verification_code(&mut txn, user_email, Duration::hours(8), code).await?;
|
||||||
|
|
||||||
// And send the verification email
|
// And send the verification email
|
||||||
let mailbox = Mailbox::new(Some(user.username.clone()), address);
|
let mailbox = Mailbox::new(Some(user.username.clone()), address);
|
||||||
|
@ -11,3 +11,4 @@ serde = { version = "1.0.147", features = ["derive"] }
|
|||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
serde_with = "2.0.1"
|
serde_with = "2.0.1"
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
|
ulid = "1.0.0"
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
pub use crate::traits::*;
|
pub use crate::traits::*;
|
||||||
|
|
||||||
@ -23,23 +24,23 @@ pub use crate::traits::*;
|
|||||||
pub enum PostAuthAction {
|
pub enum PostAuthAction {
|
||||||
ContinueAuthorizationGrant {
|
ContinueAuthorizationGrant {
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
data: i64,
|
data: Ulid,
|
||||||
},
|
},
|
||||||
ContinueCompatSsoLogin {
|
ContinueCompatSsoLogin {
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
data: i64,
|
data: Ulid,
|
||||||
},
|
},
|
||||||
ChangePassword,
|
ChangePassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostAuthAction {
|
impl PostAuthAction {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn continue_grant(data: i64) -> Self {
|
pub fn continue_grant(data: Ulid) -> Self {
|
||||||
PostAuthAction::ContinueAuthorizationGrant { data }
|
PostAuthAction::ContinueAuthorizationGrant { data }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn continue_compat_sso_login(data: i64) -> Self {
|
pub fn continue_compat_sso_login(data: Ulid) -> Self {
|
||||||
PostAuthAction::ContinueCompatSsoLogin { data }
|
PostAuthAction::ContinueCompatSsoLogin { data }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,14 +167,14 @@ impl Login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn and_continue_grant(data: i64) -> Self {
|
pub fn and_continue_grant(data: Ulid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
post_auth_action: Some(PostAuthAction::continue_grant(data)),
|
post_auth_action: Some(PostAuthAction::continue_grant(data)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn and_continue_compat_sso_login(data: i64) -> Self {
|
pub fn and_continue_compat_sso_login(data: Ulid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
post_auth_action: Some(PostAuthAction::continue_compat_sso_login(data)),
|
post_auth_action: Some(PostAuthAction::continue_compat_sso_login(data)),
|
||||||
}
|
}
|
||||||
@ -222,7 +223,7 @@ impl Reauth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn and_continue_grant(data: i64) -> Self {
|
pub fn and_continue_grant(data: Ulid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
post_auth_action: Some(PostAuthAction::continue_grant(data)),
|
post_auth_action: Some(PostAuthAction::continue_grant(data)),
|
||||||
}
|
}
|
||||||
@ -275,14 +276,14 @@ impl Register {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn and_continue_grant(data: i64) -> Self {
|
pub fn and_continue_grant(data: Ulid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
post_auth_action: Some(PostAuthAction::continue_grant(data)),
|
post_auth_action: Some(PostAuthAction::continue_grant(data)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn and_continue_compat_sso_login(data: i64) -> Self {
|
pub fn and_continue_compat_sso_login(data: Ulid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
post_auth_action: Some(PostAuthAction::continue_compat_sso_login(data)),
|
post_auth_action: Some(PostAuthAction::continue_compat_sso_login(data)),
|
||||||
}
|
}
|
||||||
@ -323,13 +324,13 @@ impl From<Option<PostAuthAction>> for Register {
|
|||||||
/// `GET|POST /account/emails/verify/:id`
|
/// `GET|POST /account/emails/verify/:id`
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AccountVerifyEmail {
|
pub struct AccountVerifyEmail {
|
||||||
id: i64,
|
id: Ulid,
|
||||||
post_auth_action: Option<PostAuthAction>,
|
post_auth_action: Option<PostAuthAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountVerifyEmail {
|
impl AccountVerifyEmail {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(id: i64) -> Self {
|
pub fn new(id: Ulid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
post_auth_action: None,
|
post_auth_action: None,
|
||||||
@ -415,7 +416,7 @@ impl SimpleRoute for AccountEmails {
|
|||||||
|
|
||||||
/// `GET /authorize/:grant_id`
|
/// `GET /authorize/:grant_id`
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ContinueAuthorizationGrant(pub i64);
|
pub struct ContinueAuthorizationGrant(pub Ulid);
|
||||||
|
|
||||||
impl Route for ContinueAuthorizationGrant {
|
impl Route for ContinueAuthorizationGrant {
|
||||||
type Query = ();
|
type Query = ();
|
||||||
@ -430,7 +431,7 @@ impl Route for ContinueAuthorizationGrant {
|
|||||||
|
|
||||||
/// `GET /consent/:grant_id`
|
/// `GET /consent/:grant_id`
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Consent(pub i64);
|
pub struct Consent(pub Ulid);
|
||||||
|
|
||||||
impl Route for Consent {
|
impl Route for Consent {
|
||||||
type Query = ();
|
type Query = ();
|
||||||
@ -493,13 +494,13 @@ pub struct CompatLoginSsoActionParams {
|
|||||||
|
|
||||||
/// `GET|POST /complete-compat-sso/:id`
|
/// `GET|POST /complete-compat-sso/:id`
|
||||||
pub struct CompatLoginSsoComplete {
|
pub struct CompatLoginSsoComplete {
|
||||||
id: i64,
|
id: Ulid,
|
||||||
query: Option<CompatLoginSsoActionParams>,
|
query: Option<CompatLoginSsoActionParams>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompatLoginSsoComplete {
|
impl CompatLoginSsoComplete {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(id: i64, action: Option<CompatLoginSsoAction>) -> Self {
|
pub fn new(id: Ulid, action: Option<CompatLoginSsoAction>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
query: action.map(|action| CompatLoginSsoActionParams { action }),
|
query: action.map(|action| CompatLoginSsoActionParams { action }),
|
||||||
|
@ -30,6 +30,7 @@ pub use self::{endpoints::*, traits::Route, url_builder::UrlBuilder};
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -42,8 +43,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(Index.relative_url(), Cow::Borrowed("/"));
|
assert_eq!(Index.relative_url(), Cow::Borrowed("/"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Login::and_continue_grant(42).relative_url(),
|
Login::and_continue_grant(Ulid::nil()).relative_url(),
|
||||||
Cow::Borrowed("/login?next=continue_authorization_grant&data=42")
|
Cow::Borrowed(
|
||||||
|
"/login?next=continue_authorization_grant&data=00000000000000000000000000"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ license = "Apache-2.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = "1.21.2"
|
tokio = "1.21.2"
|
||||||
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline", "json"] }
|
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline", "json", "uuid"] }
|
||||||
chrono = { version = "0.4.22", features = ["serde"] }
|
chrono = { version = "0.4.22", features = ["serde"] }
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
serde_json = "1.0.87"
|
serde_json = "1.0.87"
|
||||||
@ -20,6 +20,8 @@ argon2 = { version = "0.4.1", features = ["password-hash"] }
|
|||||||
password-hash = { version = "0.4.2", features = ["std"] }
|
password-hash = { version = "0.4.2", features = ["std"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
url = { version = "2.3.1", features = ["serde"] }
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
|
uuid = "1.2.1"
|
||||||
|
ulid = { version = "1.0.0", features = ["uuid", "serde"] }
|
||||||
|
|
||||||
oauth2-types = { path = "../oauth2-types" }
|
oauth2-types = { path = "../oauth2-types" }
|
||||||
mas-data-model = { path = "../data-model" }
|
mas-data-model = { path = "../data-model" }
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
-- 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 FUNCTION IF EXISTS trigger_set_timestamp();
|
|
@ -1,21 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.updated_at = NOW();
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
@ -1,16 +0,0 @@
|
|||||||
-- 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 TRIGGER set_timestamp ON users;
|
|
||||||
DROP TABLE users;
|
|
@ -1,26 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"username" TEXT NOT NULL UNIQUE,
|
|
||||||
"hashed_password" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER set_timestamp
|
|
||||||
BEFORE UPDATE ON users
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
|
@ -1,17 +0,0 @@
|
|||||||
-- 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 TRIGGER set_timestamp ON user_sessions;
|
|
||||||
DROP TABLE user_session_authentications;
|
|
||||||
DROP TABLE user_sessions;
|
|
@ -1,35 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
-- A logged in session
|
|
||||||
CREATE TABLE user_sessions (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
"active" BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER set_timestamp
|
|
||||||
BEFORE UPDATE ON user_sessions
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
|
||||||
|
|
||||||
-- An authentication within a session
|
|
||||||
CREATE TABLE user_session_authentications (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"session_id" BIGINT NOT NULL REFERENCES user_sessions (id) ON DELETE CASCADE,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
@ -1,17 +0,0 @@
|
|||||||
-- 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 TRIGGER set_timestamp ON oauth2_sessions;
|
|
||||||
DROP TABLE oauth2_codes;
|
|
||||||
DROP TABLE oauth2_sessions;
|
|
@ -1,45 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
CREATE TABLE oauth2_sessions (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"user_session_id" BIGINT REFERENCES user_sessions (id) ON DELETE CASCADE,
|
|
||||||
"client_id" TEXT NOT NULL,
|
|
||||||
"redirect_uri" TEXT NOT NULL,
|
|
||||||
"scope" TEXT NOT NULL,
|
|
||||||
"state" TEXT,
|
|
||||||
"nonce" TEXT,
|
|
||||||
"max_age" INT,
|
|
||||||
"response_type" TEXT NOT NULL,
|
|
||||||
"response_mode" TEXT NOT NULL,
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER set_timestamp
|
|
||||||
BEFORE UPDATE ON oauth2_sessions
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
|
||||||
|
|
||||||
CREATE TABLE oauth2_codes (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
|
||||||
"code" TEXT UNIQUE NOT NULL,
|
|
||||||
"code_challenge_method" SMALLINT,
|
|
||||||
"code_challenge" TEXT,
|
|
||||||
|
|
||||||
CHECK (("code_challenge" IS NULL AND "code_challenge_method" IS NULL)
|
|
||||||
OR ("code_challenge" IS NOT NULL AND "code_challenge_method" IS NOT NULL))
|
|
||||||
);
|
|
@ -1,15 +0,0 @@
|
|||||||
-- 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_access_tokens;
|
|
@ -1,23 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
CREATE TABLE oauth2_access_tokens (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
|
||||||
|
|
||||||
"token" TEXT UNIQUE NOT NULL,
|
|
||||||
|
|
||||||
"expires_after" INT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
@ -1,16 +0,0 @@
|
|||||||
-- 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 TRIGGER set_timestamp ON oauth2_refresh_tokens;
|
|
||||||
DROP TABLE oauth2_refresh_tokens;
|
|
@ -1,30 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
CREATE TABLE oauth2_refresh_tokens (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
|
||||||
"oauth2_access_token_id" BIGINT REFERENCES oauth2_access_tokens (id) ON DELETE SET NULL,
|
|
||||||
|
|
||||||
"token" TEXT UNIQUE NOT NULL,
|
|
||||||
"next_token_id" BIGINT REFERENCES oauth2_refresh_tokens (id),
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER set_timestamp
|
|
||||||
BEFORE UPDATE ON oauth2_refresh_tokens
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
|
@ -1,103 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
|
|
||||||
-- Replace the old "sessions" table
|
|
||||||
ALTER TABLE oauth2_sessions RENAME TO oauth2_sessions_old;
|
|
||||||
|
|
||||||
-- TODO: how do we handle temporary session upgrades (aka. sudo mode)?
|
|
||||||
CREATE TABLE oauth2_sessions (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"user_session_id" BIGINT NOT NULL REFERENCES user_sessions (id) ON DELETE CASCADE,
|
|
||||||
"client_id" TEXT NOT NULL, -- The "authorization party" would be more accurate in that case
|
|
||||||
"scope" TEXT NOT NULL,
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
TRUNCATE oauth2_access_tokens, oauth2_refresh_tokens;
|
|
||||||
ALTER TABLE oauth2_access_tokens
|
|
||||||
DROP CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey,
|
|
||||||
ADD CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey
|
|
||||||
FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id);
|
|
||||||
ALTER TABLE oauth2_refresh_tokens
|
|
||||||
DROP CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey,
|
|
||||||
ADD CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey
|
|
||||||
FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id);
|
|
||||||
DROP TABLE oauth2_codes, oauth2_sessions_old;
|
|
||||||
|
|
||||||
CREATE TABLE oauth2_authorization_grants (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY, -- Saved as encrypted cookie
|
|
||||||
|
|
||||||
-- All this comes from the authorization request
|
|
||||||
"client_id" TEXT NOT NULL, -- This should be verified before insertion
|
|
||||||
"redirect_uri" TEXT NOT NULL, -- This should be verified before insertion
|
|
||||||
"scope" TEXT NOT NULL, -- This should be verified before insertion
|
|
||||||
"state" TEXT,
|
|
||||||
"nonce" TEXT,
|
|
||||||
"max_age" INT CHECK ("max_age" IS NULL OR "max_age" > 0),
|
|
||||||
"acr_values" TEXT, -- This should be verified before insertion
|
|
||||||
"response_mode" TEXT NOT NULL,
|
|
||||||
"code_challenge_method" TEXT,
|
|
||||||
"code_challenge" TEXT,
|
|
||||||
|
|
||||||
-- The "response_type" parameter broken down
|
|
||||||
"response_type_code" BOOLEAN NOT NULL,
|
|
||||||
"response_type_token" BOOLEAN NOT NULL,
|
|
||||||
"response_type_id_token" BOOLEAN NOT NULL,
|
|
||||||
|
|
||||||
-- This one is created eagerly on grant creation if the response_type
|
|
||||||
-- includes "code"
|
|
||||||
-- When looking up codes, it should do "where fulfilled_at is not null" and
|
|
||||||
-- "inner join on oauth2_sessions". When doing that, it should check the
|
|
||||||
-- "exchanged_at" field: if it is not null and was exchanged more than 30s
|
|
||||||
-- ago, the session shold be considered as hijacked and fully invalidated
|
|
||||||
"code" TEXT UNIQUE,
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"fulfilled_at" TIMESTAMP WITH TIME ZONE, -- When we got back to the client
|
|
||||||
"cancelled_at" TIMESTAMP WITH TIME ZONE, -- When that grant was cancelled
|
|
||||||
"exchanged_at" TIMESTAMP WITH TIME ZONE, -- When the code was exchanged by the client
|
|
||||||
|
|
||||||
"oauth2_session_id" BIGINT REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
|
||||||
|
|
||||||
-- Check a few invariants to keep a coherent state.
|
|
||||||
-- Even though the service should never violate those, it helps ensuring we're not doing anything wrong
|
|
||||||
|
|
||||||
-- Code exchange can only happen after the grant was fulfilled
|
|
||||||
CONSTRAINT "oauth2_authorization_grants_exchanged_after_fullfill"
|
|
||||||
CHECK (("exchanged_at" IS NULL)
|
|
||||||
OR ("exchanged_at" IS NOT NULL AND
|
|
||||||
"fulfilled_at" IS NOT NULL AND
|
|
||||||
"exchanged_at" >= "fulfilled_at")),
|
|
||||||
|
|
||||||
-- A grant can be either fulfilled or cancelled, but not both
|
|
||||||
CONSTRAINT "oauth2_authorization_grants_fulfilled_xor_cancelled"
|
|
||||||
CHECK ("fulfilled_at" IS NULL OR "cancelled_at" IS NULL),
|
|
||||||
|
|
||||||
-- If it was fulfilled there is an oauth2_session_id attached to it
|
|
||||||
CONSTRAINT "oauth2_authorization_grants_fulfilled_and_session"
|
|
||||||
CHECK (("fulfilled_at" IS NULL AND "oauth2_session_id" IS NULL)
|
|
||||||
OR ("fulfilled_at" IS NOT NULL AND "oauth2_session_id" IS NOT NULL)),
|
|
||||||
|
|
||||||
-- We should have a code if and only if the "code" response_type was asked
|
|
||||||
CONSTRAINT "oauth2_authorization_grants_code"
|
|
||||||
CHECK (("response_type_code" IS TRUE AND "code" IS NOT NULL)
|
|
||||||
OR ("response_type_code" IS FALSE AND "code" IS NULL)),
|
|
||||||
|
|
||||||
-- If we have a challenge, we also have a challenge method and a code
|
|
||||||
CONSTRAINT "oauth2_authorization_grants_code_challenge"
|
|
||||||
CHECK (("code_challenge" IS NULL AND "code_challenge_method" IS NULL)
|
|
||||||
OR ("code_challenge" IS NOT NULL AND "code_challenge_method" IS NOT NULL AND "response_type_code" IS TRUE))
|
|
||||||
);
|
|
@ -1,26 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
ALTER TABLE users ADD COLUMN hashed_password TEXT;
|
|
||||||
|
|
||||||
UPDATE users u
|
|
||||||
SET hashed_password = up.hashed_password
|
|
||||||
FROM user_passwords up
|
|
||||||
WHERE up.user_id = u.id;
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
ALTER COLUMN hashed_password
|
|
||||||
SET NOT NULL;
|
|
||||||
|
|
||||||
DROP TABLE user_passwords;
|
|
@ -1,27 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
CREATE TABLE user_passwords (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
"hashed_password" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO
|
|
||||||
user_passwords (user_id, hashed_password, created_at)
|
|
||||||
SELECT id, hashed_password, updated_at
|
|
||||||
FROM users;
|
|
||||||
|
|
||||||
ALTER TABLE users DROP COLUMN hashed_password;
|
|
@ -1,16 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_sessions
|
|
||||||
DROP COLUMN "ended_at";
|
|
@ -1,16 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_sessions
|
|
||||||
ADD COLUMN "ended_at" TIMESTAMP WITH TIME ZONE DEFAULT NULL;
|
|
@ -1,15 +0,0 @@
|
|||||||
-- 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 user_emails;
|
|
@ -1,24 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
CREATE TABLE user_emails (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
"email" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"confirmed_at" TIMESTAMP WITH TIME ZONE DEFAULT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN "primary_email_id" BIGINT REFERENCES user_emails (id) ON DELETE SET NULL;
|
|
@ -1,15 +0,0 @@
|
|||||||
-- 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 user_email_verifications;
|
|
@ -1,21 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
CREATE TABLE user_email_verifications (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"user_email_id" BIGINT NOT NULL REFERENCES user_emails (id) ON DELETE CASCADE,
|
|
||||||
"code" TEXT UNIQUE NOT NULL,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"consumed_at" TIMESTAMP WITH TIME ZONE DEFAULT NULL
|
|
||||||
);
|
|
@ -1,17 +0,0 @@
|
|||||||
-- 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;
|
|
@ -1,51 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
@ -1,16 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_clients
|
|
||||||
DROP COLUMN "userinfo_signed_response_alg" TEXT;
|
|
@ -1,16 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_clients
|
|
||||||
ADD COLUMN "userinfo_signed_response_alg" TEXT;
|
|
@ -1,23 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
TRUNCATE TABLE oauth2_sessions, oauth2_authorization_grants RESTART IDENTITY CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_sessions
|
|
||||||
DROP COLUMN "oauth2_client_id",
|
|
||||||
ADD COLUMN "client_id" TEXT NOT NULL;
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_authorization_grants
|
|
||||||
DROP COLUMN "oauth2_client_id",
|
|
||||||
ADD COLUMN "client_id" TEXT NOT NULL;
|
|
@ -1,27 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
TRUNCATE TABLE oauth2_sessions, oauth2_authorization_grants RESTART IDENTITY CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_sessions
|
|
||||||
DROP COLUMN "client_id",
|
|
||||||
ADD COLUMN "oauth2_client_id" BIGINT
|
|
||||||
NOT NULL
|
|
||||||
REFERENCES oauth2_clients (id) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_authorization_grants
|
|
||||||
DROP COLUMN "client_id",
|
|
||||||
ADD COLUMN "oauth2_client_id" BIGINT
|
|
||||||
NOT NULL
|
|
||||||
REFERENCES oauth2_clients (id) ON DELETE CASCADE;
|
|
@ -1,17 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
DROP TRIGGER set_timestamp ON oauth2_consents;
|
|
||||||
DROP INDEX oauth2_consents_client_id_user_id_key;
|
|
||||||
DROP TABLE oauth2_consents;
|
|
@ -1,32 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
CREATE TABLE oauth2_consents (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"oauth2_client_id" BIGINT NOT NULL REFERENCES oauth2_clients (id) ON DELETE CASCADE,
|
|
||||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
"scope_token" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
|
|
||||||
CONSTRAINT user_client_scope_tuple UNIQUE ("oauth2_client_id", "user_id", "scope_token")
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX oauth2_consents_client_id_user_id_key
|
|
||||||
ON oauth2_consents ("oauth2_client_id", "user_id");
|
|
||||||
|
|
||||||
CREATE TRIGGER set_timestamp
|
|
||||||
BEFORE UPDATE ON oauth2_consents
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
|
@ -1,16 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_authorization_grants
|
|
||||||
DROP COLUMN requires_consent;
|
|
@ -1,16 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_authorization_grants
|
|
||||||
ADD COLUMN requires_consent BOOLEAN NOT NULL DEFAULT 'f';
|
|
@ -1,17 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
DROP TABLE compat_refresh_tokens;
|
|
||||||
DROP TABLE compat_access_tokens;
|
|
||||||
DROP TABLE compat_session;
|
|
@ -1,42 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
CREATE TABLE compat_sessions (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
"device_id" TEXT UNIQUE NOT NULL,
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"deleted_at" TIMESTAMP WITH TIME ZONE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE compat_access_tokens (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"compat_session_id" BIGINT NOT NULL REFERENCES compat_sessions (id) ON DELETE CASCADE,
|
|
||||||
"token" TEXT UNIQUE NOT NULL,
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"expires_at" TIMESTAMP WITH TIME ZONE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE compat_refresh_tokens (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"compat_session_id" BIGINT NOT NULL REFERENCES compat_sessions (id) ON DELETE CASCADE,
|
|
||||||
"compat_access_token_id" BIGINT REFERENCES compat_access_tokens (id) ON DELETE SET NULL,
|
|
||||||
|
|
||||||
"token" TEXT UNIQUE NOT NULL,
|
|
||||||
"next_token_id" BIGINT REFERENCES compat_refresh_tokens (id),
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
@ -1,25 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
CREATE TABLE compat_sso_logins (
|
|
||||||
"id" BIGSERIAL PRIMARY KEY,
|
|
||||||
"redirect_uri" TEXT NOT NULL,
|
|
||||||
"token" TEXT UNIQUE NOT NULL,
|
|
||||||
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"fullfilled_at" TIMESTAMP WITH TIME ZONE,
|
|
||||||
"exchanged_at" TIMESTAMP WITH TIME ZONE,
|
|
||||||
|
|
||||||
"compat_session_id" BIGINT REFERENCES compat_sessions (id) ON DELETE CASCADE
|
|
||||||
);
|
|
@ -1,19 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_authorization_grants
|
|
||||||
ADD COLUMN "response_type_token" BOOLEAN NOT NULL DEFAULT 'f';
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_authorization_grants
|
|
||||||
ALTER COLUMN "response_type_token" DROP DEFAULT;
|
|
@ -1,16 +0,0 @@
|
|||||||
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
--
|
|
||||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
-- you may not use this file except in compliance with the License.
|
|
||||||
-- You may obtain a copy of the License at
|
|
||||||
--
|
|
||||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
--
|
|
||||||
-- Unless required by applicable law or agreed to in writing, software
|
|
||||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
-- See the License for the specific language governing permissions and
|
|
||||||
-- limitations under the License.
|
|
||||||
|
|
||||||
ALTER TABLE oauth2_authorization_grants
|
|
||||||
DROP COLUMN "response_type_token";
|
|
@ -12,4 +12,3 @@
|
|||||||
-- See the License for the specific language governing permissions and
|
-- See the License for the specific language governing permissions and
|
||||||
-- limitations under the License.
|
-- limitations under the License.
|
||||||
|
|
||||||
DROP TABLE compat_sso_logins;
|
|
350
crates/storage/migrations/20221018142001_init.up.sql
Normal file
350
crates/storage/migrations/20221018142001_init.up.sql
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
-- 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.
|
||||||
|
|
||||||
|
-----------
|
||||||
|
-- Users --
|
||||||
|
-----------
|
||||||
|
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"user_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "users_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"username" TEXT NOT NULL
|
||||||
|
CONSTRAINT "users_username_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "user_passwords" (
|
||||||
|
"user_password_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_passwords_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"user_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_passwords_user_id_fkey"
|
||||||
|
REFERENCES "users" ("user_id"),
|
||||||
|
|
||||||
|
"hashed_password" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "user_emails" (
|
||||||
|
"user_email_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_emails_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"user_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_emails_user_id_fkey"
|
||||||
|
REFERENCES "users" ("user_id")
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"confirmed_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "users"
|
||||||
|
ADD COLUMN "primary_user_email_id" UUID
|
||||||
|
CONSTRAINT "users_primary_user_email_id_fkey"
|
||||||
|
REFERENCES "user_emails" ("user_email_id")
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE TABLE "user_email_confirmation_codes" (
|
||||||
|
"user_email_confirmation_code_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_email_confirmation_codes_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"user_email_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_email_confirmation_codes_user_email_id_fkey"
|
||||||
|
REFERENCES "user_emails" ("user_email_id"),
|
||||||
|
|
||||||
|
"code" TEXT NOT NULL
|
||||||
|
CONSTRAINT "user_email_confirmation_codes_code_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"expires_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"consumed_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "user_sessions" (
|
||||||
|
"user_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_sessions_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"user_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_sessions_user_id_fkey"
|
||||||
|
REFERENCES "users" ("user_id"),
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"finished_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "user_session_authentications" (
|
||||||
|
"user_session_authentication_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_session_authentications_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"user_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "user_session_authentications_user_session_id_fkey"
|
||||||
|
REFERENCES "user_sessions" ("user_session_id"),
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
-- Compat sessions --
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
CREATE TABLE "compat_sessions" (
|
||||||
|
"compat_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_sessions_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"user_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_sessions_user_id_fkey"
|
||||||
|
REFERENCES "users" ("user_id"),
|
||||||
|
|
||||||
|
"device_id" TEXT NOT NULL
|
||||||
|
CONSTRAINT "compat_sessions_device_id_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"finished_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "compat_sso_logins" (
|
||||||
|
"compat_sso_login_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_sso_logins_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"redirect_uri" TEXT NOT NULL,
|
||||||
|
|
||||||
|
"login_token" TEXT NOT NULL
|
||||||
|
CONSTRAINT "compat_sessions_login_token_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"compat_session_id" UUID
|
||||||
|
CONSTRAINT "compat_sso_logins_compat_session_id_fkey"
|
||||||
|
REFERENCES "compat_sessions" ("compat_session_id")
|
||||||
|
ON DELETE SET NULL,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"fulfilled_at" TIMESTAMP WITH TIME ZONE,
|
||||||
|
"exchanged_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "compat_access_tokens" (
|
||||||
|
"compat_access_token_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_access_tokens_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"compat_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_access_tokens_compat_session_id_fkey"
|
||||||
|
REFERENCES "compat_sessions" ("compat_session_id"),
|
||||||
|
|
||||||
|
"access_token" TEXT NOT NULL
|
||||||
|
CONSTRAINT "compat_access_tokens_access_token_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"expires_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "compat_refresh_tokens" (
|
||||||
|
"compat_refresh_token_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_refresh_tokens_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"compat_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_refresh_tokens_compat_session_id_fkey"
|
||||||
|
REFERENCES "compat_sessions" ("compat_session_id"),
|
||||||
|
|
||||||
|
"compat_access_token_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "compat_refresh_tokens_compat_access_token_id_fkey"
|
||||||
|
REFERENCES "compat_access_tokens" ("compat_access_token_id"),
|
||||||
|
|
||||||
|
"refresh_token" TEXT NOT NULL
|
||||||
|
CONSTRAINT "compat_refresh_tokens_refresh_token_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"consumed_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
----------------
|
||||||
|
-- OAuth 2.0 ---
|
||||||
|
----------------
|
||||||
|
|
||||||
|
CREATE TABLE "oauth2_clients" (
|
||||||
|
"oauth2_client_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_clients_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"encrypted_client_secret" TEXT,
|
||||||
|
|
||||||
|
"grant_type_authorization_code" BOOLEAN NOT NULL,
|
||||||
|
"grant_type_refresh_token" BOOLEAN 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,
|
||||||
|
"userinfo_signed_response_alg" TEXT,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "oauth2_client_redirect_uris" (
|
||||||
|
"oauth2_client_redirect_uri_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_client_redirect_uris_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"oauth2_client_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "tbl_oauth2_client_id_fkey"
|
||||||
|
REFERENCES "oauth2_clients" ("oauth2_client_id"),
|
||||||
|
|
||||||
|
"redirect_uri" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "oauth2_sessions" (
|
||||||
|
"oauth2_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_sessions_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"user_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_sessions_user_session_id_fkey"
|
||||||
|
REFERENCES "user_sessions" ("user_session_id"),
|
||||||
|
|
||||||
|
"oauth2_client_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_sessions_oauth2_client_id_fkey"
|
||||||
|
REFERENCES "oauth2_clients" ("oauth2_client_id"),
|
||||||
|
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"finished_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "oauth2_consents" (
|
||||||
|
"oauth2_consent_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_consents_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"oauth2_client_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_consents_oauth2_client_id_fkey"
|
||||||
|
REFERENCES "oauth2_clients" ("oauth2_client_id"),
|
||||||
|
|
||||||
|
"user_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_consents_user_id_fkey"
|
||||||
|
REFERENCES "users" ("user_id"),
|
||||||
|
|
||||||
|
"scope_token" TEXT NOT NULL,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"refreshed_at" TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
CONSTRAINT "oauth2_consents_unique"
|
||||||
|
UNIQUE ("oauth2_client_id", "user_id", "scope_token")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "oauth2_consents_oauth2_client_id_user_id"
|
||||||
|
ON "oauth2_consents" ("oauth2_client_id", "user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "oauth2_access_tokens" (
|
||||||
|
"oauth2_access_token_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_access_tokens_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"oauth2_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_access_tokens_oauth2_session_id_fkey"
|
||||||
|
REFERENCES "oauth2_sessions" ("oauth2_session_id"),
|
||||||
|
|
||||||
|
"access_token" TEXT NOT NULL
|
||||||
|
CONSTRAINT "oauth2_access_tokens_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"expires_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"revoked_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "oauth2_refresh_tokens" (
|
||||||
|
"oauth2_refresh_token_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_refresh_tokens_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"oauth2_session_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_access_tokens_oauth2_session_id_fkey"
|
||||||
|
REFERENCES "oauth2_sessions" ("oauth2_session_id"),
|
||||||
|
|
||||||
|
"oauth2_access_token_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_refresh_tokens_oauth2_access_token_id_fkey"
|
||||||
|
REFERENCES "oauth2_access_tokens" ("oauth2_access_token_id"),
|
||||||
|
|
||||||
|
"refresh_token" TEXT NOT NULL
|
||||||
|
CONSTRAINT "oauth2_refresh_tokens_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"consumed_at" TIMESTAMP WITH TIME ZONE,
|
||||||
|
"revoked_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "oauth2_authorization_grants" (
|
||||||
|
"oauth2_authorization_grant_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "oauth2_authorization_grants_pkey"
|
||||||
|
PRIMARY KEY,
|
||||||
|
|
||||||
|
"oauth2_client_id" UUID NOT NULL
|
||||||
|
CONSTRAINT "tbl_oauth2_client_fkey"
|
||||||
|
REFERENCES "oauth2_clients" ("oauth2_client_id"),
|
||||||
|
|
||||||
|
"oauth2_session_id" UUID
|
||||||
|
CONSTRAINT "tbl_oauth2_session_fkey"
|
||||||
|
REFERENCES "oauth2_sessions" ("oauth2_session_id"),
|
||||||
|
|
||||||
|
"authorization_code" TEXT
|
||||||
|
CONSTRAINT "oauth2_authorization_grants_authorization_code_unique"
|
||||||
|
UNIQUE,
|
||||||
|
|
||||||
|
"redirect_uri" TEXT NOT NULL,
|
||||||
|
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"state" TEXT,
|
||||||
|
"nonce" TEXT,
|
||||||
|
"max_age" INTEGER,
|
||||||
|
"response_mode" TEXT NOT NULL,
|
||||||
|
"code_challenge_method" TEXT,
|
||||||
|
"code_challenge" TEXT,
|
||||||
|
"response_type_code" BOOLEAN NOT NULL,
|
||||||
|
"response_type_id_token" BOOLEAN NOT NULL,
|
||||||
|
"requires_consent" BOOLEAN NOT NULL,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"fulfilled_at" TIMESTAMP WITH TIME ZONE,
|
||||||
|
"cancelled_at" TIMESTAMP WITH TIME ZONE,
|
||||||
|
"exchanged_at" TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
File diff suppressed because it is too large
Load Diff
@ -19,28 +19,28 @@ use mas_data_model::{
|
|||||||
CompatAccessToken, CompatRefreshToken, CompatSession, CompatSsoLogin, CompatSsoLoginState,
|
CompatAccessToken, CompatRefreshToken, CompatSession, CompatSsoLogin, CompatSsoLoginState,
|
||||||
Device, User, UserEmail,
|
Device, User, UserEmail,
|
||||||
};
|
};
|
||||||
use sqlx::{postgres::types::PgInterval, Acquire, PgExecutor, Postgres};
|
use sqlx::{Acquire, PgExecutor, Postgres};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tracing::{info_span, Instrument};
|
use tracing::{info_span, Instrument};
|
||||||
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{user::lookup_user_by_username, DatabaseInconsistencyError, PostgresqlBackend};
|
||||||
user::lookup_user_by_username, DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CompatAccessTokenLookup {
|
struct CompatAccessTokenLookup {
|
||||||
compat_access_token_id: i64,
|
compat_access_token_id: Uuid,
|
||||||
compat_access_token: String,
|
compat_access_token: String,
|
||||||
compat_access_token_created_at: DateTime<Utc>,
|
compat_access_token_created_at: DateTime<Utc>,
|
||||||
compat_access_token_expires_at: Option<DateTime<Utc>>,
|
compat_access_token_expires_at: Option<DateTime<Utc>>,
|
||||||
compat_session_id: i64,
|
compat_session_id: Uuid,
|
||||||
compat_session_created_at: DateTime<Utc>,
|
compat_session_created_at: DateTime<Utc>,
|
||||||
compat_session_deleted_at: Option<DateTime<Utc>>,
|
compat_session_finished_at: Option<DateTime<Utc>>,
|
||||||
compat_session_device_id: String,
|
compat_session_device_id: String,
|
||||||
user_id: i64,
|
user_id: Uuid,
|
||||||
user_username: String,
|
user_username: String,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -49,6 +49,7 @@ struct CompatAccessTokenLookup {
|
|||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error("failed to lookup compat access token")]
|
#[error("failed to lookup compat access token")]
|
||||||
pub enum CompatAccessTokenLookupError {
|
pub enum CompatAccessTokenLookupError {
|
||||||
|
Expired { when: DateTime<Utc> },
|
||||||
Database(#[from] sqlx::Error),
|
Database(#[from] sqlx::Error),
|
||||||
Inconsistency(#[from] DatabaseInconsistencyError),
|
Inconsistency(#[from] DatabaseInconsistencyError),
|
||||||
}
|
}
|
||||||
@ -56,7 +57,10 @@ pub enum CompatAccessTokenLookupError {
|
|||||||
impl CompatAccessTokenLookupError {
|
impl CompatAccessTokenLookupError {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn not_found(&self) -> bool {
|
pub fn not_found(&self) -> bool {
|
||||||
matches!(self, Self::Database(sqlx::Error::RowNotFound))
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::Database(sqlx::Error::RowNotFound) | Self::Expired { .. }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,41 +79,48 @@ pub async fn lookup_active_compat_access_token(
|
|||||||
CompatAccessTokenLookup,
|
CompatAccessTokenLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
ct.id AS "compat_access_token_id",
|
ct.compat_access_token_id,
|
||||||
ct.token AS "compat_access_token",
|
ct.access_token AS "compat_access_token",
|
||||||
ct.created_at AS "compat_access_token_created_at",
|
ct.created_at AS "compat_access_token_created_at",
|
||||||
ct.expires_at AS "compat_access_token_expires_at",
|
ct.expires_at AS "compat_access_token_expires_at",
|
||||||
cs.id AS "compat_session_id",
|
cs.compat_session_id,
|
||||||
cs.created_at AS "compat_session_created_at",
|
cs.created_at AS "compat_session_created_at",
|
||||||
cs.deleted_at AS "compat_session_deleted_at",
|
cs.finished_at AS "compat_session_finished_at",
|
||||||
cs.device_id AS "compat_session_device_id",
|
cs.device_id AS "compat_session_device_id",
|
||||||
u.id AS "user_id!",
|
u.user_id AS "user_id!",
|
||||||
u.username AS "user_username!",
|
u.username AS "user_username!",
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
|
|
||||||
FROM compat_access_tokens ct
|
FROM compat_access_tokens ct
|
||||||
INNER JOIN compat_sessions cs
|
INNER JOIN compat_sessions cs
|
||||||
ON cs.id = ct.compat_session_id
|
USING (compat_session_id)
|
||||||
INNER JOIN users u
|
INNER JOIN users u
|
||||||
ON u.id = cs.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
|
|
||||||
WHERE ct.token = $1
|
WHERE ct.access_token = $1
|
||||||
AND (ct.expires_at IS NULL OR ct.expires_at > NOW())
|
AND (ct.expires_at IS NULL OR ct.expires_at > NOW())
|
||||||
AND cs.deleted_at IS NULL
|
AND cs.finished_at IS NULL
|
||||||
"#,
|
"#,
|
||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.instrument(info_span!("Fetch compat access token"))
|
.instrument(info_span!("Fetch compat access token"))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Check for token expiration
|
||||||
|
if let Some(expires_at) = res.compat_access_token_expires_at {
|
||||||
|
if expires_at < Utc::now() {
|
||||||
|
return Err(CompatAccessTokenLookupError::Expired { when: expires_at });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let token = CompatAccessToken {
|
let token = CompatAccessToken {
|
||||||
data: res.compat_access_token_id,
|
data: res.compat_access_token_id.into(),
|
||||||
token: res.compat_access_token,
|
token: res.compat_access_token,
|
||||||
created_at: res.compat_access_token_created_at,
|
created_at: res.compat_access_token_created_at,
|
||||||
expires_at: res.compat_access_token_expires_at,
|
expires_at: res.compat_access_token_expires_at,
|
||||||
@ -122,7 +133,7 @@ pub async fn lookup_active_compat_access_token(
|
|||||||
res.user_email_confirmed_at,
|
res.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -131,41 +142,42 @@ pub async fn lookup_active_compat_access_token(
|
|||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let id = Ulid::from(res.user_id);
|
||||||
let user = User {
|
let user = User {
|
||||||
data: res.user_id,
|
data: id,
|
||||||
username: res.user_username,
|
username: res.user_username,
|
||||||
sub: format!("fake-sub-{}", res.user_id),
|
sub: id.to_string(),
|
||||||
primary_email,
|
primary_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
let device = Device::try_from(res.compat_session_device_id).unwrap();
|
let device = Device::try_from(res.compat_session_device_id).unwrap();
|
||||||
|
|
||||||
let session = CompatSession {
|
let session = CompatSession {
|
||||||
data: res.compat_session_id,
|
data: res.compat_session_id.into(),
|
||||||
user,
|
user,
|
||||||
device,
|
device,
|
||||||
created_at: res.compat_session_created_at,
|
created_at: res.compat_session_created_at,
|
||||||
deleted_at: res.compat_session_deleted_at,
|
finished_at: res.compat_session_finished_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((token, session))
|
Ok((token, session))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CompatRefreshTokenLookup {
|
pub struct CompatRefreshTokenLookup {
|
||||||
compat_refresh_token_id: i64,
|
compat_refresh_token_id: Uuid,
|
||||||
compat_refresh_token: String,
|
compat_refresh_token: String,
|
||||||
compat_refresh_token_created_at: DateTime<Utc>,
|
compat_refresh_token_created_at: DateTime<Utc>,
|
||||||
compat_access_token_id: i64,
|
compat_access_token_id: Uuid,
|
||||||
compat_access_token: String,
|
compat_access_token: String,
|
||||||
compat_access_token_created_at: DateTime<Utc>,
|
compat_access_token_created_at: DateTime<Utc>,
|
||||||
compat_access_token_expires_at: Option<DateTime<Utc>>,
|
compat_access_token_expires_at: Option<DateTime<Utc>>,
|
||||||
compat_session_id: i64,
|
compat_session_id: Uuid,
|
||||||
compat_session_created_at: DateTime<Utc>,
|
compat_session_created_at: DateTime<Utc>,
|
||||||
compat_session_deleted_at: Option<DateTime<Utc>>,
|
compat_session_finished_at: Option<DateTime<Utc>>,
|
||||||
compat_session_device_id: String,
|
compat_session_device_id: String,
|
||||||
user_id: i64,
|
user_id: Uuid,
|
||||||
user_username: String,
|
user_username: String,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -202,37 +214,37 @@ pub async fn lookup_active_compat_refresh_token(
|
|||||||
CompatRefreshTokenLookup,
|
CompatRefreshTokenLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
cr.id AS "compat_refresh_token_id",
|
cr.compat_refresh_token_id,
|
||||||
cr.token AS "compat_refresh_token",
|
cr.refresh_token AS "compat_refresh_token",
|
||||||
cr.created_at AS "compat_refresh_token_created_at",
|
cr.created_at AS "compat_refresh_token_created_at",
|
||||||
ct.id AS "compat_access_token_id",
|
ct.compat_access_token_id,
|
||||||
ct.token AS "compat_access_token",
|
ct.access_token AS "compat_access_token",
|
||||||
ct.created_at AS "compat_access_token_created_at",
|
ct.created_at AS "compat_access_token_created_at",
|
||||||
ct.expires_at AS "compat_access_token_expires_at",
|
ct.expires_at AS "compat_access_token_expires_at",
|
||||||
cs.id AS "compat_session_id",
|
cs.compat_session_id,
|
||||||
cs.created_at AS "compat_session_created_at",
|
cs.created_at AS "compat_session_created_at",
|
||||||
cs.deleted_at AS "compat_session_deleted_at",
|
cs.finished_at AS "compat_session_finished_at",
|
||||||
cs.device_id AS "compat_session_device_id",
|
cs.device_id AS "compat_session_device_id",
|
||||||
u.id AS "user_id!",
|
u.user_id,
|
||||||
u.username AS "user_username!",
|
u.username AS "user_username!",
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
|
|
||||||
FROM compat_refresh_tokens cr
|
FROM compat_refresh_tokens cr
|
||||||
INNER JOIN compat_access_tokens ct
|
|
||||||
ON ct.id = cr.compat_access_token_id
|
|
||||||
INNER JOIN compat_sessions cs
|
INNER JOIN compat_sessions cs
|
||||||
ON cs.id = cr.compat_session_id
|
USING (compat_session_id)
|
||||||
|
INNER JOIN compat_access_tokens ct
|
||||||
|
USING (compat_access_token_id)
|
||||||
INNER JOIN users u
|
INNER JOIN users u
|
||||||
ON u.id = cs.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
|
|
||||||
WHERE cr.token = $1
|
WHERE cr.refresh_token = $1
|
||||||
AND cr.next_token_id IS NULL
|
AND cr.consumed_at IS NULL
|
||||||
AND cs.deleted_at IS NULL
|
AND cs.finished_at IS NULL
|
||||||
"#,
|
"#,
|
||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
@ -241,13 +253,13 @@ pub async fn lookup_active_compat_refresh_token(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let refresh_token = CompatRefreshToken {
|
let refresh_token = CompatRefreshToken {
|
||||||
data: res.compat_refresh_token_id,
|
data: res.compat_refresh_token_id.into(),
|
||||||
token: res.compat_refresh_token,
|
token: res.compat_refresh_token,
|
||||||
created_at: res.compat_refresh_token_created_at,
|
created_at: res.compat_refresh_token_created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let access_token = CompatAccessToken {
|
let access_token = CompatAccessToken {
|
||||||
data: res.compat_access_token_id,
|
data: res.compat_access_token_id.into(),
|
||||||
token: res.compat_access_token,
|
token: res.compat_access_token,
|
||||||
created_at: res.compat_access_token_created_at,
|
created_at: res.compat_access_token_created_at,
|
||||||
expires_at: res.compat_access_token_expires_at,
|
expires_at: res.compat_access_token_expires_at,
|
||||||
@ -260,7 +272,7 @@ pub async fn lookup_active_compat_refresh_token(
|
|||||||
res.user_email_confirmed_at,
|
res.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -269,21 +281,22 @@ pub async fn lookup_active_compat_refresh_token(
|
|||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let id = Ulid::from(res.user_id);
|
||||||
let user = User {
|
let user = User {
|
||||||
data: res.user_id,
|
data: id,
|
||||||
username: res.user_username,
|
username: res.user_username,
|
||||||
sub: format!("fake-sub-{}", res.user_id),
|
sub: id.to_string(),
|
||||||
primary_email,
|
primary_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
let device = Device::try_from(res.compat_session_device_id).unwrap();
|
let device = Device::try_from(res.compat_session_device_id).unwrap();
|
||||||
|
|
||||||
let session = CompatSession {
|
let session = CompatSession {
|
||||||
data: res.compat_session_id,
|
data: res.compat_session_id.into(),
|
||||||
user,
|
user,
|
||||||
device,
|
device,
|
||||||
created_at: res.compat_session_created_at,
|
created_at: res.compat_session_created_at,
|
||||||
deleted_at: res.compat_session_deleted_at,
|
finished_at: res.compat_session_finished_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((refresh_token, access_token, session))
|
Ok((refresh_token, access_token, session))
|
||||||
@ -310,7 +323,7 @@ pub async fn compat_login(
|
|||||||
ORDER BY up.created_at DESC
|
ORDER BY up.created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(user.data),
|
||||||
)
|
)
|
||||||
.fetch_one(&mut txn)
|
.fetch_one(&mut txn)
|
||||||
.instrument(tracing::info_span!("Lookup hashed password"))
|
.instrument(tracing::info_span!("Lookup hashed password"))
|
||||||
@ -327,27 +340,30 @@ pub async fn compat_login(
|
|||||||
.instrument(tracing::info_span!("Verify hashed password"))
|
.instrument(tracing::info_span!("Verify hashed password"))
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO compat_sessions (user_id, device_id)
|
INSERT INTO compat_sessions
|
||||||
VALUES ($1, $2)
|
(compat_session_id, user_id, device_id, created_at)
|
||||||
RETURNING id, created_at
|
VALUES ($1, $2, $3, $4)
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(id),
|
||||||
|
Uuid::from(user.data),
|
||||||
device.as_str(),
|
device.as_str(),
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut txn)
|
.execute(&mut txn)
|
||||||
.instrument(tracing::info_span!("Insert compat session"))
|
.instrument(tracing::info_span!("Insert compat session"))
|
||||||
.await
|
.await
|
||||||
.context("could not insert compat session")?;
|
.context("could not insert compat session")?;
|
||||||
|
|
||||||
let session = CompatSession {
|
let session = CompatSession {
|
||||||
data: res.id,
|
data: id,
|
||||||
user,
|
user,
|
||||||
device,
|
device,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
deleted_at: None,
|
finished_at: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
txn.commit().await.context("could not commit transaction")?;
|
txn.commit().await.context("could not commit transaction")?;
|
||||||
@ -361,70 +377,48 @@ pub async fn add_compat_access_token(
|
|||||||
token: String,
|
token: String,
|
||||||
expires_after: Option<Duration>,
|
expires_after: Option<Duration>,
|
||||||
) -> Result<CompatAccessToken<PostgresqlBackend>, anyhow::Error> {
|
) -> Result<CompatAccessToken<PostgresqlBackend>, anyhow::Error> {
|
||||||
if let Some(expires_after) = expires_after {
|
let created_at = Utc::now();
|
||||||
// For some reason, we need to convert the type first
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
let pg_expires_after = PgInterval::try_from(expires_after)
|
let expires_at = expires_after.map(|expires_after| created_at + expires_after);
|
||||||
// For some reason, this error type does not let me to just bubble up the error here
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to encode duration: {}", e))?;
|
|
||||||
|
|
||||||
let res = sqlx::query_as!(
|
sqlx::query!(
|
||||||
IdAndCreationTime,
|
r#"
|
||||||
r#"
|
INSERT INTO compat_access_tokens
|
||||||
INSERT INTO compat_access_tokens (compat_session_id, token, created_at, expires_at)
|
(compat_access_token_id, compat_session_id, access_token, created_at, expires_at)
|
||||||
VALUES ($1, $2, NOW(), NOW() + $3)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING id, created_at
|
"#,
|
||||||
"#,
|
Uuid::from(id),
|
||||||
session.data,
|
Uuid::from(session.data),
|
||||||
token,
|
token,
|
||||||
pg_expires_after,
|
created_at,
|
||||||
)
|
expires_at,
|
||||||
.fetch_one(executor)
|
)
|
||||||
.instrument(tracing::info_span!("Insert compat access token"))
|
.execute(executor)
|
||||||
.await
|
.instrument(tracing::info_span!("Insert compat access token"))
|
||||||
.context("could not insert compat access token")?;
|
.await
|
||||||
|
.context("could not insert compat access token")?;
|
||||||
|
|
||||||
Ok(CompatAccessToken {
|
Ok(CompatAccessToken {
|
||||||
data: res.id,
|
data: id,
|
||||||
token,
|
token,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
expires_at: Some(res.created_at + expires_after),
|
expires_at,
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
let res = sqlx::query_as!(
|
|
||||||
IdAndCreationTime,
|
|
||||||
r#"
|
|
||||||
INSERT INTO compat_access_tokens (compat_session_id, token)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
RETURNING id, created_at
|
|
||||||
"#,
|
|
||||||
session.data,
|
|
||||||
token,
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.instrument(tracing::info_span!("Insert compat access token"))
|
|
||||||
.await
|
|
||||||
.context("could not insert compat access token")?;
|
|
||||||
|
|
||||||
Ok(CompatAccessToken {
|
|
||||||
data: res.id,
|
|
||||||
token,
|
|
||||||
created_at: res.created_at,
|
|
||||||
expires_at: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn expire_compat_access_token(
|
pub async fn expire_compat_access_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
access_token: CompatAccessToken<PostgresqlBackend>,
|
access_token: CompatAccessToken<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let expires_at = Utc::now();
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE compat_access_tokens
|
UPDATE compat_access_tokens
|
||||||
SET expires_at = NOW()
|
SET expires_at = $2
|
||||||
WHERE id = $1
|
WHERE compat_access_token_id = $1
|
||||||
"#,
|
"#,
|
||||||
access_token.data,
|
Uuid::from(access_token.data),
|
||||||
|
expires_at,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
@ -445,26 +439,30 @@ pub async fn add_compat_refresh_token(
|
|||||||
access_token: &CompatAccessToken<PostgresqlBackend>,
|
access_token: &CompatAccessToken<PostgresqlBackend>,
|
||||||
token: String,
|
token: String,
|
||||||
) -> Result<CompatRefreshToken<PostgresqlBackend>, anyhow::Error> {
|
) -> Result<CompatRefreshToken<PostgresqlBackend>, anyhow::Error> {
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO compat_refresh_tokens (compat_session_id, compat_access_token_id, token)
|
INSERT INTO compat_refresh_tokens
|
||||||
VALUES ($1, $2, $3)
|
(compat_refresh_token_id, compat_session_id,
|
||||||
RETURNING id, created_at
|
compat_access_token_id, refresh_token, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
"#,
|
"#,
|
||||||
session.data,
|
Uuid::from(id),
|
||||||
access_token.data,
|
Uuid::from(session.data),
|
||||||
|
Uuid::from(access_token.data),
|
||||||
token,
|
token,
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.instrument(tracing::info_span!("Insert compat refresh token"))
|
.instrument(tracing::info_span!("Insert compat refresh token"))
|
||||||
.await
|
.await
|
||||||
.context("could not insert compat refresh token")?;
|
.context("could not insert compat refresh token")?;
|
||||||
|
|
||||||
Ok(CompatRefreshToken {
|
Ok(CompatRefreshToken {
|
||||||
data: res.id,
|
data: id,
|
||||||
token,
|
token,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,16 +471,19 @@ pub async fn compat_logout(
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let finished_at = Utc::now();
|
||||||
|
// TODO: this does not check for token expiration
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE compat_sessions
|
UPDATE compat_sessions cs
|
||||||
SET deleted_at = NOW()
|
SET finished_at = $2
|
||||||
FROM compat_access_tokens
|
FROM compat_access_tokens ca
|
||||||
WHERE compat_access_tokens.token = $1
|
WHERE ca.access_token = $1
|
||||||
AND compat_sessions.id = compat_access_tokens.id
|
AND ca.compat_session_id = cs.compat_session_id
|
||||||
AND compat_sessions.deleted_at IS NULL
|
AND cs.finished_at IS NULL
|
||||||
"#,
|
"#,
|
||||||
token,
|
token,
|
||||||
|
finished_at,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
@ -495,19 +496,19 @@ pub async fn compat_logout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn replace_compat_refresh_token(
|
pub async fn consume_compat_refresh_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
refresh_token: &CompatRefreshToken<PostgresqlBackend>,
|
refresh_token: CompatRefreshToken<PostgresqlBackend>,
|
||||||
next_refresh_token: &CompatRefreshToken<PostgresqlBackend>,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let consumed_at = Utc::now();
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE compat_refresh_tokens
|
UPDATE compat_refresh_tokens
|
||||||
SET next_token_id = $2
|
SET consumed_at = $2
|
||||||
WHERE id = $1
|
WHERE compat_refresh_token_id = $1
|
||||||
"#,
|
"#,
|
||||||
refresh_token.data,
|
Uuid::from(refresh_token.data),
|
||||||
next_refresh_token.data
|
consumed_at,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
@ -524,47 +525,50 @@ pub async fn replace_compat_refresh_token(
|
|||||||
|
|
||||||
pub async fn insert_compat_sso_login(
|
pub async fn insert_compat_sso_login(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
token: String,
|
login_token: String,
|
||||||
redirect_uri: Url,
|
redirect_uri: Url,
|
||||||
) -> anyhow::Result<CompatSsoLogin<PostgresqlBackend>> {
|
) -> anyhow::Result<CompatSsoLogin<PostgresqlBackend>> {
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO compat_sso_logins (token, redirect_uri)
|
INSERT INTO compat_sso_logins
|
||||||
VALUES ($1, $2)
|
(compat_sso_login_id, login_token, redirect_uri, created_at)
|
||||||
RETURNING id, created_at
|
VALUES ($1, $2, $3, $4)
|
||||||
"#,
|
"#,
|
||||||
&token,
|
Uuid::from(id),
|
||||||
|
&login_token,
|
||||||
redirect_uri.as_str(),
|
redirect_uri.as_str(),
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.instrument(tracing::info_span!("Insert compat SSO login"))
|
.instrument(tracing::info_span!("Insert compat SSO login"))
|
||||||
.await
|
.await
|
||||||
.context("could not insert compat SSO login")?;
|
.context("could not insert compat SSO login")?;
|
||||||
|
|
||||||
Ok(CompatSsoLogin {
|
Ok(CompatSsoLogin {
|
||||||
data: res.id,
|
data: id,
|
||||||
token,
|
login_token,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
state: CompatSsoLoginState::Pending,
|
state: CompatSsoLoginState::Pending,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CompatSsoLoginLookup {
|
struct CompatSsoLoginLookup {
|
||||||
compat_sso_login_id: i64,
|
compat_sso_login_id: Uuid,
|
||||||
compat_sso_login_token: String,
|
compat_sso_login_token: String,
|
||||||
compat_sso_login_redirect_uri: String,
|
compat_sso_login_redirect_uri: String,
|
||||||
compat_sso_login_created_at: DateTime<Utc>,
|
compat_sso_login_created_at: DateTime<Utc>,
|
||||||
compat_sso_login_fullfilled_at: Option<DateTime<Utc>>,
|
compat_sso_login_fulfilled_at: Option<DateTime<Utc>>,
|
||||||
compat_sso_login_exchanged_at: Option<DateTime<Utc>>,
|
compat_sso_login_exchanged_at: Option<DateTime<Utc>>,
|
||||||
compat_session_id: Option<i64>,
|
compat_session_id: Option<Uuid>,
|
||||||
compat_session_created_at: Option<DateTime<Utc>>,
|
compat_session_created_at: Option<DateTime<Utc>>,
|
||||||
compat_session_deleted_at: Option<DateTime<Utc>>,
|
compat_session_finished_at: Option<DateTime<Utc>>,
|
||||||
compat_session_device_id: Option<String>,
|
compat_session_device_id: Option<String>,
|
||||||
user_id: Option<i64>,
|
user_id: Option<Uuid>,
|
||||||
user_username: Option<String>,
|
user_username: Option<String>,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -584,7 +588,7 @@ impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin<PostgresqlBackend> {
|
|||||||
res.user_email_confirmed_at,
|
res.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -594,12 +598,16 @@ impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin<PostgresqlBackend> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let user = match (res.user_id, res.user_username, primary_email) {
|
let user = match (res.user_id, res.user_username, primary_email) {
|
||||||
(Some(id), Some(username), primary_email) => Some(User {
|
(Some(id), Some(username), primary_email) => {
|
||||||
data: id,
|
let id = Ulid::from(id);
|
||||||
username,
|
Some(User {
|
||||||
sub: format!("fake-sub-{}", id),
|
data: id,
|
||||||
primary_email,
|
username,
|
||||||
}),
|
sub: id.to_string(),
|
||||||
|
primary_email,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
(None, None, None) => None,
|
(None, None, None) => None,
|
||||||
_ => return Err(DatabaseInconsistencyError),
|
_ => return Err(DatabaseInconsistencyError),
|
||||||
};
|
};
|
||||||
@ -608,17 +616,17 @@ impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin<PostgresqlBackend> {
|
|||||||
res.compat_session_id,
|
res.compat_session_id,
|
||||||
res.compat_session_device_id,
|
res.compat_session_device_id,
|
||||||
res.compat_session_created_at,
|
res.compat_session_created_at,
|
||||||
res.compat_session_deleted_at,
|
res.compat_session_finished_at,
|
||||||
user,
|
user,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(device_id), Some(created_at), deleted_at, Some(user)) => {
|
(Some(id), Some(device_id), Some(created_at), finished_at, Some(user)) => {
|
||||||
let device = Device::try_from(device_id).map_err(|_| DatabaseInconsistencyError)?;
|
let device = Device::try_from(device_id).map_err(|_| DatabaseInconsistencyError)?;
|
||||||
Some(CompatSession {
|
Some(CompatSession {
|
||||||
data: id,
|
data: id.into(),
|
||||||
user,
|
user,
|
||||||
device,
|
device,
|
||||||
created_at,
|
created_at,
|
||||||
deleted_at,
|
finished_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
(None, None, None, None, None) => None,
|
(None, None, None, None, None) => None,
|
||||||
@ -626,18 +634,18 @@ impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin<PostgresqlBackend> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let state = match (
|
let state = match (
|
||||||
res.compat_sso_login_fullfilled_at,
|
res.compat_sso_login_fulfilled_at,
|
||||||
res.compat_sso_login_exchanged_at,
|
res.compat_sso_login_exchanged_at,
|
||||||
session,
|
session,
|
||||||
) {
|
) {
|
||||||
(None, None, None) => CompatSsoLoginState::Pending,
|
(None, None, None) => CompatSsoLoginState::Pending,
|
||||||
(Some(fullfilled_at), None, Some(session)) => CompatSsoLoginState::Fullfilled {
|
(Some(fulfilled_at), None, Some(session)) => CompatSsoLoginState::Fulfilled {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
session,
|
session,
|
||||||
},
|
},
|
||||||
(Some(fullfilled_at), Some(exchanged_at), Some(session)) => {
|
(Some(fulfilled_at), Some(exchanged_at), Some(session)) => {
|
||||||
CompatSsoLoginState::Exchanged {
|
CompatSsoLoginState::Exchanged {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
exchanged_at,
|
exchanged_at,
|
||||||
session,
|
session,
|
||||||
}
|
}
|
||||||
@ -646,8 +654,8 @@ impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin<PostgresqlBackend> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(CompatSsoLogin {
|
Ok(CompatSsoLogin {
|
||||||
data: res.compat_sso_login_id,
|
data: res.compat_sso_login_id.into(),
|
||||||
token: res.compat_sso_login_token,
|
login_token: res.compat_sso_login_token,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
created_at: res.compat_sso_login_created_at,
|
created_at: res.compat_sso_login_created_at,
|
||||||
state,
|
state,
|
||||||
@ -673,38 +681,38 @@ impl CompatSsoLoginLookupError {
|
|||||||
#[tracing::instrument(skip(executor), err)]
|
#[tracing::instrument(skip(executor), err)]
|
||||||
pub async fn get_compat_sso_login_by_id(
|
pub async fn get_compat_sso_login_by_id(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
id: i64,
|
id: Ulid,
|
||||||
) -> Result<CompatSsoLogin<PostgresqlBackend>, CompatSsoLoginLookupError> {
|
) -> Result<CompatSsoLogin<PostgresqlBackend>, CompatSsoLoginLookupError> {
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
CompatSsoLoginLookup,
|
CompatSsoLoginLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
cl.id AS "compat_sso_login_id",
|
cl.compat_sso_login_id,
|
||||||
cl.token AS "compat_sso_login_token",
|
cl.login_token AS "compat_sso_login_token",
|
||||||
cl.redirect_uri AS "compat_sso_login_redirect_uri",
|
cl.redirect_uri AS "compat_sso_login_redirect_uri",
|
||||||
cl.created_at AS "compat_sso_login_created_at",
|
cl.created_at AS "compat_sso_login_created_at",
|
||||||
cl.fullfilled_at AS "compat_sso_login_fullfilled_at",
|
cl.fulfilled_at AS "compat_sso_login_fulfilled_at",
|
||||||
cl.exchanged_at AS "compat_sso_login_exchanged_at",
|
cl.exchanged_at AS "compat_sso_login_exchanged_at",
|
||||||
cs.id AS "compat_session_id?",
|
cs.compat_session_id AS "compat_session_id?",
|
||||||
cs.created_at AS "compat_session_created_at?",
|
cs.created_at AS "compat_session_created_at?",
|
||||||
cs.deleted_at AS "compat_session_deleted_at?",
|
cs.finished_at AS "compat_session_finished_at?",
|
||||||
cs.device_id AS "compat_session_device_id?",
|
cs.device_id AS "compat_session_device_id?",
|
||||||
u.id AS "user_id?",
|
u.user_id AS "user_id?",
|
||||||
u.username AS "user_username?",
|
u.username AS "user_username?",
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
FROM compat_sso_logins cl
|
FROM compat_sso_logins cl
|
||||||
LEFT JOIN compat_sessions cs
|
LEFT JOIN compat_sessions cs
|
||||||
ON cs.id = cl.compat_session_id
|
USING (compat_session_id)
|
||||||
LEFT JOIN users u
|
LEFT JOIN users u
|
||||||
ON u.id = cs.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
WHERE cl.id = $1
|
WHERE cl.compat_sso_login_id = $1
|
||||||
"#,
|
"#,
|
||||||
id,
|
Uuid::from(id),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.instrument(tracing::info_span!("Lookup compat SSO login"))
|
.instrument(tracing::info_span!("Lookup compat SSO login"))
|
||||||
@ -723,30 +731,30 @@ pub async fn get_compat_sso_login_by_token(
|
|||||||
CompatSsoLoginLookup,
|
CompatSsoLoginLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
cl.id AS "compat_sso_login_id",
|
cl.compat_sso_login_id,
|
||||||
cl.token AS "compat_sso_login_token",
|
cl.login_token AS "compat_sso_login_token",
|
||||||
cl.redirect_uri AS "compat_sso_login_redirect_uri",
|
cl.redirect_uri AS "compat_sso_login_redirect_uri",
|
||||||
cl.created_at AS "compat_sso_login_created_at",
|
cl.created_at AS "compat_sso_login_created_at",
|
||||||
cl.fullfilled_at AS "compat_sso_login_fullfilled_at",
|
cl.fulfilled_at AS "compat_sso_login_fulfilled_at",
|
||||||
cl.exchanged_at AS "compat_sso_login_exchanged_at",
|
cl.exchanged_at AS "compat_sso_login_exchanged_at",
|
||||||
cs.id AS "compat_session_id?",
|
cs.compat_session_id AS "compat_session_id?",
|
||||||
cs.created_at AS "compat_session_created_at?",
|
cs.created_at AS "compat_session_created_at?",
|
||||||
cs.deleted_at AS "compat_session_deleted_at?",
|
cs.finished_at AS "compat_session_finished_at?",
|
||||||
cs.device_id AS "compat_session_device_id?",
|
cs.device_id AS "compat_session_device_id?",
|
||||||
u.id AS "user_id?",
|
u.user_id AS "user_id?",
|
||||||
u.username AS "user_username?",
|
u.username AS "user_username?",
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
FROM compat_sso_logins cl
|
FROM compat_sso_logins cl
|
||||||
LEFT JOIN compat_sessions cs
|
LEFT JOIN compat_sessions cs
|
||||||
ON cs.id = cl.compat_session_id
|
USING (compat_session_id)
|
||||||
LEFT JOIN users u
|
LEFT JOIN users u
|
||||||
ON u.id = cs.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
WHERE cl.token = $1
|
WHERE cl.login_token = $1
|
||||||
"#,
|
"#,
|
||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
@ -769,49 +777,52 @@ pub async fn fullfill_compat_sso_login(
|
|||||||
|
|
||||||
let mut txn = conn.begin().await.context("could not start transaction")?;
|
let mut txn = conn.begin().await.context("could not start transaction")?;
|
||||||
|
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO compat_sessions (user_id, device_id)
|
INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING id, created_at
|
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(id),
|
||||||
|
Uuid::from(user.data),
|
||||||
device.as_str(),
|
device.as_str(),
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut txn)
|
.execute(&mut txn)
|
||||||
.instrument(tracing::info_span!("Insert compat session"))
|
.instrument(tracing::info_span!("Insert compat session"))
|
||||||
.await
|
.await
|
||||||
.context("could not insert compat session")?;
|
.context("could not insert compat session")?;
|
||||||
|
|
||||||
let session = CompatSession {
|
let session = CompatSession {
|
||||||
data: res.id,
|
data: id,
|
||||||
user,
|
user,
|
||||||
device,
|
device,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
deleted_at: None,
|
finished_at: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = sqlx::query_scalar!(
|
let fulfilled_at = Utc::now();
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE compat_sso_logins
|
UPDATE compat_sso_logins
|
||||||
SET
|
SET
|
||||||
fullfilled_at = NOW(),
|
compat_session_id = $2,
|
||||||
compat_session_id = $2
|
fulfilled_at = $3
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
compat_sso_login_id = $1
|
||||||
RETURNING fullfilled_at AS "fullfilled_at!"
|
|
||||||
"#,
|
"#,
|
||||||
login.data,
|
Uuid::from(login.data),
|
||||||
session.data,
|
Uuid::from(session.data),
|
||||||
|
fulfilled_at,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut txn)
|
.execute(&mut txn)
|
||||||
.instrument(tracing::info_span!("Update compat SSO login"))
|
.instrument(tracing::info_span!("Update compat SSO login"))
|
||||||
.await
|
.await
|
||||||
.context("could not update compat SSO login")?;
|
.context("could not update compat SSO login")?;
|
||||||
|
|
||||||
let state = CompatSsoLoginState::Fullfilled {
|
let state = CompatSsoLoginState::Fulfilled {
|
||||||
fullfilled_at: res,
|
fulfilled_at,
|
||||||
session,
|
session,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -826,33 +837,34 @@ pub async fn mark_compat_sso_login_as_exchanged(
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
mut login: CompatSsoLogin<PostgresqlBackend>,
|
mut login: CompatSsoLogin<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<CompatSsoLogin<PostgresqlBackend>> {
|
) -> anyhow::Result<CompatSsoLogin<PostgresqlBackend>> {
|
||||||
let (fullfilled_at, session) = match login.state {
|
let (fulfilled_at, session) = match login.state {
|
||||||
CompatSsoLoginState::Fullfilled {
|
CompatSsoLoginState::Fulfilled {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
session,
|
session,
|
||||||
} => (fullfilled_at, session),
|
} => (fulfilled_at, session),
|
||||||
_ => bail!("sso login in wrong state"),
|
_ => bail!("sso login in wrong state"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = sqlx::query_scalar!(
|
let exchanged_at = Utc::now();
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE compat_sso_logins
|
UPDATE compat_sso_logins
|
||||||
SET
|
SET
|
||||||
exchanged_at = NOW()
|
exchanged_at = $2
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
compat_sso_login_id = $1
|
||||||
RETURNING exchanged_at AS "exchanged_at!"
|
|
||||||
"#,
|
"#,
|
||||||
login.data,
|
Uuid::from(login.data),
|
||||||
|
exchanged_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.instrument(tracing::info_span!("Update compat SSO login"))
|
.instrument(tracing::info_span!("Update compat SSO login"))
|
||||||
.await
|
.await
|
||||||
.context("could not update compat SSO login")?;
|
.context("could not update compat SSO login")?;
|
||||||
|
|
||||||
let state = CompatSsoLoginState::Exchanged {
|
let state = CompatSsoLoginState::Exchanged {
|
||||||
fullfilled_at,
|
fulfilled_at,
|
||||||
exchanged_at: res,
|
exchanged_at,
|
||||||
session,
|
session,
|
||||||
};
|
};
|
||||||
login.state = state;
|
login.state = state;
|
||||||
|
@ -23,11 +23,11 @@
|
|||||||
clippy::module_name_repetitions
|
clippy::module_name_repetitions
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use mas_data_model::{StorageBackend, StorageBackendMarker};
|
use mas_data_model::{StorageBackend, StorageBackendMarker};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::migrate::Migrator;
|
use sqlx::migrate::Migrator;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error("database query returned an inconsistent state")]
|
#[error("database query returned an inconsistent state")]
|
||||||
@ -37,29 +37,24 @@ pub struct DatabaseInconsistencyError;
|
|||||||
pub struct PostgresqlBackend;
|
pub struct PostgresqlBackend;
|
||||||
|
|
||||||
impl StorageBackend for PostgresqlBackend {
|
impl StorageBackend for PostgresqlBackend {
|
||||||
type AccessTokenData = i64;
|
type AccessTokenData = Ulid;
|
||||||
type AuthenticationData = i64;
|
type AuthenticationData = Ulid;
|
||||||
type AuthorizationGrantData = i64;
|
type AuthorizationGrantData = Ulid;
|
||||||
type BrowserSessionData = i64;
|
type BrowserSessionData = Ulid;
|
||||||
type ClientData = i64;
|
type ClientData = Ulid;
|
||||||
type CompatAccessTokenData = i64;
|
type CompatAccessTokenData = Ulid;
|
||||||
type CompatRefreshTokenData = i64;
|
type CompatRefreshTokenData = Ulid;
|
||||||
type CompatSessionData = i64;
|
type CompatSessionData = Ulid;
|
||||||
type CompatSsoLoginData = i64;
|
type CompatSsoLoginData = Ulid;
|
||||||
type RefreshTokenData = i64;
|
type RefreshTokenData = Ulid;
|
||||||
type SessionData = i64;
|
type SessionData = Ulid;
|
||||||
type UserData = i64;
|
type UserData = Ulid;
|
||||||
type UserEmailData = i64;
|
type UserEmailData = Ulid;
|
||||||
type UserEmailVerificationData = i64;
|
type UserEmailVerificationData = Ulid;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StorageBackendMarker for PostgresqlBackend {}
|
impl StorageBackendMarker for PostgresqlBackend {}
|
||||||
|
|
||||||
struct IdAndCreationTime {
|
|
||||||
id: i64,
|
|
||||||
created_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod compat;
|
pub mod compat;
|
||||||
pub mod oauth2;
|
pub mod oauth2;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
@ -17,62 +17,76 @@ use chrono::{DateTime, Duration, Utc};
|
|||||||
use mas_data_model::{AccessToken, Authentication, BrowserSession, Session, User, UserEmail};
|
use mas_data_model::{AccessToken, Authentication, BrowserSession, Session, User, UserEmail};
|
||||||
use sqlx::{Acquire, PgExecutor, Postgres};
|
use sqlx::{Acquire, PgExecutor, Postgres};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ulid::Ulid;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::client::{lookup_client, ClientFetchError};
|
use super::client::{lookup_client, ClientFetchError};
|
||||||
use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend};
|
use crate::{DatabaseInconsistencyError, PostgresqlBackend};
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
session.id = %session.data,
|
||||||
|
client.id = %session.client.data,
|
||||||
|
user.id = %session.browser_session.user.data,
|
||||||
|
access_token.id,
|
||||||
|
),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
pub async fn add_access_token(
|
pub async fn add_access_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
session: &Session<PostgresqlBackend>,
|
session: &Session<PostgresqlBackend>,
|
||||||
token: &str,
|
access_token: String,
|
||||||
expires_after: Duration,
|
expires_after: Duration,
|
||||||
) -> anyhow::Result<AccessToken<PostgresqlBackend>> {
|
) -> anyhow::Result<AccessToken<PostgresqlBackend>> {
|
||||||
// Checked convertion of duration to i32, maxing at i32::MAX
|
let created_at = Utc::now();
|
||||||
let expires_after_seconds = i32::try_from(expires_after.num_seconds()).unwrap_or(i32::MAX);
|
let expires_at = created_at + expires_after;
|
||||||
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
|
||||||
let res = sqlx::query_as!(
|
tracing::Span::current().record("access_token.id", tracing::field::display(id));
|
||||||
IdAndCreationTime,
|
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO oauth2_access_tokens
|
INSERT INTO oauth2_access_tokens
|
||||||
(oauth2_session_id, token, expires_after)
|
(oauth2_access_token_id, oauth2_session_id, access_token, created_at, expires_at)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3)
|
($1, $2, $3, $4, $5)
|
||||||
RETURNING
|
|
||||||
id, created_at
|
|
||||||
"#,
|
"#,
|
||||||
session.data,
|
Uuid::from(id),
|
||||||
token,
|
Uuid::from(session.data),
|
||||||
expires_after_seconds,
|
&access_token,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
.context("could not insert oauth2 access token")?;
|
.context("could not insert oauth2 access token")?;
|
||||||
|
|
||||||
Ok(AccessToken {
|
Ok(AccessToken {
|
||||||
data: res.id,
|
data: id,
|
||||||
expires_after,
|
access_token,
|
||||||
token: token.to_owned(),
|
jti: id.to_string(),
|
||||||
jti: format!("{}", res.id),
|
created_at,
|
||||||
created_at: res.created_at,
|
expires_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OAuth2AccessTokenLookup {
|
pub struct OAuth2AccessTokenLookup {
|
||||||
access_token_id: i64,
|
oauth2_access_token_id: Uuid,
|
||||||
access_token: String,
|
oauth2_access_token: String,
|
||||||
access_token_expires_after: i32,
|
oauth2_access_token_created_at: DateTime<Utc>,
|
||||||
access_token_created_at: DateTime<Utc>,
|
oauth2_access_token_expires_at: DateTime<Utc>,
|
||||||
session_id: i64,
|
oauth2_session_id: Uuid,
|
||||||
oauth2_client_id: i64,
|
oauth2_client_id: Uuid,
|
||||||
scope: String,
|
scope: String,
|
||||||
user_session_id: i64,
|
user_session_id: Uuid,
|
||||||
user_session_created_at: DateTime<Utc>,
|
user_session_created_at: DateTime<Utc>,
|
||||||
user_id: i64,
|
user_id: Uuid,
|
||||||
user_username: String,
|
user_username: String,
|
||||||
user_session_last_authentication_id: Option<i64>,
|
user_session_last_authentication_id: Option<Uuid>,
|
||||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -114,40 +128,39 @@ where
|
|||||||
OAuth2AccessTokenLookup,
|
OAuth2AccessTokenLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
at.id AS "access_token_id",
|
at.oauth2_access_token_id,
|
||||||
at.token AS "access_token",
|
at.access_token AS "oauth2_access_token",
|
||||||
at.expires_after AS "access_token_expires_after",
|
at.created_at AS "oauth2_access_token_created_at",
|
||||||
at.created_at AS "access_token_created_at",
|
at.expires_at AS "oauth2_access_token_expires_at",
|
||||||
os.id AS "session_id!",
|
os.oauth2_session_id AS "oauth2_session_id!",
|
||||||
os.oauth2_client_id AS "oauth2_client_id!",
|
os.oauth2_client_id AS "oauth2_client_id!",
|
||||||
os.scope AS "scope!",
|
os.scope AS "scope!",
|
||||||
us.id AS "user_session_id!",
|
us.user_session_id AS "user_session_id!",
|
||||||
us.created_at AS "user_session_created_at!",
|
us.created_at AS "user_session_created_at!",
|
||||||
u.id AS "user_id!",
|
u.user_id AS "user_id!",
|
||||||
u.username AS "user_username!",
|
u.username AS "user_username!",
|
||||||
usa.id AS "user_session_last_authentication_id?",
|
usa.user_session_authentication_id AS "user_session_last_authentication_id?",
|
||||||
usa.created_at AS "user_session_last_authentication_created_at?",
|
usa.created_at AS "user_session_last_authentication_created_at?",
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
|
|
||||||
FROM oauth2_access_tokens at
|
FROM oauth2_access_tokens at
|
||||||
INNER JOIN oauth2_sessions os
|
INNER JOIN oauth2_sessions os
|
||||||
ON os.id = at.oauth2_session_id
|
USING (oauth2_session_id)
|
||||||
INNER JOIN user_sessions us
|
INNER JOIN user_sessions us
|
||||||
ON us.id = os.user_session_id
|
USING (user_session_id)
|
||||||
INNER JOIN users u
|
INNER JOIN users u
|
||||||
ON u.id = us.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_session_authentications usa
|
LEFT JOIN user_session_authentications usa
|
||||||
ON usa.session_id = us.id
|
USING (user_session_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
|
|
||||||
WHERE at.token = $1
|
WHERE at.access_token = $1
|
||||||
AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()
|
AND at.revoked_at IS NULL
|
||||||
AND us.active
|
AND os.finished_at IS NULL
|
||||||
AND os.ended_at IS NULL
|
|
||||||
|
|
||||||
ORDER BY usa.created_at DESC
|
ORDER BY usa.created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@ -158,14 +171,14 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let access_token = AccessToken {
|
let access_token = AccessToken {
|
||||||
data: res.access_token_id,
|
data: res.oauth2_access_token_id.into(),
|
||||||
jti: format!("{}", res.access_token_id),
|
jti: res.oauth2_access_token_id.to_string(),
|
||||||
token: res.access_token,
|
access_token: res.oauth2_access_token,
|
||||||
created_at: res.access_token_created_at,
|
created_at: res.oauth2_access_token_created_at,
|
||||||
expires_after: Duration::seconds(res.access_token_expires_after.into()),
|
expires_at: res.oauth2_access_token_expires_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = lookup_client(&mut *conn, res.oauth2_client_id).await?;
|
let client = lookup_client(&mut *conn, res.oauth2_client_id.into()).await?;
|
||||||
|
|
||||||
let primary_email = match (
|
let primary_email = match (
|
||||||
res.user_email_id,
|
res.user_email_id,
|
||||||
@ -174,7 +187,7 @@ where
|
|||||||
res.user_email_confirmed_at,
|
res.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -183,10 +196,11 @@ where
|
|||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let id = Ulid::from(res.user_id);
|
||||||
let user = User {
|
let user = User {
|
||||||
data: res.user_id,
|
data: id,
|
||||||
username: res.user_username,
|
username: res.user_username,
|
||||||
sub: format!("fake-sub-{}", res.user_id),
|
sub: id.to_string(),
|
||||||
primary_email,
|
primary_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -196,14 +210,14 @@ where
|
|||||||
) {
|
) {
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(Some(id), Some(created_at)) => Some(Authentication {
|
(Some(id), Some(created_at)) => Some(Authentication {
|
||||||
data: id,
|
data: id.into(),
|
||||||
created_at,
|
created_at,
|
||||||
}),
|
}),
|
||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let browser_session = BrowserSession {
|
let browser_session = BrowserSession {
|
||||||
data: res.user_session_id,
|
data: res.user_session_id.into(),
|
||||||
created_at: res.user_session_created_at,
|
created_at: res.user_session_created_at,
|
||||||
user,
|
user,
|
||||||
last_authentication,
|
last_authentication,
|
||||||
@ -212,7 +226,7 @@ where
|
|||||||
let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?;
|
let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
let session = Session {
|
let session = Session {
|
||||||
data: res.session_id,
|
data: res.oauth2_session_id.into(),
|
||||||
client,
|
client,
|
||||||
browser_session,
|
browser_session,
|
||||||
scope,
|
scope,
|
||||||
@ -222,16 +236,24 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(access_token.id = %access_token.data),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
pub async fn revoke_access_token(
|
pub async fn revoke_access_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
access_token: &AccessToken<PostgresqlBackend>,
|
access_token: AccessToken<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let revoked_at = Utc::now();
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM oauth2_access_tokens
|
UPDATE oauth2_access_tokens
|
||||||
WHERE id = $1
|
SET revoked_at = $2
|
||||||
|
WHERE oauth2_access_token_id = $1
|
||||||
"#,
|
"#,
|
||||||
access_token.data,
|
Uuid::from(access_token.data),
|
||||||
|
revoked_at,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
@ -245,11 +267,14 @@ pub async fn revoke_access_token(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cleanup_expired(executor: impl PgExecutor<'_>) -> anyhow::Result<u64> {
|
pub async fn cleanup_expired(executor: impl PgExecutor<'_>) -> anyhow::Result<u64> {
|
||||||
|
// Cleanup token which expired more than 15 minutes ago
|
||||||
|
let threshold = Utc::now() - Duration::minutes(15);
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM oauth2_access_tokens
|
DELETE FROM oauth2_access_tokens
|
||||||
WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()
|
WHERE expires_at < $1
|
||||||
"#,
|
"#,
|
||||||
|
threshold,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
|
@ -25,11 +25,21 @@ use mas_data_model::{
|
|||||||
use mas_iana::oauth::PkceCodeChallengeMethod;
|
use mas_iana::oauth::PkceCodeChallengeMethod;
|
||||||
use oauth2_types::{requests::ResponseMode, scope::Scope};
|
use oauth2_types::{requests::ResponseMode, scope::Scope};
|
||||||
use sqlx::{PgConnection, PgExecutor};
|
use sqlx::{PgConnection, PgExecutor};
|
||||||
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::client::lookup_client;
|
use super::client::lookup_client;
|
||||||
use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend};
|
use crate::{DatabaseInconsistencyError, PostgresqlBackend};
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
client.id = %client.data,
|
||||||
|
grant.id,
|
||||||
|
),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn new_authorization_grant(
|
pub async fn new_authorization_grant(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
@ -40,7 +50,7 @@ pub async fn new_authorization_grant(
|
|||||||
state: Option<String>,
|
state: Option<String>,
|
||||||
nonce: Option<String>,
|
nonce: Option<String>,
|
||||||
max_age: Option<NonZeroU32>,
|
max_age: Option<NonZeroU32>,
|
||||||
acr_values: Option<String>,
|
_acr_values: Option<String>,
|
||||||
response_mode: ResponseMode,
|
response_mode: ResponseMode,
|
||||||
response_type_id_token: bool,
|
response_type_id_token: bool,
|
||||||
requires_consent: bool,
|
requires_consent: bool,
|
||||||
@ -53,26 +63,43 @@ pub async fn new_authorization_grant(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| c.pkce.as_ref())
|
.and_then(|c| c.pkce.as_ref())
|
||||||
.map(|p| p.challenge_method.to_string());
|
.map(|p| p.challenge_method.to_string());
|
||||||
|
// TODO: this conversion is a bit ugly
|
||||||
|
let max_age_i32 = max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX));
|
||||||
let code_str = code.as_ref().map(|c| &c.code);
|
let code_str = code.as_ref().map(|c| &c.code);
|
||||||
let res = sqlx::query_as!(
|
|
||||||
IdAndCreationTime,
|
let created_at = Utc::now();
|
||||||
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
tracing::Span::current().record("grant.id", tracing::field::display(id));
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO oauth2_authorization_grants
|
INSERT INTO oauth2_authorization_grants (
|
||||||
(oauth2_client_id, redirect_uri, scope, state, nonce, max_age,
|
oauth2_authorization_grant_id,
|
||||||
acr_values, response_mode, code_challenge, code_challenge_method,
|
oauth2_client_id,
|
||||||
response_type_code, response_type_id_token, code, requires_consent)
|
redirect_uri,
|
||||||
|
scope,
|
||||||
|
state,
|
||||||
|
nonce,
|
||||||
|
max_age,
|
||||||
|
response_mode,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
response_type_code,
|
||||||
|
response_type_id_token,
|
||||||
|
authorization_code,
|
||||||
|
requires_consent,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING id, created_at
|
|
||||||
"#,
|
"#,
|
||||||
&client.data,
|
Uuid::from(id),
|
||||||
|
Uuid::from(client.data),
|
||||||
redirect_uri.to_string(),
|
redirect_uri.to_string(),
|
||||||
scope.to_string(),
|
scope.to_string(),
|
||||||
state,
|
state,
|
||||||
nonce,
|
nonce,
|
||||||
// TODO: this conversion is a bit ugly
|
max_age_i32,
|
||||||
max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX)),
|
|
||||||
acr_values,
|
|
||||||
response_mode.to_string(),
|
response_mode.to_string(),
|
||||||
code_challenge,
|
code_challenge,
|
||||||
code_challenge_method,
|
code_challenge_method,
|
||||||
@ -80,13 +107,14 @@ pub async fn new_authorization_grant(
|
|||||||
response_type_id_token,
|
response_type_id_token,
|
||||||
code_str,
|
code_str,
|
||||||
requires_consent,
|
requires_consent,
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
.context("could not insert oauth2 authorization grant")?;
|
.context("could not insert oauth2 authorization grant")?;
|
||||||
|
|
||||||
Ok(AuthorizationGrant {
|
Ok(AuthorizationGrant {
|
||||||
data: res.id,
|
data: id,
|
||||||
stage: AuthorizationGrantStage::Pending,
|
stage: AuthorizationGrantStage::Pending,
|
||||||
code,
|
code,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
@ -95,9 +123,8 @@ pub async fn new_authorization_grant(
|
|||||||
state,
|
state,
|
||||||
nonce,
|
nonce,
|
||||||
max_age,
|
max_age,
|
||||||
acr_values,
|
|
||||||
response_mode,
|
response_mode,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
response_type_id_token,
|
response_type_id_token,
|
||||||
requires_consent,
|
requires_consent,
|
||||||
})
|
})
|
||||||
@ -105,33 +132,32 @@ pub async fn new_authorization_grant(
|
|||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
struct GrantLookup {
|
struct GrantLookup {
|
||||||
grant_id: i64,
|
oauth2_authorization_grant_id: Uuid,
|
||||||
grant_created_at: DateTime<Utc>,
|
oauth2_authorization_grant_created_at: DateTime<Utc>,
|
||||||
grant_cancelled_at: Option<DateTime<Utc>>,
|
oauth2_authorization_grant_cancelled_at: Option<DateTime<Utc>>,
|
||||||
grant_fulfilled_at: Option<DateTime<Utc>>,
|
oauth2_authorization_grant_fulfilled_at: Option<DateTime<Utc>>,
|
||||||
grant_exchanged_at: Option<DateTime<Utc>>,
|
oauth2_authorization_grant_exchanged_at: Option<DateTime<Utc>>,
|
||||||
grant_scope: String,
|
oauth2_authorization_grant_scope: String,
|
||||||
grant_state: Option<String>,
|
oauth2_authorization_grant_state: Option<String>,
|
||||||
grant_redirect_uri: String,
|
oauth2_authorization_grant_nonce: Option<String>,
|
||||||
grant_response_mode: String,
|
oauth2_authorization_grant_redirect_uri: String,
|
||||||
grant_nonce: Option<String>,
|
oauth2_authorization_grant_response_mode: String,
|
||||||
grant_max_age: Option<i32>,
|
oauth2_authorization_grant_max_age: Option<i32>,
|
||||||
grant_acr_values: Option<String>,
|
oauth2_authorization_grant_response_type_code: bool,
|
||||||
grant_response_type_code: bool,
|
oauth2_authorization_grant_response_type_id_token: bool,
|
||||||
grant_response_type_id_token: bool,
|
oauth2_authorization_grant_code: Option<String>,
|
||||||
grant_code: Option<String>,
|
oauth2_authorization_grant_code_challenge: Option<String>,
|
||||||
grant_code_challenge: Option<String>,
|
oauth2_authorization_grant_code_challenge_method: Option<String>,
|
||||||
grant_code_challenge_method: Option<String>,
|
oauth2_authorization_grant_requires_consent: bool,
|
||||||
grant_requires_consent: bool,
|
oauth2_client_id: Uuid,
|
||||||
oauth2_client_id: i64,
|
oauth2_session_id: Option<Uuid>,
|
||||||
session_id: Option<i64>,
|
user_session_id: Option<Uuid>,
|
||||||
user_session_id: Option<i64>,
|
|
||||||
user_session_created_at: Option<DateTime<Utc>>,
|
user_session_created_at: Option<DateTime<Utc>>,
|
||||||
user_id: Option<i64>,
|
user_id: Option<Uuid>,
|
||||||
user_username: Option<String>,
|
user_username: Option<String>,
|
||||||
user_session_last_authentication_id: Option<i64>,
|
user_session_last_authentication_id: Option<Uuid>,
|
||||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -144,12 +170,12 @@ impl GrantLookup {
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
) -> Result<AuthorizationGrant<PostgresqlBackend>, DatabaseInconsistencyError> {
|
) -> Result<AuthorizationGrant<PostgresqlBackend>, DatabaseInconsistencyError> {
|
||||||
let scope: Scope = self
|
let scope: Scope = self
|
||||||
.grant_scope
|
.oauth2_authorization_grant_scope
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
// TODO: don't unwrap
|
// TODO: don't unwrap
|
||||||
let client = lookup_client(executor, self.oauth2_client_id)
|
let client = lookup_client(executor, self.oauth2_client_id.into())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -158,7 +184,7 @@ impl GrantLookup {
|
|||||||
self.user_session_last_authentication_created_at,
|
self.user_session_last_authentication_created_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(created_at)) => Some(Authentication {
|
(Some(id), Some(created_at)) => Some(Authentication {
|
||||||
data: id,
|
data: id.into(),
|
||||||
created_at,
|
created_at,
|
||||||
}),
|
}),
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
@ -172,7 +198,7 @@ impl GrantLookup {
|
|||||||
self.user_email_confirmed_at,
|
self.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -182,7 +208,7 @@ impl GrantLookup {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let session = match (
|
let session = match (
|
||||||
self.session_id,
|
self.oauth2_session_id,
|
||||||
self.user_session_id,
|
self.user_session_id,
|
||||||
self.user_session_created_at,
|
self.user_session_created_at,
|
||||||
self.user_id,
|
self.user_id,
|
||||||
@ -199,15 +225,16 @@ impl GrantLookup {
|
|||||||
last_authentication,
|
last_authentication,
|
||||||
primary_email,
|
primary_email,
|
||||||
) => {
|
) => {
|
||||||
|
let user_id = Ulid::from(user_id);
|
||||||
let user = User {
|
let user = User {
|
||||||
data: user_id,
|
data: user_id,
|
||||||
username: user_username,
|
username: user_username,
|
||||||
sub: format!("fake-sub-{}", user_id),
|
sub: user_id.to_string(),
|
||||||
primary_email,
|
primary_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
let browser_session = BrowserSession {
|
let browser_session = BrowserSession {
|
||||||
data: user_session_id,
|
data: user_session_id.into(),
|
||||||
user,
|
user,
|
||||||
created_at: user_session_created_at,
|
created_at: user_session_created_at,
|
||||||
last_authentication,
|
last_authentication,
|
||||||
@ -217,7 +244,7 @@ impl GrantLookup {
|
|||||||
let scope = scope.clone();
|
let scope = scope.clone();
|
||||||
|
|
||||||
let session = Session {
|
let session = Session {
|
||||||
data: session_id,
|
data: session_id.into(),
|
||||||
client,
|
client,
|
||||||
browser_session,
|
browser_session,
|
||||||
scope,
|
scope,
|
||||||
@ -230,9 +257,9 @@ impl GrantLookup {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let stage = match (
|
let stage = match (
|
||||||
self.grant_fulfilled_at,
|
self.oauth2_authorization_grant_fulfilled_at,
|
||||||
self.grant_exchanged_at,
|
self.oauth2_authorization_grant_exchanged_at,
|
||||||
self.grant_cancelled_at,
|
self.oauth2_authorization_grant_cancelled_at,
|
||||||
session,
|
session,
|
||||||
) {
|
) {
|
||||||
(None, None, None, None) => AuthorizationGrantStage::Pending,
|
(None, None, None, None) => AuthorizationGrantStage::Pending,
|
||||||
@ -255,7 +282,10 @@ impl GrantLookup {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let pkce = match (self.grant_code_challenge, self.grant_code_challenge_method) {
|
let pkce = match (
|
||||||
|
self.oauth2_authorization_grant_code_challenge,
|
||||||
|
self.oauth2_authorization_grant_code_challenge_method,
|
||||||
|
) {
|
||||||
(Some(challenge), Some(challenge_method)) if challenge_method == "plain" => {
|
(Some(challenge), Some(challenge_method)) if challenge_method == "plain" => {
|
||||||
Some(Pkce {
|
Some(Pkce {
|
||||||
challenge_method: PkceCodeChallengeMethod::Plain,
|
challenge_method: PkceCodeChallengeMethod::Plain,
|
||||||
@ -272,27 +302,30 @@ impl GrantLookup {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let code: Option<AuthorizationCode> =
|
let code: Option<AuthorizationCode> = match (
|
||||||
match (self.grant_response_type_code, self.grant_code, pkce) {
|
self.oauth2_authorization_grant_response_type_code,
|
||||||
(false, None, None) => None,
|
self.oauth2_authorization_grant_code,
|
||||||
(true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }),
|
pkce,
|
||||||
_ => {
|
) {
|
||||||
return Err(DatabaseInconsistencyError);
|
(false, None, None) => None,
|
||||||
}
|
(true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }),
|
||||||
};
|
_ => {
|
||||||
|
return Err(DatabaseInconsistencyError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let redirect_uri = self
|
let redirect_uri = self
|
||||||
.grant_redirect_uri
|
.oauth2_authorization_grant_redirect_uri
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
let response_mode = self
|
let response_mode = self
|
||||||
.grant_response_mode
|
.oauth2_authorization_grant_response_mode
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
let max_age = self
|
let max_age = self
|
||||||
.grant_max_age
|
.oauth2_authorization_grant_max_age
|
||||||
.map(u32::try_from)
|
.map(u32::try_from)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|_e| DatabaseInconsistencyError)?
|
.map_err(|_e| DatabaseInconsistencyError)?
|
||||||
@ -301,82 +334,85 @@ impl GrantLookup {
|
|||||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
Ok(AuthorizationGrant {
|
Ok(AuthorizationGrant {
|
||||||
data: self.grant_id,
|
data: self.oauth2_authorization_grant_id.into(),
|
||||||
stage,
|
stage,
|
||||||
client,
|
client,
|
||||||
code,
|
code,
|
||||||
acr_values: self.grant_acr_values,
|
|
||||||
scope,
|
scope,
|
||||||
state: self.grant_state,
|
state: self.oauth2_authorization_grant_state,
|
||||||
nonce: self.grant_nonce,
|
nonce: self.oauth2_authorization_grant_nonce,
|
||||||
max_age, // TODO
|
max_age, // TODO
|
||||||
response_mode,
|
response_mode,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
created_at: self.grant_created_at,
|
created_at: self.oauth2_authorization_grant_created_at,
|
||||||
response_type_id_token: self.grant_response_type_id_token,
|
response_type_id_token: self.oauth2_authorization_grant_response_type_id_token,
|
||||||
requires_consent: self.grant_requires_consent,
|
requires_consent: self.oauth2_authorization_grant_requires_consent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(grant.id = %id),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
pub async fn get_grant_by_id(
|
pub async fn get_grant_by_id(
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
id: i64,
|
id: Ulid,
|
||||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||||
// TODO: handle "not found" cases
|
// TODO: handle "not found" cases
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
GrantLookup,
|
GrantLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
og.id AS grant_id,
|
og.oauth2_authorization_grant_id,
|
||||||
og.created_at AS grant_created_at,
|
og.created_at AS oauth2_authorization_grant_created_at,
|
||||||
og.cancelled_at AS grant_cancelled_at,
|
og.cancelled_at AS oauth2_authorization_grant_cancelled_at,
|
||||||
og.fulfilled_at AS grant_fulfilled_at,
|
og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,
|
||||||
og.exchanged_at AS grant_exchanged_at,
|
og.exchanged_at AS oauth2_authorization_grant_exchanged_at,
|
||||||
og.scope AS grant_scope,
|
og.scope AS oauth2_authorization_grant_scope,
|
||||||
og.state AS grant_state,
|
og.state AS oauth2_authorization_grant_state,
|
||||||
og.redirect_uri AS grant_redirect_uri,
|
og.redirect_uri AS oauth2_authorization_grant_redirect_uri,
|
||||||
og.response_mode AS grant_response_mode,
|
og.response_mode AS oauth2_authorization_grant_response_mode,
|
||||||
og.nonce AS grant_nonce,
|
og.nonce AS oauth2_authorization_grant_nonce,
|
||||||
og.max_age AS grant_max_age,
|
og.max_age AS oauth2_authorization_grant_max_age,
|
||||||
og.acr_values AS grant_acr_values,
|
og.oauth2_client_id AS oauth2_client_id,
|
||||||
og.oauth2_client_id AS oauth2_client_id,
|
og.authorization_code AS oauth2_authorization_grant_code,
|
||||||
og.code AS grant_code,
|
og.response_type_code AS oauth2_authorization_grant_response_type_code,
|
||||||
og.response_type_code AS grant_response_type_code,
|
og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,
|
||||||
og.response_type_id_token AS grant_response_type_id_token,
|
og.code_challenge AS oauth2_authorization_grant_code_challenge,
|
||||||
og.code_challenge AS grant_code_challenge,
|
og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,
|
||||||
og.code_challenge_method AS grant_code_challenge_method,
|
og.requires_consent AS oauth2_authorization_grant_requires_consent,
|
||||||
og.requires_consent AS grant_requires_consent,
|
os.oauth2_session_id AS "oauth2_session_id?",
|
||||||
os.id AS "session_id?",
|
us.user_session_id AS "user_session_id?",
|
||||||
us.id AS "user_session_id?",
|
us.created_at AS "user_session_created_at?",
|
||||||
us.created_at AS "user_session_created_at?",
|
u.user_id AS "user_id?",
|
||||||
u.id AS "user_id?",
|
u.username AS "user_username?",
|
||||||
u.username AS "user_username?",
|
usa.user_session_authentication_id AS "user_session_last_authentication_id?",
|
||||||
usa.id AS "user_session_last_authentication_id?",
|
usa.created_at AS "user_session_last_authentication_created_at?",
|
||||||
usa.created_at AS "user_session_last_authentication_created_at?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.id AS "user_email_id?",
|
ue.email AS "user_email?",
|
||||||
ue.email AS "user_email?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
|
||||||
FROM
|
FROM
|
||||||
oauth2_authorization_grants og
|
oauth2_authorization_grants og
|
||||||
LEFT JOIN oauth2_sessions os
|
LEFT JOIN oauth2_sessions os
|
||||||
ON os.id = og.oauth2_session_id
|
USING (oauth2_session_id)
|
||||||
LEFT JOIN user_sessions us
|
LEFT JOIN user_sessions us
|
||||||
ON us.id = os.user_session_id
|
USING (user_session_id)
|
||||||
LEFT JOIN users u
|
LEFT JOIN users u
|
||||||
ON u.id = us.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_session_authentications usa
|
LEFT JOIN user_session_authentications usa
|
||||||
ON usa.session_id = us.id
|
USING (user_session_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
|
|
||||||
WHERE og.id = $1
|
WHERE og.oauth2_authorization_grant_id = $1
|
||||||
|
|
||||||
ORDER BY usa.created_at DESC
|
ORDER BY usa.created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#,
|
"#,
|
||||||
id,
|
Uuid::from(id),
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *conn)
|
.fetch_one(&mut *conn)
|
||||||
.await
|
.await
|
||||||
@ -387,6 +423,7 @@ pub async fn get_grant_by_id(
|
|||||||
Ok(grant)
|
Ok(grant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, err(Debug))]
|
||||||
pub async fn lookup_grant_by_code(
|
pub async fn lookup_grant_by_code(
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
code: &str,
|
code: &str,
|
||||||
@ -396,50 +433,49 @@ pub async fn lookup_grant_by_code(
|
|||||||
GrantLookup,
|
GrantLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
og.id AS grant_id,
|
og.oauth2_authorization_grant_id,
|
||||||
og.created_at AS grant_created_at,
|
og.created_at AS oauth2_authorization_grant_created_at,
|
||||||
og.cancelled_at AS grant_cancelled_at,
|
og.cancelled_at AS oauth2_authorization_grant_cancelled_at,
|
||||||
og.fulfilled_at AS grant_fulfilled_at,
|
og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,
|
||||||
og.exchanged_at AS grant_exchanged_at,
|
og.exchanged_at AS oauth2_authorization_grant_exchanged_at,
|
||||||
og.scope AS grant_scope,
|
og.scope AS oauth2_authorization_grant_scope,
|
||||||
og.state AS grant_state,
|
og.state AS oauth2_authorization_grant_state,
|
||||||
og.redirect_uri AS grant_redirect_uri,
|
og.redirect_uri AS oauth2_authorization_grant_redirect_uri,
|
||||||
og.response_mode AS grant_response_mode,
|
og.response_mode AS oauth2_authorization_grant_response_mode,
|
||||||
og.nonce AS grant_nonce,
|
og.nonce AS oauth2_authorization_grant_nonce,
|
||||||
og.max_age AS grant_max_age,
|
og.max_age AS oauth2_authorization_grant_max_age,
|
||||||
og.acr_values AS grant_acr_values,
|
og.oauth2_client_id AS oauth2_client_id,
|
||||||
og.oauth2_client_id AS oauth2_client_id,
|
og.authorization_code AS oauth2_authorization_grant_code,
|
||||||
og.code AS grant_code,
|
og.response_type_code AS oauth2_authorization_grant_response_type_code,
|
||||||
og.response_type_code AS grant_response_type_code,
|
og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,
|
||||||
og.response_type_id_token AS grant_response_type_id_token,
|
og.code_challenge AS oauth2_authorization_grant_code_challenge,
|
||||||
og.code_challenge AS grant_code_challenge,
|
og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,
|
||||||
og.code_challenge_method AS grant_code_challenge_method,
|
og.requires_consent AS oauth2_authorization_grant_requires_consent,
|
||||||
og.requires_consent AS grant_requires_consent,
|
os.oauth2_session_id AS "oauth2_session_id?",
|
||||||
os.id AS "session_id?",
|
us.user_session_id AS "user_session_id?",
|
||||||
us.id AS "user_session_id?",
|
us.created_at AS "user_session_created_at?",
|
||||||
us.created_at AS "user_session_created_at?",
|
u.user_id AS "user_id?",
|
||||||
u.id AS "user_id?",
|
u.username AS "user_username?",
|
||||||
u.username AS "user_username?",
|
usa.user_session_authentication_id AS "user_session_last_authentication_id?",
|
||||||
usa.id AS "user_session_last_authentication_id?",
|
usa.created_at AS "user_session_last_authentication_created_at?",
|
||||||
usa.created_at AS "user_session_last_authentication_created_at?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.id AS "user_email_id?",
|
ue.email AS "user_email?",
|
||||||
ue.email AS "user_email?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
|
||||||
FROM
|
FROM
|
||||||
oauth2_authorization_grants og
|
oauth2_authorization_grants og
|
||||||
LEFT JOIN oauth2_sessions os
|
LEFT JOIN oauth2_sessions os
|
||||||
ON os.id = og.oauth2_session_id
|
USING (oauth2_session_id)
|
||||||
LEFT JOIN user_sessions us
|
LEFT JOIN user_sessions us
|
||||||
ON us.id = os.user_session_id
|
USING (user_session_id)
|
||||||
LEFT JOIN users u
|
LEFT JOIN users u
|
||||||
ON u.id = us.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_session_authentications usa
|
LEFT JOIN user_session_authentications usa
|
||||||
ON usa.session_id = us.id
|
USING (user_session_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
|
|
||||||
WHERE og.code = $1
|
WHERE og.authorization_code = $1
|
||||||
|
|
||||||
ORDER BY usa.created_at DESC
|
ORDER BY usa.created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@ -455,41 +491,69 @@ pub async fn lookup_grant_by_code(
|
|||||||
Ok(grant)
|
Ok(grant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
grant.id = %grant.data,
|
||||||
|
client.id = %grant.client.data,
|
||||||
|
session.id,
|
||||||
|
user_session.id = %browser_session.data,
|
||||||
|
user.id = %browser_session.user.data,
|
||||||
|
),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
pub async fn derive_session(
|
pub async fn derive_session(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
grant: &AuthorizationGrant<PostgresqlBackend>,
|
grant: &AuthorizationGrant<PostgresqlBackend>,
|
||||||
browser_session: BrowserSession<PostgresqlBackend>,
|
browser_session: BrowserSession<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<Session<PostgresqlBackend>> {
|
) -> anyhow::Result<Session<PostgresqlBackend>> {
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
tracing::Span::current().record("session.id", tracing::field::display(id));
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO oauth2_sessions
|
INSERT INTO oauth2_sessions
|
||||||
(user_session_id, oauth2_client_id, scope)
|
(oauth2_session_id, user_session_id, oauth2_client_id, scope, created_at)
|
||||||
SELECT
|
SELECT
|
||||||
$1,
|
$1,
|
||||||
|
$2,
|
||||||
og.oauth2_client_id,
|
og.oauth2_client_id,
|
||||||
og.scope
|
og.scope,
|
||||||
|
$3
|
||||||
FROM
|
FROM
|
||||||
oauth2_authorization_grants og
|
oauth2_authorization_grants og
|
||||||
WHERE
|
WHERE
|
||||||
og.id = $2
|
og.oauth2_authorization_grant_id = $4
|
||||||
RETURNING id, created_at
|
|
||||||
"#,
|
"#,
|
||||||
browser_session.data,
|
Uuid::from(id),
|
||||||
grant.data,
|
Uuid::from(browser_session.data),
|
||||||
|
created_at,
|
||||||
|
Uuid::from(grant.data),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
.context("could not insert oauth2 session")?;
|
.context("could not insert oauth2 session")?;
|
||||||
|
|
||||||
Ok(Session {
|
Ok(Session {
|
||||||
data: res.id,
|
data: id,
|
||||||
browser_session,
|
browser_session,
|
||||||
client: grant.client.clone(),
|
client: grant.client.clone(),
|
||||||
scope: grant.scope.clone(),
|
scope: grant.scope.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
grant.id = %grant.data,
|
||||||
|
client.id = %grant.client.data,
|
||||||
|
session.id = %session.data,
|
||||||
|
user_session.id = %session.browser_session.data,
|
||||||
|
user.id = %session.browser_session.user.data,
|
||||||
|
),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
pub async fn fulfill_grant(
|
pub async fn fulfill_grant(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||||
@ -499,15 +563,16 @@ pub async fn fulfill_grant(
|
|||||||
r#"
|
r#"
|
||||||
UPDATE oauth2_authorization_grants AS og
|
UPDATE oauth2_authorization_grants AS og
|
||||||
SET
|
SET
|
||||||
oauth2_session_id = os.id,
|
oauth2_session_id = os.oauth2_session_id,
|
||||||
fulfilled_at = os.created_at
|
fulfilled_at = os.created_at
|
||||||
FROM oauth2_sessions os
|
FROM oauth2_sessions os
|
||||||
WHERE
|
WHERE
|
||||||
og.id = $1 AND os.id = $2
|
og.oauth2_authorization_grant_id = $1
|
||||||
|
AND os.oauth2_session_id = $2
|
||||||
RETURNING fulfilled_at AS "fulfilled_at!: DateTime<Utc>"
|
RETURNING fulfilled_at AS "fulfilled_at!: DateTime<Utc>"
|
||||||
"#,
|
"#,
|
||||||
grant.data,
|
Uuid::from(grant.data),
|
||||||
session.data,
|
Uuid::from(session.data),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await
|
.await
|
||||||
@ -518,6 +583,14 @@ pub async fn fulfill_grant(
|
|||||||
Ok(grant)
|
Ok(grant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
grant.id = %grant.data,
|
||||||
|
client.id = %grant.client.data,
|
||||||
|
),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
pub async fn give_consent_to_grant(
|
pub async fn give_consent_to_grant(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||||
@ -528,9 +601,9 @@ pub async fn give_consent_to_grant(
|
|||||||
SET
|
SET
|
||||||
requires_consent = 'f'
|
requires_consent = 'f'
|
||||||
WHERE
|
WHERE
|
||||||
og.id = $1
|
og.oauth2_authorization_grant_id = $1
|
||||||
"#,
|
"#,
|
||||||
grant.data,
|
Uuid::from(grant.data),
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await?;
|
.await?;
|
||||||
@ -540,22 +613,29 @@ pub async fn give_consent_to_grant(
|
|||||||
Ok(grant)
|
Ok(grant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
grant.id = %grant.data,
|
||||||
|
client.id = %grant.client.data,
|
||||||
|
),
|
||||||
|
err(Debug),
|
||||||
|
)]
|
||||||
pub async fn exchange_grant(
|
pub async fn exchange_grant(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||||
let exchanged_at = sqlx::query_scalar!(
|
let exchanged_at = Utc::now();
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE oauth2_authorization_grants
|
UPDATE oauth2_authorization_grants
|
||||||
SET
|
SET exchanged_at = $2
|
||||||
exchanged_at = NOW()
|
WHERE oauth2_authorization_grant_id = $1
|
||||||
WHERE
|
|
||||||
id = $1
|
|
||||||
RETURNING exchanged_at AS "exchanged_at!: DateTime<Utc>"
|
|
||||||
"#,
|
"#,
|
||||||
grant.data,
|
Uuid::from(grant.data),
|
||||||
|
exchanged_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
.context("could not mark grant as exchanged")?;
|
.context("could not mark grant as exchanged")?;
|
||||||
|
|
||||||
|
@ -20,23 +20,25 @@ use mas_iana::{
|
|||||||
oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod},
|
oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod},
|
||||||
};
|
};
|
||||||
use mas_jose::jwk::PublicJsonWebKeySet;
|
use mas_jose::jwk::PublicJsonWebKeySet;
|
||||||
use oauth2_types::{requests::GrantType, response_type::ResponseType};
|
use oauth2_types::requests::GrantType;
|
||||||
use sqlx::{PgConnection, PgExecutor};
|
use sqlx::{PgConnection, PgExecutor};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::PostgresqlBackend;
|
use crate::PostgresqlBackend;
|
||||||
|
|
||||||
|
// XXX: response_types & contacts
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OAuth2ClientLookup {
|
pub struct OAuth2ClientLookup {
|
||||||
id: i64,
|
oauth2_client_id: Uuid,
|
||||||
client_id: String,
|
|
||||||
encrypted_client_secret: Option<String>,
|
encrypted_client_secret: Option<String>,
|
||||||
redirect_uris: Vec<String>,
|
redirect_uris: Vec<String>,
|
||||||
response_types: Vec<String>,
|
// response_types: Vec<String>,
|
||||||
grant_type_authorization_code: bool,
|
grant_type_authorization_code: bool,
|
||||||
grant_type_refresh_token: bool,
|
grant_type_refresh_token: bool,
|
||||||
contacts: Vec<String>,
|
// contacts: Vec<String>,
|
||||||
client_name: Option<String>,
|
client_name: Option<String>,
|
||||||
logo_uri: Option<String>,
|
logo_uri: Option<String>,
|
||||||
client_uri: Option<String>,
|
client_uri: Option<String>,
|
||||||
@ -53,6 +55,9 @@ pub struct OAuth2ClientLookup {
|
|||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ClientFetchError {
|
pub enum ClientFetchError {
|
||||||
|
#[error("invalid client ID")]
|
||||||
|
InvalidClientId(#[from] ulid::DecodeError),
|
||||||
|
|
||||||
#[error("malformed jwks column")]
|
#[error("malformed jwks column")]
|
||||||
MalformedJwks(#[source] serde_json::Error),
|
MalformedJwks(#[source] serde_json::Error),
|
||||||
|
|
||||||
@ -78,7 +83,10 @@ pub enum ClientFetchError {
|
|||||||
impl ClientFetchError {
|
impl ClientFetchError {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn not_found(&self) -> bool {
|
pub fn not_found(&self) -> bool {
|
||||||
matches!(self, Self::Database(sqlx::Error::RowNotFound))
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::Database(sqlx::Error::RowNotFound) | Self::InvalidClientId(_)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,12 +102,19 @@ impl TryInto<Client<PostgresqlBackend>> for OAuth2ClientLookup {
|
|||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let response_types = vec![
|
||||||
|
OAuthAuthorizationEndpointResponseType::Code,
|
||||||
|
OAuthAuthorizationEndpointResponseType::IdToken,
|
||||||
|
OAuthAuthorizationEndpointResponseType::None,
|
||||||
|
];
|
||||||
|
/* XXX
|
||||||
let response_types: Result<Vec<OAuthAuthorizationEndpointResponseType>, _> =
|
let response_types: Result<Vec<OAuthAuthorizationEndpointResponseType>, _> =
|
||||||
self.response_types.iter().map(|s| s.parse()).collect();
|
self.response_types.iter().map(|s| s.parse()).collect();
|
||||||
let response_types = response_types.map_err(|source| ClientFetchError::ParseField {
|
let response_types = response_types.map_err(|source| ClientFetchError::ParseField {
|
||||||
field: "response_types",
|
field: "response_types",
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
*/
|
||||||
|
|
||||||
let mut grant_types = Vec::new();
|
let mut grant_types = Vec::new();
|
||||||
if self.grant_type_authorization_code {
|
if self.grant_type_authorization_code {
|
||||||
@ -210,13 +225,14 @@ impl TryInto<Client<PostgresqlBackend>> for OAuth2ClientLookup {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Client {
|
Ok(Client {
|
||||||
data: self.id,
|
data: self.oauth2_client_id.into(),
|
||||||
client_id: self.client_id,
|
client_id: self.oauth2_client_id.to_string(),
|
||||||
encrypted_client_secret: self.encrypted_client_secret,
|
encrypted_client_secret: self.encrypted_client_secret,
|
||||||
redirect_uris,
|
redirect_uris,
|
||||||
response_types,
|
response_types,
|
||||||
grant_types,
|
grant_types,
|
||||||
contacts: self.contacts,
|
// contacts: self.contacts,
|
||||||
|
contacts: vec![],
|
||||||
client_name: self.client_name,
|
client_name: self.client_name,
|
||||||
logo_uri,
|
logo_uri,
|
||||||
client_uri,
|
client_uri,
|
||||||
@ -234,20 +250,21 @@ impl TryInto<Client<PostgresqlBackend>> for OAuth2ClientLookup {
|
|||||||
|
|
||||||
pub async fn lookup_client(
|
pub async fn lookup_client(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
id: i64,
|
id: Ulid,
|
||||||
) -> Result<Client<PostgresqlBackend>, ClientFetchError> {
|
) -> Result<Client<PostgresqlBackend>, ClientFetchError> {
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
OAuth2ClientLookup,
|
OAuth2ClientLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
c.id,
|
c.oauth2_client_id,
|
||||||
c.client_id,
|
|
||||||
c.encrypted_client_secret,
|
c.encrypted_client_secret,
|
||||||
ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS "redirect_uris!",
|
ARRAY(
|
||||||
c.response_types,
|
SELECT redirect_uri
|
||||||
|
FROM oauth2_client_redirect_uris r
|
||||||
|
WHERE r.oauth2_client_id = c.oauth2_client_id
|
||||||
|
) AS "redirect_uris!",
|
||||||
c.grant_type_authorization_code,
|
c.grant_type_authorization_code,
|
||||||
c.grant_type_refresh_token,
|
c.grant_type_refresh_token,
|
||||||
c.contacts,
|
|
||||||
c.client_name,
|
c.client_name,
|
||||||
c.logo_uri,
|
c.logo_uri,
|
||||||
c.client_uri,
|
c.client_uri,
|
||||||
@ -262,9 +279,9 @@ pub async fn lookup_client(
|
|||||||
c.initiate_login_uri
|
c.initiate_login_uri
|
||||||
FROM oauth2_clients c
|
FROM oauth2_clients c
|
||||||
|
|
||||||
WHERE c.id = $1
|
WHERE c.oauth2_client_id = $1
|
||||||
"#,
|
"#,
|
||||||
id,
|
Uuid::from(id),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?;
|
.await?;
|
||||||
@ -278,53 +295,18 @@ pub async fn lookup_client_by_client_id(
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
) -> Result<Client<PostgresqlBackend>, ClientFetchError> {
|
) -> Result<Client<PostgresqlBackend>, ClientFetchError> {
|
||||||
let res = sqlx::query_as!(
|
let id: Ulid = client_id.parse()?;
|
||||||
OAuth2ClientLookup,
|
lookup_client(executor, id).await
|
||||||
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.userinfo_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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn insert_client(
|
pub async fn insert_client(
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
client_id: &str,
|
client_id: Ulid,
|
||||||
redirect_uris: &[Url],
|
redirect_uris: &[Url],
|
||||||
encrypted_client_secret: Option<&str>,
|
encrypted_client_secret: Option<&str>,
|
||||||
response_types: &[ResponseType],
|
|
||||||
grant_types: &[GrantType],
|
grant_types: &[GrantType],
|
||||||
contacts: &[String],
|
_contacts: &[String],
|
||||||
client_name: Option<&str>,
|
client_name: Option<&str>,
|
||||||
logo_uri: Option<&Url>,
|
logo_uri: Option<&Url>,
|
||||||
client_uri: Option<&Url>,
|
client_uri: Option<&Url>,
|
||||||
@ -338,7 +320,6 @@ pub async fn insert_client(
|
|||||||
token_endpoint_auth_signing_alg: Option<&JsonWebSignatureAlg>,
|
token_endpoint_auth_signing_alg: Option<&JsonWebSignatureAlg>,
|
||||||
initiate_login_uri: Option<&Url>,
|
initiate_login_uri: Option<&Url>,
|
||||||
) -> Result<(), sqlx::Error> {
|
) -> Result<(), sqlx::Error> {
|
||||||
let response_types: Vec<String> = response_types.iter().map(ToString::to_string).collect();
|
|
||||||
let grant_type_authorization_code = grant_types.contains(&GrantType::AuthorizationCode);
|
let grant_type_authorization_code = grant_types.contains(&GrantType::AuthorizationCode);
|
||||||
let grant_type_refresh_token = grant_types.contains(&GrantType::RefreshToken);
|
let grant_type_refresh_token = grant_types.contains(&GrantType::RefreshToken);
|
||||||
let logo_uri = logo_uri.map(Url::as_str);
|
let logo_uri = logo_uri.map(Url::as_str);
|
||||||
@ -353,15 +334,13 @@ pub async fn insert_client(
|
|||||||
let token_endpoint_auth_signing_alg = token_endpoint_auth_signing_alg.map(ToString::to_string);
|
let token_endpoint_auth_signing_alg = token_endpoint_auth_signing_alg.map(ToString::to_string);
|
||||||
let initiate_login_uri = initiate_login_uri.map(Url::as_str);
|
let initiate_login_uri = initiate_login_uri.map(Url::as_str);
|
||||||
|
|
||||||
let id = sqlx::query_scalar!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO oauth2_clients
|
INSERT INTO oauth2_clients
|
||||||
(client_id,
|
(oauth2_client_id,
|
||||||
encrypted_client_secret,
|
encrypted_client_secret,
|
||||||
response_types,
|
|
||||||
grant_type_authorization_code,
|
grant_type_authorization_code,
|
||||||
grant_type_refresh_token,
|
grant_type_refresh_token,
|
||||||
contacts,
|
|
||||||
client_name,
|
client_name,
|
||||||
logo_uri,
|
logo_uri,
|
||||||
client_uri,
|
client_uri,
|
||||||
@ -375,15 +354,12 @@ pub async fn insert_client(
|
|||||||
token_endpoint_auth_signing_alg,
|
token_endpoint_auth_signing_alg,
|
||||||
initiate_login_uri)
|
initiate_login_uri)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||||
RETURNING id
|
|
||||||
"#,
|
"#,
|
||||||
client_id,
|
Uuid::from(client_id),
|
||||||
encrypted_client_secret,
|
encrypted_client_secret,
|
||||||
&response_types,
|
|
||||||
grant_type_authorization_code,
|
grant_type_authorization_code,
|
||||||
grant_type_refresh_token,
|
grant_type_refresh_token,
|
||||||
contacts,
|
|
||||||
client_name,
|
client_name,
|
||||||
logo_uri,
|
logo_uri,
|
||||||
client_uri,
|
client_uri,
|
||||||
@ -397,96 +373,87 @@ pub async fn insert_client(
|
|||||||
token_endpoint_auth_signing_alg,
|
token_endpoint_auth_signing_alg,
|
||||||
initiate_login_uri,
|
initiate_login_uri,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let redirect_uris: Vec<String> = 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)
|
.execute(&mut *conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
for redirect_uri in redirect_uris {
|
||||||
|
let id = Ulid::new();
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO oauth2_client_redirect_uris
|
||||||
|
(oauth2_client_redirect_uri_id, oauth2_client_id, redirect_uri)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
"#,
|
||||||
|
Uuid::from(id),
|
||||||
|
Uuid::from(client_id),
|
||||||
|
redirect_uri.as_str(),
|
||||||
|
)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_client_from_config(
|
pub async fn insert_client_from_config(
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
client_id: &str,
|
client_id: Ulid,
|
||||||
client_auth_method: OAuthClientAuthenticationMethod,
|
client_auth_method: OAuthClientAuthenticationMethod,
|
||||||
encrypted_client_secret: Option<&str>,
|
encrypted_client_secret: Option<&str>,
|
||||||
jwks: Option<&PublicJsonWebKeySet>,
|
jwks: Option<&PublicJsonWebKeySet>,
|
||||||
jwks_uri: Option<&Url>,
|
jwks_uri: Option<&Url>,
|
||||||
redirect_uris: &[Url],
|
redirect_uris: &[Url],
|
||||||
) -> anyhow::Result<()> {
|
) -> 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 = jwks.map(serde_json::to_value).transpose()?;
|
||||||
let jwks_uri = jwks_uri.map(Url::as_str);
|
let jwks_uri = jwks_uri.map(Url::as_str);
|
||||||
|
|
||||||
let client_auth_method = client_auth_method.to_string();
|
let client_auth_method = client_auth_method.to_string();
|
||||||
|
|
||||||
let id = sqlx::query_scalar!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO oauth2_clients
|
INSERT INTO oauth2_clients
|
||||||
(client_id,
|
(oauth2_client_id,
|
||||||
encrypted_client_secret,
|
encrypted_client_secret,
|
||||||
response_types,
|
|
||||||
grant_type_authorization_code,
|
grant_type_authorization_code,
|
||||||
grant_type_refresh_token,
|
grant_type_refresh_token,
|
||||||
token_endpoint_auth_method,
|
token_endpoint_auth_method,
|
||||||
jwks,
|
jwks,
|
||||||
jwks_uri,
|
jwks_uri)
|
||||||
contacts)
|
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, '{}')
|
($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id
|
|
||||||
"#,
|
"#,
|
||||||
client_id,
|
Uuid::from(client_id),
|
||||||
encrypted_client_secret,
|
encrypted_client_secret,
|
||||||
&response_types,
|
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
client_auth_method,
|
client_auth_method,
|
||||||
jwks,
|
jwks,
|
||||||
jwks_uri,
|
jwks_uri,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let redirect_uris: Vec<String> = 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)
|
.execute(&mut *conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
for redirect_uri in redirect_uris {
|
||||||
|
let id = Ulid::new();
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO oauth2_client_redirect_uris
|
||||||
|
(oauth2_client_redirect_uri_id, oauth2_client_id, redirect_uri)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
"#,
|
||||||
|
Uuid::from(id),
|
||||||
|
Uuid::from(client_id),
|
||||||
|
redirect_uri.as_str(),
|
||||||
|
)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn truncate_clients(executor: impl PgExecutor<'_>) -> anyhow::Result<()> {
|
pub async fn truncate_clients(executor: impl PgExecutor<'_>) -> anyhow::Result<()> {
|
||||||
sqlx::query!("TRUNCATE oauth2_client_redirect_uris, oauth2_clients RESTART IDENTITY CASCADE")
|
sqlx::query!("TRUNCATE oauth2_client_redirect_uris, oauth2_clients CASCADE")
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -14,9 +14,12 @@
|
|||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use mas_data_model::{Client, User};
|
use mas_data_model::{Client, User};
|
||||||
use oauth2_types::scope::{Scope, ScopeToken};
|
use oauth2_types::scope::{Scope, ScopeToken};
|
||||||
use sqlx::PgExecutor;
|
use sqlx::PgExecutor;
|
||||||
|
use ulid::Ulid;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::PostgresqlBackend;
|
use crate::PostgresqlBackend;
|
||||||
|
|
||||||
@ -31,8 +34,8 @@ pub async fn fetch_client_consent(
|
|||||||
FROM oauth2_consents
|
FROM oauth2_consents
|
||||||
WHERE user_id = $1 AND oauth2_client_id = $2
|
WHERE user_id = $1 AND oauth2_client_id = $2
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(user.data),
|
||||||
client.data,
|
Uuid::from(client.data),
|
||||||
)
|
)
|
||||||
.fetch_all(executor)
|
.fetch_all(executor)
|
||||||
.await?;
|
.await?;
|
||||||
@ -51,17 +54,29 @@ pub async fn insert_client_consent(
|
|||||||
client: &Client<PostgresqlBackend>,
|
client: &Client<PostgresqlBackend>,
|
||||||
scope: &Scope,
|
scope: &Scope,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let tokens: Vec<String> = scope.iter().map(ToString::to_string).collect();
|
let now = Utc::now();
|
||||||
|
let (tokens, ids): (Vec<String>, Vec<Uuid>) = scope
|
||||||
|
.iter()
|
||||||
|
.map(|token| {
|
||||||
|
(
|
||||||
|
token.to_string(),
|
||||||
|
Uuid::from(Ulid::from_datetime(now.into())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO oauth2_consents (user_id, oauth2_client_id, scope_token)
|
INSERT INTO oauth2_consents
|
||||||
SELECT $1, $2, scope_token FROM UNNEST($3::text[]) scope_token
|
(oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at)
|
||||||
ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET updated_at = NOW()
|
SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token)
|
||||||
|
ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
&ids,
|
||||||
client.data,
|
Uuid::from(user.data),
|
||||||
|
Uuid::from(client.data),
|
||||||
&tokens,
|
&tokens,
|
||||||
|
now,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -12,8 +12,10 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use mas_data_model::Session;
|
use mas_data_model::Session;
|
||||||
use sqlx::PgExecutor;
|
use sqlx::PgExecutor;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::PostgresqlBackend;
|
use crate::PostgresqlBackend;
|
||||||
|
|
||||||
@ -27,13 +29,15 @@ pub async fn end_oauth_session(
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
session: Session<PostgresqlBackend>,
|
session: Session<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let finished_at = Utc::now();
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE oauth2_sessions
|
UPDATE oauth2_sessions
|
||||||
SET ended_at = NOW()
|
SET finished_at = $2
|
||||||
WHERE id = $1
|
WHERE oauth2_session_id = $1
|
||||||
"#,
|
"#,
|
||||||
session.data,
|
Uuid::from(session.data),
|
||||||
|
finished_at,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -13,66 +13,71 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
AccessToken, Authentication, BrowserSession, RefreshToken, Session, User, UserEmail,
|
AccessToken, Authentication, BrowserSession, RefreshToken, Session, User, UserEmail,
|
||||||
};
|
};
|
||||||
use sqlx::{PgConnection, PgExecutor};
|
use sqlx::{PgConnection, PgExecutor};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ulid::Ulid;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::client::{lookup_client, ClientFetchError};
|
use super::client::{lookup_client, ClientFetchError};
|
||||||
use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend};
|
use crate::{DatabaseInconsistencyError, PostgresqlBackend};
|
||||||
|
|
||||||
pub async fn add_refresh_token(
|
pub async fn add_refresh_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
session: &Session<PostgresqlBackend>,
|
session: &Session<PostgresqlBackend>,
|
||||||
access_token: AccessToken<PostgresqlBackend>,
|
access_token: AccessToken<PostgresqlBackend>,
|
||||||
token: &str,
|
refresh_token: String,
|
||||||
) -> anyhow::Result<RefreshToken<PostgresqlBackend>> {
|
) -> anyhow::Result<RefreshToken<PostgresqlBackend>> {
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO oauth2_refresh_tokens
|
INSERT INTO oauth2_refresh_tokens
|
||||||
(oauth2_session_id, oauth2_access_token_id, token)
|
(oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id,
|
||||||
|
refresh_token, created_at)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3)
|
($1, $2, $3, $4, $5)
|
||||||
RETURNING
|
|
||||||
id, created_at
|
|
||||||
"#,
|
"#,
|
||||||
session.data,
|
Uuid::from(id),
|
||||||
access_token.data,
|
Uuid::from(session.data),
|
||||||
token,
|
Uuid::from(access_token.data),
|
||||||
|
refresh_token,
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
.context("could not insert oauth2 refresh token")?;
|
.context("could not insert oauth2 refresh token")?;
|
||||||
|
|
||||||
Ok(RefreshToken {
|
Ok(RefreshToken {
|
||||||
data: res.id,
|
data: id,
|
||||||
token: token.to_owned(),
|
refresh_token,
|
||||||
access_token: Some(access_token),
|
access_token: Some(access_token),
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OAuth2RefreshTokenLookup {
|
struct OAuth2RefreshTokenLookup {
|
||||||
refresh_token_id: i64,
|
oauth2_refresh_token_id: Uuid,
|
||||||
refresh_token: String,
|
oauth2_refresh_token: String,
|
||||||
refresh_token_created_at: DateTime<Utc>,
|
oauth2_refresh_token_created_at: DateTime<Utc>,
|
||||||
access_token_id: Option<i64>,
|
oauth2_access_token_id: Option<Uuid>,
|
||||||
access_token: Option<String>,
|
oauth2_access_token: Option<String>,
|
||||||
access_token_expires_after: Option<i32>,
|
oauth2_access_token_created_at: Option<DateTime<Utc>>,
|
||||||
access_token_created_at: Option<DateTime<Utc>>,
|
oauth2_access_token_expires_at: Option<DateTime<Utc>>,
|
||||||
session_id: i64,
|
oauth2_session_id: Uuid,
|
||||||
oauth2_client_id: i64,
|
oauth2_client_id: Uuid,
|
||||||
scope: String,
|
oauth2_session_scope: String,
|
||||||
user_session_id: i64,
|
user_session_id: Uuid,
|
||||||
user_session_created_at: DateTime<Utc>,
|
user_session_created_at: DateTime<Utc>,
|
||||||
user_id: i64,
|
user_id: Uuid,
|
||||||
user_username: String,
|
user_username: String,
|
||||||
user_session_last_authentication_id: Option<i64>,
|
user_session_last_authentication_id: Option<Uuid>,
|
||||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -103,44 +108,45 @@ pub async fn lookup_active_refresh_token(
|
|||||||
OAuth2RefreshTokenLookup,
|
OAuth2RefreshTokenLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
rt.id AS refresh_token_id,
|
rt.oauth2_refresh_token_id,
|
||||||
rt.token AS refresh_token,
|
rt.refresh_token AS oauth2_refresh_token,
|
||||||
rt.created_at AS refresh_token_created_at,
|
rt.created_at AS oauth2_refresh_token_created_at,
|
||||||
at.id AS "access_token_id?",
|
at.oauth2_access_token_id AS "oauth2_access_token_id?",
|
||||||
at.token AS "access_token?",
|
at.access_token AS "oauth2_access_token?",
|
||||||
at.expires_after AS "access_token_expires_after?",
|
at.created_at AS "oauth2_access_token_created_at?",
|
||||||
at.created_at AS "access_token_created_at?",
|
at.expires_at AS "oauth2_access_token_expires_at?",
|
||||||
os.id AS "session_id!",
|
os.oauth2_session_id AS "oauth2_session_id!",
|
||||||
os.oauth2_client_id AS "oauth2_client_id!",
|
os.oauth2_client_id AS "oauth2_client_id!",
|
||||||
os.scope AS "scope!",
|
os.scope AS "oauth2_session_scope!",
|
||||||
us.id AS "user_session_id!",
|
us.user_session_id AS "user_session_id!",
|
||||||
us.created_at AS "user_session_created_at!",
|
us.created_at AS "user_session_created_at!",
|
||||||
u.id AS "user_id!",
|
u.user_id AS "user_id!",
|
||||||
u.username AS "user_username!",
|
u.username AS "user_username!",
|
||||||
usa.id AS "user_session_last_authentication_id?",
|
usa.user_session_authentication_id AS "user_session_last_authentication_id?",
|
||||||
usa.created_at AS "user_session_last_authentication_created_at?",
|
usa.created_at AS "user_session_last_authentication_created_at?",
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
FROM oauth2_refresh_tokens rt
|
FROM oauth2_refresh_tokens rt
|
||||||
LEFT JOIN oauth2_access_tokens at
|
|
||||||
ON at.id = rt.oauth2_access_token_id
|
|
||||||
INNER JOIN oauth2_sessions os
|
INNER JOIN oauth2_sessions os
|
||||||
ON os.id = rt.oauth2_session_id
|
USING (oauth2_session_id)
|
||||||
|
LEFT JOIN oauth2_access_tokens at
|
||||||
|
USING (oauth2_access_token_id)
|
||||||
INNER JOIN user_sessions us
|
INNER JOIN user_sessions us
|
||||||
ON us.id = os.user_session_id
|
USING (user_session_id)
|
||||||
INNER JOIN users u
|
INNER JOIN users u
|
||||||
ON u.id = us.user_id
|
USING (user_id)
|
||||||
LEFT JOIN user_session_authentications usa
|
LEFT JOIN user_session_authentications usa
|
||||||
ON usa.session_id = us.id
|
USING (user_session_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
|
|
||||||
WHERE rt.token = $1
|
WHERE rt.refresh_token = $1
|
||||||
AND rt.next_token_id IS NULL
|
AND rt.consumed_at IS NULL
|
||||||
AND us.active
|
AND rt.revoked_at IS NULL
|
||||||
AND os.ended_at IS NULL
|
AND us.finished_at IS NULL
|
||||||
|
AND os.finished_at IS NULL
|
||||||
|
|
||||||
ORDER BY usa.created_at DESC
|
ORDER BY usa.created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@ -151,30 +157,31 @@ pub async fn lookup_active_refresh_token(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let access_token = match (
|
let access_token = match (
|
||||||
res.access_token_id,
|
res.oauth2_access_token_id,
|
||||||
res.access_token,
|
res.oauth2_access_token,
|
||||||
res.access_token_created_at,
|
res.oauth2_access_token_created_at,
|
||||||
res.access_token_expires_after,
|
res.oauth2_access_token_expires_at,
|
||||||
) {
|
) {
|
||||||
(None, None, None, None) => None,
|
(None, None, None, None) => None,
|
||||||
(Some(id), Some(token), Some(created_at), Some(expires_after)) => Some(AccessToken {
|
(Some(id), Some(access_token), Some(created_at), Some(expires_at)) => Some(AccessToken {
|
||||||
data: id,
|
data: id.into(),
|
||||||
jti: format!("{}", id),
|
// XXX: are we doing that everywhere?
|
||||||
token,
|
jti: Ulid::from(id).to_string(),
|
||||||
|
access_token,
|
||||||
created_at,
|
created_at,
|
||||||
expires_after: Duration::seconds(expires_after.into()),
|
expires_at,
|
||||||
}),
|
}),
|
||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let refresh_token = RefreshToken {
|
let refresh_token = RefreshToken {
|
||||||
data: res.refresh_token_id,
|
data: res.oauth2_refresh_token_id.into(),
|
||||||
token: res.refresh_token,
|
refresh_token: res.oauth2_refresh_token,
|
||||||
created_at: res.refresh_token_created_at,
|
created_at: res.oauth2_refresh_token_created_at,
|
||||||
access_token,
|
access_token,
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = lookup_client(&mut *conn, res.oauth2_client_id).await?;
|
let client = lookup_client(&mut *conn, res.oauth2_client_id.into()).await?;
|
||||||
|
|
||||||
let primary_email = match (
|
let primary_email = match (
|
||||||
res.user_email_id,
|
res.user_email_id,
|
||||||
@ -183,7 +190,7 @@ pub async fn lookup_active_refresh_token(
|
|||||||
res.user_email_confirmed_at,
|
res.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -192,10 +199,11 @@ pub async fn lookup_active_refresh_token(
|
|||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let id = Ulid::from(res.user_id);
|
||||||
let user = User {
|
let user = User {
|
||||||
data: res.user_id,
|
data: id,
|
||||||
username: res.user_username,
|
username: res.user_username,
|
||||||
sub: format!("fake-sub-{}", res.user_id),
|
sub: id.to_string(),
|
||||||
primary_email,
|
primary_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -205,23 +213,26 @@ pub async fn lookup_active_refresh_token(
|
|||||||
) {
|
) {
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(Some(id), Some(created_at)) => Some(Authentication {
|
(Some(id), Some(created_at)) => Some(Authentication {
|
||||||
data: id,
|
data: id.into(),
|
||||||
created_at,
|
created_at,
|
||||||
}),
|
}),
|
||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let browser_session = BrowserSession {
|
let browser_session = BrowserSession {
|
||||||
data: res.user_session_id,
|
data: res.user_session_id.into(),
|
||||||
created_at: res.user_session_created_at,
|
created_at: res.user_session_created_at,
|
||||||
user,
|
user,
|
||||||
last_authentication,
|
last_authentication,
|
||||||
};
|
};
|
||||||
|
|
||||||
let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?;
|
let scope = res
|
||||||
|
.oauth2_session_scope
|
||||||
|
.parse()
|
||||||
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
let session = Session {
|
let session = Session {
|
||||||
data: res.session_id,
|
data: res.oauth2_session_id.into(),
|
||||||
client,
|
client,
|
||||||
browser_session,
|
browser_session,
|
||||||
scope,
|
scope,
|
||||||
@ -230,19 +241,19 @@ pub async fn lookup_active_refresh_token(
|
|||||||
Ok((refresh_token, session))
|
Ok((refresh_token, session))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn replace_refresh_token(
|
pub async fn consume_refresh_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
refresh_token: &RefreshToken<PostgresqlBackend>,
|
refresh_token: &RefreshToken<PostgresqlBackend>,
|
||||||
next_refresh_token: &RefreshToken<PostgresqlBackend>,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let consumed_at = Utc::now();
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE oauth2_refresh_tokens
|
UPDATE oauth2_refresh_tokens
|
||||||
SET next_token_id = $2
|
SET consumed_at = $2
|
||||||
WHERE id = $1
|
WHERE oauth2_refresh_token_id = $1
|
||||||
"#,
|
"#,
|
||||||
refresh_token.data,
|
Uuid::from(refresh_token.data),
|
||||||
next_refresh_token.data
|
consumed_at,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
|
@ -22,20 +22,21 @@ use mas_data_model::{
|
|||||||
UserEmailVerificationState,
|
UserEmailVerificationState,
|
||||||
};
|
};
|
||||||
use password_hash::{PasswordHash, PasswordHasher, SaltString};
|
use password_hash::{PasswordHash, PasswordHasher, SaltString};
|
||||||
use rand::rngs::OsRng;
|
use rand::thread_rng;
|
||||||
use sqlx::{postgres::types::PgInterval, Acquire, PgExecutor, Postgres, Transaction};
|
use sqlx::{Acquire, PgExecutor, Postgres, Transaction};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tracing::{info_span, Instrument};
|
use tracing::{info_span, Instrument};
|
||||||
|
use ulid::Ulid;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{DatabaseInconsistencyError, PostgresqlBackend};
|
use super::{DatabaseInconsistencyError, PostgresqlBackend};
|
||||||
use crate::IdAndCreationTime;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct UserLookup {
|
struct UserLookup {
|
||||||
user_id: i64,
|
user_id: Uuid,
|
||||||
user_username: String,
|
user_username: String,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -114,13 +115,13 @@ impl ActiveSessionLookupError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SessionLookup {
|
struct SessionLookup {
|
||||||
id: i64,
|
user_session_id: Uuid,
|
||||||
user_id: i64,
|
user_id: Uuid,
|
||||||
username: String,
|
username: String,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
last_authentication_id: Option<i64>,
|
last_authentication_id: Option<Uuid>,
|
||||||
last_authd_at: Option<DateTime<Utc>>,
|
last_authd_at: Option<DateTime<Utc>>,
|
||||||
user_email_id: Option<i64>,
|
user_email_id: Option<Uuid>,
|
||||||
user_email: Option<String>,
|
user_email: Option<String>,
|
||||||
user_email_created_at: Option<DateTime<Utc>>,
|
user_email_created_at: Option<DateTime<Utc>>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -137,7 +138,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
|
|||||||
self.user_email_confirmed_at,
|
self.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -146,16 +147,17 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
|
|||||||
_ => return Err(DatabaseInconsistencyError),
|
_ => return Err(DatabaseInconsistencyError),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let id = Ulid::from(self.user_id);
|
||||||
let user = User {
|
let user = User {
|
||||||
data: self.user_id,
|
data: id,
|
||||||
username: self.username,
|
username: self.username,
|
||||||
sub: format!("fake-sub-{}", self.user_id),
|
sub: id.to_string(),
|
||||||
primary_email,
|
primary_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
let last_authentication = match (self.last_authentication_id, self.last_authd_at) {
|
let last_authentication = match (self.last_authentication_id, self.last_authd_at) {
|
||||||
(Some(id), Some(created_at)) => Some(Authentication {
|
(Some(id), Some(created_at)) => Some(Authentication {
|
||||||
data: id,
|
data: id.into(),
|
||||||
created_at,
|
created_at,
|
||||||
}),
|
}),
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
@ -163,7 +165,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(BrowserSession {
|
Ok(BrowserSession {
|
||||||
data: self.id,
|
data: self.user_session_id.into(),
|
||||||
user,
|
user,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
last_authentication,
|
last_authentication,
|
||||||
@ -171,37 +173,37 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(session.id = id))]
|
#[tracing::instrument(skip_all, fields(session.id = %id))]
|
||||||
pub async fn lookup_active_session(
|
pub async fn lookup_active_session(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
id: i64,
|
id: Ulid,
|
||||||
) -> Result<BrowserSession<PostgresqlBackend>, ActiveSessionLookupError> {
|
) -> Result<BrowserSession<PostgresqlBackend>, ActiveSessionLookupError> {
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
SessionLookup,
|
SessionLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.user_session_id,
|
||||||
u.id AS user_id,
|
u.user_id,
|
||||||
u.username,
|
u.username,
|
||||||
s.created_at,
|
s.created_at,
|
||||||
a.id AS "last_authentication_id?",
|
a.user_session_authentication_id AS "last_authentication_id?",
|
||||||
a.created_at AS "last_authd_at?",
|
a.created_at AS "last_authd_at?",
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
FROM user_sessions s
|
FROM user_sessions s
|
||||||
INNER JOIN users u
|
INNER JOIN users u
|
||||||
ON s.user_id = u.id
|
USING (user_id)
|
||||||
LEFT JOIN user_session_authentications a
|
LEFT JOIN user_session_authentications a
|
||||||
ON a.session_id = s.id
|
USING (user_session_id)
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
WHERE s.id = $1 AND s.active
|
WHERE s.user_session_id = $1 AND s.finished_at IS NULL
|
||||||
ORDER BY a.created_at DESC
|
ORDER BY a.created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#,
|
"#,
|
||||||
id,
|
Uuid::from(id),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?
|
.await?
|
||||||
@ -210,35 +212,37 @@ pub async fn lookup_active_session(
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(user.id = user.data))]
|
#[tracing::instrument(skip_all, fields(user.id = %user.data))]
|
||||||
pub async fn start_session(
|
pub async fn start_session(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
user: User<PostgresqlBackend>,
|
user: User<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<BrowserSession<PostgresqlBackend>> {
|
) -> anyhow::Result<BrowserSession<PostgresqlBackend>> {
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO user_sessions (user_id)
|
INSERT INTO user_sessions (user_session_id, user_id, created_at)
|
||||||
VALUES ($1)
|
VALUES ($1, $2, $3)
|
||||||
RETURNING id, created_at
|
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(id),
|
||||||
|
Uuid::from(user.data),
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
.context("could not create session")?;
|
.context("could not create session")?;
|
||||||
|
|
||||||
let session = BrowserSession {
|
let session = BrowserSession {
|
||||||
data: res.id,
|
data: id,
|
||||||
user,
|
user,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
last_authentication: None,
|
last_authentication: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(user.id = user.data))]
|
#[tracing::instrument(skip_all, fields(user.id = %user.data))]
|
||||||
pub async fn count_active_sessions(
|
pub async fn count_active_sessions(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
user: &User<PostgresqlBackend>,
|
user: &User<PostgresqlBackend>,
|
||||||
@ -247,9 +251,9 @@ pub async fn count_active_sessions(
|
|||||||
r#"
|
r#"
|
||||||
SELECT COUNT(*) as "count!"
|
SELECT COUNT(*) as "count!"
|
||||||
FROM user_sessions s
|
FROM user_sessions s
|
||||||
WHERE s.user_id = $1 AND s.active
|
WHERE s.user_id = $1 AND s.finished_at IS NULL
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(user.data),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?
|
.await?
|
||||||
@ -273,7 +277,7 @@ pub enum AuthenticationError {
|
|||||||
Internal(#[from] tokio::task::JoinError),
|
Internal(#[from] tokio::task::JoinError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(session.id = session.data, user.id = session.user.data))]
|
#[tracing::instrument(skip_all, fields(session.id = %session.data, user.id = %session.user.data))]
|
||||||
pub async fn authenticate_session(
|
pub async fn authenticate_session(
|
||||||
txn: &mut Transaction<'_, Postgres>,
|
txn: &mut Transaction<'_, Postgres>,
|
||||||
session: &mut BrowserSession<PostgresqlBackend>,
|
session: &mut BrowserSession<PostgresqlBackend>,
|
||||||
@ -288,7 +292,7 @@ pub async fn authenticate_session(
|
|||||||
ORDER BY up.created_at DESC
|
ORDER BY up.created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#,
|
"#,
|
||||||
session.user.data,
|
Uuid::from(session.user.data),
|
||||||
)
|
)
|
||||||
.fetch_one(txn.borrow_mut())
|
.fetch_one(txn.borrow_mut())
|
||||||
.instrument(tracing::info_span!("Lookup hashed password"))
|
.instrument(tracing::info_span!("Lookup hashed password"))
|
||||||
@ -309,44 +313,50 @@ pub async fn authenticate_session(
|
|||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
// That went well, let's insert the auth info
|
// That went well, let's insert the auth info
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO user_session_authentications (session_id)
|
INSERT INTO user_session_authentications
|
||||||
VALUES ($1)
|
(user_session_authentication_id, user_session_id, created_at)
|
||||||
RETURNING id, created_at
|
VALUES ($1, $2, $3)
|
||||||
"#,
|
"#,
|
||||||
session.data,
|
Uuid::from(id),
|
||||||
|
Uuid::from(session.data),
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(txn.borrow_mut())
|
.execute(txn.borrow_mut())
|
||||||
.instrument(tracing::info_span!("Save authentication"))
|
.instrument(tracing::info_span!("Save authentication"))
|
||||||
.await
|
.await
|
||||||
.map_err(AuthenticationError::Save)?;
|
.map_err(AuthenticationError::Save)?;
|
||||||
|
|
||||||
session.last_authentication = Some(Authentication {
|
session.last_authentication = Some(Authentication {
|
||||||
data: res.id,
|
data: id,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(txn, phf, password))]
|
#[tracing::instrument(skip(txn, phf, password), err)]
|
||||||
pub async fn register_user(
|
pub async fn register_user(
|
||||||
txn: &mut Transaction<'_, Postgres>,
|
txn: &mut Transaction<'_, Postgres>,
|
||||||
phf: impl PasswordHasher,
|
phf: impl PasswordHasher,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> anyhow::Result<User<PostgresqlBackend>> {
|
) -> anyhow::Result<User<PostgresqlBackend>> {
|
||||||
let id: i64 = sqlx::query_scalar!(
|
let created_at = Utc::now();
|
||||||
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO users (username)
|
INSERT INTO users (user_id, username, created_at)
|
||||||
VALUES ($1)
|
VALUES ($1, $2, $3)
|
||||||
RETURNING id
|
|
||||||
"#,
|
"#,
|
||||||
|
Uuid::from(id),
|
||||||
username,
|
username,
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(txn.borrow_mut())
|
.execute(txn.borrow_mut())
|
||||||
.instrument(info_span!("Register user"))
|
.instrument(info_span!("Register user"))
|
||||||
.await
|
.await
|
||||||
.context("could not insert user")?;
|
.context("could not insert user")?;
|
||||||
@ -354,7 +364,7 @@ pub async fn register_user(
|
|||||||
let user = User {
|
let user = User {
|
||||||
data: id,
|
data: id,
|
||||||
username: username.to_owned(),
|
username: username.to_owned(),
|
||||||
sub: format!("fake-sub-{}", id),
|
sub: id.to_string(),
|
||||||
primary_email: None,
|
primary_email: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -363,23 +373,28 @@ pub async fn register_user(
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(user.id = user.data))]
|
#[tracing::instrument(skip_all, fields(user.id = %user.data))]
|
||||||
pub async fn set_password(
|
pub async fn set_password(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
phf: impl PasswordHasher,
|
phf: impl PasswordHasher,
|
||||||
user: &User<PostgresqlBackend>,
|
user: &User<PostgresqlBackend>,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let created_at = Utc::now();
|
||||||
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
|
||||||
|
let salt = SaltString::generate(thread_rng());
|
||||||
let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?;
|
let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?;
|
||||||
|
|
||||||
sqlx::query_scalar!(
|
sqlx::query_scalar!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO user_passwords (user_id, hashed_password)
|
INSERT INTO user_passwords (user_password_id, user_id, hashed_password, created_at)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2, $3, $4)
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(id),
|
||||||
|
Uuid::from(user.data),
|
||||||
hashed_password.to_string(),
|
hashed_password.to_string(),
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("Save user credentials"))
|
.instrument(info_span!("Save user credentials"))
|
||||||
@ -389,14 +404,20 @@ pub async fn set_password(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(session.id = session.data))]
|
#[tracing::instrument(skip_all, fields(session.id = %session.data))]
|
||||||
pub async fn end_session(
|
pub async fn end_session(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
session: &BrowserSession<PostgresqlBackend>,
|
session: &BrowserSession<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let now = Utc::now();
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
"UPDATE user_sessions SET active = FALSE WHERE id = $1",
|
r#"
|
||||||
session.data,
|
UPDATE user_sessions
|
||||||
|
SET finished_at = $1
|
||||||
|
WHERE user_session_id = $2
|
||||||
|
"#,
|
||||||
|
now,
|
||||||
|
Uuid::from(session.data),
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("End session"))
|
.instrument(info_span!("End session"))
|
||||||
@ -433,16 +454,16 @@ pub async fn lookup_user_by_username(
|
|||||||
UserLookup,
|
UserLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
u.id AS user_id,
|
u.user_id,
|
||||||
u.username AS user_username,
|
u.username AS user_username,
|
||||||
ue.id AS "user_email_id?",
|
ue.user_email_id AS "user_email_id?",
|
||||||
ue.email AS "user_email?",
|
ue.email AS "user_email?",
|
||||||
ue.created_at AS "user_email_created_at?",
|
ue.created_at AS "user_email_created_at?",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||||
FROM users u
|
FROM users u
|
||||||
|
|
||||||
LEFT JOIN user_emails ue
|
LEFT JOIN user_emails ue
|
||||||
ON ue.id = u.primary_email_id
|
USING (user_id)
|
||||||
|
|
||||||
WHERE u.username = $1
|
WHERE u.username = $1
|
||||||
"#,
|
"#,
|
||||||
@ -459,7 +480,7 @@ pub async fn lookup_user_by_username(
|
|||||||
res.user_email_confirmed_at,
|
res.user_email_confirmed_at,
|
||||||
) {
|
) {
|
||||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||||
data: id,
|
data: id.into(),
|
||||||
email,
|
email,
|
||||||
created_at,
|
created_at,
|
||||||
confirmed_at,
|
confirmed_at,
|
||||||
@ -468,10 +489,11 @@ pub async fn lookup_user_by_username(
|
|||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let id = Ulid::from(res.user_id);
|
||||||
Ok(User {
|
Ok(User {
|
||||||
data: res.user_id,
|
data: id,
|
||||||
username: res.user_username,
|
username: res.user_username,
|
||||||
sub: format!("fake-sub-{}", res.user_id),
|
sub: id.to_string(),
|
||||||
primary_email,
|
primary_email,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -494,7 +516,7 @@ pub async fn username_exists(
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct UserEmailLookup {
|
struct UserEmailLookup {
|
||||||
user_email_id: i64,
|
user_email_id: Uuid,
|
||||||
user_email: String,
|
user_email: String,
|
||||||
user_email_created_at: DateTime<Utc>,
|
user_email_created_at: DateTime<Utc>,
|
||||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||||
@ -503,7 +525,7 @@ struct UserEmailLookup {
|
|||||||
impl From<UserEmailLookup> for UserEmail<PostgresqlBackend> {
|
impl From<UserEmailLookup> for UserEmail<PostgresqlBackend> {
|
||||||
fn from(e: UserEmailLookup) -> UserEmail<PostgresqlBackend> {
|
fn from(e: UserEmailLookup) -> UserEmail<PostgresqlBackend> {
|
||||||
UserEmail {
|
UserEmail {
|
||||||
data: e.user_email_id,
|
data: e.user_email_id.into(),
|
||||||
email: e.user_email,
|
email: e.user_email,
|
||||||
created_at: e.user_email_created_at,
|
created_at: e.user_email_created_at,
|
||||||
confirmed_at: e.user_email_confirmed_at,
|
confirmed_at: e.user_email_confirmed_at,
|
||||||
@ -511,7 +533,7 @@ impl From<UserEmailLookup> for UserEmail<PostgresqlBackend> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(user.id = user.data, %user.username))]
|
#[tracing::instrument(skip_all, fields(user.id = %user.data, %user.username))]
|
||||||
pub async fn get_user_emails(
|
pub async fn get_user_emails(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
user: &User<PostgresqlBackend>,
|
user: &User<PostgresqlBackend>,
|
||||||
@ -520,7 +542,7 @@ pub async fn get_user_emails(
|
|||||||
UserEmailLookup,
|
UserEmailLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
ue.id AS "user_email_id",
|
ue.user_email_id,
|
||||||
ue.email AS "user_email",
|
ue.email AS "user_email",
|
||||||
ue.created_at AS "user_email_created_at",
|
ue.created_at AS "user_email_created_at",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at"
|
ue.confirmed_at AS "user_email_confirmed_at"
|
||||||
@ -530,7 +552,7 @@ pub async fn get_user_emails(
|
|||||||
|
|
||||||
ORDER BY ue.email ASC
|
ORDER BY ue.email ASC
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(user.data),
|
||||||
)
|
)
|
||||||
.fetch_all(executor)
|
.fetch_all(executor)
|
||||||
.instrument(info_span!("Fetch user emails"))
|
.instrument(info_span!("Fetch user emails"))
|
||||||
@ -539,27 +561,27 @@ pub async fn get_user_emails(
|
|||||||
Ok(res.into_iter().map(Into::into).collect())
|
Ok(res.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(user.id = user.data, %user.username, email.id = id))]
|
#[tracing::instrument(skip_all, fields(user.id = %user.data, %user.username, email.id = %id))]
|
||||||
pub async fn get_user_email(
|
pub async fn get_user_email(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
user: &User<PostgresqlBackend>,
|
user: &User<PostgresqlBackend>,
|
||||||
id: i64,
|
id: Ulid,
|
||||||
) -> Result<UserEmail<PostgresqlBackend>, anyhow::Error> {
|
) -> Result<UserEmail<PostgresqlBackend>, anyhow::Error> {
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
UserEmailLookup,
|
UserEmailLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
ue.id AS "user_email_id",
|
ue.user_email_id,
|
||||||
ue.email AS "user_email",
|
ue.email AS "user_email",
|
||||||
ue.created_at AS "user_email_created_at",
|
ue.created_at AS "user_email_created_at",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at"
|
ue.confirmed_at AS "user_email_confirmed_at"
|
||||||
FROM user_emails ue
|
FROM user_emails ue
|
||||||
|
|
||||||
WHERE ue.user_id = $1
|
WHERE ue.user_id = $1
|
||||||
AND ue.id = $2
|
AND ue.user_email_id = $2
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(user.data),
|
||||||
id,
|
Uuid::from(id),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.instrument(info_span!("Fetch user emails"))
|
.instrument(info_span!("Fetch user emails"))
|
||||||
@ -568,32 +590,35 @@ pub async fn get_user_email(
|
|||||||
Ok(res.into())
|
Ok(res.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(executor, user), fields(user.id = user.data, %user.username))]
|
#[tracing::instrument(skip(executor, user), fields(user.id = %user.data, %user.username))]
|
||||||
pub async fn add_user_email(
|
pub async fn add_user_email(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
user: &User<PostgresqlBackend>,
|
user: &User<PostgresqlBackend>,
|
||||||
email: &str,
|
email: String,
|
||||||
) -> anyhow::Result<UserEmail<PostgresqlBackend>> {
|
) -> anyhow::Result<UserEmail<PostgresqlBackend>> {
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
UserEmailLookup,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO user_emails (user_id, email)
|
INSERT INTO user_emails (user_email_id, user_id, email, created_at)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING
|
|
||||||
id AS user_email_id,
|
|
||||||
email AS user_email,
|
|
||||||
created_at AS user_email_created_at,
|
|
||||||
confirmed_at AS user_email_confirmed_at
|
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(id),
|
||||||
email,
|
Uuid::from(user.data),
|
||||||
|
&email,
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("Add user email"))
|
.instrument(info_span!("Add user email"))
|
||||||
.await
|
.await
|
||||||
.context("could not insert user email")?;
|
.context("could not insert user email")?;
|
||||||
|
|
||||||
Ok(res.into())
|
Ok(UserEmail {
|
||||||
|
data: id,
|
||||||
|
email,
|
||||||
|
created_at,
|
||||||
|
confirmed_at: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(executor))]
|
#[tracing::instrument(skip(executor))]
|
||||||
@ -604,12 +629,12 @@ pub async fn set_user_email_as_primary(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET primary_email_id = user_emails.id
|
SET primary_user_email_id = user_emails.user_email_id
|
||||||
FROM user_emails
|
FROM user_emails
|
||||||
WHERE user_emails.id = $1
|
WHERE user_emails.user_email_id = $1
|
||||||
AND users.id = user_emails.user_id
|
AND users.user_id = user_emails.user_id
|
||||||
"#,
|
"#,
|
||||||
email.data,
|
Uuid::from(email.data),
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("Add user email"))
|
.instrument(info_span!("Add user email"))
|
||||||
@ -627,9 +652,9 @@ pub async fn remove_user_email(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM user_emails
|
DELETE FROM user_emails
|
||||||
WHERE user_emails.id = $1
|
WHERE user_emails.user_email_id = $1
|
||||||
"#,
|
"#,
|
||||||
email.data,
|
Uuid::from(email.data),
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("Remove user email"))
|
.instrument(info_span!("Remove user email"))
|
||||||
@ -649,7 +674,7 @@ pub async fn lookup_user_email(
|
|||||||
UserEmailLookup,
|
UserEmailLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
ue.id AS "user_email_id",
|
ue.user_email_id,
|
||||||
ue.email AS "user_email",
|
ue.email AS "user_email",
|
||||||
ue.created_at AS "user_email_created_at",
|
ue.created_at AS "user_email_created_at",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at"
|
ue.confirmed_at AS "user_email_confirmed_at"
|
||||||
@ -658,7 +683,7 @@ pub async fn lookup_user_email(
|
|||||||
WHERE ue.user_id = $1
|
WHERE ue.user_id = $1
|
||||||
AND ue.email = $2
|
AND ue.email = $2
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(user.data),
|
||||||
email,
|
email,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
@ -673,23 +698,23 @@ pub async fn lookup_user_email(
|
|||||||
pub async fn lookup_user_email_by_id(
|
pub async fn lookup_user_email_by_id(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
user: &User<PostgresqlBackend>,
|
user: &User<PostgresqlBackend>,
|
||||||
id: i64,
|
id: Ulid,
|
||||||
) -> anyhow::Result<UserEmail<PostgresqlBackend>> {
|
) -> anyhow::Result<UserEmail<PostgresqlBackend>> {
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
UserEmailLookup,
|
UserEmailLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
ue.id AS "user_email_id",
|
ue.user_email_id,
|
||||||
ue.email AS "user_email",
|
ue.email AS "user_email",
|
||||||
ue.created_at AS "user_email_created_at",
|
ue.created_at AS "user_email_created_at",
|
||||||
ue.confirmed_at AS "user_email_confirmed_at"
|
ue.confirmed_at AS "user_email_confirmed_at"
|
||||||
FROM user_emails ue
|
FROM user_emails ue
|
||||||
|
|
||||||
WHERE ue.user_id = $1
|
WHERE ue.user_id = $1
|
||||||
AND ue.id = $2
|
AND ue.user_email_id = $2
|
||||||
"#,
|
"#,
|
||||||
user.data,
|
Uuid::from(user.data),
|
||||||
id,
|
Uuid::from(id),
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.instrument(info_span!("Lookup user email"))
|
.instrument(info_span!("Lookup user email"))
|
||||||
@ -704,31 +729,32 @@ pub async fn mark_user_email_as_verified(
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
mut email: UserEmail<PostgresqlBackend>,
|
mut email: UserEmail<PostgresqlBackend>,
|
||||||
) -> anyhow::Result<UserEmail<PostgresqlBackend>> {
|
) -> anyhow::Result<UserEmail<PostgresqlBackend>> {
|
||||||
let confirmed_at = sqlx::query_scalar!(
|
let confirmed_at = Utc::now();
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE user_emails
|
UPDATE user_emails
|
||||||
SET confirmed_at = NOW()
|
SET confirmed_at = $2
|
||||||
WHERE id = $1
|
WHERE user_email_id = $1
|
||||||
RETURNING confirmed_at
|
|
||||||
"#,
|
"#,
|
||||||
email.data,
|
Uuid::from(email.data),
|
||||||
|
confirmed_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("Confirm user email"))
|
.instrument(info_span!("Confirm user email"))
|
||||||
.await
|
.await
|
||||||
.context("could not update user email")?;
|
.context("could not update user email")?;
|
||||||
|
|
||||||
email.confirmed_at = confirmed_at;
|
email.confirmed_at = Some(confirmed_at);
|
||||||
|
|
||||||
Ok(email)
|
Ok(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UserEmailVerificationLookup {
|
struct UserEmailConfirmationCodeLookup {
|
||||||
verification_id: i64,
|
user_email_confirmation_code_id: Uuid,
|
||||||
verification_code: String,
|
code: String,
|
||||||
verification_expired: bool,
|
created_at: DateTime<Utc>,
|
||||||
verification_created_at: DateTime<Utc>,
|
expires_at: DateTime<Utc>,
|
||||||
verification_consumed_at: Option<DateTime<Utc>>,
|
consumed_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(executor))]
|
#[tracing::instrument(skip(executor))]
|
||||||
@ -736,49 +762,46 @@ pub async fn lookup_user_email_verification_code(
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
email: UserEmail<PostgresqlBackend>,
|
email: UserEmail<PostgresqlBackend>,
|
||||||
code: &str,
|
code: &str,
|
||||||
max_age: chrono::Duration,
|
|
||||||
) -> anyhow::Result<UserEmailVerification<PostgresqlBackend>> {
|
) -> anyhow::Result<UserEmailVerification<PostgresqlBackend>> {
|
||||||
// For some reason, we need to convert the type first
|
let now = Utc::now();
|
||||||
let max_age = PgInterval::try_from(max_age)
|
|
||||||
// For some reason, this error type does not let me to just bubble up the error here
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to encode duration: {}", e))?;
|
|
||||||
|
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
UserEmailVerificationLookup,
|
UserEmailConfirmationCodeLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
ev.id AS "verification_id",
|
ec.user_email_confirmation_code_id,
|
||||||
ev.code AS "verification_code",
|
ec.code,
|
||||||
(ev.created_at + $3 < NOW()) AS "verification_expired!",
|
ec.created_at,
|
||||||
ev.created_at AS "verification_created_at",
|
ec.expires_at,
|
||||||
ev.consumed_at AS "verification_consumed_at"
|
ec.consumed_at
|
||||||
FROM user_email_verifications ev
|
FROM user_email_confirmation_codes ec
|
||||||
WHERE ev.code = $1
|
WHERE ec.code = $1
|
||||||
AND ev.user_email_id = $2
|
AND ec.user_email_id = $2
|
||||||
"#,
|
"#,
|
||||||
code,
|
code,
|
||||||
email.data,
|
Uuid::from(email.data),
|
||||||
max_age,
|
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.instrument(info_span!("Lookup user email verification"))
|
.instrument(info_span!("Lookup user email verification"))
|
||||||
.await
|
.await
|
||||||
.context("could not lookup user email verification")?;
|
.context("could not lookup user email verification")?;
|
||||||
|
|
||||||
let state = if res.verification_expired {
|
let state = if let Some(when) = res.consumed_at {
|
||||||
UserEmailVerificationState::Expired
|
|
||||||
} else if let Some(when) = res.verification_consumed_at {
|
|
||||||
UserEmailVerificationState::AlreadyUsed { when }
|
UserEmailVerificationState::AlreadyUsed { when }
|
||||||
|
} else if res.expires_at < now {
|
||||||
|
UserEmailVerificationState::Expired {
|
||||||
|
when: res.expires_at,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
UserEmailVerificationState::Valid
|
UserEmailVerificationState::Valid
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(UserEmailVerification {
|
Ok(UserEmailVerification {
|
||||||
data: res.verification_id,
|
data: res.user_email_confirmation_code_id.into(),
|
||||||
code: res.verification_code,
|
code: res.code,
|
||||||
email,
|
email,
|
||||||
state,
|
state,
|
||||||
created_at: res.verification_created_at,
|
created_at: res.created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,16 +814,18 @@ pub async fn consume_email_verification(
|
|||||||
bail!("user email verification in wrong state");
|
bail!("user email verification in wrong state");
|
||||||
}
|
}
|
||||||
|
|
||||||
let consumed_at = sqlx::query_scalar!(
|
let consumed_at = Utc::now();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE user_email_verifications
|
UPDATE user_email_confirmation_codes
|
||||||
SET consumed_at = NOW()
|
SET consumed_at = $2
|
||||||
WHERE id = $1
|
WHERE user_email_confirmation_code_id = $1
|
||||||
RETURNING consumed_at AS "consumed_at!"
|
|
||||||
"#,
|
"#,
|
||||||
verification.data,
|
Uuid::from(verification.data),
|
||||||
|
consumed_at
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("Consume user email verification"))
|
.instrument(info_span!("Consume user email verification"))
|
||||||
.await
|
.await
|
||||||
.context("could not update user email verification")?;
|
.context("could not update user email verification")?;
|
||||||
@ -810,32 +835,39 @@ pub async fn consume_email_verification(
|
|||||||
Ok(verification)
|
Ok(verification)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(executor, email), fields(email.id = email.data, %email.email))]
|
#[tracing::instrument(skip(executor, email), fields(email.id = %email.data, %email.email))]
|
||||||
pub async fn add_user_email_verification_code(
|
pub async fn add_user_email_verification_code(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
email: UserEmail<PostgresqlBackend>,
|
email: UserEmail<PostgresqlBackend>,
|
||||||
|
max_age: chrono::Duration,
|
||||||
code: String,
|
code: String,
|
||||||
) -> anyhow::Result<UserEmailVerification<PostgresqlBackend>> {
|
) -> anyhow::Result<UserEmailVerification<PostgresqlBackend>> {
|
||||||
let res = sqlx::query_as!(
|
let created_at = Utc::now();
|
||||||
IdAndCreationTime,
|
let id = Ulid::from_datetime(created_at.into());
|
||||||
|
let expires_at = created_at + max_age;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO user_email_verifications (user_email_id, code)
|
INSERT INTO user_email_confirmation_codes
|
||||||
VALUES ($1, $2)
|
(user_email_confirmation_code_id, user_email_id, code, created_at, expires_at)
|
||||||
RETURNING id, created_at
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
"#,
|
"#,
|
||||||
email.data,
|
Uuid::from(id),
|
||||||
|
Uuid::from(email.data),
|
||||||
code,
|
code,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.execute(executor)
|
||||||
.instrument(info_span!("Add user email verification code"))
|
.instrument(info_span!("Add user email verification code"))
|
||||||
.await
|
.await
|
||||||
.context("could not insert user email verification code")?;
|
.context("could not insert user email verification code")?;
|
||||||
|
|
||||||
let verification = UserEmailVerification {
|
let verification = UserEmailVerification {
|
||||||
data: res.id,
|
data: id,
|
||||||
email,
|
email,
|
||||||
code,
|
code,
|
||||||
created_at: res.created_at,
|
created_at,
|
||||||
state: UserEmailVerificationState::Valid,
|
state: UserEmailVerificationState::Valid,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ serde_urlencoded = "0.7.1"
|
|||||||
|
|
||||||
chrono = "0.4.22"
|
chrono = "0.4.22"
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
|
ulid = { version = "1.0.0", features = ["serde"] }
|
||||||
|
|
||||||
oauth2-types = { path = "../oauth2-types" }
|
oauth2-types = { path = "../oauth2-types" }
|
||||||
mas-data-model = { path = "../data-model" }
|
mas-data-model = { path = "../data-model" }
|
||||||
|
@ -23,6 +23,7 @@ use mas_data_model::{
|
|||||||
};
|
};
|
||||||
use mas_router::PostAuthAction;
|
use mas_router::PostAuthAction;
|
||||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||||
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{FormField, FormState};
|
use crate::{FormField, FormState};
|
||||||
@ -517,11 +518,11 @@ impl TemplateContext for CompatSsoContext {
|
|||||||
login: CompatSsoLogin {
|
login: CompatSsoLogin {
|
||||||
data: (),
|
data: (),
|
||||||
redirect_uri: Url::parse("https://app.element.io/").unwrap(),
|
redirect_uri: Url::parse("https://app.element.io/").unwrap(),
|
||||||
token: "abcdefghijklmnopqrstuvwxyz012345".into(),
|
login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
state: CompatSsoLoginState::Pending,
|
state: CompatSsoLoginState::Pending,
|
||||||
},
|
},
|
||||||
action: PostAuthAction::ContinueCompatSsoLogin { data: 1 },
|
action: PostAuthAction::ContinueCompatSsoLogin { data: Ulid::new() },
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user