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