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 OR LicenseRef-Element-Commercial
5// Please see LICENSE files 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    GenericError, InternalError,
15    cookies::CookieJar,
16    csrf::{CsrfExt, ProtectedForm},
17};
18use mas_data_model::{AuthorizationGrantStage, BoxClock, BoxRng};
19use mas_keystore::Keystore;
20use mas_policy::Policy;
21use mas_router::{PostAuthAction, UrlBuilder};
22use mas_storage::{
23    BoxRepository,
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        match self {
68            Self::Internal(e) => InternalError::new(e).into_response(),
69            e @ Self::NoSuchClient(_) => InternalError::new(Box::new(e)).into_response(),
70            e @ Self::GrantNotFound => GenericError::new(StatusCode::NOT_FOUND, e).into_response(),
71            e @ Self::GrantNotPending(_) => {
72                GenericError::new(StatusCode::CONFLICT, e).into_response()
73            }
74            e @ Self::Csrf(_) => GenericError::new(StatusCode::BAD_REQUEST, e).into_response(),
75        }
76    }
77}
78
79#[tracing::instrument(
80    name = "handlers.oauth2.authorization.consent.get",
81    fields(grant.id = %grant_id),
82    skip_all,
83)]
84pub(crate) async fn get(
85    mut rng: BoxRng,
86    clock: BoxClock,
87    PreferredLanguage(locale): PreferredLanguage,
88    State(templates): State<Templates>,
89    State(url_builder): State<UrlBuilder>,
90    mut policy: Policy,
91    mut repo: BoxRepository,
92    activity_tracker: BoundActivityTracker,
93    user_agent: Option<TypedHeader<headers::UserAgent>>,
94    cookie_jar: CookieJar,
95    Path(grant_id): Path<Ulid>,
96) -> Result<Response, RouteError> {
97    let (cookie_jar, maybe_session) = match load_session_or_fallback(
98        cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
99    )
100    .await?
101    {
102        SessionOrFallback::MaybeSession {
103            cookie_jar,
104            maybe_session,
105            ..
106        } => (cookie_jar, maybe_session),
107        SessionOrFallback::Fallback { response } => return Ok(response),
108    };
109
110    let user_agent = user_agent.map(|ua| ua.to_string());
111
112    let grant = repo
113        .oauth2_authorization_grant()
114        .lookup(grant_id)
115        .await?
116        .ok_or(RouteError::GrantNotFound)?;
117
118    let client = repo
119        .oauth2_client()
120        .lookup(grant.client_id)
121        .await?
122        .ok_or(RouteError::NoSuchClient(grant.client_id))?;
123
124    if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
125        return Err(RouteError::GrantNotPending(grant.id));
126    }
127
128    let Some(session) = maybe_session else {
129        let login = mas_router::Login::and_continue_grant(grant_id);
130        return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
131    };
132
133    activity_tracker
134        .record_browser_session(&clock, &session)
135        .await;
136
137    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
138
139    let res = policy
140        .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
141            user: Some(&session.user),
142            client: &client,
143            scope: &grant.scope,
144            grant_type: mas_policy::GrantType::AuthorizationCode,
145            requester: mas_policy::Requester {
146                ip_address: activity_tracker.ip(),
147                user_agent,
148            },
149        })
150        .await?;
151    if !res.valid() {
152        let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
153            .with_session(session)
154            .with_csrf(csrf_token.form_value())
155            .with_language(locale);
156
157        let content = templates.render_policy_violation(&ctx)?;
158
159        return Ok((cookie_jar, Html(content)).into_response());
160    }
161
162    let ctx = ConsentContext::new(grant, client)
163        .with_session(session)
164        .with_csrf(csrf_token.form_value())
165        .with_language(locale);
166
167    let content = templates.render_consent(&ctx)?;
168
169    Ok((cookie_jar, Html(content)).into_response())
170}
171
172#[tracing::instrument(
173    name = "handlers.oauth2.authorization.consent.post",
174    fields(grant.id = %grant_id),
175    skip_all,
176)]
177pub(crate) async fn post(
178    mut rng: BoxRng,
179    clock: BoxClock,
180    PreferredLanguage(locale): PreferredLanguage,
181    State(templates): State<Templates>,
182    State(key_store): State<Keystore>,
183    mut policy: Policy,
184    mut repo: BoxRepository,
185    activity_tracker: BoundActivityTracker,
186    user_agent: Option<TypedHeader<headers::UserAgent>>,
187    cookie_jar: CookieJar,
188    State(url_builder): State<UrlBuilder>,
189    Path(grant_id): Path<Ulid>,
190    Form(form): Form<ProtectedForm<()>>,
191) -> Result<Response, RouteError> {
192    cookie_jar.verify_form(&clock, form)?;
193
194    let (cookie_jar, maybe_session) = match load_session_or_fallback(
195        cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
196    )
197    .await?
198    {
199        SessionOrFallback::MaybeSession {
200            cookie_jar,
201            maybe_session,
202            ..
203        } => (cookie_jar, maybe_session),
204        SessionOrFallback::Fallback { response } => return Ok(response),
205    };
206
207    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
208
209    let user_agent = user_agent.map(|ua| ua.to_string());
210
211    let grant = repo
212        .oauth2_authorization_grant()
213        .lookup(grant_id)
214        .await?
215        .ok_or(RouteError::GrantNotFound)?;
216    let callback_destination = CallbackDestination::try_from(&grant)?;
217
218    let Some(browser_session) = maybe_session else {
219        let next = PostAuthAction::continue_grant(grant_id);
220        let login = mas_router::Login::and_then(next);
221        return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
222    };
223
224    activity_tracker
225        .record_browser_session(&clock, &browser_session)
226        .await;
227
228    let client = repo
229        .oauth2_client()
230        .lookup(grant.client_id)
231        .await?
232        .ok_or(RouteError::NoSuchClient(grant.client_id))?;
233
234    if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
235        return Err(RouteError::GrantNotPending(grant.id));
236    }
237
238    let res = policy
239        .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
240            user: Some(&browser_session.user),
241            client: &client,
242            scope: &grant.scope,
243            grant_type: mas_policy::GrantType::AuthorizationCode,
244            requester: mas_policy::Requester {
245                ip_address: activity_tracker.ip(),
246                user_agent,
247            },
248        })
249        .await?;
250
251    if !res.valid() {
252        let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
253            .with_session(browser_session)
254            .with_csrf(csrf_token.form_value())
255            .with_language(locale);
256
257        let content = templates.render_policy_violation(&ctx)?;
258
259        return Ok((cookie_jar, Html(content)).into_response());
260    }
261
262    // All good, let's start the session
263    let session = repo
264        .oauth2_session()
265        .add_from_browser_session(
266            &mut rng,
267            &clock,
268            &client,
269            &browser_session,
270            grant.scope.clone(),
271        )
272        .await?;
273
274    let grant = repo
275        .oauth2_authorization_grant()
276        .fulfill(&clock, &session, grant)
277        .await?;
278
279    let mut params = AuthorizationResponse::default();
280
281    // Did they request an ID token?
282    if grant.response_type_id_token {
283        // Fetch the last authentication
284        let last_authentication = repo
285            .browser_session()
286            .get_last_authentication(&browser_session)
287            .await?;
288
289        params.id_token = Some(generate_id_token(
290            &mut rng,
291            &clock,
292            &url_builder,
293            &key_store,
294            &client,
295            Some(&grant),
296            &browser_session,
297            None,
298            last_authentication.as_ref(),
299        )?);
300    }
301
302    // Did they request an auth code?
303    if let Some(code) = grant.code {
304        params.code = Some(code.code);
305    }
306
307    repo.save().await?;
308
309    activity_tracker
310        .record_oauth2_session(&clock, &session)
311        .await;
312
313    Ok((
314        cookie_jar,
315        callback_destination.go(&templates, &locale, params)?,
316    )
317        .into_response())
318}