You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Working legacy login endpoint
This commit is contained in:
@ -192,6 +192,8 @@ impl Options {
|
||||
|
||||
let static_files = mas_static_files::service(&config.http.web_root);
|
||||
|
||||
let matrix_config = config.matrix.clone();
|
||||
|
||||
// Explicitely the config to properly zeroize secret keys
|
||||
drop(config);
|
||||
|
||||
@ -214,6 +216,7 @@ impl Options {
|
||||
&encrypter,
|
||||
&mailer,
|
||||
&url_builder,
|
||||
&matrix_config,
|
||||
)
|
||||
.fallback(static_files)
|
||||
.layer(ServerLayer::default());
|
||||
|
82
crates/config/src/sections/matrix.rs
Normal file
82
crates/config/src/sections/matrix.rs
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
use super::ConfigurationSection;
|
||||
|
||||
fn default_homeserver() -> String {
|
||||
"localhost:8008".to_string()
|
||||
}
|
||||
|
||||
/// Configuration related to the Matrix homeserver
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MatrixConfig {
|
||||
/// Time-to-live of a CSRF token in seconds
|
||||
#[serde(default = "default_homeserver")]
|
||||
pub homeserver: String,
|
||||
}
|
||||
|
||||
impl Default for MatrixConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
homeserver: default_homeserver(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ConfigurationSection<'_> for MatrixConfig {
|
||||
fn path() -> &'static str {
|
||||
"matrix"
|
||||
}
|
||||
|
||||
async fn generate() -> anyhow::Result<Self> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
fn test() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use figment::Jail;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn load_config() {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
r#"
|
||||
matrix:
|
||||
homeserver: matrix.org
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = MatrixConfig::load_from_file("config.yaml")?;
|
||||
|
||||
assert_eq!(config.homeserver, "matrix.org".to_string());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ mod csrf;
|
||||
mod database;
|
||||
mod email;
|
||||
mod http;
|
||||
mod matrix;
|
||||
mod secrets;
|
||||
mod telemetry;
|
||||
mod templates;
|
||||
@ -31,6 +32,7 @@ pub use self::{
|
||||
database::DatabaseConfig,
|
||||
email::{EmailConfig, EmailSmtpMode, EmailTransportConfig},
|
||||
http::HttpConfig,
|
||||
matrix::MatrixConfig,
|
||||
secrets::{Encrypter, SecretsConfig},
|
||||
telemetry::{
|
||||
MetricsConfig, MetricsExporterConfig, Propagator, TelemetryConfig, TracingConfig,
|
||||
@ -73,6 +75,10 @@ pub struct RootConfig {
|
||||
|
||||
/// Application secrets
|
||||
pub secrets: SecretsConfig,
|
||||
|
||||
/// Configuration related to the homeserver
|
||||
#[serde(default)]
|
||||
pub matrix: MatrixConfig,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -91,6 +97,7 @@ impl ConfigurationSection<'_> for RootConfig {
|
||||
csrf: CsrfConfig::generate().await?,
|
||||
email: EmailConfig::generate().await?,
|
||||
secrets: SecretsConfig::generate().await?,
|
||||
matrix: MatrixConfig::generate().await?,
|
||||
})
|
||||
}
|
||||
|
||||
@ -104,6 +111,7 @@ impl ConfigurationSection<'_> for RootConfig {
|
||||
csrf: CsrfConfig::test(),
|
||||
email: EmailConfig::test(),
|
||||
secrets: SecretsConfig::test(),
|
||||
matrix: MatrixConfig::test(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -32,7 +32,7 @@ pub use self::{
|
||||
AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client,
|
||||
InvalidRedirectUriError, JwksOrJwksUri, Pkce, Session,
|
||||
},
|
||||
tokens::{AccessToken, RefreshToken, TokenFormatError, TokenType},
|
||||
tokens::{AccessToken, CompatAccessToken, RefreshToken, TokenFormatError, TokenType},
|
||||
traits::{StorageBackend, StorageBackendMarker},
|
||||
users::{
|
||||
Authentication, BrowserSession, User, UserEmail, UserEmailVerification,
|
||||
|
@ -66,13 +66,26 @@ impl<S: StorageBackendMarker> From<RefreshToken<S>> for RefreshToken<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CompatAccessToken<T: StorageBackend> {
|
||||
pub data: T::CompatAccessTokenData,
|
||||
pub token: String,
|
||||
pub device_id: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Type of token to generate or validate
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TokenType {
|
||||
/// An access token, used by Relying Parties to authenticate requests
|
||||
AccessToken,
|
||||
|
||||
/// A refresh token, used by the refresh token grant
|
||||
RefreshToken,
|
||||
|
||||
/// A legacy access token
|
||||
CompatAccessToken,
|
||||
}
|
||||
|
||||
impl TokenType {
|
||||
@ -80,6 +93,7 @@ impl TokenType {
|
||||
match self {
|
||||
TokenType::AccessToken => "mat",
|
||||
TokenType::RefreshToken => "mar",
|
||||
TokenType::CompatAccessToken => "mct",
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +101,7 @@ impl TokenType {
|
||||
match prefix {
|
||||
"mat" => Some(TokenType::AccessToken),
|
||||
"mar" => Some(TokenType::RefreshToken),
|
||||
"mct" => Some(TokenType::CompatAccessToken),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -163,8 +178,10 @@ impl PartialEq<OAuthTokenTypeHint> for TokenType {
|
||||
fn eq(&self, other: &OAuthTokenTypeHint) -> bool {
|
||||
matches!(
|
||||
(self, other),
|
||||
(TokenType::AccessToken, OAuthTokenTypeHint::AccessToken)
|
||||
| (TokenType::RefreshToken, OAuthTokenTypeHint::RefreshToken)
|
||||
(
|
||||
TokenType::AccessToken | TokenType::CompatAccessToken,
|
||||
OAuthTokenTypeHint::AccessToken
|
||||
) | (TokenType::RefreshToken, OAuthTokenTypeHint::RefreshToken)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -217,7 +234,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_prefix_match() {
|
||||
use TokenType::{AccessToken, RefreshToken};
|
||||
use TokenType::{AccessToken, CompatAccessToken, RefreshToken};
|
||||
assert_eq!(TokenType::match_prefix("mct"), Some(CompatAccessToken));
|
||||
assert_eq!(TokenType::match_prefix("mat"), Some(AccessToken));
|
||||
assert_eq!(TokenType::match_prefix("mar"), Some(RefreshToken));
|
||||
assert_eq!(TokenType::match_prefix("matt"), None);
|
||||
|
@ -34,6 +34,7 @@ pub trait StorageBackend {
|
||||
type AuthorizationGrantData: Clone + Debug + PartialEq + Serialize + DeserializeOwned + Default;
|
||||
type AccessTokenData: Clone + Debug + PartialEq + Serialize + DeserializeOwned + Default;
|
||||
type RefreshTokenData: Clone + Debug + PartialEq + Serialize + DeserializeOwned + Default;
|
||||
type CompatAccessTokenData: Clone + Debug + PartialEq + Serialize + DeserializeOwned + Default;
|
||||
}
|
||||
|
||||
impl StorageBackend for () {
|
||||
@ -42,6 +43,7 @@ impl StorageBackend for () {
|
||||
type AuthorizationGrantData = ();
|
||||
type BrowserSessionData = ();
|
||||
type ClientData = ();
|
||||
type CompatAccessTokenData = ();
|
||||
type RefreshTokenData = ();
|
||||
type SessionData = ();
|
||||
type UserData = ();
|
||||
|
@ -12,9 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use axum::{response::IntoResponse, Json};
|
||||
use axum::{response::IntoResponse, Extension, Json};
|
||||
use hyper::StatusCode;
|
||||
use mas_config::MatrixConfig;
|
||||
use mas_data_model::TokenType;
|
||||
use mas_storage::compat::compat_login;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct MatrixError {
|
||||
@ -73,11 +78,84 @@ pub enum LoginIdentifier {
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
pub(crate) async fn post(Json(input): Json<IncomingLogin>) -> impl IntoResponse {
|
||||
tracing::info!(?input, "Got Matrix login");
|
||||
MatrixError {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Not implemented",
|
||||
status: StatusCode::NOT_IMPLEMENTED,
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SuccessfulLogin {
|
||||
access_token: String,
|
||||
device_id: String,
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
pub enum RouteError {
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
Unsupported,
|
||||
LoginFailed,
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for RouteError {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
Self::Internal(_e) => MatrixError {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Internal server error",
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
Self::Unsupported => MatrixError {
|
||||
errcode: "M_UNRECOGNIZED",
|
||||
error: "Invalid login type",
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
},
|
||||
Self::LoginFailed => MatrixError {
|
||||
errcode: "M_UNAUTHORIZED",
|
||||
error: "Invalid username/password",
|
||||
status: StatusCode::FORBIDDEN,
|
||||
},
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn post(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(config): Extension<MatrixConfig>,
|
||||
Json(input): Json<IncomingLogin>,
|
||||
) -> Result<impl IntoResponse, RouteError> {
|
||||
let mut conn = pool.acquire().await?;
|
||||
let (username, password) = match input {
|
||||
IncomingLogin::Password {
|
||||
identifier: LoginIdentifier::User { user },
|
||||
password,
|
||||
} => (user, password),
|
||||
_ => {
|
||||
return Err(RouteError::Unsupported);
|
||||
}
|
||||
};
|
||||
|
||||
let (token, device_id) = {
|
||||
let mut rng = thread_rng();
|
||||
let token = TokenType::CompatAccessToken.generate(&mut rng);
|
||||
let device_id: String = rng
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(10)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
(token, device_id)
|
||||
};
|
||||
|
||||
let (token, user) = compat_login(&mut conn, &username, &password, device_id, token)
|
||||
.await
|
||||
.map_err(|_| RouteError::LoginFailed)?;
|
||||
|
||||
let user_id = format!("@{}:{}", user.username, config.homeserver);
|
||||
|
||||
Ok(Json(SuccessfulLogin {
|
||||
access_token: token.token,
|
||||
device_id: token.device_id,
|
||||
user_id,
|
||||
}))
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use hyper::header::{ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_TYPE};
|
||||
use mas_config::Encrypter;
|
||||
use mas_config::{Encrypter, MatrixConfig};
|
||||
use mas_email::Mailer;
|
||||
use mas_http::CorsLayerExt;
|
||||
use mas_jose::StaticKeystore;
|
||||
@ -53,6 +53,7 @@ pub fn router<B>(
|
||||
encrypter: &Encrypter,
|
||||
mailer: &Mailer,
|
||||
url_builder: &UrlBuilder,
|
||||
matrix_config: &MatrixConfig,
|
||||
) -> Router<B>
|
||||
where
|
||||
B: HttpBody + Send + 'static,
|
||||
@ -186,4 +187,5 @@ where
|
||||
.layer(Extension(encrypter.clone()))
|
||||
.layer(Extension(url_builder.clone()))
|
||||
.layer(Extension(mailer.clone()))
|
||||
.layer(Extension(matrix_config.clone()))
|
||||
}
|
||||
|
@ -18,21 +18,40 @@ use mas_axum_utils::client_authorization::{ClientAuthorization, CredentialsVerif
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::{TokenFormatError, TokenType};
|
||||
use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint};
|
||||
use mas_storage::oauth2::{
|
||||
access_token::{lookup_active_access_token, AccessTokenLookupError},
|
||||
client::ClientFetchError,
|
||||
refresh_token::{lookup_active_refresh_token, RefreshTokenLookupError},
|
||||
use mas_storage::{
|
||||
compat::{lookup_active_compat_access_token, CompatAccessTokenLookupError},
|
||||
oauth2::{
|
||||
access_token::{lookup_active_access_token, AccessTokenLookupError},
|
||||
client::ClientFetchError,
|
||||
refresh_token::{lookup_active_refresh_token, RefreshTokenLookupError},
|
||||
},
|
||||
};
|
||||
use oauth2_types::{
|
||||
requests::{IntrospectionRequest, IntrospectionResponse},
|
||||
scope::ScopeToken,
|
||||
};
|
||||
use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse};
|
||||
use sqlx::PgPool;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("could not find client")]
|
||||
ClientNotFound,
|
||||
|
||||
#[error("client is not allowed to introspect")]
|
||||
NotAllowed,
|
||||
|
||||
#[error("unknown token")]
|
||||
UnknownToken,
|
||||
|
||||
#[error("bad request")]
|
||||
BadRequest,
|
||||
ClientCredentialsVerification(CredentialsVerificationError),
|
||||
|
||||
#[error(transparent)]
|
||||
ClientCredentialsVerification(#[from] CredentialsVerificationError),
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
@ -88,8 +107,8 @@ impl From<AccessTokenLookupError> for RouteError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefreshTokenLookupError> for RouteError {
|
||||
fn from(e: RefreshTokenLookupError) -> Self {
|
||||
impl From<CompatAccessTokenLookupError> for RouteError {
|
||||
fn from(e: CompatAccessTokenLookupError) -> Self {
|
||||
if e.not_found() {
|
||||
Self::UnknownToken
|
||||
} else {
|
||||
@ -98,9 +117,13 @@ impl From<RefreshTokenLookupError> for RouteError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CredentialsVerificationError> for RouteError {
|
||||
fn from(e: CredentialsVerificationError) -> Self {
|
||||
Self::ClientCredentialsVerification(e)
|
||||
impl From<RefreshTokenLookupError> for RouteError {
|
||||
fn from(e: RefreshTokenLookupError) -> Self {
|
||||
if e.not_found() {
|
||||
Self::UnknownToken
|
||||
} else {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +142,7 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse {
|
||||
jti: None,
|
||||
};
|
||||
|
||||
#[tracing::instrument(skip_all, err)]
|
||||
pub(crate) async fn post(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(encrypter): Extension<Encrypter>,
|
||||
@ -192,6 +216,29 @@ pub(crate) async fn post(
|
||||
jti: None,
|
||||
}
|
||||
}
|
||||
TokenType::CompatAccessToken => {
|
||||
let (token, user) = lookup_active_compat_access_token(&mut conn, token).await?;
|
||||
|
||||
let device_scope: ScopeToken = format!("urn:matrix:device:{}", token.device_id)
|
||||
.parse()
|
||||
.unwrap();
|
||||
let scope = [device_scope].into_iter().collect();
|
||||
|
||||
IntrospectionResponse {
|
||||
active: true,
|
||||
scope: Some(scope),
|
||||
client_id: Some("legacy".into()),
|
||||
username: Some(user.username),
|
||||
token_type: Some(OAuthTokenTypeHint::AccessToken),
|
||||
exp: None,
|
||||
iat: Some(token.created_at),
|
||||
nbf: Some(token.created_at),
|
||||
sub: Some(user.sub),
|
||||
aud: None,
|
||||
iss: None,
|
||||
jti: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(reply))
|
||||
|
@ -0,0 +1,15 @@
|
||||
-- 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_access_tokens;
|
23
crates/storage/migrations/20220512150806_compat_login.up.sql
Normal file
23
crates/storage/migrations/20220512150806_compat_login.up.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- 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_access_tokens (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"token" TEXT UNIQUE NOT NULL,
|
||||
"device_id" TEXT UNIQUE NOT NULL,
|
||||
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"deleted_at" TIMESTAMP WITH TIME ZONE
|
||||
)
|
@ -1331,6 +1331,34 @@
|
||||
},
|
||||
"query": "UPDATE user_sessions SET active = FALSE WHERE id = $1"
|
||||
},
|
||||
"a741cb29c617fb2df97f9e4bc84dbf85ff3c02ac87066770291d7bc153451695": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 1,
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO compat_access_tokens (user_id, token, device_id)\n VALUES ($1, $2, $3)\n RETURNING id, created_at\n "
|
||||
},
|
||||
"a80c14ba82cfc29493048d9e9578ec5ca482c9228efc7c7212dae4fed86b8367": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1659,6 +1687,86 @@
|
||||
},
|
||||
"query": "\n INSERT INTO users (username)\n VALUES ($1)\n RETURNING id\n "
|
||||
},
|
||||
"de1ed5db37e48382a38075247c001cf4d01b7eeff165aa3aa62bb21a3284d5b7": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "compat_access_token_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "compat_access_token",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "compat_access_token_created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "compat_access_token_deleted_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "compat_access_token_device_id",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_id!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_username!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_email_id?",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_email?",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_email_created_at?",
|
||||
"ordinal": 9,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_email_confirmed_at?",
|
||||
"ordinal": 10,
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT\n ct.id AS \"compat_access_token_id\",\n ct.token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.deleted_at AS \"compat_access_token_deleted_at\",\n ct.device_id AS \"compat_access_token_device_id\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM compat_access_tokens ct\n INNER JOIN users u\n ON u.id = ct.user_id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE ct.token = $1\n AND ct.deleted_at IS NULL\n "
|
||||
},
|
||||
"df38de13e2f345175f9ef46b4ae2a4f6637dbf74bb28559da8f4d8969f411d14": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
202
crates/storage/src/compat.rs
Normal file
202
crates/storage/src/compat.rs
Normal file
@ -0,0 +1,202 @@
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context;
|
||||
use argon2::{Argon2, PasswordHash};
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{CompatAccessToken, User, UserEmail};
|
||||
use sqlx::{Acquire, PgExecutor, Postgres};
|
||||
use thiserror::Error;
|
||||
use tokio::task;
|
||||
use tracing::{info_span, Instrument};
|
||||
|
||||
use crate::{
|
||||
user::lookup_user_by_username, DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend,
|
||||
};
|
||||
|
||||
pub struct CompatAccessTokenLookup {
|
||||
compat_access_token_id: i64,
|
||||
compat_access_token: String,
|
||||
compat_access_token_created_at: DateTime<Utc>,
|
||||
compat_access_token_deleted_at: Option<DateTime<Utc>>,
|
||||
compat_access_token_device_id: String,
|
||||
user_id: i64,
|
||||
user_username: String,
|
||||
user_email_id: Option<i64>,
|
||||
user_email: Option<String>,
|
||||
user_email_created_at: Option<DateTime<Utc>>,
|
||||
user_email_confirmed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("failed to lookup compat access token")]
|
||||
pub enum CompatAccessTokenLookupError {
|
||||
Database(#[from] sqlx::Error),
|
||||
Inconsistency(#[from] DatabaseInconsistencyError),
|
||||
}
|
||||
|
||||
impl CompatAccessTokenLookupError {
|
||||
#[must_use]
|
||||
pub fn not_found(&self) -> bool {
|
||||
matches!(self, Self::Database(sqlx::Error::RowNotFound))
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(executor))]
|
||||
pub async fn lookup_active_compat_access_token(
|
||||
executor: impl PgExecutor<'_>,
|
||||
token: &str,
|
||||
) -> Result<
|
||||
(
|
||||
CompatAccessToken<PostgresqlBackend>,
|
||||
User<PostgresqlBackend>,
|
||||
),
|
||||
CompatAccessTokenLookupError,
|
||||
> {
|
||||
let res = sqlx::query_as!(
|
||||
CompatAccessTokenLookup,
|
||||
r#"
|
||||
SELECT
|
||||
ct.id AS "compat_access_token_id",
|
||||
ct.token AS "compat_access_token",
|
||||
ct.created_at AS "compat_access_token_created_at",
|
||||
ct.deleted_at AS "compat_access_token_deleted_at",
|
||||
ct.device_id AS "compat_access_token_device_id",
|
||||
u.id AS "user_id!",
|
||||
u.username AS "user_username!",
|
||||
ue.id AS "user_email_id?",
|
||||
ue.email AS "user_email?",
|
||||
ue.created_at AS "user_email_created_at?",
|
||||
ue.confirmed_at AS "user_email_confirmed_at?"
|
||||
|
||||
FROM compat_access_tokens ct
|
||||
INNER JOIN users u
|
||||
ON u.id = ct.user_id
|
||||
LEFT JOIN user_emails ue
|
||||
ON ue.id = u.primary_email_id
|
||||
|
||||
WHERE ct.token = $1
|
||||
AND ct.deleted_at IS NULL
|
||||
"#,
|
||||
token,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.instrument(info_span!("Fetch compat access token"))
|
||||
.await?;
|
||||
|
||||
let token = CompatAccessToken {
|
||||
data: res.compat_access_token_id,
|
||||
token: res.compat_access_token,
|
||||
created_at: res.compat_access_token_created_at,
|
||||
deleted_at: res.compat_access_token_deleted_at,
|
||||
device_id: res.compat_access_token_device_id,
|
||||
};
|
||||
|
||||
let primary_email = match (
|
||||
res.user_email_id,
|
||||
res.user_email,
|
||||
res.user_email_created_at,
|
||||
res.user_email_confirmed_at,
|
||||
) {
|
||||
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
|
||||
data: id,
|
||||
email,
|
||||
created_at,
|
||||
confirmed_at,
|
||||
}),
|
||||
(None, None, None, None) => None,
|
||||
_ => return Err(DatabaseInconsistencyError.into()),
|
||||
};
|
||||
|
||||
let user = User {
|
||||
data: res.user_id,
|
||||
username: res.user_username,
|
||||
sub: format!("fake-sub-{}", res.user_id),
|
||||
primary_email,
|
||||
};
|
||||
|
||||
Ok((token, user))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(conn, password))]
|
||||
pub async fn compat_login(
|
||||
conn: impl Acquire<'_, Database = Postgres>,
|
||||
username: &str,
|
||||
password: &str,
|
||||
device_id: String,
|
||||
token: String,
|
||||
) -> Result<
|
||||
(
|
||||
CompatAccessToken<PostgresqlBackend>,
|
||||
User<PostgresqlBackend>,
|
||||
),
|
||||
anyhow::Error,
|
||||
> {
|
||||
let mut txn = conn.begin().await.context("could not start transaction")?;
|
||||
|
||||
// First, lookup the user
|
||||
let user = lookup_user_by_username(&mut txn, username).await?;
|
||||
|
||||
// Now, fetch the hashed password from the user associated with that session
|
||||
let hashed_password: String = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT up.hashed_password
|
||||
FROM user_passwords up
|
||||
WHERE up.user_id = $1
|
||||
ORDER BY up.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
user.data,
|
||||
)
|
||||
.fetch_one(&mut txn)
|
||||
.instrument(tracing::info_span!("Lookup hashed password"))
|
||||
.await?;
|
||||
|
||||
// TODO: pass verifiers list as parameter
|
||||
// Verify the password in a blocking thread to avoid blocking the async executor
|
||||
let password = password.to_string();
|
||||
task::spawn_blocking(move || {
|
||||
let context = Argon2::default();
|
||||
let hasher = PasswordHash::new(&hashed_password)?;
|
||||
hasher.verify_password(&[&context], &password)
|
||||
})
|
||||
.instrument(tracing::info_span!("Verify hashed password"))
|
||||
.await??;
|
||||
|
||||
let res = sqlx::query_as!(
|
||||
IdAndCreationTime,
|
||||
r#"
|
||||
INSERT INTO compat_access_tokens (user_id, token, device_id)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, created_at
|
||||
"#,
|
||||
user.data,
|
||||
token,
|
||||
device_id,
|
||||
)
|
||||
.fetch_one(&mut txn)
|
||||
.await
|
||||
.context("could not insert compat access token")?;
|
||||
|
||||
let token = CompatAccessToken {
|
||||
data: res.id,
|
||||
token,
|
||||
created_at: res.created_at,
|
||||
deleted_at: None,
|
||||
device_id,
|
||||
};
|
||||
|
||||
txn.commit().await.context("could not commit transaction")?;
|
||||
Ok((token, user))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -42,6 +42,7 @@ impl StorageBackend for PostgresqlBackend {
|
||||
type AuthorizationGrantData = i64;
|
||||
type BrowserSessionData = i64;
|
||||
type ClientData = i64;
|
||||
type CompatAccessTokenData = i64;
|
||||
type RefreshTokenData = i64;
|
||||
type SessionData = i64;
|
||||
type UserData = i64;
|
||||
@ -56,6 +57,7 @@ struct IdAndCreationTime {
|
||||
created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub mod compat;
|
||||
pub mod oauth2;
|
||||
pub mod user;
|
||||
|
||||
|
Reference in New Issue
Block a user