mas_handlers/oauth2/authorization/
complete.rs
1use 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 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 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 if !grant.stage.is_pending() {
218 return Err(GrantCompletionError::NotPending);
219 }
220
221 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 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(¤t_consent)
259 .filter(|scope| Device::from_scope_token(scope).is_none())
260 .any(|_| true);
261
262 if lacks_consent || grant.requires_consent {
264 repo.save().await?;
265 return Err(GrantCompletionError::RequiresConsent);
266 }
267
268 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 let mut params = AuthorizationResponse::default();
281
282 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 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}