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 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 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 if grant.response_type_id_token {
283 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 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}