1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-07 17:03:01 +03:00

WIP: Refactor higher-level data-model to its own crate

This commit is contained in:
Quentin Gliech
2021-10-12 19:03:01 +02:00
parent 29bf149921
commit b3587c677c
17 changed files with 456 additions and 326 deletions

11
Cargo.lock generated
View File

@@ -1390,6 +1390,7 @@ dependencies = [
"jwt-compact", "jwt-compact",
"k256", "k256",
"mas-config", "mas-config",
"mas-data-model",
"mime", "mime",
"oauth2-types", "oauth2-types",
"password-hash", "password-hash",
@@ -1413,6 +1414,16 @@ dependencies = [
"warp", "warp",
] ]
[[package]]
name = "mas-data-model"
version = "0.1.0"
dependencies = [
"chrono",
"oauth2-types",
"serde",
"thiserror",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.0.1" version = "0.0.1"

View File

@@ -66,6 +66,7 @@ cookie = "0.15.1"
oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] } oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] }
mas-config = { path = "../config" } mas-config = { path = "../config" }
mas-data-model = { path = "../data-model" }
[dependencies.jwt-compact] [dependencies.jwt-compact]
# Waiting on the next release because of the bump of the `rsa` dependency # Waiting on the next release because of the bump of the `rsa` dependency

View File

@@ -93,8 +93,8 @@
] ]
} }
}, },
"35bedaa6fdf7ac91d54b458b4637f2182c2f82be3e2f80cd2db934ee279a7f2a": { "307fd9f71e7a94a0a0d9ce523ee9792e127485d0d12480c43f179dd9b75afbab": {
"query": "\n SELECT id, username\n FROM users\n WHERE id = $1\n ", "query": "\n INSERT INTO user_sessions (user_id)\n VALUES ($1)\n RETURNING id, created_at\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -104,8 +104,8 @@
}, },
{ {
"ordinal": 1, "ordinal": 1,
"name": "username", "name": "created_at",
"type_info": "Text" "type_info": "Timestamptz"
} }
], ],
"parameters": { "parameters": {
@@ -169,56 +169,6 @@
] ]
} }
}, },
"4f925a277d73df779360f81e0cf5d7983b50ebe744f461559dd561b7e36c20d4": {
"query": "\n SELECT\n s.id,\n u.id as user_id,\n u.username,\n s.active,\n s.created_at,\n a.created_at as \"last_authd_at?\"\n FROM user_sessions s\n INNER JOIN users u \n ON s.user_id = u.id\n LEFT JOIN user_session_authentications a\n ON a.session_id = s.id\n WHERE s.id = $1 AND s.active\n ORDER BY a.created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "active",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "last_authd_at?",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"562b0d4dcf857e99c20e9288e9c8bd46232290715c0d2459b0398a1c746cf65d": { "562b0d4dcf857e99c20e9288e9c8bd46232290715c0d2459b0398a1c746cf65d": {
"query": "\n SELECT\n rt.id,\n rt.oauth2_session_id,\n rt.oauth2_access_token_id,\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\"\n FROM oauth2_refresh_tokens rt\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n WHERE rt.token = $1 AND rt.next_token_id IS NULL\n ", "query": "\n SELECT\n rt.id,\n rt.oauth2_session_id,\n rt.oauth2_access_token_id,\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\"\n FROM oauth2_refresh_tokens rt\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n WHERE rt.token = $1 AND rt.next_token_id IS NULL\n ",
"describe": { "describe": {
@@ -273,56 +223,6 @@
"nullable": [] "nullable": []
} }
}, },
"62986972431bfc4649e3d8c8c7648f9049c4197773e53496422ad8b8aa15b459": {
"query": "\n SELECT\n s.id,\n u.id as user_id,\n u.username,\n s.active,\n s.created_at,\n a.created_at as \"last_authd_at?\"\n FROM user_sessions s\n INNER JOIN users u \n ON s.user_id = u.id\n LEFT JOIN user_session_authentications a\n ON a.session_id = s.id\n WHERE s.id = $1\n ORDER BY a.created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "active",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "last_authd_at?",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"73f2d928f7bf88af79a3685bd6346652b4e4454b0ce75e38343840c9765e3f27": { "73f2d928f7bf88af79a3685bd6346652b4e4454b0ce75e38343840c9765e3f27": {
"query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_session_id, oauth2_access_token_id, token)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, oauth2_session_id, oauth2_access_token_id, token, next_token_id, \n created_at, updated_at\n ", "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_session_id, oauth2_access_token_id, token)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, oauth2_session_id, oauth2_access_token_id, token, next_token_id, \n created_at, updated_at\n ",
"describe": { "describe": {
@@ -381,6 +281,56 @@
] ]
} }
}, },
"7fa7d001583e98f5e4626751fa2c8743e7b83a240d1a51de50a7dba95c4e8a6b": {
"query": "\n SELECT\n s.id,\n u.id as user_id,\n u.username,\n s.created_at,\n a.id as \"last_authentication_id?\",\n a.created_at as \"last_authd_at?\"\n FROM user_sessions s\n INNER JOIN users u \n ON s.user_id = u.id\n LEFT JOIN user_session_authentications a\n ON a.session_id = s.id\n WHERE s.id = $1 AND s.active\n ORDER BY a.created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "last_authentication_id?",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "last_authd_at?",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"88ac8783bd5881c42eafd9cf87a16fe6031f3153fd6a8618e689694584aeb2de": { "88ac8783bd5881c42eafd9cf87a16fe6031f3153fd6a8618e689694584aeb2de": {
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n ", "query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n ",
"describe": { "describe": {
@@ -393,26 +343,6 @@
"nullable": [] "nullable": []
} }
}, },
"9ba45ab114b656105cc46b0c10fb05769860fcdc05eaf54d6225640fb914dab9": {
"query": "\n INSERT INTO user_session_authentications (session_id)\n VALUES ($1)\n RETURNING created_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
}
},
"a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": { "a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": {
"query": "UPDATE user_sessions SET active = FALSE WHERE id = $1", "query": "UPDATE user_sessions SET active = FALSE WHERE id = $1",
"describe": { "describe": {
@@ -611,6 +541,32 @@
] ]
} }
}, },
"d9d27eb4a0c11818a636d407438c4bc567a39396e7e236b3e776504417988eab": {
"query": "\n INSERT INTO user_session_authentications (session_id)\n VALUES ($1)\n RETURNING id, created_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false
]
}
},
"eaddc1e33715ad31b4195fda72dbe870f179dd8da53a88d0543b72a278ed1d3d": { "eaddc1e33715ad31b4195fda72dbe870f179dd8da53a88d0543b72a278ed1d3d": {
"query": "\n DELETE FROM oauth2_codes\n WHERE id = $1\n ", "query": "\n DELETE FROM oauth2_codes\n WHERE id = $1\n ",
"describe": { "describe": {

View File

@@ -14,6 +14,7 @@
//! Load user sessions from the database //! Load user sessions from the database
use mas_data_model::BrowserSession;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{pool::PoolConnection, Executor, PgPool, Postgres}; use sqlx::{pool::PoolConnection, Executor, PgPool, Postgres};
use thiserror::Error; use thiserror::Error;
@@ -30,7 +31,7 @@ use super::{
}; };
use crate::{ use crate::{
config::CookiesConfig, config::CookiesConfig,
storage::{lookup_active_session, user::ActiveSessionLookupError, SessionInfo}, storage::{lookup_active_session, user::ActiveSessionLookupError, PostgresqlBackend},
}; };
/// The session is missing or failed to load /// The session is missing or failed to load
@@ -58,19 +59,19 @@ pub struct SessionCookie {
} }
impl SessionCookie { impl SessionCookie {
/// Forge the cookie from a [`SessionInfo`] /// Forge the cookie from a [`BrowserSession`]
#[must_use] #[must_use]
pub fn from_session_info(info: &SessionInfo) -> Self { pub fn from_session(session: &BrowserSession<PostgresqlBackend>) -> Self {
Self { Self {
current: info.key(), current: session.data,
} }
} }
/// Load the [`SessionInfo`] from database /// Load the [`BrowserSession`] from database
pub async fn load_session_info( pub async fn load_session(
&self, &self,
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
) -> Result<SessionInfo, ActiveSessionLookupError> { ) -> Result<BrowserSession<PostgresqlBackend>, ActiveSessionLookupError> {
let res = lookup_active_session(executor, self.current).await?; let res = lookup_active_session(executor, self.current).await?;
Ok(res) Ok(res)
} }
@@ -87,8 +88,11 @@ impl EncryptableCookieValue for SessionCookie {
pub fn optional_session( pub fn optional_session(
pool: &PgPool, pool: &PgPool,
cookies_config: &CookiesConfig, cookies_config: &CookiesConfig,
) -> impl Filter<Extract = (Option<SessionInfo>,), Error = Rejection> + Clone + Send + Sync + 'static ) -> impl Filter<Extract = (Option<BrowserSession<PostgresqlBackend>>,), Error = Rejection>
{ + Clone
+ Send
+ Sync
+ 'static {
session(pool, cookies_config) session(pool, cookies_config)
.map(Some) .map(Some)
.recover(none_on_error::<_, SessionLoadError>) .recover(none_on_error::<_, SessionLoadError>)
@@ -106,7 +110,11 @@ pub fn optional_session(
pub fn session( pub fn session(
pool: &PgPool, pool: &PgPool,
cookies_config: &CookiesConfig, cookies_config: &CookiesConfig,
) -> impl Filter<Extract = (SessionInfo,), Error = Rejection> + Clone + Send + Sync + 'static { ) -> impl Filter<Extract = (BrowserSession<PostgresqlBackend>,), Error = Rejection>
+ Clone
+ Send
+ Sync
+ 'static {
encrypted(cookies_config) encrypted(cookies_config)
.and(connection(pool)) .and(connection(pool))
.and_then(load_session) .and_then(load_session)
@@ -117,8 +125,8 @@ pub fn session(
async fn load_session( async fn load_session(
session: SessionCookie, session: SessionCookie,
mut conn: PoolConnection<Postgres>, mut conn: PoolConnection<Postgres>,
) -> Result<SessionInfo, Rejection> { ) -> Result<BrowserSession<PostgresqlBackend>, Rejection> {
let session_info = session.load_session_info(&mut conn).await?; let session_info = session.load_session(&mut conn).await?;
Ok(session_info) Ok(session_info)
} }

View File

@@ -24,6 +24,7 @@ use hyper::{
StatusCode, StatusCode,
}; };
use itertools::Itertools; use itertools::Itertools;
use mas_data_model::BrowserSession;
use oauth2_types::{ use oauth2_types::{
errors::{ErrorResponse, InvalidRequest, OAuth2Error}, errors::{ErrorResponse, InvalidRequest, OAuth2Error},
pkce, pkce,
@@ -59,7 +60,7 @@ use crate::{
refresh_token::add_refresh_token, refresh_token::add_refresh_token,
session::{get_session_by_id, start_session}, session::{get_session_by_id, start_session},
}, },
SessionInfo, PostgresqlBackend,
}, },
templates::{FormPostContext, Templates}, templates::{FormPostContext, Templates},
tokens::{AccessToken, RefreshToken}, tokens::{AccessToken, RefreshToken},
@@ -297,7 +298,7 @@ async fn actually_reply(
async fn get( async fn get(
clients: Vec<OAuth2ClientConfig>, clients: Vec<OAuth2ClientConfig>,
params: Params, params: Params,
maybe_session: Option<SessionInfo>, maybe_session: Option<BrowserSession<PostgresqlBackend>>,
mut txn: Transaction<'_, Postgres>, mut txn: Transaction<'_, Postgres>,
) -> Result<ReplyOrBackToClient, Rejection> { ) -> Result<ReplyOrBackToClient, Rejection> {
// First, find out what client it is // First, find out what client it is
@@ -307,7 +308,7 @@ async fn get(
.ok_or_else(|| anyhow::anyhow!("could not find client")) .ok_or_else(|| anyhow::anyhow!("could not find client"))
.wrap_error()?; .wrap_error()?;
let maybe_session_id = maybe_session.as_ref().map(SessionInfo::key); let maybe_session_id = maybe_session.as_ref().map(|s| s.data);
let scope: String = { let scope: String = {
let it = params.auth.scope.iter().map(ToString::to_string); let it = params.auth.scope.iter().map(ToString::to_string);
@@ -394,7 +395,7 @@ impl StepRequest {
async fn step( async fn step(
oauth2_session_id: i64, oauth2_session_id: i64,
user_session: SessionInfo, user_session: BrowserSession<PostgresqlBackend>,
mut txn: Transaction<'_, Postgres>, mut txn: Transaction<'_, Postgres>,
) -> Result<ReplyOrBackToClient, Rejection> { ) -> Result<ReplyOrBackToClient, Rejection> {
let mut oauth2_session = get_session_by_id(&mut txn, oauth2_session_id) let mut oauth2_session = get_session_by_id(&mut txn, oauth2_session_id)
@@ -411,8 +412,9 @@ async fn step(
let redirect_uri = oauth2_session.redirect_uri().wrap_error()?; let redirect_uri = oauth2_session.redirect_uri().wrap_error()?;
// Check if the active session is valid // Check if the active session is valid
let reply = if user_session.active // TODO: this is ugly & should check if the session is active
&& user_session.last_authd_at >= oauth2_session.max_auth_time() let reply = if user_session.last_authentication.map(|x| x.created_at)
>= oauth2_session.max_auth_time()
{ {
// Yep! Let's complete the auth now // Yep! Let's complete the auth now
let mut params = AuthorizationResponse::default(); let mut params = AuthorizationResponse::default();

View File

@@ -12,6 +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 mas_data_model::BrowserSession;
use sqlx::PgPool; use sqlx::PgPool;
use url::Url; use url::Url;
use warp::{reply::html, Filter, Rejection, Reply}; use warp::{reply::html, Filter, Rejection, Reply};
@@ -24,7 +25,7 @@ use crate::{
session::optional_session, session::optional_session,
with_templates, CsrfToken, with_templates, CsrfToken,
}, },
storage::SessionInfo, storage::PostgresqlBackend,
templates::{IndexContext, TemplateContext, Templates}, templates::{IndexContext, TemplateContext, Templates},
}; };
@@ -51,10 +52,10 @@ async fn get(
templates: Templates, templates: Templates,
cookie_saver: EncryptedCookieSaver, cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken, csrf_token: CsrfToken,
session: Option<SessionInfo>, maybe_session: Option<BrowserSession<PostgresqlBackend>>,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let ctx = IndexContext::new(discovery_url) let ctx = IndexContext::new(discovery_url)
.maybe_with_session(session) .maybe_with_session(maybe_session)
.with_csrf(&csrf_token); .with_csrf(&csrf_token);
let content = templates.render_index(&ctx)?; let content = templates.render_index(&ctx)?;

View File

@@ -15,6 +15,7 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use hyper::http::uri::{Parts, PathAndQuery, Uri}; use hyper::http::uri::{Parts, PathAndQuery, Uri};
use mas_data_model::BrowserSession;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{pool::PoolConnection, PgPool, Postgres}; use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{reply::html, Filter, Rejection, Reply}; use warp::{reply::html, Filter, Rejection, Reply};
@@ -29,7 +30,7 @@ use crate::{
session::{optional_session, SessionCookie}, session::{optional_session, SessionCookie},
with_templates, CsrfToken, with_templates, CsrfToken,
}, },
storage::{login, SessionInfo}, storage::{login, PostgresqlBackend},
templates::{LoginContext, LoginFormField, TemplateContext, Templates}, templates::{LoginContext, LoginFormField, TemplateContext, Templates},
}; };
@@ -108,7 +109,7 @@ async fn get(
cookie_saver: EncryptedCookieSaver, cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken, csrf_token: CsrfToken,
query: LoginRequest, query: LoginRequest,
maybe_session: Option<SessionInfo>, maybe_session: Option<BrowserSession<PostgresqlBackend>>,
) -> Result<Box<dyn Reply>, Rejection> { ) -> Result<Box<dyn Reply>, Rejection> {
if maybe_session.is_some() { if maybe_session.is_some() {
Ok(Box::new(query.redirect()?)) Ok(Box::new(query.redirect()?))
@@ -133,7 +134,7 @@ async fn post(
// TODO: recover // TODO: recover
match login(&mut conn, &form.username, form.password).await { match login(&mut conn, &form.username, form.password).await {
Ok(session_info) => { Ok(session_info) => {
let session_cookie = SessionCookie::from_session_info(&session_info); let session_cookie = SessionCookie::from_session(&session_info);
let reply = query.redirect()?; let reply = query.redirect()?;
let reply = cookie_saver.save_encrypted(&session_cookie, reply)?; let reply = cookie_saver.save_encrypted(&session_cookie, reply)?;
Ok(Box::new(reply)) Ok(Box::new(reply))

View File

@@ -12,14 +12,15 @@
// 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 sqlx::{pool::PoolConnection, PgPool, Postgres}; use mas_data_model::BrowserSession;
use sqlx::{PgPool, Postgres, Transaction};
use warp::{hyper::Uri, Filter, Rejection, Reply}; use warp::{hyper::Uri, Filter, Rejection, Reply};
use crate::{ use crate::{
config::CookiesConfig, config::CookiesConfig,
errors::WrapError, errors::WrapError,
filters::{csrf::protected_form, database::connection, session::session}, filters::{csrf::protected_form, database::transaction, session::session},
storage::SessionInfo, storage::{user::end_session, PostgresqlBackend},
}; };
pub(super) fn filter( pub(super) fn filter(
@@ -29,16 +30,18 @@ pub(super) fn filter(
warp::path!("logout") warp::path!("logout")
.and(warp::post()) .and(warp::post())
.and(session(pool, cookies_config)) .and(session(pool, cookies_config))
.and(connection(pool)) .and(transaction(pool))
.and(protected_form(cookies_config)) .and(protected_form(cookies_config))
.and_then(post) .and_then(post)
} }
async fn post( async fn post(
session: SessionInfo, session: BrowserSession<PostgresqlBackend>,
mut conn: PoolConnection<Postgres>, mut txn: Transaction<'_, Postgres>,
_form: (), _form: (),
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
session.end(&mut conn).await.wrap_error()?; end_session(&mut txn, &session).await.wrap_error()?;
txn.commit().await.wrap_error()?;
Ok(warp::redirect(Uri::from_static("/login"))) Ok(warp::redirect(Uri::from_static("/login")))
} }

View File

@@ -12,8 +12,9 @@
// 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 mas_data_model::BrowserSession;
use serde::Deserialize; use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres}; use sqlx::{PgPool, Postgres, Transaction};
use warp::{hyper::Uri, reply::html, Filter, Rejection, Reply}; use warp::{hyper::Uri, reply::html, Filter, Rejection, Reply};
use crate::{ use crate::{
@@ -22,11 +23,11 @@ use crate::{
filters::{ filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver}, cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::{protected_form, updated_csrf_token}, csrf::{protected_form, updated_csrf_token},
database::connection, database::transaction,
session::session, session::session,
with_templates, CsrfToken, with_templates, CsrfToken,
}, },
storage::SessionInfo, storage::{user::authenticate_session, PostgresqlBackend},
templates::{EmptyContext, TemplateContext, Templates}, templates::{EmptyContext, TemplateContext, Templates},
}; };
@@ -50,7 +51,7 @@ pub(super) fn filter(
let post = warp::post() let post = warp::post()
.and(session(pool, cookies_config)) .and(session(pool, cookies_config))
.and(connection(pool)) .and(transaction(pool))
.and(protected_form(cookies_config)) .and(protected_form(cookies_config))
.and_then(post); .and_then(post);
@@ -61,7 +62,7 @@ async fn get(
templates: Templates, templates: Templates,
cookie_saver: EncryptedCookieSaver, cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken, csrf_token: CsrfToken,
session: SessionInfo, session: BrowserSession<PostgresqlBackend>,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let ctx = EmptyContext.with_session(session).with_csrf(&csrf_token); let ctx = EmptyContext.with_session(session).with_csrf(&csrf_token);
@@ -72,14 +73,14 @@ async fn get(
} }
async fn post( async fn post(
session: SessionInfo, session: BrowserSession<PostgresqlBackend>,
mut conn: PoolConnection<Postgres>, mut txn: Transaction<'_, Postgres>,
form: ReauthForm, form: ReauthForm,
) -> Result<impl Reply, Rejection> { ) -> Result<impl Reply, Rejection> {
let _session = session authenticate_session(&mut txn, &session, form.password)
.reauth(&mut conn, form.password)
.await .await
.wrap_error()?; .wrap_error()?;
txn.commit().await.wrap_error()?;
Ok(warp::redirect(Uri::from_static("/"))) Ok(warp::redirect(Uri::from_static("/")))
} }

View File

@@ -16,6 +16,7 @@ use std::convert::TryFrom;
use argon2::Argon2; use argon2::Argon2;
use hyper::http::uri::{Parts, PathAndQuery, Uri}; use hyper::http::uri::{Parts, PathAndQuery, Uri};
use mas_data_model::BrowserSession;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{pool::PoolConnection, PgPool, Postgres}; use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{reply::html, Filter, Rejection, Reply}; use warp::{reply::html, Filter, Rejection, Reply};
@@ -30,7 +31,7 @@ use crate::{
session::{optional_session, SessionCookie}, session::{optional_session, SessionCookie},
with_templates, CsrfToken, with_templates, CsrfToken,
}, },
storage::{register_user, user::start_session, SessionInfo}, storage::{register_user, user::start_session, PostgresqlBackend},
templates::{EmptyContext, TemplateContext, Templates}, templates::{EmptyContext, TemplateContext, Templates},
}; };
@@ -110,7 +111,7 @@ async fn get(
cookie_saver: EncryptedCookieSaver, cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken, csrf_token: CsrfToken,
query: RegisterRequest, query: RegisterRequest,
maybe_session: Option<SessionInfo>, maybe_session: Option<BrowserSession<PostgresqlBackend>>,
) -> Result<Box<dyn Reply>, Rejection> { ) -> Result<Box<dyn Reply>, Rejection> {
if maybe_session.is_some() { if maybe_session.is_some() {
Ok(Box::new(query.redirect()?)) Ok(Box::new(query.redirect()?))
@@ -140,7 +141,7 @@ async fn post(
let session_info = start_session(&mut conn, user).await.wrap_error()?; let session_info = start_session(&mut conn, user).await.wrap_error()?;
let session_cookie = SessionCookie::from_session_info(&session_info); let session_cookie = SessionCookie::from_session(&session_info);
let reply = query.redirect()?; let reply = query.redirect()?;
let reply = cookie_saver.save_encrypted(&session_cookie, reply)?; let reply = cookie_saver.save_encrypted(&session_cookie, reply)?;
Ok(reply) Ok(reply)

View File

@@ -16,12 +16,32 @@
#![allow(clippy::used_underscore_binding)] // This is needed by sqlx macros #![allow(clippy::used_underscore_binding)] // This is needed by sqlx macros
use mas_data_model::StorageBackend;
use serde::Serialize;
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("databse query returned an inconsistent state")]
pub struct DatabaseInconsistencyError;
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct PostgresqlBackend;
impl StorageBackend for PostgresqlBackend {
type AccessTokenData = i64;
type AuthenticationData = i64;
type AuthorizationCodeData = i64;
type BrowserSessionData = i64;
type ClientData = ();
type SessionData = i64;
type UserData = i64;
}
pub mod oauth2; pub mod oauth2;
pub mod user; pub mod user;
pub use self::user::{login, lookup_active_session, register_user, SessionInfo, User}; pub use self::user::{login, lookup_active_session, register_user};
/// Embedded migrations, allowing them to run on startup /// Embedded migrations, allowing them to run on startup
pub static MIGRATOR: Migrator = sqlx::migrate!(); pub static MIGRATOR: Migrator = sqlx::migrate!();

View File

@@ -17,6 +17,7 @@ use std::{collections::HashSet, convert::TryFrom, str::FromStr, string::ToString
use anyhow::Context; use anyhow::Context;
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use itertools::Itertools; use itertools::Itertools;
use mas_data_model::BrowserSession;
use oauth2_types::{ use oauth2_types::{
pkce, pkce,
requests::{ResponseMode, ResponseType}, requests::{ResponseMode, ResponseType},
@@ -25,10 +26,8 @@ use serde::Serialize;
use sqlx::{Executor, FromRow, Postgres}; use sqlx::{Executor, FromRow, Postgres};
use url::Url; use url::Url;
use super::{ use super::authorization_code::{add_code, OAuth2Code};
super::{user::lookup_session, SessionInfo}, use crate::storage::{lookup_active_session, PostgresqlBackend};
authorization_code::{add_code, OAuth2Code},
};
#[derive(FromRow, Serialize)] #[derive(FromRow, Serialize)]
pub struct OAuth2Session { pub struct OAuth2Session {
@@ -60,10 +59,11 @@ impl OAuth2Session {
pub async fn fetch_session( pub async fn fetch_session(
&self, &self,
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
) -> anyhow::Result<Option<SessionInfo>> { ) -> anyhow::Result<Option<BrowserSession<PostgresqlBackend>>> {
match self.user_session_id { match self.user_session_id {
Some(id) => { Some(id) => {
let info = lookup_session(executor, id).await?; // TODO: and if the session is inactive?
let info = lookup_active_session(executor, id).await?;
Ok(Some(info)) Ok(Some(info))
} }
None => Ok(None), None => Ok(None),
@@ -80,19 +80,19 @@ impl OAuth2Session {
pub async fn match_or_set_session( pub async fn match_or_set_session(
&mut self, &mut self,
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
session: SessionInfo, session: BrowserSession<PostgresqlBackend>,
) -> anyhow::Result<SessionInfo> { ) -> anyhow::Result<BrowserSession<PostgresqlBackend>> {
match self.user_session_id { match self.user_session_id {
Some(id) if id == session.key() => Ok(session), Some(id) if id == session.data => Ok(session),
Some(id) => Err(anyhow::anyhow!( Some(id) => Err(anyhow::anyhow!(
"session mismatch, expected {}, got {}", "session mismatch, expected {}, got {}",
id, id,
session.key() session.data
)), )),
None => { None => {
sqlx::query!( sqlx::query!(
"UPDATE oauth2_sessions SET user_session_id = $1 WHERE id = $2", "UPDATE oauth2_sessions SET user_session_id = $1 WHERE id = $2",
session.key(), session.data,
self.id, self.id,
) )
.execute(executor) .execute(executor)

View File

@@ -12,76 +12,29 @@
// 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 std::borrow::BorrowMut; use std::{borrow::BorrowMut, convert::TryInto};
use anyhow::Context; use anyhow::Context;
use argon2::Argon2; use argon2::Argon2;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use mas_data_model::{Authentication, BrowserSession, User};
use password_hash::{PasswordHash, PasswordHasher, SaltString}; use password_hash::{PasswordHash, PasswordHasher, SaltString};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use serde::Serialize;
use sqlx::{Acquire, Executor, FromRow, Postgres, Transaction}; use sqlx::{Acquire, Executor, FromRow, 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 warp::reject::Reject; use warp::reject::Reject;
use super::{DatabaseInconsistencyError, PostgresqlBackend};
use crate::errors::HtmlError; use crate::errors::HtmlError;
#[derive(Serialize, Debug, Clone, FromRow)] #[derive(Debug, Clone, FromRow)]
pub struct User { struct UserLookup {
pub id: i64, pub id: i64,
pub username: String, pub username: String,
} }
#[derive(Serialize, Debug, Clone, FromRow)]
pub struct SessionInfo {
id: i64,
user_id: i64,
username: String,
pub active: bool,
created_at: DateTime<Utc>,
pub last_authd_at: Option<DateTime<Utc>>,
}
impl SessionInfo {
#[must_use]
pub fn key(&self) -> i64 {
self.id
}
pub async fn reauth(
mut self,
conn: impl Acquire<'_, Database = Postgres>,
password: String,
) -> anyhow::Result<Self> {
let mut txn = conn.begin().await?;
self.last_authd_at = Some(authenticate_session(&mut txn, self.id, password).await?);
txn.commit().await?;
Ok(self)
}
pub async fn end(
mut self,
executor: impl Executor<'_, Database = Postgres>,
) -> anyhow::Result<Self> {
end_session(executor, self.id).await?;
self.active = false;
Ok(self)
}
pub(crate) fn samples() -> Vec<Self> {
vec![SessionInfo {
id: 1,
user_id: 2,
username: "john".to_string(),
active: true,
created_at: Utc::now(),
last_authd_at: Some(Utc::now()),
}]
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum LoginError { pub enum LoginError {
#[error("could not find user {username:?}")] #[error("could not find user {username:?}")]
@@ -116,7 +69,7 @@ pub async fn login(
conn: impl Acquire<'_, Database = Postgres>, conn: impl Acquire<'_, Database = Postgres>,
username: &str, username: &str,
password: String, password: String,
) -> Result<SessionInfo, LoginError> { ) -> Result<BrowserSession<PostgresqlBackend>, LoginError> {
let mut txn = conn.begin().await.context("could not start transaction")?; let mut txn = conn.begin().await.context("could not start transaction")?;
let user = lookup_user_by_username(&mut txn, username) let user = lookup_user_by_username(&mut txn, username)
.await .await
@@ -132,8 +85,8 @@ pub async fn login(
})?; })?;
let mut session = start_session(&mut txn, user).await?; let mut session = start_session(&mut txn, user).await?;
session.last_authd_at = Some( session.last_authentication = Some(
authenticate_session(&mut txn, session.id, password) authenticate_session(&mut txn, &session, password)
.await .await
.map_err(|source| { .map_err(|source| {
if matches!(source, AuthenticationError::Password { .. }) { if matches!(source, AuthenticationError::Password { .. }) {
@@ -152,30 +105,73 @@ pub async fn login(
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("could not fetch session")] #[error("could not fetch session")]
pub struct ActiveSessionLookupError(#[from] sqlx::Error); pub enum ActiveSessionLookupError {
Fetch(#[from] sqlx::Error),
Conversion(#[from] DatabaseInconsistencyError),
}
impl Reject for ActiveSessionLookupError {} impl Reject for ActiveSessionLookupError {}
impl ActiveSessionLookupError { impl ActiveSessionLookupError {
#[must_use] #[must_use]
pub fn not_found(&self) -> bool { pub fn not_found(&self) -> bool {
matches!(self.0, sqlx::Error::RowNotFound) matches!(
self,
ActiveSessionLookupError::Fetch(sqlx::Error::RowNotFound)
)
}
}
struct SessionLookup {
id: i64,
user_id: i64,
username: String,
created_at: DateTime<Utc>,
last_authentication_id: Option<i64>,
last_authd_at: Option<DateTime<Utc>>,
}
impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
type Error = DatabaseInconsistencyError;
fn try_into(self) -> Result<BrowserSession<PostgresqlBackend>, Self::Error> {
let user = User {
data: self.user_id,
username: self.username,
sub: format!("fake-sub-{}", self.user_id),
};
let last_authentication = match (self.last_authentication_id, self.last_authd_at) {
(Some(id), Some(created_at)) => Some(Authentication {
data: id,
created_at,
}),
(None, None) => None,
_ => return Err(DatabaseInconsistencyError),
};
Ok(BrowserSession {
data: self.id,
user,
created_at: self.created_at,
last_authentication,
})
} }
} }
pub async fn lookup_active_session( pub async fn lookup_active_session(
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
id: i64, id: i64,
) -> Result<SessionInfo, ActiveSessionLookupError> { ) -> Result<BrowserSession<PostgresqlBackend>, ActiveSessionLookupError> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
SessionInfo, SessionLookup,
r#" r#"
SELECT SELECT
s.id, s.id,
u.id as user_id, u.id as user_id,
u.username, u.username,
s.active,
s.created_at, s.created_at,
a.id as "last_authentication_id?",
a.created_at as "last_authd_at?" a.created_at as "last_authd_at?"
FROM user_sessions s FROM user_sessions s
INNER JOIN users u INNER JOIN users u
@@ -189,65 +185,43 @@ pub async fn lookup_active_session(
id, id,
) )
.fetch_one(executor) .fetch_one(executor)
.await?; .await?
.try_into()?;
Ok(res) Ok(res)
} }
pub async fn lookup_session( #[derive(FromRow)]
executor: impl Executor<'_, Database = Postgres>, struct SessionStartResult {
id: i64, id: i64,
) -> anyhow::Result<SessionInfo> { created_at: DateTime<Utc>,
sqlx::query_as!(
SessionInfo,
r#"
SELECT
s.id,
u.id as user_id,
u.username,
s.active,
s.created_at,
a.created_at as "last_authd_at?"
FROM user_sessions s
INNER JOIN users u
ON s.user_id = u.id
LEFT JOIN user_session_authentications a
ON a.session_id = s.id
WHERE s.id = $1
ORDER BY a.created_at DESC
LIMIT 1
"#,
id,
)
.fetch_one(executor)
.await
.context("could not fetch session")
} }
pub async fn start_session( pub async fn start_session(
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
user: User, user: User<PostgresqlBackend>,
) -> anyhow::Result<SessionInfo> { ) -> anyhow::Result<BrowserSession<PostgresqlBackend>> {
let (id, created_at): (i64, DateTime<Utc>) = sqlx::query_as( let res = sqlx::query_as!(
SessionStartResult,
r#" r#"
INSERT INTO user_sessions (user_id) INSERT INTO user_sessions (user_id)
VALUES ($1) VALUES ($1)
RETURNING id, created_at RETURNING id, created_at
"#, "#,
user.data,
) )
.bind(user.id)
.fetch_one(executor) .fetch_one(executor)
.await .await
.context("could not create session")?; .context("could not create session")?;
Ok(SessionInfo { let session = BrowserSession {
id, data: res.id,
user_id: user.id, user,
username: user.username, created_at: res.created_at,
active: true, last_authentication: None,
created_at, };
last_authd_at: None,
}) Ok(session)
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -265,11 +239,17 @@ pub enum AuthenticationError {
Internal(#[from] tokio::task::JoinError), Internal(#[from] tokio::task::JoinError),
} }
#[derive(FromRow)]
struct AuthenticationInsertionResult {
id: i64,
created_at: DateTime<Utc>,
}
pub async fn authenticate_session( pub async fn authenticate_session(
txn: &mut Transaction<'_, Postgres>, txn: &mut Transaction<'_, Postgres>,
session_id: i64, session: &BrowserSession<PostgresqlBackend>,
password: String, password: String,
) -> Result<DateTime<Utc>, AuthenticationError> { ) -> Result<Authentication<PostgresqlBackend>, AuthenticationError> {
// First, fetch the hashed password from the user associated with that session // First, fetch the hashed password from the user associated with that session
let hashed_password: String = sqlx::query_scalar!( let hashed_password: String = sqlx::query_scalar!(
r#" r#"
@@ -279,7 +259,7 @@ pub async fn authenticate_session(
ON u.id = s.user_id ON u.id = s.user_id
WHERE s.id = $1 WHERE s.id = $1
"#, "#,
session_id, session.data,
) )
.fetch_one(txn.borrow_mut()) .fetch_one(txn.borrow_mut())
.await .await
@@ -297,19 +277,23 @@ 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 created_at: DateTime<Utc> = sqlx::query_scalar!( let res = sqlx::query_as!(
AuthenticationInsertionResult,
r#" r#"
INSERT INTO user_session_authentications (session_id) INSERT INTO user_session_authentications (session_id)
VALUES ($1) VALUES ($1)
RETURNING created_at RETURNING id, created_at
"#, "#,
session_id, session.data,
) )
.fetch_one(txn.borrow_mut()) .fetch_one(txn.borrow_mut())
.await .await
.map_err(AuthenticationError::Save)?; .map_err(AuthenticationError::Save)?;
Ok(created_at) Ok(Authentication {
data: res.id,
created_at: res.created_at,
})
} }
pub async fn register_user( pub async fn register_user(
@@ -317,7 +301,7 @@ pub async fn register_user(
phf: impl PasswordHasher, phf: impl PasswordHasher,
username: &str, username: &str,
password: &str, password: &str,
) -> anyhow::Result<User> { ) -> anyhow::Result<User<PostgresqlBackend>> {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?; let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?;
@@ -336,16 +320,20 @@ pub async fn register_user(
.context("could not insert user")?; .context("could not insert user")?;
Ok(User { Ok(User {
id, data: id,
username: username.to_string(), username: username.to_string(),
sub: format!("fake-sub-{}", id),
}) })
} }
pub async fn end_session( pub async fn end_session(
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
id: i64, session: &BrowserSession<PostgresqlBackend>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let res = sqlx::query!("UPDATE user_sessions SET active = FALSE WHERE id = $1", id) let res = sqlx::query!(
"UPDATE user_sessions SET active = FALSE WHERE id = $1",
session.data,
)
.execute(executor) .execute(executor)
.instrument(info_span!("End session")) .instrument(info_span!("End session"))
.await .await
@@ -358,32 +346,12 @@ pub async fn end_session(
} }
} }
#[allow(dead_code)]
pub async fn lookup_user_by_id(
executor: impl Executor<'_, Database = Postgres>,
id: i64,
) -> anyhow::Result<User> {
sqlx::query_as!(
User,
r#"
SELECT id, username
FROM users
WHERE id = $1
"#,
id
)
.fetch_one(executor)
.instrument(info_span!("Fetch user"))
.await
.context("could not fetch user")
}
pub async fn lookup_user_by_username( pub async fn lookup_user_by_username(
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
username: &str, username: &str,
) -> Result<User, sqlx::Error> { ) -> Result<User<PostgresqlBackend>, sqlx::Error> {
sqlx::query_as!( let res = sqlx::query_as!(
User, UserLookup,
r#" r#"
SELECT id, username SELECT id, username
FROM users FROM users
@@ -393,5 +361,11 @@ pub async fn lookup_user_by_username(
) )
.fetch_one(executor) .fetch_one(executor)
.instrument(info_span!("Fetch user")) .instrument(info_span!("Fetch user"))
.await .await?;
Ok(User {
data: res.id,
username: res.username,
sub: format!("fake-sub-{}", res.id),
})
} }

View File

@@ -14,16 +14,17 @@
//! Contexts used in templates //! Contexts used in templates
use mas_data_model::BrowserSession;
use oauth2_types::errors::OAuth2Error; use oauth2_types::errors::OAuth2Error;
use serde::{ser::SerializeStruct, Serialize}; use serde::{ser::SerializeStruct, Serialize};
use url::Url; use url::Url;
use crate::{errors::ErroredForm, filters::CsrfToken, storage::SessionInfo}; use crate::{errors::ErroredForm, filters::CsrfToken, storage::PostgresqlBackend};
/// Helper trait to construct context wrappers /// Helper trait to construct context wrappers
pub trait TemplateContext { pub trait TemplateContext {
/// Attach a user session to the template context /// Attach a user session to the template context
fn with_session(self, current_session: SessionInfo) -> WithSession<Self> fn with_session(self, current_session: BrowserSession<PostgresqlBackend>) -> WithSession<Self>
where where
Self: Sized, Self: Sized,
{ {
@@ -34,7 +35,10 @@ pub trait TemplateContext {
} }
/// Attach an optional user session to the template context /// Attach an optional user session to the template context
fn maybe_with_session(self, current_session: Option<SessionInfo>) -> WithOptionalSession<Self> fn maybe_with_session(
self,
current_session: Option<BrowserSession<PostgresqlBackend>>,
) -> WithOptionalSession<Self>
where where
Self: Sized, Self: Sized,
{ {
@@ -91,7 +95,7 @@ impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
/// Context with a user session in it /// Context with a user session in it
#[derive(Serialize)] #[derive(Serialize)]
pub struct WithSession<T> { pub struct WithSession<T> {
current_session: SessionInfo, current_session: BrowserSession<PostgresqlBackend>,
#[serde(flatten)] #[serde(flatten)]
inner: T, inner: T,
@@ -102,7 +106,7 @@ impl<T: TemplateContext> TemplateContext for WithSession<T> {
where where
Self: Sized, Self: Sized,
{ {
SessionInfo::samples() BrowserSession::<PostgresqlBackend>::samples()
.into_iter() .into_iter()
.flat_map(|session| { .flat_map(|session| {
T::sample().into_iter().map(move |inner| WithSession { T::sample().into_iter().map(move |inner| WithSession {
@@ -117,7 +121,7 @@ impl<T: TemplateContext> TemplateContext for WithSession<T> {
/// Context with an optional user session in it /// Context with an optional user session in it
#[derive(Serialize)] #[derive(Serialize)]
pub struct WithOptionalSession<T> { pub struct WithOptionalSession<T> {
current_session: Option<SessionInfo>, current_session: Option<BrowserSession<PostgresqlBackend>>,
#[serde(flatten)] #[serde(flatten)]
inner: T, inner: T,
@@ -128,7 +132,7 @@ impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
where where
Self: Sized, Self: Sized,
{ {
SessionInfo::samples() BrowserSession::<PostgresqlBackend>::samples()
.into_iter() .into_iter()
.map(Some) // Wrap all samples in an Option .map(Some) // Wrap all samples in an Option
.chain(std::iter::once(None)) // Add the "None" option .chain(std::iter::once(None)) // Add the "None" option

View File

@@ -34,7 +34,7 @@ limitations under the License.
<div class="navbar-end"> <div class="navbar-end">
{% if current_session %} {% if current_session %}
<div class="navbar-item"> <div class="navbar-item">
Howdy {{ current_session.username }}! Howdy {{ current_session.user.username }}!
</div> </div>
<div class="navbar-item"> <div class="navbar-item">
<form method="POST" action="/logout"> <form method="POST" action="/logout">

View File

@@ -0,0 +1,13 @@
[package]
name = "mas-data-model"
version = "0.1.0"
authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2018"
license = "Apache-2.0"
[dependencies]
chrono = "0.4.19"
thiserror = "1.0.29"
serde = "1.0.130"
oauth2-types = { path = "../oauth2-types" }

View File

@@ -0,0 +1,134 @@
// 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.
use chrono::{DateTime, Duration, Utc};
use oauth2_types::{pkce::CodeChallengeMethod, scope::Scope};
use serde::Serialize;
pub trait StorageBackend {
type UserData: Clone + std::fmt::Debug + PartialEq;
type AuthenticationData: Clone + std::fmt::Debug + PartialEq;
type BrowserSessionData: Clone + std::fmt::Debug + PartialEq;
type ClientData: Clone + std::fmt::Debug + PartialEq;
type SessionData: Clone + std::fmt::Debug + PartialEq;
type AuthorizationCodeData: Clone + std::fmt::Debug + PartialEq;
type AccessTokenData: Clone + std::fmt::Debug + PartialEq;
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct User<T: StorageBackend> {
#[serde(skip_serializing)]
pub data: T::UserData,
pub username: String,
pub sub: String,
}
impl<T: StorageBackend> User<T>
where
T::UserData: Default,
{
pub fn samples() -> Vec<Self> {
vec![User {
data: Default::default(),
username: "john".to_string(),
sub: "123-456".to_string(),
}]
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Authentication<T: StorageBackend> {
#[serde(skip_serializing)]
pub data: T::AuthenticationData,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct BrowserSession<T: StorageBackend> {
#[serde(skip_serializing)]
pub data: T::BrowserSessionData,
pub user: User<T>,
pub created_at: DateTime<Utc>,
pub last_authentication: Option<Authentication<T>>,
}
impl<T: StorageBackend> BrowserSession<T>
where
T::BrowserSessionData: Default,
T::UserData: Default,
{
pub fn samples() -> Vec<Self> {
User::<T>::samples()
.into_iter()
.map(|user| BrowserSession {
data: Default::default(),
user,
created_at: Utc::now(),
last_authentication: None,
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Client<T: StorageBackend> {
#[serde(skip_serializing)]
pub data: T::ClientData,
pub client_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Session<T: StorageBackend> {
#[serde(skip_serializing)]
pub data: T::SessionData,
pub browser_session: Option<BrowserSession<T>>,
pub client: Client<T>,
pub scope: Scope,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Pkce {
challenge_method: CodeChallengeMethod,
challenge: String,
}
impl Pkce {
pub fn new(challenge_method: CodeChallengeMethod, challenge: String) -> Self {
Pkce {
challenge_method,
challenge,
}
}
pub fn verify(&self, verifier: &str) -> bool {
self.challenge_method.verify(&self.challenge, verifier)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuthorizationCode<T: StorageBackend> {
#[serde(skip_serializing)]
pub data: T::AuthorizationCodeData,
pub code: String,
pub pkce: Pkce,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessToken<T: StorageBackend> {
pub data: T::AccessTokenData,
pub jti: String,
pub token: String,
pub expires_after: Duration,
pub created_at: DateTime<Utc>,
}