mas_handlers/oauth2/authorization/
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    record_error,
17};
18use mas_data_model::AuthorizationGrantStage;
19use mas_keystore::Keystore;
20use mas_policy::Policy;
21use mas_router::{PostAuthAction, UrlBuilder};
22use mas_storage::{
23    BoxClock, BoxRepository, BoxRng,
24    oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
25};
26use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
27use oauth2_types::requests::AuthorizationResponse;
28use thiserror::Error;
29use ulid::Ulid;
30
31use super::callback::CallbackDestination;
32use crate::{
33    BoundActivityTracker, PreferredLanguage, impl_from_error_for_route,
34    oauth2::generate_id_token,
35    session::{SessionOrFallback, load_session_or_fallback},
36};
37
38#[derive(Debug, Error)]
39pub enum RouteError {
40    #[error(transparent)]
41    Internal(Box<dyn std::error::Error + Send + Sync>),
42
43    #[error(transparent)]
44    Csrf(#[from] mas_axum_utils::csrf::CsrfError),
45
46    #[error("Authorization grant not found")]
47    GrantNotFound,
48
49    #[error("Authorization grant {0} already used")]
50    GrantNotPending(Ulid),
51
52    #[error("Failed to load client {0}")]
53    NoSuchClient(Ulid),
54}
55
56impl_from_error_for_route!(mas_templates::TemplateError);
57impl_from_error_for_route!(mas_storage::RepositoryError);
58impl_from_error_for_route!(mas_policy::LoadError);
59impl_from_error_for_route!(mas_policy::EvaluationError);
60impl_from_error_for_route!(crate::session::SessionLoadError);
61impl_from_error_for_route!(crate::oauth2::IdTokenSignatureError);
62impl_from_error_for_route!(super::callback::IntoCallbackDestinationError);
63impl_from_error_for_route!(super::callback::CallbackDestinationError);
64
65impl IntoResponse for RouteError {
66    fn into_response(self) -> axum::response::Response {
67        let sentry_event_id = record_error!(self, Self::Internal(_) | Self::NoSuchClient(_));
68        (
69            StatusCode::INTERNAL_SERVER_ERROR,
70            sentry_event_id,
71            self.to_string(),
72        )
73            .into_response()
74    }
75}
76
77#[tracing::instrument(
78    name = "handlers.oauth2.authorization.consent.get",
79    fields(grant.id = %grant_id),
80    skip_all,
81)]
82pub(crate) async fn get(
83    mut rng: BoxRng,
84    clock: BoxClock,
85    PreferredLanguage(locale): PreferredLanguage,
86    State(templates): State<Templates>,
87    State(url_builder): State<UrlBuilder>,
88    mut policy: Policy,
89    mut repo: BoxRepository,
90    activity_tracker: BoundActivityTracker,
91    user_agent: Option<TypedHeader<headers::UserAgent>>,
92    cookie_jar: CookieJar,
93    Path(grant_id): Path<Ulid>,
94) -> Result<Response, RouteError> {
95    let (cookie_jar, maybe_session) = match load_session_or_fallback(
96        cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
97    )
98    .await?
99    {
100        SessionOrFallback::MaybeSession {
101            cookie_jar,
102            maybe_session,
103            ..
104        } => (cookie_jar, maybe_session),
105        SessionOrFallback::Fallback { response } => return Ok(response),
106    };
107
108    let user_agent = user_agent.map(|ua| ua.to_string());
109
110    let grant = repo
111        .oauth2_authorization_grant()
112        .lookup(grant_id)
113        .await?
114        .ok_or(RouteError::GrantNotFound)?;
115
116    let client = repo
117        .oauth2_client()
118        .lookup(grant.client_id)
119        .await?
120        .ok_or(RouteError::NoSuchClient(grant.client_id))?;
121
122    if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
123        return Err(RouteError::GrantNotPending(grant.id));
124    }
125
126    let Some(session) = maybe_session else {
127        let login = mas_router::Login::and_continue_grant(grant_id);
128        return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
129    };
130
131    activity_tracker
132        .record_browser_session(&clock, &session)
133        .await;
134
135    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
136
137    let res = policy
138        .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
139            user: Some(&session.user),
140            client: &client,
141            scope: &grant.scope,
142            grant_type: mas_policy::GrantType::AuthorizationCode,
143            requester: mas_policy::Requester {
144                ip_address: activity_tracker.ip(),
145                user_agent,
146            },
147        })
148        .await?;
149    if !res.valid() {
150        let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
151            .with_session(session)
152            .with_csrf(csrf_token.form_value())
153            .with_language(locale);
154
155        let content = templates.render_policy_violation(&ctx)?;
156
157        return Ok((cookie_jar, Html(content)).into_response());
158    }
159
160    let ctx = ConsentContext::new(grant, client)
161        .with_session(session)
162        .with_csrf(csrf_token.form_value())
163        .with_language(locale);
164
165    let content = templates.render_consent(&ctx)?;
166
167    Ok((cookie_jar, Html(content)).into_response())
168}
169
170#[tracing::instrument(
171    name = "handlers.oauth2.authorization.consent.post",
172    fields(grant.id = %grant_id),
173    skip_all,
174)]
175pub(crate) async fn post(
176    mut rng: BoxRng,
177    clock: BoxClock,
178    PreferredLanguage(locale): PreferredLanguage,
179    State(templates): State<Templates>,
180    State(key_store): State<Keystore>,
181    mut policy: Policy,
182    mut repo: BoxRepository,
183    activity_tracker: BoundActivityTracker,
184    user_agent: Option<TypedHeader<headers::UserAgent>>,
185    cookie_jar: CookieJar,
186    State(url_builder): State<UrlBuilder>,
187    Path(grant_id): Path<Ulid>,
188    Form(form): Form<ProtectedForm<()>>,
189) -> Result<Response, RouteError> {
190    cookie_jar.verify_form(&clock, form)?;
191
192    let (cookie_jar, maybe_session) = match load_session_or_fallback(
193        cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
194    )
195    .await?
196    {
197        SessionOrFallback::MaybeSession {
198            cookie_jar,
199            maybe_session,
200            ..
201        } => (cookie_jar, maybe_session),
202        SessionOrFallback::Fallback { response } => return Ok(response),
203    };
204
205    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
206
207    let user_agent = user_agent.map(|ua| ua.to_string());
208
209    let grant = repo
210        .oauth2_authorization_grant()
211        .lookup(grant_id)
212        .await?
213        .ok_or(RouteError::GrantNotFound)?;
214    let callback_destination = CallbackDestination::try_from(&grant)?;
215
216    let Some(browser_session) = maybe_session else {
217        let next = PostAuthAction::continue_grant(grant_id);
218        let login = mas_router::Login::and_then(next);
219        return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
220    };
221
222    activity_tracker
223        .record_browser_session(&clock, &browser_session)
224        .await;
225
226    let client = repo
227        .oauth2_client()
228        .lookup(grant.client_id)
229        .await?
230        .ok_or(RouteError::NoSuchClient(grant.client_id))?;
231
232    if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
233        return Err(RouteError::GrantNotPending(grant.id));
234    }
235
236    let res = policy
237        .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
238            user: Some(&browser_session.user),
239            client: &client,
240            scope: &grant.scope,
241            grant_type: mas_policy::GrantType::AuthorizationCode,
242            requester: mas_policy::Requester {
243                ip_address: activity_tracker.ip(),
244                user_agent,
245            },
246        })
247        .await?;
248
249    if !res.valid() {
250        let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
251            .with_session(browser_session)
252            .with_csrf(csrf_token.form_value())
253            .with_language(locale);
254
255        let content = templates.render_policy_violation(&ctx)?;
256
257        return Ok((cookie_jar, Html(content)).into_response());
258    }
259
260    // All good, let's start the session
261    let session = repo
262        .oauth2_session()
263        .add_from_browser_session(
264            &mut rng,
265            &clock,
266            &client,
267            &browser_session,
268            grant.scope.clone(),
269        )
270        .await?;
271
272    let grant = repo
273        .oauth2_authorization_grant()
274        .fulfill(&clock, &session, grant)
275        .await?;
276
277    let mut params = AuthorizationResponse::default();
278
279    // Did they request an ID token?
280    if grant.response_type_id_token {
281        // Fetch the last authentication
282        let last_authentication = repo
283            .browser_session()
284            .get_last_authentication(&browser_session)
285            .await?;
286
287        params.id_token = Some(generate_id_token(
288            &mut rng,
289            &clock,
290            &url_builder,
291            &key_store,
292            &client,
293            Some(&grant),
294            &browser_session,
295            None,
296            last_authentication.as_ref(),
297        )?);
298    }
299
300    // Did they request an auth code?
301    if let Some(code) = grant.code {
302        params.code = Some(code.code);
303    }
304
305    repo.save().await?;
306
307    activity_tracker
308        .record_oauth2_session(&clock, &session)
309        .await;
310
311    Ok((
312        cookie_jar,
313        callback_destination.go(&templates, &locale, params)?,
314    )
315        .into_response())
316}