diff --git a/crates/data-model/src/oauth2/authorization_grant.rs b/crates/data-model/src/oauth2/authorization_grant.rs index a1b321e4c..170e476d0 100644 --- a/crates/data-model/src/oauth2/authorization_grant.rs +++ b/crates/data-model/src/oauth2/authorization_grant.rs @@ -4,9 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::num::NonZeroU32; - -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc}; use mas_iana::oauth::PkceCodeChallengeMethod; use oauth2_types::{ pkce::{CodeChallengeError, CodeChallengeMethodExt}, @@ -158,11 +156,9 @@ pub struct AuthorizationGrant { pub scope: Scope, pub state: Option, pub nonce: Option, - pub max_age: Option, pub response_mode: ResponseMode, pub response_type_id_token: bool, pub created_at: DateTime, - pub requires_consent: bool, pub login_hint: Option, } @@ -174,18 +170,7 @@ impl std::ops::Deref for AuthorizationGrant { } } -const DEFAULT_MAX_AGE: Duration = Duration::microseconds(3600 * 24 * 365 * 1000 * 1000); - impl AuthorizationGrant { - #[must_use] - pub fn max_auth_time(&self) -> DateTime { - let max_age = self - .max_age - .and_then(|x| Duration::try_seconds(x.get().into())) - .unwrap_or(DEFAULT_MAX_AGE); - self.created_at - max_age - } - #[must_use] pub fn parse_login_hint(&self, homeserver: &str) -> LoginHint { let Some(login_hint) = &self.login_hint else { @@ -274,11 +259,9 @@ impl AuthorizationGrant { scope: Scope::from_iter([OPENID, PROFILE]), state: Some(Alphanumeric.sample_string(rng, 10)), nonce: Some(Alphanumeric.sample_string(rng, 10)), - max_age: None, response_mode: ResponseMode::Query, response_type_id_token: false, created_at: now, - requires_consent: false, login_hint: Some(String::from("mxid:@example-user:example.com")), } } diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 3b7f15c02..4d610482f 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -371,10 +371,6 @@ where get(self::views::login::get).post(self::views::login::post), ) .route(mas_router::Logout::route(), post(self::views::logout::post)) - .route( - mas_router::Reauth::route(), - get(self::views::reauth::get).post(self::views::reauth::post), - ) .route( mas_router::Register::route(), get(self::views::register::get), @@ -409,13 +405,10 @@ where mas_router::OAuth2AuthorizationEndpoint::route(), get(self::oauth2::authorization::get), ) - .route( - mas_router::ContinueAuthorizationGrant::route(), - get(self::oauth2::authorization::complete::get), - ) .route( mas_router::Consent::route(), - get(self::oauth2::consent::get).post(self::oauth2::consent::post), + get(self::oauth2::authorization::consent::get) + .post(self::oauth2::authorization::consent::post), ) .route( mas_router::CompatLoginSsoComplete::route(), diff --git a/crates/handlers/src/oauth2/authorization/callback.rs b/crates/handlers/src/oauth2/authorization/callback.rs index b76722d5a..beb0868d7 100644 --- a/crates/handlers/src/oauth2/authorization/callback.rs +++ b/crates/handlers/src/oauth2/authorization/callback.rs @@ -101,7 +101,7 @@ impl CallbackDestination { }) } - pub async fn go( + pub fn go( self, templates: &Templates, locale: &DataLocale, diff --git a/crates/handlers/src/oauth2/authorization/complete.rs b/crates/handlers/src/oauth2/authorization/complete.rs deleted file mode 100644 index bfd07531b..000000000 --- a/crates/handlers/src/oauth2/authorization/complete.rs +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2022-2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -use axum::{ - extract::{Path, State}, - response::{Html, IntoResponse, Response}, -}; -use axum_extra::TypedHeader; -use hyper::StatusCode; -use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID}; -use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device}; -use mas_keystore::Keystore; -use mas_policy::{EvaluationResult, Policy}; -use mas_router::{PostAuthAction, UrlBuilder}; -use mas_storage::{ - BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess, - oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository}, - user::BrowserSessionRepository, -}; -use mas_templates::{PolicyViolationContext, TemplateContext, Templates}; -use oauth2_types::requests::AuthorizationResponse; -use thiserror::Error; -use tracing::warn; -use ulid::Ulid; - -use super::callback::CallbackDestination; -use crate::{ - BoundActivityTracker, PreferredLanguage, impl_from_error_for_route, oauth2::generate_id_token, -}; - -#[derive(Debug, Error)] -pub enum RouteError { - #[error(transparent)] - Internal(Box), - - #[error("authorization grant was not found")] - NotFound, - - #[error("authorization grant is not in a pending state")] - NotPending, - - #[error("failed to load client")] - NoSuchClient, -} - -impl IntoResponse for RouteError { - fn into_response(self) -> axum::response::Response { - let event = sentry::capture_error(&self); - // TODO: better error pages - let response = match self { - RouteError::NotFound => { - (StatusCode::NOT_FOUND, "authorization grant was not found").into_response() - } - RouteError::NotPending => ( - StatusCode::BAD_REQUEST, - "authorization grant not in a pending state", - ) - .into_response(), - RouteError::Internal(_) | Self::NoSuchClient => { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() - } - }; - - (SentryEventID::from(event), response).into_response() - } -} - -impl_from_error_for_route!(mas_storage::RepositoryError); -impl_from_error_for_route!(mas_templates::TemplateError); -impl_from_error_for_route!(mas_policy::LoadError); -impl_from_error_for_route!(mas_policy::EvaluationError); -impl_from_error_for_route!(super::callback::IntoCallbackDestinationError); -impl_from_error_for_route!(super::callback::CallbackDestinationError); - -#[tracing::instrument( - name = "handlers.oauth2.authorization_complete.get", - fields(grant.id = %grant_id), - skip_all, - err, -)] -pub(crate) async fn get( - mut rng: BoxRng, - clock: BoxClock, - PreferredLanguage(locale): PreferredLanguage, - State(templates): State, - State(url_builder): State, - State(key_store): State, - policy: Policy, - activity_tracker: BoundActivityTracker, - user_agent: Option>, - mut repo: BoxRepository, - cookie_jar: CookieJar, - Path(grant_id): Path, -) -> Result { - let (session_info, cookie_jar) = cookie_jar.session_info(); - - let maybe_session = session_info.load_active_session(&mut repo).await?; - - let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string()); - - let grant = repo - .oauth2_authorization_grant() - .lookup(grant_id) - .await? - .ok_or(RouteError::NotFound)?; - - let callback_destination = CallbackDestination::try_from(&grant)?; - let continue_grant = PostAuthAction::continue_grant(grant.id); - - let Some(session) = maybe_session else { - // If there is no session, redirect to the login screen, redirecting here after - // logout - return Ok(( - cookie_jar, - url_builder.redirect(&mas_router::Login::and_then(continue_grant)), - ) - .into_response()); - }; - - activity_tracker - .record_browser_session(&clock, &session) - .await; - - let client = repo - .oauth2_client() - .lookup(grant.client_id) - .await? - .ok_or(RouteError::NoSuchClient)?; - - match complete( - &mut rng, - &clock, - &activity_tracker, - user_agent, - repo, - key_store, - policy, - &url_builder, - grant, - &client, - &session, - ) - .await - { - Ok(params) => { - let res = callback_destination.go(&templates, &locale, params).await?; - Ok((cookie_jar, res).into_response()) - } - Err(GrantCompletionError::RequiresReauth) => Ok(( - cookie_jar, - url_builder.redirect(&mas_router::Reauth::and_then(continue_grant)), - ) - .into_response()), - Err(GrantCompletionError::RequiresConsent) => { - let next = mas_router::Consent(grant_id); - Ok((cookie_jar, url_builder.redirect(&next)).into_response()) - } - Err(GrantCompletionError::PolicyViolation(grant, res)) => { - warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id); - - let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); - let ctx = PolicyViolationContext::for_authorization_grant(grant, client) - .with_session(session) - .with_csrf(csrf_token.form_value()) - .with_language(locale); - - let content = templates.render_policy_violation(&ctx)?; - - Ok((cookie_jar, Html(content)).into_response()) - } - Err(GrantCompletionError::NotPending) => Err(RouteError::NotPending), - Err(GrantCompletionError::Internal(e)) => Err(RouteError::Internal(e)), - } -} - -#[derive(Debug, Error)] -pub enum GrantCompletionError { - #[error(transparent)] - Internal(Box), - - #[error("authorization grant is not in a pending state")] - NotPending, - - #[error("user needs to reauthenticate")] - RequiresReauth, - - #[error("client lacks consent")] - RequiresConsent, - - #[error("denied by the policy")] - PolicyViolation(AuthorizationGrant, EvaluationResult), -} - -impl_from_error_for_route!(GrantCompletionError: mas_storage::RepositoryError); -impl_from_error_for_route!(GrantCompletionError: super::callback::IntoCallbackDestinationError); -impl_from_error_for_route!(GrantCompletionError: mas_policy::LoadError); -impl_from_error_for_route!(GrantCompletionError: mas_policy::EvaluationError); -impl_from_error_for_route!(GrantCompletionError: super::super::IdTokenSignatureError); - -pub(crate) async fn complete( - rng: &mut (impl rand::RngCore + rand::CryptoRng + Send), - clock: &impl Clock, - activity_tracker: &BoundActivityTracker, - user_agent: Option, - mut repo: BoxRepository, - key_store: Keystore, - mut policy: Policy, - url_builder: &UrlBuilder, - grant: AuthorizationGrant, - client: &Client, - browser_session: &BrowserSession, -) -> Result { - // Verify that the grant is in a pending stage - if !grant.stage.is_pending() { - return Err(GrantCompletionError::NotPending); - } - - // Check if the authentication is fresh enough - let authentication = repo - .browser_session() - .get_last_authentication(browser_session) - .await?; - let authentication = authentication.filter(|auth| auth.created_at > grant.max_auth_time()); - - let Some(valid_authentication) = authentication else { - repo.save().await?; - return Err(GrantCompletionError::RequiresReauth); - }; - - // Run through the policy - let res = policy - .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { - user: Some(&browser_session.user), - client, - scope: &grant.scope, - grant_type: mas_policy::GrantType::AuthorizationCode, - requester: mas_policy::Requester { - ip_address: activity_tracker.ip(), - user_agent, - }, - }) - .await?; - - if !res.valid() { - return Err(GrantCompletionError::PolicyViolation(grant, res)); - } - - let current_consent = repo - .oauth2_client() - .get_consent_for_user(client, &browser_session.user) - .await?; - - let lacks_consent = grant - .scope - .difference(¤t_consent) - .filter(|scope| Device::from_scope_token(scope).is_none()) - .any(|_| true); - - // Check if the client lacks consent *or* if consent was explicitly asked - if lacks_consent || grant.requires_consent { - repo.save().await?; - return Err(GrantCompletionError::RequiresConsent); - } - - // All good, let's start the session - let session = repo - .oauth2_session() - .add_from_browser_session(rng, clock, client, browser_session, grant.scope.clone()) - .await?; - - let grant = repo - .oauth2_authorization_grant() - .fulfill(clock, &session, grant) - .await?; - - // Yep! Let's complete the auth now - let mut params = AuthorizationResponse::default(); - - // Did they request an ID token? - if grant.response_type_id_token { - params.id_token = Some(generate_id_token( - rng, - clock, - url_builder, - &key_store, - client, - Some(&grant), - browser_session, - None, - Some(&valid_authentication), - )?); - } - - // Did they request an auth code? - if let Some(code) = grant.code { - params.code = Some(code.code); - } - - repo.save().await?; - - activity_tracker - .record_oauth2_session(clock, &session) - .await; - - Ok(params) -} diff --git a/crates/handlers/src/oauth2/consent.rs b/crates/handlers/src/oauth2/authorization/consent.rs similarity index 70% rename from crates/handlers/src/oauth2/consent.rs rename to crates/handlers/src/oauth2/authorization/consent.rs index 599ba080d..273542383 100644 --- a/crates/handlers/src/oauth2/consent.rs +++ b/crates/handlers/src/oauth2/authorization/consent.rs @@ -15,7 +15,8 @@ use mas_axum_utils::{ csrf::{CsrfExt, ProtectedForm}, sentry::SentryEventID, }; -use mas_data_model::{AuthorizationGrantStage, Device}; +use mas_data_model::AuthorizationGrantStage; +use mas_keystore::Keystore; use mas_policy::Policy; use mas_router::{PostAuthAction, UrlBuilder}; use mas_storage::{ @@ -23,11 +24,14 @@ use mas_storage::{ oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository}, }; use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates}; +use oauth2_types::requests::AuthorizationResponse; use thiserror::Error; use ulid::Ulid; +use super::callback::CallbackDestination; use crate::{ BoundActivityTracker, PreferredLanguage, impl_from_error_for_route, + oauth2::generate_id_token, session::{SessionOrFallback, load_session_or_fallback}, }; @@ -45,9 +49,6 @@ pub enum RouteError { #[error("Authorization grant already used")] GrantNotPending, - #[error("Policy violation")] - PolicyViolation, - #[error("Failed to load client")] NoSuchClient, } @@ -57,20 +58,24 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl_from_error_for_route!(mas_policy::LoadError); impl_from_error_for_route!(mas_policy::EvaluationError); impl_from_error_for_route!(crate::session::SessionLoadError); +impl_from_error_for_route!(crate::oauth2::IdTokenSignatureError); +impl_from_error_for_route!(super::callback::IntoCallbackDestinationError); +impl_from_error_for_route!(super::callback::CallbackDestinationError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let event_id = sentry::capture_error(&self); ( - SentryEventID::from(event_id), StatusCode::INTERNAL_SERVER_ERROR, + SentryEventID::from(event_id), + self.to_string(), ) .into_response() } } #[tracing::instrument( - name = "handlers.oauth2.consent.get", + name = "handlers.oauth2.authorization.consent.get", fields(grant.id = %grant_id), skip_all, err, @@ -142,17 +147,7 @@ pub(crate) async fn get( }, }) .await?; - - if res.valid() { - let ctx = ConsentContext::new(grant, client) - .with_session(session) - .with_csrf(csrf_token.form_value()) - .with_language(locale); - - let content = templates.render_consent(&ctx)?; - - Ok((cookie_jar, Html(content)).into_response()) - } else { + if !res.valid() { let ctx = PolicyViolationContext::for_authorization_grant(grant, client) .with_session(session) .with_csrf(csrf_token.form_value()) @@ -160,12 +155,21 @@ pub(crate) async fn get( let content = templates.render_policy_violation(&ctx)?; - Ok((cookie_jar, Html(content)).into_response()) + return Ok((cookie_jar, Html(content)).into_response()); } + + let ctx = ConsentContext::new(grant, client) + .with_session(session) + .with_csrf(csrf_token.form_value()) + .with_language(locale); + + let content = templates.render_consent(&ctx)?; + + Ok((cookie_jar, Html(content)).into_response()) } #[tracing::instrument( - name = "handlers.oauth2.consent.post", + name = "handlers.oauth2.authorization.consent.post", fields(grant.id = %grant_id), skip_all, err, @@ -175,6 +179,7 @@ pub(crate) async fn post( clock: BoxClock, PreferredLanguage(locale): PreferredLanguage, State(templates): State, + State(key_store): State, mut policy: Policy, mut repo: BoxRepository, activity_tracker: BoundActivityTracker, @@ -199,6 +204,8 @@ pub(crate) async fn post( SessionOrFallback::Fallback { response } => return Ok(response), }; + let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); + let user_agent = user_agent.map(|ua| ua.to_string()); let grant = repo @@ -206,15 +213,16 @@ pub(crate) async fn post( .lookup(grant_id) .await? .ok_or(RouteError::GrantNotFound)?; - let next = PostAuthAction::continue_grant(grant_id); + let callback_destination = CallbackDestination::try_from(&grant)?; - let Some(session) = maybe_session else { + let Some(browser_session) = maybe_session else { + let next = PostAuthAction::continue_grant(grant_id); let login = mas_router::Login::and_then(next); return Ok((cookie_jar, url_builder.redirect(&login)).into_response()); }; activity_tracker - .record_browser_session(&clock, &session) + .record_browser_session(&clock, &browser_session) .await; let client = repo @@ -225,7 +233,7 @@ pub(crate) async fn post( let res = policy .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { - user: Some(&session.user), + user: Some(&browser_session.user), client: &client, scope: &grant.scope, grant_type: mas_policy::GrantType::AuthorizationCode, @@ -237,32 +245,70 @@ pub(crate) async fn post( .await?; if !res.valid() { - return Err(RouteError::PolicyViolation); - } + let ctx = PolicyViolationContext::for_authorization_grant(grant, client) + .with_session(browser_session) + .with_csrf(csrf_token.form_value()) + .with_language(locale); - // Do not consent for the "urn:matrix:org.matrix.msc2967.client:device:*" scope - let scope_without_device = grant - .scope - .iter() - .filter(|s| Device::from_scope_token(s).is_none()) - .cloned() - .collect(); + let content = templates.render_policy_violation(&ctx)?; + + return Ok((cookie_jar, Html(content)).into_response()); + } - repo.oauth2_client() - .give_consent_for_user( + // All good, let's start the session + let session = repo + .oauth2_session() + .add_from_browser_session( &mut rng, &clock, &client, - &session.user, - &scope_without_device, + &browser_session, + grant.scope.clone(), ) .await?; - repo.oauth2_authorization_grant() - .give_consent(grant) + let grant = repo + .oauth2_authorization_grant() + .fulfill(&clock, &session, grant) .await?; + let mut params = AuthorizationResponse::default(); + + // Did they request an ID token? + if grant.response_type_id_token { + // Fetch the last authentication + let last_authentication = repo + .browser_session() + .get_last_authentication(&browser_session) + .await?; + + params.id_token = Some(generate_id_token( + &mut rng, + &clock, + &url_builder, + &key_store, + &client, + Some(&grant), + &browser_session, + None, + last_authentication.as_ref(), + )?); + } + + // Did they request an auth code? + if let Some(code) = grant.code { + params.code = Some(code.code); + } + repo.save().await?; - Ok((cookie_jar, next.go_next(&url_builder)).into_response()) + activity_tracker + .record_oauth2_session(&clock, &session) + .await; + + Ok(( + cookie_jar, + callback_destination.go(&templates, &locale, params)?, + ) + .into_response()) } diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index 54d0641e3..7e131f812 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -6,20 +6,17 @@ use axum::{ extract::{Form, State}, - response::{Html, IntoResponse, Response}, + response::{IntoResponse, Response}, }; -use axum_extra::TypedHeader; use hyper::StatusCode; -use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID}; +use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, sentry::SentryEventID}; use mas_data_model::{AuthorizationCode, Pkce}; -use mas_keystore::Keystore; -use mas_policy::Policy; use mas_router::{PostAuthAction, UrlBuilder}; use mas_storage::{ BoxClock, BoxRepository, BoxRng, oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository}, }; -use mas_templates::{PolicyViolationContext, TemplateContext, Templates}; +use mas_templates::Templates; use oauth2_types::{ errors::{ClientError, ClientErrorCode}, pkce, @@ -29,13 +26,12 @@ use oauth2_types::{ use rand::{Rng, distributions::Alphanumeric}; use serde::Deserialize; use thiserror::Error; -use tracing::warn; -use self::{callback::CallbackDestination, complete::GrantCompletionError}; +use self::callback::CallbackDestination; use crate::{BoundActivityTracker, PreferredLanguage, impl_from_error_for_route}; mod callback; -pub mod complete; +pub(crate) mod consent; #[derive(Debug, Error)] pub enum RouteError { @@ -134,10 +130,7 @@ pub(crate) async fn get( clock: BoxClock, PreferredLanguage(locale): PreferredLanguage, State(templates): State, - State(key_store): State, State(url_builder): State, - policy: Policy, - user_agent: Option>, activity_tracker: BoundActivityTracker, mut repo: BoxRepository, cookie_jar: CookieJar, @@ -166,9 +159,6 @@ pub(crate) async fn get( // Get the session info from the cookie let (session_info, cookie_jar) = cookie_jar.session_info(); - let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); - - let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string()); // One day, we will have try blocks let res: Result = ({ @@ -182,80 +172,66 @@ pub(crate) async fn get( // Check if the request/request_uri/registration params are used. If so, reply // with the right error since we don't support them. if params.auth.request.is_some() { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::RequestNotSupported), - ) - .await?); + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::RequestNotSupported), + )?); } if params.auth.request_uri.is_some() { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::RequestUriNotSupported), - ) - .await?); + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::RequestUriNotSupported), + )?); } // Check if the client asked for a `token` response type, and bail out if it's // the case, since we don't support them if response_type.has_token() { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::UnsupportedResponseType), - ) - .await?); + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::UnsupportedResponseType), + )?); } // If the client asked for a `id_token` response type, we must check if it can // use the `implicit` grant type if response_type.has_id_token() && !client.grant_types.contains(&GrantType::Implicit) { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::UnauthorizedClient), - ) - .await?); + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::UnauthorizedClient), + )?); } if params.auth.registration.is_some() { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::RegistrationNotSupported), - ) - .await?); + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::RegistrationNotSupported), + )?); } - // Fail early if prompt=none and there is no active session - if prompt.contains(&Prompt::None) && maybe_session.is_none() { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::LoginRequired), - ) - .await?); + // Fail early if prompt=none; we never let it go through + if prompt.contains(&Prompt::None) { + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::LoginRequired), + )?); } let code: Option = if response_type.has_code() { // Check if it is allowed to use this grant type if !client.grant_types.contains(&GrantType::AuthorizationCode) { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::UnauthorizedClient), - ) - .await?); + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::UnauthorizedClient), + )?); } // 32 random alphanumeric characters, about 190bit of entropy @@ -275,20 +251,16 @@ pub(crate) async fn get( // If the request had PKCE params but no code asked, it should get back with an // error if params.pkce.is_some() { - return Ok(callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::InvalidRequest), - ) - .await?); + return Ok(callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::InvalidRequest), + )?); } None }; - let requires_consent = prompt.contains(&Prompt::Consent); - let grant = repo .oauth2_authorization_grant() .add( @@ -300,151 +272,43 @@ pub(crate) async fn get( code, params.auth.state.clone(), params.auth.nonce, - params.auth.max_age, response_mode, response_type.has_id_token(), - requires_consent, params.auth.login_hint, ) .await?; let continue_grant = PostAuthAction::continue_grant(grant.id); let res = match maybe_session { - // Cases where there is no active session, redirect to the relevant page - None if prompt.contains(&Prompt::None) => { - // This case should already be handled earlier - unreachable!(); - } None if prompt.contains(&Prompt::Create) => { // Client asked for a registration, show the registration prompt repo.save().await?; - url_builder.redirect(&mas_router::Register::and_then(continue_grant)) + url_builder + .redirect(&mas_router::Register::and_then(continue_grant)) .into_response() } + None => { // Other cases where we don't have a session, ask for a login repo.save().await?; - url_builder.redirect(&mas_router::Login::and_then(continue_grant)) + url_builder + .redirect(&mas_router::Login::and_then(continue_grant)) .into_response() } - // Special case when we already have a session but prompt=login|select_account - Some(session) - if prompt.contains(&Prompt::Login) - || prompt.contains(&Prompt::SelectAccount) => - { - // TODO: better pages here + Some(user_session) => { + // TODO: better support for prompt=create when we have a session repo.save().await?; - activity_tracker.record_browser_session(&clock, &session).await; - - url_builder.redirect(&mas_router::Reauth::and_then(continue_grant)) + activity_tracker + .record_browser_session(&clock, &user_session) + .await; + url_builder + .redirect(&mas_router::Consent(grant.id)) .into_response() } - - // Else, we immediately try to complete the authorization grant - Some(user_session) if prompt.contains(&Prompt::None) => { - activity_tracker.record_browser_session(&clock, &user_session).await; - - // With prompt=none, we should get back to the client immediately - match self::complete::complete( - &mut rng, - &clock, - &activity_tracker, - user_agent, - repo, - key_store, - policy, - &url_builder, - grant, - &client, - &user_session, - ) - .await - { - Ok(params) => callback_destination.go(&templates, &locale, params).await?, - Err(GrantCompletionError::RequiresConsent) => { - callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::ConsentRequired), - ) - .await? - } - Err(GrantCompletionError::RequiresReauth) => { - callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::InteractionRequired), - ) - .await? - } - Err(GrantCompletionError::PolicyViolation(_grant, _res)) => { - callback_destination - .go(&templates, &locale, ClientError::from(ClientErrorCode::AccessDenied)) - .await? - } - Err(GrantCompletionError::Internal(e)) => { - return Err(RouteError::Internal(e)) - } - Err(e @ GrantCompletionError::NotPending) => { - // This should never happen - return Err(RouteError::Internal(Box::new(e))); - } - } - } - Some(user_session) => { - activity_tracker.record_browser_session(&clock, &user_session).await; - - let grant_id = grant.id; - // Else, we show the relevant reauth/consent page if necessary - match self::complete::complete( - &mut rng, - &clock, - &activity_tracker, - user_agent, - repo, - key_store, - policy, - &url_builder, - grant, - &client, - &user_session, - ) - .await - { - Ok(params) => callback_destination.go(&templates, &locale, params).await?, - Err(GrantCompletionError::RequiresConsent) => { - url_builder.redirect(&mas_router::Consent(grant_id)).into_response() - } - Err(GrantCompletionError::PolicyViolation(grant, res)) => { - warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id); - - let ctx = PolicyViolationContext::for_authorization_grant(grant, client) - .with_session(user_session) - .with_csrf(csrf_token.form_value()) - .with_language(locale); - - let content = templates.render_policy_violation(&ctx)?; - Html(content).into_response() - } - Err(GrantCompletionError::RequiresReauth) => { - url_builder.redirect(&mas_router::Reauth::and_then(continue_grant)) - .into_response() - } - Err(GrantCompletionError::Internal(e)) => { - return Err(RouteError::Internal(e)) - } - Err(e @ GrantCompletionError::NotPending) => { - // This should never happen - return Err(RouteError::Internal(Box::new(e))); - } - } - } }; Ok(res) @@ -456,13 +320,11 @@ pub(crate) async fn get( Ok(r) => r, Err(err) => { tracing::error!(%err); - callback_destination - .go( - &templates, - &locale, - ClientError::from(ClientErrorCode::ServerError), - ) - .await? + callback_destination.go( + &templates, + &locale, + ClientError::from(ClientErrorCode::ServerError), + )? } }; diff --git a/crates/handlers/src/oauth2/discovery.rs b/crates/handlers/src/oauth2/discovery.rs index e72ebb358..bdefa3e62 100644 --- a/crates/handlers/src/oauth2/discovery.rs +++ b/crates/handlers/src/oauth2/discovery.rs @@ -132,7 +132,7 @@ pub(crate) async fn get( let request_uri_parameter_supported = Some(false); let prompt_values_supported = Some({ - let mut v = vec![Prompt::None, Prompt::Login]; + let mut v = vec![Prompt::Login]; // Advertise for prompt=create if password registration is enabled // TODO: we may want to be able to forward that to upstream providers if they // support it diff --git a/crates/handlers/src/oauth2/mod.rs b/crates/handlers/src/oauth2/mod.rs index c80d7212b..f15a1ae9d 100644 --- a/crates/handlers/src/oauth2/mod.rs +++ b/crates/handlers/src/oauth2/mod.rs @@ -23,7 +23,6 @@ use mas_storage::{Clock, RepositoryAccess}; use thiserror::Error; pub mod authorization; -pub mod consent; pub mod device; pub mod discovery; pub mod introspection; diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index c1a842be7..d361bcc99 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -978,10 +978,8 @@ mod tests { }), Some("state".to_owned()), Some("nonce".to_owned()), - None, ResponseMode::Query, false, - false, None, ) .await @@ -1079,10 +1077,8 @@ mod tests { }), Some("state".to_owned()), Some("nonce".to_owned()), - None, ResponseMode::Query, false, - false, None, ) .await diff --git a/crates/handlers/src/views/mod.rs b/crates/handlers/src/views/mod.rs index 336ec9f2f..5d5c615e8 100644 --- a/crates/handlers/src/views/mod.rs +++ b/crates/handlers/src/views/mod.rs @@ -8,7 +8,6 @@ pub mod app; pub mod index; pub mod login; pub mod logout; -pub mod reauth; pub mod recovery; pub mod register; pub mod shared; diff --git a/crates/handlers/src/views/reauth.rs b/crates/handlers/src/views/reauth.rs deleted file mode 100644 index d7f238c71..000000000 --- a/crates/handlers/src/views/reauth.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2021-2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -use anyhow::Context; -use axum::{ - extract::{Form, Query, State}, - response::{Html, IntoResponse, Response}, -}; -use hyper::StatusCode; -use mas_axum_utils::{ - FancyError, SessionInfoExt, - cookies::CookieJar, - csrf::{CsrfExt, ProtectedForm}, -}; -use mas_router::UrlBuilder; -use mas_storage::{ - BoxClock, BoxRepository, BoxRng, - user::{BrowserSessionRepository, UserPasswordRepository}, -}; -use mas_templates::{ReauthContext, TemplateContext, Templates}; -use serde::Deserialize; -use zeroize::Zeroizing; - -use super::shared::OptionalPostAuthAction; -use crate::{ - BoundActivityTracker, PreferredLanguage, SiteConfig, - passwords::PasswordManager, - session::{SessionOrFallback, load_session_or_fallback}, -}; - -#[derive(Deserialize, Debug)] -pub(crate) struct ReauthForm { - password: String, -} - -#[tracing::instrument(name = "handlers.views.reauth.get", skip_all, err)] -pub(crate) async fn get( - mut rng: BoxRng, - clock: BoxClock, - PreferredLanguage(locale): PreferredLanguage, - State(templates): State, - State(url_builder): State, - State(site_config): State, - activity_tracker: BoundActivityTracker, - mut repo: BoxRepository, - Query(query): Query, - cookie_jar: CookieJar, -) -> Result { - if !site_config.password_login_enabled { - // XXX: do something better here - return Ok(url_builder - .redirect(&mas_router::Account::default()) - .into_response()); - } - - let (cookie_jar, maybe_session) = match load_session_or_fallback( - cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo, - ) - .await? - { - SessionOrFallback::MaybeSession { - cookie_jar, - maybe_session, - .. - } => (cookie_jar, maybe_session), - SessionOrFallback::Fallback { response } => return Ok(response), - }; - - let Some(session) = maybe_session else { - // If there is no session, redirect to the login screen, keeping the - // PostAuthAction - let login = mas_router::Login::from(query.post_auth_action); - return Ok((cookie_jar, url_builder.redirect(&login)).into_response()); - }; - - let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); - - activity_tracker - .record_browser_session(&clock, &session) - .await; - - let ctx = ReauthContext::default(); - let next = query.load_context(&mut repo).await?; - let ctx = if let Some(next) = next { - ctx.with_post_action(next) - } else { - ctx - }; - let ctx = ctx - .with_session(session) - .with_csrf(csrf_token.form_value()) - .with_language(locale); - - let content = templates.render_reauth(&ctx)?; - - Ok((cookie_jar, Html(content)).into_response()) -} - -#[tracing::instrument(name = "handlers.views.reauth.post", skip_all, err)] -pub(crate) async fn post( - mut rng: BoxRng, - clock: BoxClock, - PreferredLanguage(locale): PreferredLanguage, - State(templates): State, - State(password_manager): State, - State(url_builder): State, - State(site_config): State, - mut repo: BoxRepository, - Query(query): Query, - cookie_jar: CookieJar, - Form(form): Form>, -) -> Result { - if !site_config.password_login_enabled { - // XXX: do something better here - return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()); - } - - let form = cookie_jar.verify_form(&clock, form)?; - - let (cookie_jar, maybe_session) = match load_session_or_fallback( - cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo, - ) - .await? - { - SessionOrFallback::MaybeSession { - cookie_jar, - maybe_session, - .. - } => (cookie_jar, maybe_session), - SessionOrFallback::Fallback { response } => return Ok(response), - }; - - let Some(session) = maybe_session else { - // If there is no session, redirect to the login screen, keeping the - // PostAuthAction - let login = mas_router::Login::from(query.post_auth_action); - return Ok((cookie_jar, url_builder.redirect(&login)).into_response()); - }; - - // Load the user password - let user_password = repo - .user_password() - .active(&session.user) - .await? - .context("User has no password")?; - - let password = Zeroizing::new(form.password.as_bytes().to_vec()); - - // TODO: recover from errors - // Verify the password, and upgrade it on-the-fly if needed - let new_password_hash = password_manager - .verify_and_upgrade( - &mut rng, - user_password.version, - password, - user_password.hashed_password.clone(), - ) - .await?; - - let user_password = if let Some((version, new_password_hash)) = new_password_hash { - // Save the upgraded password - repo.user_password() - .add( - &mut rng, - &clock, - &session.user, - version, - new_password_hash, - Some(&user_password), - ) - .await? - } else { - user_password - }; - - // Mark the session as authenticated by the password - repo.browser_session() - .authenticate_with_password(&mut rng, &clock, &session, &user_password) - .await?; - - let cookie_jar = cookie_jar.set_session(&session); - repo.save().await?; - - let reply = query.go_next(&url_builder); - Ok((cookie_jar, reply).into_response()) -} diff --git a/crates/router/src/endpoints.rs b/crates/router/src/endpoints.rs index 059dda31e..89a9d726e 100644 --- a/crates/router/src/endpoints.rs +++ b/crates/router/src/endpoints.rs @@ -60,9 +60,7 @@ impl PostAuthAction { pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect { match self { - Self::ContinueAuthorizationGrant { id } => { - url_builder.redirect(&ContinueAuthorizationGrant(*id)) - } + Self::ContinueAuthorizationGrant { id } => url_builder.redirect(&Consent(*id)), Self::ContinueDeviceCodeGrant { id } => { url_builder.redirect(&DeviceCodeConsent::new(*id)) } @@ -255,66 +253,6 @@ impl SimpleRoute for Logout { const PATH: &'static str = "/logout"; } -/// `GET|POST /reauth` -#[derive(Default, Debug, Clone)] -pub struct Reauth { - post_auth_action: Option, -} - -impl Reauth { - #[must_use] - pub fn and_then(action: PostAuthAction) -> Self { - Self { - post_auth_action: Some(action), - } - } - - #[must_use] - pub fn and_continue_grant(data: Ulid) -> Self { - Self { - post_auth_action: Some(PostAuthAction::continue_grant(data)), - } - } - - #[must_use] - pub fn and_continue_device_code_grant(data: Ulid) -> Self { - Self { - post_auth_action: Some(PostAuthAction::continue_device_code_grant(data)), - } - } - - /// Get a reference to the reauth's post auth action. - #[must_use] - pub fn post_auth_action(&self) -> Option<&PostAuthAction> { - self.post_auth_action.as_ref() - } - - pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect { - match &self.post_auth_action { - Some(action) => action.go_next(url_builder), - None => url_builder.redirect(&Index), - } - } -} - -impl Route for Reauth { - type Query = PostAuthAction; - - fn route() -> &'static str { - "/reauth" - } - - fn query(&self) -> Option<&Self::Query> { - self.post_auth_action.as_ref() - } -} - -impl From> for Reauth { - fn from(post_auth_action: Option) -> Self { - Self { post_auth_action } - } -} - /// `POST /register` #[derive(Default, Debug, Clone)] pub struct Register { @@ -581,21 +519,6 @@ impl SimpleRoute for AccountPasswordChange { const PATH: &'static str = "/account/password/change"; } -/// `GET /authorize/{grant_id}` -#[derive(Debug, Clone)] -pub struct ContinueAuthorizationGrant(pub Ulid); - -impl Route for ContinueAuthorizationGrant { - type Query = (); - fn route() -> &'static str { - "/authorize/{grant_id}" - } - - fn path(&self) -> std::borrow::Cow<'static, str> { - format!("/authorize/{}", self.0).into() - } -} - /// `GET /consent/{grant_id}` #[derive(Debug, Clone)] pub struct Consent(pub Ulid); diff --git a/crates/storage-pg/.sqlx/query-854cc8cd3c1fc3dbbdf4ce81b561aafadb0f4e98caeaba01597c6f62875ae691.json b/crates/storage-pg/.sqlx/query-854cc8cd3c1fc3dbbdf4ce81b561aafadb0f4e98caeaba01597c6f62875ae691.json deleted file mode 100644 index 0e114d418..000000000 --- a/crates/storage-pg/.sqlx/query-854cc8cd3c1fc3dbbdf4ce81b561aafadb0f4e98caeaba01597c6f62875ae691.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n max_age,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n requires_consent,\n login_hint,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Int4", - "Text", - "Text", - "Text", - "Bool", - "Bool", - "Text", - "Bool", - "Text", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "854cc8cd3c1fc3dbbdf4ce81b561aafadb0f4e98caeaba01597c6f62875ae691" -} diff --git a/crates/storage-pg/.sqlx/query-1d9c478c7a5e3a672610376a290b9a1afaaa6fa2fb137f7307002f058b206dbd.json b/crates/storage-pg/.sqlx/query-890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251.json similarity index 75% rename from crates/storage-pg/.sqlx/query-1d9c478c7a5e3a672610376a290b9a1afaaa6fa2fb137f7307002f058b206dbd.json rename to crates/storage-pg/.sqlx/query-890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251.json index 3f0d2177d..d8fd25487 100644 --- a/crates/storage-pg/.sqlx/query-1d9c478c7a5e3a672610376a290b9a1afaaa6fa2fb137f7307002f058b206dbd.json +++ b/crates/storage-pg/.sqlx/query-890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , max_age\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , requires_consent\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ", + "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ", "describe": { "columns": [ { @@ -55,51 +55,41 @@ }, { "ordinal": 10, - "name": "max_age", - "type_info": "Int4" - }, - { - "ordinal": 11, "name": "oauth2_client_id", "type_info": "Uuid" }, { - "ordinal": 12, + "ordinal": 11, "name": "authorization_code", "type_info": "Text" }, { - "ordinal": 13, + "ordinal": 12, "name": "response_type_code", "type_info": "Bool" }, { - "ordinal": 14, + "ordinal": 13, "name": "response_type_id_token", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 14, "name": "code_challenge", "type_info": "Text" }, { - "ordinal": 16, + "ordinal": 15, "name": "code_challenge_method", "type_info": "Text" }, { - "ordinal": 17, - "name": "requires_consent", - "type_info": "Bool" - }, - { - "ordinal": 18, + "ordinal": 16, "name": "login_hint", "type_info": "Text" }, { - "ordinal": 19, + "ordinal": 17, "name": "oauth2_session_id", "type_info": "Uuid" } @@ -120,17 +110,15 @@ false, false, true, - true, false, true, false, false, true, true, - false, true, true ] }, - "hash": "1d9c478c7a5e3a672610376a290b9a1afaaa6fa2fb137f7307002f058b206dbd" + "hash": "890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251" } diff --git a/crates/storage-pg/.sqlx/query-8b7297c263336d70c2b647212b16f7ae39bc5cb1572e3a2dcfcd67f196a1fa39.json b/crates/storage-pg/.sqlx/query-8b7297c263336d70c2b647212b16f7ae39bc5cb1572e3a2dcfcd67f196a1fa39.json deleted file mode 100644 index 0389ab030..000000000 --- a/crates/storage-pg/.sqlx/query-8b7297c263336d70c2b647212b16f7ae39bc5cb1572e3a2dcfcd67f196a1fa39.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT scope_token\n FROM oauth2_consents\n WHERE user_id = $1 AND oauth2_client_id = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "scope_token", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "8b7297c263336d70c2b647212b16f7ae39bc5cb1572e3a2dcfcd67f196a1fa39" -} diff --git a/crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json b/crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json new file mode 100644 index 000000000..2f372898b --- /dev/null +++ b/crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json @@ -0,0 +1,27 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28" +} diff --git a/crates/storage-pg/.sqlx/query-9a6c197ff4ad80217262d48f8792ce7e16bc5df0677c7cd4ecb4fdbc5ee86395.json b/crates/storage-pg/.sqlx/query-9a6c197ff4ad80217262d48f8792ce7e16bc5df0677c7cd4ecb4fdbc5ee86395.json deleted file mode 100644 index 07d5aa55f..000000000 --- a/crates/storage-pg/.sqlx/query-9a6c197ff4ad80217262d48f8792ce7e16bc5df0677c7cd4ecb4fdbc5ee86395.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO oauth2_consents\n (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at)\n SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token)\n ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "Uuid", - "Uuid", - "TextArray", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "9a6c197ff4ad80217262d48f8792ce7e16bc5df0677c7cd4ecb4fdbc5ee86395" -} diff --git a/crates/storage-pg/.sqlx/query-e0d3be7e741581430e3e4719c7e19596837234c94a398570bdac42652c2c4652.json b/crates/storage-pg/.sqlx/query-bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4.json similarity index 75% rename from crates/storage-pg/.sqlx/query-e0d3be7e741581430e3e4719c7e19596837234c94a398570bdac42652c2c4652.json rename to crates/storage-pg/.sqlx/query-bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4.json index 485b7fd9d..7a52e4781 100644 --- a/crates/storage-pg/.sqlx/query-e0d3be7e741581430e3e4719c7e19596837234c94a398570bdac42652c2c4652.json +++ b/crates/storage-pg/.sqlx/query-bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , max_age\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , requires_consent\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ", + "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ", "describe": { "columns": [ { @@ -55,51 +55,41 @@ }, { "ordinal": 10, - "name": "max_age", - "type_info": "Int4" - }, - { - "ordinal": 11, "name": "oauth2_client_id", "type_info": "Uuid" }, { - "ordinal": 12, + "ordinal": 11, "name": "authorization_code", "type_info": "Text" }, { - "ordinal": 13, + "ordinal": 12, "name": "response_type_code", "type_info": "Bool" }, { - "ordinal": 14, + "ordinal": 13, "name": "response_type_id_token", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 14, "name": "code_challenge", "type_info": "Text" }, { - "ordinal": 16, + "ordinal": 15, "name": "code_challenge_method", "type_info": "Text" }, { - "ordinal": 17, - "name": "requires_consent", - "type_info": "Bool" - }, - { - "ordinal": 18, + "ordinal": 16, "name": "login_hint", "type_info": "Text" }, { - "ordinal": 19, + "ordinal": 17, "name": "oauth2_session_id", "type_info": "Uuid" } @@ -120,17 +110,15 @@ false, false, true, - true, false, true, false, false, true, true, - false, true, true ] }, - "hash": "e0d3be7e741581430e3e4719c7e19596837234c94a398570bdac42652c2c4652" + "hash": "bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4" } diff --git a/crates/storage-pg/.sqlx/query-d83421d4a16f4ad084dd0db5abb56d3688851c36a48a50aa6104e8291e73630d.json b/crates/storage-pg/.sqlx/query-d83421d4a16f4ad084dd0db5abb56d3688851c36a48a50aa6104e8291e73630d.json deleted file mode 100644 index 2d671aa52..000000000 --- a/crates/storage-pg/.sqlx/query-d83421d4a16f4ad084dd0db5abb56d3688851c36a48a50aa6104e8291e73630d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.oauth2_authorization_grant_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "d83421d4a16f4ad084dd0db5abb56d3688851c36a48a50aa6104e8291e73630d" -} diff --git a/crates/storage-pg/migrations/20250410174306_oauth2_authorization_default_requires_consent.sql b/crates/storage-pg/migrations/20250410174306_oauth2_authorization_default_requires_consent.sql new file mode 100644 index 000000000..05960c32e --- /dev/null +++ b/crates/storage-pg/migrations/20250410174306_oauth2_authorization_default_requires_consent.sql @@ -0,0 +1,9 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + +-- We stopped reading/writing to this column, but it's not nullable. +-- So we need to add a default value, and drop it in the next release +ALTER TABLE oauth2_authorization_grants + ALTER COLUMN requires_consent SET DEFAULT false; diff --git a/crates/storage-pg/src/oauth2/authorization_grant.rs b/crates/storage-pg/src/oauth2/authorization_grant.rs index 7a6162171..d619573e7 100644 --- a/crates/storage-pg/src/oauth2/authorization_grant.rs +++ b/crates/storage-pg/src/oauth2/authorization_grant.rs @@ -4,8 +4,6 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::num::NonZeroU32; - use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{ @@ -48,13 +46,11 @@ struct GrantLookup { nonce: Option, redirect_uri: String, response_mode: String, - max_age: Option, response_type_code: bool, response_type_id_token: bool, authorization_code: Option, code_challenge: Option, code_challenge_method: Option, - requires_consent: bool, login_hint: Option, oauth2_client_id: Uuid, oauth2_session_id: Option, @@ -153,25 +149,6 @@ impl TryFrom for AuthorizationGrant { .source(e) })?; - let max_age = value - .max_age - .map(u32::try_from) - .transpose() - .map_err(|e| { - DatabaseInconsistencyError::on("oauth2_authorization_grants") - .column("max_age") - .row(id) - .source(e) - })? - .map(NonZeroU32::try_from) - .transpose() - .map_err(|e| { - DatabaseInconsistencyError::on("oauth2_authorization_grants") - .column("max_age") - .row(id) - .source(e) - })?; - Ok(AuthorizationGrant { id, stage, @@ -180,12 +157,10 @@ impl TryFrom for AuthorizationGrant { scope, state: value.state, nonce: value.nonce, - max_age, response_mode, redirect_uri, created_at: value.created_at, response_type_id_token: value.response_type_id_token, - requires_consent: value.requires_consent, login_hint: value.login_hint, }) } @@ -216,10 +191,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository code: Option, state: Option, nonce: Option, - max_age: Option, response_mode: ResponseMode, response_type_id_token: bool, - requires_consent: bool, login_hint: Option, ) -> Result { let code_challenge = code @@ -230,8 +203,6 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository .as_ref() .and_then(|c| c.pkce.as_ref()) .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 created_at = clock.now(); @@ -247,19 +218,17 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository scope, state, nonce, - max_age, response_mode, code_challenge, code_challenge_method, response_type_code, response_type_id_token, authorization_code, - requires_consent, login_hint, created_at ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) "#, Uuid::from(id), Uuid::from(client.id), @@ -267,14 +236,12 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository scope.to_string(), state, nonce, - max_age_i32, response_mode.to_string(), code_challenge, code_challenge_method, code.is_some(), response_type_id_token, code_str, - requires_consent, login_hint, created_at, ) @@ -291,11 +258,9 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository scope, state, nonce, - max_age, response_mode, created_at, response_type_id_token, - requires_consent, login_hint, }) } @@ -323,14 +288,12 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository , redirect_uri , response_mode , nonce - , max_age , oauth2_client_id , authorization_code , response_type_code , response_type_id_token , code_challenge , code_challenge_method - , requires_consent , login_hint , oauth2_session_id FROM @@ -374,14 +337,12 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository , redirect_uri , response_mode , nonce - , max_age , oauth2_client_id , authorization_code , response_type_code , response_type_id_token , code_challenge , code_challenge_method - , requires_consent , login_hint , oauth2_session_id FROM @@ -480,37 +441,4 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository Ok(grant) } - - #[tracing::instrument( - name = "db.oauth2_authorization_grant.give_consent", - skip_all, - fields( - db.query.text, - %grant.id, - client.id = %grant.client_id, - ), - err, - )] - async fn give_consent( - &mut self, - mut grant: AuthorizationGrant, - ) -> Result { - sqlx::query!( - r#" - UPDATE oauth2_authorization_grants AS og - SET - requires_consent = 'f' - WHERE - og.oauth2_authorization_grant_id = $1 - "#, - Uuid::from(grant.id), - ) - .traced() - .execute(&mut *self.conn) - .await?; - - grant.requires_consent = false; - - Ok(grant) - } } diff --git a/crates/storage-pg/src/oauth2/client.rs b/crates/storage-pg/src/oauth2/client.rs index b846e80d4..02e57a01a 100644 --- a/crates/storage-pg/src/oauth2/client.rs +++ b/crates/storage-pg/src/oauth2/client.rs @@ -6,20 +6,15 @@ use std::{ collections::{BTreeMap, BTreeSet}, - str::FromStr, string::ToString, }; use async_trait::async_trait; -use mas_data_model::{Client, JwksOrJwksUri, User}; +use mas_data_model::{Client, JwksOrJwksUri}; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::jwk::PublicJsonWebKeySet; use mas_storage::{Clock, oauth2::OAuth2ClientRepository}; -use oauth2_types::{ - oidc::ApplicationType, - requests::GrantType, - scope::{Scope, ScopeToken}, -}; +use oauth2_types::{oidc::ApplicationType, requests::GrantType}; use opentelemetry_semantic_conventions::attribute::DB_QUERY_TEXT; use rand::RngCore; use sqlx::PgConnection; @@ -698,97 +693,6 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { .collect() } - #[tracing::instrument( - name = "db.oauth2_client.get_consent_for_user", - skip_all, - fields( - db.query.text, - %user.id, - %client.id, - ), - err, - )] - async fn get_consent_for_user( - &mut self, - client: &Client, - user: &User, - ) -> Result { - let scope_tokens: Vec = sqlx::query_scalar!( - r#" - SELECT scope_token - FROM oauth2_consents - WHERE user_id = $1 AND oauth2_client_id = $2 - "#, - Uuid::from(user.id), - Uuid::from(client.id), - ) - .fetch_all(&mut *self.conn) - .await?; - - let scope: Result = scope_tokens - .into_iter() - .map(|s| ScopeToken::from_str(&s)) - .collect(); - - let scope = scope.map_err(|e| { - DatabaseInconsistencyError::on("oauth2_consents") - .column("scope_token") - .source(e) - })?; - - Ok(scope) - } - - #[tracing::instrument( - name = "db.oauth2_client.give_consent_for_user", - skip_all, - fields( - db.query.text, - %user.id, - %client.id, - %scope, - ), - err, - )] - async fn give_consent_for_user( - &mut self, - rng: &mut (dyn RngCore + Send), - clock: &dyn Clock, - client: &Client, - user: &User, - scope: &Scope, - ) -> Result<(), Self::Error> { - let now = clock.now(); - let (tokens, ids): (Vec, Vec) = scope - .iter() - .map(|token| { - ( - token.to_string(), - Uuid::from(Ulid::from_datetime_with_source(now.into(), rng)), - ) - }) - .unzip(); - - sqlx::query!( - r#" - INSERT INTO oauth2_consents - (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at) - 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 - "#, - &ids, - Uuid::from(user.id), - Uuid::from(client.id), - &tokens, - now, - ) - .traced() - .execute(&mut *self.conn) - .await?; - - Ok(()) - } - #[tracing::instrument( name = "db.oauth2_client.delete_by_id", skip_all, diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index a3aadc2ad..5968e625d 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -135,10 +135,8 @@ mod tests { }), Some("state".to_owned()), Some("nonce".to_owned()), - None, ResponseMode::Query, true, - false, None, ) .await @@ -175,29 +173,6 @@ mod tests { .await .unwrap(); - // Lookup the consent the user gave to the client - let consent = repo - .oauth2_client() - .get_consent_for_user(&client, &user) - .await - .unwrap(); - assert!(consent.is_empty()); - - // Give consent to the client - let scope = Scope::from_iter([OPENID]); - repo.oauth2_client() - .give_consent_for_user(&mut rng, &clock, &client, &user, &scope) - .await - .unwrap(); - - // Lookup the consent the user gave to the client - let consent = repo - .oauth2_client() - .get_consent_for_user(&client, &user) - .await - .unwrap(); - assert_eq!(scope, consent); - // Lookup a non-existing session let session = repo.oauth2_session().lookup(Ulid::nil()).await.unwrap(); assert_eq!(session, None); diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index cff0ba418..7724ace87 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -4,8 +4,6 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::num::NonZeroU32; - use async_trait::async_trait; use mas_data_model::{AuthorizationCode, AuthorizationGrant, Client, Session}; use oauth2_types::{requests::ResponseMode, scope::Scope}; @@ -37,12 +35,9 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync { /// `response_type` was requested /// * `state`: The state the client sent, if set /// * `nonce`: The nonce the client sent, if set - /// * `max_age`: The maximum age since the user last authenticated, if asked - /// by the client /// * `response_mode`: The response mode the client requested /// * `response_type_id_token`: Whether the `id_token` `response_type` was /// requested - /// * `requires_consent`: Whether the client explicitly requested consent /// * `login_hint`: The login_hint the client sent, if set /// /// # Errors @@ -59,10 +54,8 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync { code: Option, state: Option, nonce: Option, - max_age: Option, response_mode: ResponseMode, response_type_id_token: bool, - requires_consent: bool, login_hint: Option, ) -> Result; @@ -131,22 +124,6 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync { clock: &dyn Clock, authorization_grant: AuthorizationGrant, ) -> Result; - - /// Unset the `requires_consent` flag on an authorization grant - /// - /// Returns the updated authorization grant - /// - /// # Parameters - /// - /// * `authorization_grant`: The authorization grant to update - /// - /// # Errors - /// - /// Returns [`Self::Error`] if the underlying repository fails - async fn give_consent( - &mut self, - authorization_grant: AuthorizationGrant, - ) -> Result; } repository_impl!(OAuth2AuthorizationGrantRepository: @@ -160,10 +137,8 @@ repository_impl!(OAuth2AuthorizationGrantRepository: code: Option, state: Option, nonce: Option, - max_age: Option, response_mode: ResponseMode, response_type_id_token: bool, - requires_consent: bool, login_hint: Option, ) -> Result; @@ -184,9 +159,4 @@ repository_impl!(OAuth2AuthorizationGrantRepository: clock: &dyn Clock, authorization_grant: AuthorizationGrant, ) -> Result; - - async fn give_consent( - &mut self, - authorization_grant: AuthorizationGrant, - ) -> Result; ); diff --git a/crates/storage/src/oauth2/client.rs b/crates/storage/src/oauth2/client.rs index 113fe3e37..aa5a82a2a 100644 --- a/crates/storage/src/oauth2/client.rs +++ b/crates/storage/src/oauth2/client.rs @@ -7,10 +7,10 @@ use std::collections::{BTreeMap, BTreeSet}; use async_trait::async_trait; -use mas_data_model::{Client, User}; +use mas_data_model::Client; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; use mas_jose::jwk::PublicJsonWebKeySet; -use oauth2_types::{oidc::ApplicationType, requests::GrantType, scope::Scope}; +use oauth2_types::{oidc::ApplicationType, requests::GrantType}; use rand_core::RngCore; use ulid::Ulid; use url::Url; @@ -171,45 +171,6 @@ pub trait OAuth2ClientRepository: Send + Sync { /// Returns [`Self::Error`] if the underlying repository fails async fn all_static(&mut self) -> Result, Self::Error>; - /// Get the list of scopes that the user has given consent for the given - /// client - /// - /// # Parameters - /// - /// * `client`: The client to get the consent for - /// * `user`: The user to get the consent for - /// - /// # Errors - /// - /// Returns [`Self::Error`] if the underlying repository fails - async fn get_consent_for_user( - &mut self, - client: &Client, - user: &User, - ) -> Result; - - /// Give consent for a set of scopes for the given client and user - /// - /// # Parameters - /// - /// * `rng`: The random number generator to use - /// * `clock`: The clock used to generate timestamps - /// * `client`: The client to give the consent for - /// * `user`: The user to give the consent for - /// * `scope`: The scope to give consent for - /// - /// # Errors - /// - /// Returns [`Self::Error`] if the underlying repository fails - async fn give_consent_for_user( - &mut self, - rng: &mut (dyn RngCore + Send), - clock: &dyn Clock, - client: &Client, - user: &User, - scope: &Scope, - ) -> Result<(), Self::Error>; - /// Delete a client /// /// # Parameters @@ -288,19 +249,4 @@ repository_impl!(OAuth2ClientRepository: async fn delete(&mut self, client: Client) -> Result<(), Self::Error>; async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error>; - - async fn get_consent_for_user( - &mut self, - client: &Client, - user: &User, - ) -> Result; - - async fn give_consent_for_user( - &mut self, - rng: &mut (dyn RngCore + Send), - clock: &dyn Clock, - client: &Client, - user: &User, - scope: &Scope, - ) -> Result<(), Self::Error>; ); diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 26ed200e1..b6661d540 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -381,7 +381,7 @@ impl FormField for LoginFormField { } } -/// Inner context used in login and reauth screens. See [`PostAuthContext`]. +/// Inner context used in login screen. See [`PostAuthContext`]. #[derive(Serialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum PostAuthContextInner { @@ -420,7 +420,7 @@ pub enum PostAuthContextInner { ManageAccount, } -/// Context used in login and reauth screens, for the post-auth action to do +/// Context used in login screen, for the post-auth action to do #[derive(Serialize)] pub struct PostAuthContext { /// The post auth action params from the URL @@ -734,59 +734,6 @@ impl PolicyViolationContext { } } -/// Fields of the reauthentication form -#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum ReauthFormField { - /// The password field - Password, -} - -impl FormField for ReauthFormField { - fn keep(&self) -> bool { - match self { - Self::Password => false, - } - } -} - -/// Context used by the `reauth.html` template -#[derive(Serialize, Default)] -pub struct ReauthContext { - form: FormState, - next: Option, -} - -impl TemplateContext for ReauthContext { - fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec - where - Self: Sized, - { - // TODO: samples with errors - vec![ReauthContext { - form: FormState::default(), - next: None, - }] - } -} - -impl ReauthContext { - /// Add an error on the reauthentication form - #[must_use] - pub fn with_form_state(self, form: FormState) -> Self { - Self { form, ..self } - } - - /// Add a post authentication action to the context - #[must_use] - pub fn with_post_action(self, next: PostAuthContext) -> Self { - Self { - next: Some(next), - ..self - } - } -} - /// Context used by the `sso.html` template #[derive(Serialize)] pub struct CompatSsoContext { diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 982b3fc02..05422359d 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -38,10 +38,10 @@ pub use self::{ DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext, EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext, - PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext, - ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField, - RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext, - RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField, + PolicyViolationContext, PostAuthContext, PostAuthContextInner, RecoveryExpiredContext, + RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext, + RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField, + RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField, RegisterStepsEmailInUseContext, RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField, @@ -372,9 +372,6 @@ register_templates! { /// Render the account recovery disabled page pub fn render_recovery_disabled(WithLanguage) { "pages/recovery/disabled.html" } - /// Render the re-authentication form - pub fn render_reauth(WithLanguage>>) { "pages/reauth.html" } - /// Render the form used by the form_post response mode pub fn render_form_post(WithLanguage>) { "form_post.html" } @@ -456,7 +453,6 @@ impl Templates { check::render_recovery_expired(self, now, rng)?; check::render_recovery_consumed(self, now, rng)?; check::render_recovery_disabled(self, now, rng)?; - check::render_reauth(self, now, rng)?; check::render_form_post::(self, now, rng)?; check::render_error(self, now, rng)?; check::render_email_verification_txt(self, now, rng)?;