mas_handlers/oauth2/authorization/
complete.rs

1// Copyright 2024 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::{Path, State},
9    response::{Html, IntoResponse, Response},
10};
11use axum_extra::TypedHeader;
12use hyper::StatusCode;
13use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID};
14use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device};
15use mas_keystore::Keystore;
16use mas_policy::{EvaluationResult, Policy};
17use mas_router::{PostAuthAction, UrlBuilder};
18use mas_storage::{
19    BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
20    oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
21    user::BrowserSessionRepository,
22};
23use mas_templates::{PolicyViolationContext, TemplateContext, Templates};
24use oauth2_types::requests::AuthorizationResponse;
25use thiserror::Error;
26use tracing::warn;
27use ulid::Ulid;
28
29use super::callback::CallbackDestination;
30use crate::{
31    BoundActivityTracker, PreferredLanguage, impl_from_error_for_route, oauth2::generate_id_token,
32};
33
34#[derive(Debug, Error)]
35pub enum RouteError {
36    #[error(transparent)]
37    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
38
39    #[error("authorization grant was not found")]
40    NotFound,
41
42    #[error("authorization grant is not in a pending state")]
43    NotPending,
44
45    #[error("failed to load client")]
46    NoSuchClient,
47}
48
49impl IntoResponse for RouteError {
50    fn into_response(self) -> axum::response::Response {
51        let event = sentry::capture_error(&self);
52        // TODO: better error pages
53        let response = match self {
54            RouteError::NotFound => {
55                (StatusCode::NOT_FOUND, "authorization grant was not found").into_response()
56            }
57            RouteError::NotPending => (
58                StatusCode::BAD_REQUEST,
59                "authorization grant not in a pending state",
60            )
61                .into_response(),
62            RouteError::Internal(_) | Self::NoSuchClient => {
63                (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
64            }
65        };
66
67        (SentryEventID::from(event), response).into_response()
68    }
69}
70
71impl_from_error_for_route!(mas_storage::RepositoryError);
72impl_from_error_for_route!(mas_templates::TemplateError);
73impl_from_error_for_route!(mas_policy::LoadError);
74impl_from_error_for_route!(mas_policy::EvaluationError);
75impl_from_error_for_route!(super::callback::IntoCallbackDestinationError);
76impl_from_error_for_route!(super::callback::CallbackDestinationError);
77
78#[tracing::instrument(
79    name = "handlers.oauth2.authorization_complete.get",
80    fields(grant.id = %grant_id),
81    skip_all,
82    err,
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    State(key_store): State<Keystore>,
91    policy: Policy,
92    activity_tracker: BoundActivityTracker,
93    user_agent: Option<TypedHeader<headers::UserAgent>>,
94    mut repo: BoxRepository,
95    cookie_jar: CookieJar,
96    Path(grant_id): Path<Ulid>,
97) -> Result<Response, RouteError> {
98    let (session_info, cookie_jar) = cookie_jar.session_info();
99
100    let maybe_session = session_info.load_active_session(&mut repo).await?;
101
102    let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string());
103
104    let grant = repo
105        .oauth2_authorization_grant()
106        .lookup(grant_id)
107        .await?
108        .ok_or(RouteError::NotFound)?;
109
110    let callback_destination = CallbackDestination::try_from(&grant)?;
111    let continue_grant = PostAuthAction::continue_grant(grant.id);
112
113    let Some(session) = maybe_session else {
114        // If there is no session, redirect to the login screen, redirecting here after
115        // logout
116        return Ok((
117            cookie_jar,
118            url_builder.redirect(&mas_router::Login::and_then(continue_grant)),
119        )
120            .into_response());
121    };
122
123    activity_tracker
124        .record_browser_session(&clock, &session)
125        .await;
126
127    let client = repo
128        .oauth2_client()
129        .lookup(grant.client_id)
130        .await?
131        .ok_or(RouteError::NoSuchClient)?;
132
133    match complete(
134        &mut rng,
135        &clock,
136        &activity_tracker,
137        user_agent,
138        repo,
139        key_store,
140        policy,
141        &url_builder,
142        grant,
143        &client,
144        &session,
145    )
146    .await
147    {
148        Ok(params) => {
149            let res = callback_destination.go(&templates, &locale, params).await?;
150            Ok((cookie_jar, res).into_response())
151        }
152        Err(GrantCompletionError::RequiresReauth) => Ok((
153            cookie_jar,
154            url_builder.redirect(&mas_router::Reauth::and_then(continue_grant)),
155        )
156            .into_response()),
157        Err(GrantCompletionError::RequiresConsent) => {
158            let next = mas_router::Consent(grant_id);
159            Ok((cookie_jar, url_builder.redirect(&next)).into_response())
160        }
161        Err(GrantCompletionError::PolicyViolation(grant, res)) => {
162            warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
163
164            let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
165            let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
166                .with_session(session)
167                .with_csrf(csrf_token.form_value())
168                .with_language(locale);
169
170            let content = templates.render_policy_violation(&ctx)?;
171
172            Ok((cookie_jar, Html(content)).into_response())
173        }
174        Err(GrantCompletionError::NotPending) => Err(RouteError::NotPending),
175        Err(GrantCompletionError::Internal(e)) => Err(RouteError::Internal(e)),
176    }
177}
178
179#[derive(Debug, Error)]
180pub enum GrantCompletionError {
181    #[error(transparent)]
182    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
183
184    #[error("authorization grant is not in a pending state")]
185    NotPending,
186
187    #[error("user needs to reauthenticate")]
188    RequiresReauth,
189
190    #[error("client lacks consent")]
191    RequiresConsent,
192
193    #[error("denied by the policy")]
194    PolicyViolation(AuthorizationGrant, EvaluationResult),
195}
196
197impl_from_error_for_route!(GrantCompletionError: mas_storage::RepositoryError);
198impl_from_error_for_route!(GrantCompletionError: super::callback::IntoCallbackDestinationError);
199impl_from_error_for_route!(GrantCompletionError: mas_policy::LoadError);
200impl_from_error_for_route!(GrantCompletionError: mas_policy::EvaluationError);
201impl_from_error_for_route!(GrantCompletionError: super::super::IdTokenSignatureError);
202
203pub(crate) async fn complete(
204    rng: &mut (impl rand::RngCore + rand::CryptoRng + Send),
205    clock: &impl Clock,
206    activity_tracker: &BoundActivityTracker,
207    user_agent: Option<String>,
208    mut repo: BoxRepository,
209    key_store: Keystore,
210    mut policy: Policy,
211    url_builder: &UrlBuilder,
212    grant: AuthorizationGrant,
213    client: &Client,
214    browser_session: &BrowserSession,
215) -> Result<AuthorizationResponse, GrantCompletionError> {
216    // Verify that the grant is in a pending stage
217    if !grant.stage.is_pending() {
218        return Err(GrantCompletionError::NotPending);
219    }
220
221    // Check if the authentication is fresh enough
222    let authentication = repo
223        .browser_session()
224        .get_last_authentication(browser_session)
225        .await?;
226    let authentication = authentication.filter(|auth| auth.created_at > grant.max_auth_time());
227
228    let Some(valid_authentication) = authentication else {
229        repo.save().await?;
230        return Err(GrantCompletionError::RequiresReauth);
231    };
232
233    // Run through the policy
234    let res = policy
235        .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
236            user: Some(&browser_session.user),
237            client,
238            scope: &grant.scope,
239            grant_type: mas_policy::GrantType::AuthorizationCode,
240            requester: mas_policy::Requester {
241                ip_address: activity_tracker.ip(),
242                user_agent,
243            },
244        })
245        .await?;
246
247    if !res.valid() {
248        return Err(GrantCompletionError::PolicyViolation(grant, res));
249    }
250
251    let current_consent = repo
252        .oauth2_client()
253        .get_consent_for_user(client, &browser_session.user)
254        .await?;
255
256    let lacks_consent = grant
257        .scope
258        .difference(&current_consent)
259        .filter(|scope| Device::from_scope_token(scope).is_none())
260        .any(|_| true);
261
262    // Check if the client lacks consent *or* if consent was explicitly asked
263    if lacks_consent || grant.requires_consent {
264        repo.save().await?;
265        return Err(GrantCompletionError::RequiresConsent);
266    }
267
268    // All good, let's start the session
269    let session = repo
270        .oauth2_session()
271        .add_from_browser_session(rng, clock, client, browser_session, grant.scope.clone())
272        .await?;
273
274    let grant = repo
275        .oauth2_authorization_grant()
276        .fulfill(clock, &session, grant)
277        .await?;
278
279    // Yep! Let's complete the auth now
280    let mut params = AuthorizationResponse::default();
281
282    // Did they request an ID token?
283    if grant.response_type_id_token {
284        params.id_token = Some(generate_id_token(
285            rng,
286            clock,
287            url_builder,
288            &key_store,
289            client,
290            Some(&grant),
291            browser_session,
292            None,
293            Some(&valid_authentication),
294        )?);
295    }
296
297    // Did they request an auth code?
298    if let Some(code) = grant.code {
299        params.code = Some(code.code);
300    }
301
302    repo.save().await?;
303
304    activity_tracker
305        .record_oauth2_session(clock, &session)
306        .await;
307
308    Ok(params)
309}