mas_handlers/oauth2/authorization/
consent.rs1use 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 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 if grant.response_type_id_token {
281 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 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}