1use 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 sentry::SentryEventID,
17};
18use mas_data_model::{AuthorizationGrantStage, Device};
19use mas_policy::Policy;
20use mas_router::{PostAuthAction, UrlBuilder};
21use mas_storage::{
22 BoxClock, BoxRepository, BoxRng,
23 oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
24};
25use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
26use thiserror::Error;
27use ulid::Ulid;
28
29use crate::{
30 BoundActivityTracker, PreferredLanguage, impl_from_error_for_route,
31 session::{SessionOrFallback, load_session_or_fallback},
32};
33
34#[derive(Debug, Error)]
35pub enum RouteError {
36 #[error(transparent)]
37 Internal(Box<dyn std::error::Error + Send + Sync>),
38
39 #[error(transparent)]
40 Csrf(#[from] mas_axum_utils::csrf::CsrfError),
41
42 #[error("Authorization grant not found")]
43 GrantNotFound,
44
45 #[error("Authorization grant already used")]
46 GrantNotPending,
47
48 #[error("Policy violation")]
49 PolicyViolation,
50
51 #[error("Failed to load client")]
52 NoSuchClient,
53}
54
55impl_from_error_for_route!(mas_templates::TemplateError);
56impl_from_error_for_route!(mas_storage::RepositoryError);
57impl_from_error_for_route!(mas_policy::LoadError);
58impl_from_error_for_route!(mas_policy::EvaluationError);
59impl_from_error_for_route!(crate::session::SessionLoadError);
60
61impl IntoResponse for RouteError {
62 fn into_response(self) -> axum::response::Response {
63 let event_id = sentry::capture_error(&self);
64 (
65 SentryEventID::from(event_id),
66 StatusCode::INTERNAL_SERVER_ERROR,
67 )
68 .into_response()
69 }
70}
71
72#[tracing::instrument(
73 name = "handlers.oauth2.consent.get",
74 fields(grant.id = %grant_id),
75 skip_all,
76 err,
77)]
78pub(crate) async fn get(
79 mut rng: BoxRng,
80 clock: BoxClock,
81 PreferredLanguage(locale): PreferredLanguage,
82 State(templates): State<Templates>,
83 State(url_builder): State<UrlBuilder>,
84 mut policy: Policy,
85 mut repo: BoxRepository,
86 activity_tracker: BoundActivityTracker,
87 user_agent: Option<TypedHeader<headers::UserAgent>>,
88 cookie_jar: CookieJar,
89 Path(grant_id): Path<Ulid>,
90) -> Result<Response, RouteError> {
91 let (cookie_jar, maybe_session) = match load_session_or_fallback(
92 cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
93 )
94 .await?
95 {
96 SessionOrFallback::MaybeSession {
97 cookie_jar,
98 maybe_session,
99 ..
100 } => (cookie_jar, maybe_session),
101 SessionOrFallback::Fallback { response } => return Ok(response),
102 };
103
104 let user_agent = user_agent.map(|ua| ua.to_string());
105
106 let grant = repo
107 .oauth2_authorization_grant()
108 .lookup(grant_id)
109 .await?
110 .ok_or(RouteError::GrantNotFound)?;
111
112 let client = repo
113 .oauth2_client()
114 .lookup(grant.client_id)
115 .await?
116 .ok_or(RouteError::NoSuchClient)?;
117
118 if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
119 return Err(RouteError::GrantNotPending);
120 }
121
122 let Some(session) = maybe_session else {
123 let login = mas_router::Login::and_continue_grant(grant_id);
124 return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
125 };
126
127 activity_tracker
128 .record_browser_session(&clock, &session)
129 .await;
130
131 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
132
133 let res = policy
134 .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
135 user: Some(&session.user),
136 client: &client,
137 scope: &grant.scope,
138 grant_type: mas_policy::GrantType::AuthorizationCode,
139 requester: mas_policy::Requester {
140 ip_address: activity_tracker.ip(),
141 user_agent,
142 },
143 })
144 .await?;
145
146 if res.valid() {
147 let ctx = ConsentContext::new(grant, client)
148 .with_session(session)
149 .with_csrf(csrf_token.form_value())
150 .with_language(locale);
151
152 let content = templates.render_consent(&ctx)?;
153
154 Ok((cookie_jar, Html(content)).into_response())
155 } else {
156 let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
157 .with_session(session)
158 .with_csrf(csrf_token.form_value())
159 .with_language(locale);
160
161 let content = templates.render_policy_violation(&ctx)?;
162
163 Ok((cookie_jar, Html(content)).into_response())
164 }
165}
166
167#[tracing::instrument(
168 name = "handlers.oauth2.consent.post",
169 fields(grant.id = %grant_id),
170 skip_all,
171 err,
172)]
173pub(crate) async fn post(
174 mut rng: BoxRng,
175 clock: BoxClock,
176 PreferredLanguage(locale): PreferredLanguage,
177 State(templates): State<Templates>,
178 mut policy: Policy,
179 mut repo: BoxRepository,
180 activity_tracker: BoundActivityTracker,
181 user_agent: Option<TypedHeader<headers::UserAgent>>,
182 cookie_jar: CookieJar,
183 State(url_builder): State<UrlBuilder>,
184 Path(grant_id): Path<Ulid>,
185 Form(form): Form<ProtectedForm<()>>,
186) -> Result<Response, RouteError> {
187 cookie_jar.verify_form(&clock, form)?;
188
189 let (cookie_jar, maybe_session) = match load_session_or_fallback(
190 cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
191 )
192 .await?
193 {
194 SessionOrFallback::MaybeSession {
195 cookie_jar,
196 maybe_session,
197 ..
198 } => (cookie_jar, maybe_session),
199 SessionOrFallback::Fallback { response } => return Ok(response),
200 };
201
202 let user_agent = user_agent.map(|ua| ua.to_string());
203
204 let grant = repo
205 .oauth2_authorization_grant()
206 .lookup(grant_id)
207 .await?
208 .ok_or(RouteError::GrantNotFound)?;
209 let next = PostAuthAction::continue_grant(grant_id);
210
211 let Some(session) = maybe_session else {
212 let login = mas_router::Login::and_then(next);
213 return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
214 };
215
216 activity_tracker
217 .record_browser_session(&clock, &session)
218 .await;
219
220 let client = repo
221 .oauth2_client()
222 .lookup(grant.client_id)
223 .await?
224 .ok_or(RouteError::NoSuchClient)?;
225
226 let res = policy
227 .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
228 user: Some(&session.user),
229 client: &client,
230 scope: &grant.scope,
231 grant_type: mas_policy::GrantType::AuthorizationCode,
232 requester: mas_policy::Requester {
233 ip_address: activity_tracker.ip(),
234 user_agent,
235 },
236 })
237 .await?;
238
239 if !res.valid() {
240 return Err(RouteError::PolicyViolation);
241 }
242
243 let scope_without_device = grant
245 .scope
246 .iter()
247 .filter(|s| Device::from_scope_token(s).is_none())
248 .cloned()
249 .collect();
250
251 repo.oauth2_client()
252 .give_consent_for_user(
253 &mut rng,
254 &clock,
255 &client,
256 &session.user,
257 &scope_without_device,
258 )
259 .await?;
260
261 repo.oauth2_authorization_grant()
262 .give_consent(grant)
263 .await?;
264
265 repo.save().await?;
266
267 Ok((cookie_jar, next.go_next(&url_builder)).into_response())
268}