mas_handlers/oauth2/
consent.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use axum::{
8    extract::{Form, Path, State},
9    response::{Html, IntoResponse, Response},
10};
11use axum_extra::TypedHeader;
12use hyper::StatusCode;
13use mas_axum_utils::{
14    cookies::CookieJar,
15    csrf::{CsrfExt, ProtectedForm},
16    sentry::SentryEventID,
17};
18use mas_data_model::{AuthorizationGrantStage, Device};
19use mas_policy::Policy;
20use mas_router::{PostAuthAction, UrlBuilder};
21use mas_storage::{
22    BoxClock, BoxRepository, BoxRng,
23    oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
24};
25use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
26use thiserror::Error;
27use ulid::Ulid;
28
29use crate::{
30    BoundActivityTracker, PreferredLanguage, impl_from_error_for_route,
31    session::{SessionOrFallback, load_session_or_fallback},
32};
33
34#[derive(Debug, Error)]
35pub enum RouteError {
36    #[error(transparent)]
37    Internal(Box<dyn std::error::Error + Send + Sync>),
38
39    #[error(transparent)]
40    Csrf(#[from] mas_axum_utils::csrf::CsrfError),
41
42    #[error("Authorization grant not found")]
43    GrantNotFound,
44
45    #[error("Authorization grant already used")]
46    GrantNotPending,
47
48    #[error("Policy violation")]
49    PolicyViolation,
50
51    #[error("Failed to load client")]
52    NoSuchClient,
53}
54
55impl_from_error_for_route!(mas_templates::TemplateError);
56impl_from_error_for_route!(mas_storage::RepositoryError);
57impl_from_error_for_route!(mas_policy::LoadError);
58impl_from_error_for_route!(mas_policy::EvaluationError);
59impl_from_error_for_route!(crate::session::SessionLoadError);
60
61impl IntoResponse for RouteError {
62    fn into_response(self) -> axum::response::Response {
63        let event_id = sentry::capture_error(&self);
64        (
65            SentryEventID::from(event_id),
66            StatusCode::INTERNAL_SERVER_ERROR,
67        )
68            .into_response()
69    }
70}
71
72#[tracing::instrument(
73    name = "handlers.oauth2.consent.get",
74    fields(grant.id = %grant_id),
75    skip_all,
76    err,
77)]
78pub(crate) async fn get(
79    mut rng: BoxRng,
80    clock: BoxClock,
81    PreferredLanguage(locale): PreferredLanguage,
82    State(templates): State<Templates>,
83    State(url_builder): State<UrlBuilder>,
84    mut policy: Policy,
85    mut repo: BoxRepository,
86    activity_tracker: BoundActivityTracker,
87    user_agent: Option<TypedHeader<headers::UserAgent>>,
88    cookie_jar: CookieJar,
89    Path(grant_id): Path<Ulid>,
90) -> Result<Response, RouteError> {
91    let (cookie_jar, maybe_session) = match load_session_or_fallback(
92        cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
93    )
94    .await?
95    {
96        SessionOrFallback::MaybeSession {
97            cookie_jar,
98            maybe_session,
99            ..
100        } => (cookie_jar, maybe_session),
101        SessionOrFallback::Fallback { response } => return Ok(response),
102    };
103
104    let user_agent = user_agent.map(|ua| ua.to_string());
105
106    let grant = repo
107        .oauth2_authorization_grant()
108        .lookup(grant_id)
109        .await?
110        .ok_or(RouteError::GrantNotFound)?;
111
112    let client = repo
113        .oauth2_client()
114        .lookup(grant.client_id)
115        .await?
116        .ok_or(RouteError::NoSuchClient)?;
117
118    if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
119        return Err(RouteError::GrantNotPending);
120    }
121
122    let Some(session) = maybe_session else {
123        let login = mas_router::Login::and_continue_grant(grant_id);
124        return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
125    };
126
127    activity_tracker
128        .record_browser_session(&clock, &session)
129        .await;
130
131    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
132
133    let res = policy
134        .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
135            user: Some(&session.user),
136            client: &client,
137            scope: &grant.scope,
138            grant_type: mas_policy::GrantType::AuthorizationCode,
139            requester: mas_policy::Requester {
140                ip_address: activity_tracker.ip(),
141                user_agent,
142            },
143        })
144        .await?;
145
146    if res.valid() {
147        let ctx = ConsentContext::new(grant, client)
148            .with_session(session)
149            .with_csrf(csrf_token.form_value())
150            .with_language(locale);
151
152        let content = templates.render_consent(&ctx)?;
153
154        Ok((cookie_jar, Html(content)).into_response())
155    } else {
156        let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
157            .with_session(session)
158            .with_csrf(csrf_token.form_value())
159            .with_language(locale);
160
161        let content = templates.render_policy_violation(&ctx)?;
162
163        Ok((cookie_jar, Html(content)).into_response())
164    }
165}
166
167#[tracing::instrument(
168    name = "handlers.oauth2.consent.post",
169    fields(grant.id = %grant_id),
170    skip_all,
171    err,
172)]
173pub(crate) async fn post(
174    mut rng: BoxRng,
175    clock: BoxClock,
176    PreferredLanguage(locale): PreferredLanguage,
177    State(templates): State<Templates>,
178    mut policy: Policy,
179    mut repo: BoxRepository,
180    activity_tracker: BoundActivityTracker,
181    user_agent: Option<TypedHeader<headers::UserAgent>>,
182    cookie_jar: CookieJar,
183    State(url_builder): State<UrlBuilder>,
184    Path(grant_id): Path<Ulid>,
185    Form(form): Form<ProtectedForm<()>>,
186) -> Result<Response, RouteError> {
187    cookie_jar.verify_form(&clock, form)?;
188
189    let (cookie_jar, maybe_session) = match load_session_or_fallback(
190        cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
191    )
192    .await?
193    {
194        SessionOrFallback::MaybeSession {
195            cookie_jar,
196            maybe_session,
197            ..
198        } => (cookie_jar, maybe_session),
199        SessionOrFallback::Fallback { response } => return Ok(response),
200    };
201
202    let user_agent = user_agent.map(|ua| ua.to_string());
203
204    let grant = repo
205        .oauth2_authorization_grant()
206        .lookup(grant_id)
207        .await?
208        .ok_or(RouteError::GrantNotFound)?;
209    let next = PostAuthAction::continue_grant(grant_id);
210
211    let Some(session) = maybe_session else {
212        let login = mas_router::Login::and_then(next);
213        return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
214    };
215
216    activity_tracker
217        .record_browser_session(&clock, &session)
218        .await;
219
220    let client = repo
221        .oauth2_client()
222        .lookup(grant.client_id)
223        .await?
224        .ok_or(RouteError::NoSuchClient)?;
225
226    let res = policy
227        .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
228            user: Some(&session.user),
229            client: &client,
230            scope: &grant.scope,
231            grant_type: mas_policy::GrantType::AuthorizationCode,
232            requester: mas_policy::Requester {
233                ip_address: activity_tracker.ip(),
234                user_agent,
235            },
236        })
237        .await?;
238
239    if !res.valid() {
240        return Err(RouteError::PolicyViolation);
241    }
242
243    // Do not consent for the "urn:matrix:org.matrix.msc2967.client:device:*" scope
244    let scope_without_device = grant
245        .scope
246        .iter()
247        .filter(|s| Device::from_scope_token(s).is_none())
248        .cloned()
249        .collect();
250
251    repo.oauth2_client()
252        .give_consent_for_user(
253            &mut rng,
254            &clock,
255            &client,
256            &session.user,
257            &scope_without_device,
258        )
259        .await?;
260
261    repo.oauth2_authorization_grant()
262        .give_consent(grant)
263        .await?;
264
265    repo.save().await?;
266
267    Ok((cookie_jar, next.go_next(&url_builder)).into_response())
268}