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
Have a consent screen before continuing the SSO login
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2294,6 +2294,7 @@ name = "mas-templates"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"mas-config",
|
||||
"mas-data-model",
|
||||
"oauth2-types",
|
||||
|
@ -90,6 +90,18 @@ pub struct CompatSession<T: StorageBackend> {
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl<S: StorageBackendMarker> From<CompatSession<S>> for CompatSession<()> {
|
||||
fn from(t: CompatSession<S>) -> Self {
|
||||
Self {
|
||||
data: (),
|
||||
user: t.user.into(),
|
||||
device: t.device,
|
||||
created_at: t.created_at,
|
||||
deleted_at: t.deleted_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CompatAccessToken<T: StorageBackend> {
|
||||
pub data: T::CompatAccessTokenData,
|
||||
@ -98,6 +110,17 @@ pub struct CompatAccessToken<T: StorageBackend> {
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl<S: StorageBackendMarker> From<CompatAccessToken<S>> for CompatAccessToken<()> {
|
||||
fn from(t: CompatAccessToken<S>) -> Self {
|
||||
Self {
|
||||
data: (),
|
||||
token: t.token,
|
||||
created_at: t.created_at,
|
||||
expires_at: t.expires_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CompatRefreshToken<T: StorageBackend> {
|
||||
pub data: T::RefreshTokenData,
|
||||
@ -105,13 +128,12 @@ pub struct CompatRefreshToken<T: StorageBackend> {
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl<S: StorageBackendMarker> From<CompatAccessToken<S>> for CompatAccessToken<()> {
|
||||
fn from(t: CompatAccessToken<S>) -> Self {
|
||||
CompatAccessToken {
|
||||
impl<S: StorageBackendMarker> From<CompatRefreshToken<S>> for CompatRefreshToken<()> {
|
||||
fn from(t: CompatRefreshToken<S>) -> Self {
|
||||
Self {
|
||||
data: (),
|
||||
token: t.token,
|
||||
created_at: t.created_at,
|
||||
expires_at: t.expires_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,6 +153,30 @@ pub enum CompatSsoLoginState<T: StorageBackend> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<S: StorageBackendMarker> From<CompatSsoLoginState<S>> for CompatSsoLoginState<()> {
|
||||
fn from(t: CompatSsoLoginState<S>) -> Self {
|
||||
match t {
|
||||
CompatSsoLoginState::Pending => Self::Pending,
|
||||
CompatSsoLoginState::Fullfilled {
|
||||
fullfilled_at,
|
||||
session,
|
||||
} => Self::Fullfilled {
|
||||
fullfilled_at,
|
||||
session: session.into(),
|
||||
},
|
||||
CompatSsoLoginState::Exchanged {
|
||||
fullfilled_at,
|
||||
exchanged_at,
|
||||
session,
|
||||
} => Self::Exchanged {
|
||||
fullfilled_at,
|
||||
exchanged_at,
|
||||
session: session.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[serde(bound = "T: StorageBackend")]
|
||||
pub struct CompatSsoLogin<T: StorageBackend> {
|
||||
@ -141,3 +187,15 @@ pub struct CompatSsoLogin<T: StorageBackend> {
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub state: CompatSsoLoginState<T>,
|
||||
}
|
||||
|
||||
impl<S: StorageBackendMarker> From<CompatSsoLogin<S>> for CompatSsoLogin<()> {
|
||||
fn from(t: CompatSsoLogin<S>) -> Self {
|
||||
Self {
|
||||
data: (),
|
||||
redirect_uri: t.redirect_uri,
|
||||
token: t.token,
|
||||
created_at: t.created_at,
|
||||
state: t.state.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ enum LoginType {
|
||||
|
||||
#[serde(rename = "m.login.sso")]
|
||||
Sso {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
identity_providers: Vec<SsoIdentityProvider>,
|
||||
},
|
||||
}
|
||||
|
@ -16,16 +16,20 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
extract::Path,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
extract::{Form, Path},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
Extension,
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use mas_axum_utils::{FancyError, SessionInfoExt};
|
||||
use mas_axum_utils::{
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::Device;
|
||||
use mas_router::Route;
|
||||
use mas_storage::compat::{fullfill_compat_sso_login, get_compat_sso_login_by_id};
|
||||
use mas_templates::{CompatSsoContext, TemplateContext, Templates};
|
||||
use rand::thread_rng;
|
||||
use serde::Serialize;
|
||||
use sqlx::PgPool;
|
||||
@ -41,12 +45,46 @@ struct AllParams<'s> {
|
||||
|
||||
pub async fn get(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(templates): Extension<Templates>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let mut conn = pool.acquire().await?;
|
||||
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
|
||||
|
||||
let maybe_session = session_info.load_session(&mut conn).await?;
|
||||
|
||||
let session = if let Some(session) = maybe_session {
|
||||
session
|
||||
} else {
|
||||
// If there is no session, redirect to the login screen
|
||||
let login = mas_router::Login::and_continue_compat_sso_login(id);
|
||||
return Ok((cookie_jar, login.go()).into_response());
|
||||
};
|
||||
|
||||
let login = get_compat_sso_login_by_id(&mut conn, id).await?;
|
||||
|
||||
let ctx = CompatSsoContext::new(login)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value());
|
||||
|
||||
let content = templates.render_sso_login(&ctx).await?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Path(id): Path<i64>,
|
||||
Form(form): Form<ProtectedForm<()>>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
cookie_jar.verify_form(form)?;
|
||||
|
||||
let maybe_session = session_info.load_session(&mut txn).await?;
|
||||
|
||||
|
@ -193,7 +193,8 @@ where
|
||||
)
|
||||
.route(
|
||||
mas_router::CompatLoginSsoComplete::route(),
|
||||
get(self::compat::login_sso_complete::get),
|
||||
get(self::compat::login_sso_complete::get)
|
||||
.post(self::compat::login_sso_complete::post),
|
||||
)
|
||||
.layer(ThenLayer::new(
|
||||
move |result: Result<axum::response::Response, Infallible>| async move {
|
||||
|
@ -13,7 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
use mas_router::{PostAuthAction, Route};
|
||||
use mas_storage::oauth2::authorization_grant::get_grant_by_id;
|
||||
use mas_storage::{
|
||||
compat::get_compat_sso_login_by_id, oauth2::authorization_grant::get_grant_by_id,
|
||||
};
|
||||
use mas_templates::PostAuthContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgConnection;
|
||||
@ -41,8 +43,10 @@ impl OptionalPostAuthAction {
|
||||
let grant = Box::new(grant.into());
|
||||
Ok(Some(PostAuthContext::ContinueAuthorizationGrant { grant }))
|
||||
}
|
||||
Some(PostAuthAction::ContinueCompatSsoLogin { .. }) => {
|
||||
Ok(Some(PostAuthContext::ContinueCompatSsoLogin))
|
||||
Some(PostAuthAction::ContinueCompatSsoLogin { data }) => {
|
||||
let login = get_compat_sso_login_by_id(conn, *data).await?;
|
||||
let login = Box::new(login.into());
|
||||
Ok(Some(PostAuthContext::ContinueCompatSsoLogin { login }))
|
||||
}
|
||||
Some(PostAuthAction::ChangePassword) => Ok(Some(PostAuthContext::ChangePassword)),
|
||||
None => Ok(None),
|
||||
|
@ -20,6 +20,7 @@ serde = { version = "1.0.137", features = ["derive"] }
|
||||
serde_json = "1.0.81"
|
||||
serde_urlencoded = "0.7.1"
|
||||
|
||||
chrono = "0.4.19"
|
||||
url = "2.2.2"
|
||||
|
||||
oauth2-types = { path = "../oauth2-types" }
|
||||
|
@ -16,7 +16,11 @@
|
||||
|
||||
#![allow(clippy::trait_duplication_in_bounds)]
|
||||
|
||||
use mas_data_model::{AuthorizationGrant, BrowserSession, StorageBackend, User, UserEmail};
|
||||
use chrono::Utc;
|
||||
use mas_data_model::{
|
||||
AuthorizationGrant, BrowserSession, CompatSsoLogin, CompatSsoLoginState, StorageBackend, User,
|
||||
UserEmail,
|
||||
};
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
@ -250,7 +254,10 @@ pub enum PostAuthContext {
|
||||
|
||||
/// Continue legacy login
|
||||
/// TODO: add the login context in there
|
||||
ContinueCompatSsoLogin,
|
||||
ContinueCompatSsoLogin {
|
||||
/// The compat SSO login request
|
||||
login: Box<CompatSsoLogin<()>>,
|
||||
},
|
||||
|
||||
/// Change the account password
|
||||
ChangePassword,
|
||||
@ -454,6 +461,42 @@ impl ReauthContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `sso.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct CompatSsoContext {
|
||||
login: CompatSsoLogin<()>,
|
||||
}
|
||||
|
||||
impl TemplateContext for CompatSsoContext {
|
||||
fn sample() -> Vec<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![CompatSsoContext {
|
||||
login: CompatSsoLogin {
|
||||
data: (),
|
||||
redirect_uri: Url::parse("https://app.element.io/").unwrap(),
|
||||
token: "abcdefghijklmnopqrstuvwxyz012345".into(),
|
||||
created_at: Utc::now(),
|
||||
state: CompatSsoLoginState::Pending,
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
impl CompatSsoContext {
|
||||
/// Constructs a context for the legacy SSO login page
|
||||
#[must_use]
|
||||
pub fn new<T>(login: T) -> Self
|
||||
where
|
||||
T: Into<CompatSsoLogin<()>>,
|
||||
{
|
||||
Self {
|
||||
login: login.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `account/index.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct AccountContext {
|
||||
|
@ -45,10 +45,11 @@ mod macros;
|
||||
|
||||
pub use self::{
|
||||
context::{
|
||||
AccountContext, AccountEmailsContext, ConsentContext, EmailVerificationContext,
|
||||
EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField,
|
||||
PostAuthContext, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
|
||||
TemplateContext, WithCsrf, WithOptionalSession, WithSession,
|
||||
AccountContext, AccountEmailsContext, CompatSsoContext, ConsentContext,
|
||||
EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||
LoginContext, LoginFormField, PostAuthContext, ReauthContext, ReauthFormField,
|
||||
RegisterContext, RegisterFormField, TemplateContext, WithCsrf, WithOptionalSession,
|
||||
WithSession,
|
||||
},
|
||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||
};
|
||||
@ -294,9 +295,12 @@ register_templates! {
|
||||
/// Render the registration page
|
||||
pub fn render_register(WithCsrf<RegisterContext>) { "pages/register.html" }
|
||||
|
||||
/// Render the registration page
|
||||
/// Render the client consent page
|
||||
pub fn render_consent(WithCsrf<WithSession<ConsentContext>>) { "pages/consent.html" }
|
||||
|
||||
/// Render the client consent page
|
||||
pub fn render_sso_login(WithCsrf<WithSession<CompatSsoContext>>) { "pages/sso.html" }
|
||||
|
||||
/// Render the home page
|
||||
pub fn render_index(WithCsrf<WithOptionalSession<IndexContext>>) { "pages/index.html" }
|
||||
|
||||
|
45
crates/templates/src/res/pages/sso.html
Normal file
45
crates/templates/src/res/pages/sso.html
Normal file
@ -0,0 +1,45 @@
|
||||
{#
|
||||
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.
|
||||
#}
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="w-96 m-2">
|
||||
<form method="POST" class="grid grid-cols-1 gap-6">
|
||||
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col">
|
||||
<div class="text-center">
|
||||
<h1 class="text-lg text-center font-medium">{{ login.redirect_uri }}</h1>
|
||||
<h1>wants to access your Matrix account</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
|
||||
{{ button::button(text="Allow") }}
|
||||
</form>
|
||||
<div class="text-center mt-4">
|
||||
<form method="POST" action="/logout">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
<div>
|
||||
Not {{ current_session.user.username }}?
|
||||
{{ button::button_text(text="Sign out", name="logout", type="submit") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
Reference in New Issue
Block a user