Skip to content

Commit cf732ac

Browse files
committed
Always ask for consent, never for reauth
Now that we have deduplicated clients, we're in this weird situation where authorization grants just… go through. This is because 4 years ago, I designed it to support prompt=consent and prompt=none, but that never ended up being used/mentioned in the MSCs. We also had support for max_age, but that required reauthing, which doesn't work well with upstream providers. So this removes support for prompt=consent|none and max_age, and makes sure we always go through the consent page. Lots of code deleted, yay!
1 parent 62741a0 commit cf732ac

19 files changed

+88
-638
lines changed

crates/data-model/src/oauth2/authorization_grant.rs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
// SPDX-License-Identifier: AGPL-3.0-only
55
// Please see LICENSE in the repository root for full details.
66

7-
use std::num::NonZeroU32;
8-
9-
use chrono::{DateTime, Duration, Utc};
7+
use chrono::{DateTime, Utc};
108
use mas_iana::oauth::PkceCodeChallengeMethod;
119
use oauth2_types::{
1210
pkce::{CodeChallengeError, CodeChallengeMethodExt},
@@ -158,11 +156,9 @@ pub struct AuthorizationGrant {
158156
pub scope: Scope,
159157
pub state: Option<String>,
160158
pub nonce: Option<String>,
161-
pub max_age: Option<NonZeroU32>,
162159
pub response_mode: ResponseMode,
163160
pub response_type_id_token: bool,
164161
pub created_at: DateTime<Utc>,
165-
pub requires_consent: bool,
166162
pub login_hint: Option<String>,
167163
}
168164

@@ -174,18 +170,7 @@ impl std::ops::Deref for AuthorizationGrant {
174170
}
175171
}
176172

177-
const DEFAULT_MAX_AGE: Duration = Duration::microseconds(3600 * 24 * 365 * 1000 * 1000);
178-
179173
impl AuthorizationGrant {
180-
#[must_use]
181-
pub fn max_auth_time(&self) -> DateTime<Utc> {
182-
let max_age = self
183-
.max_age
184-
.and_then(|x| Duration::try_seconds(x.get().into()))
185-
.unwrap_or(DEFAULT_MAX_AGE);
186-
self.created_at - max_age
187-
}
188-
189174
#[must_use]
190175
pub fn parse_login_hint(&self, homeserver: &str) -> LoginHint {
191176
let Some(login_hint) = &self.login_hint else {
@@ -274,11 +259,9 @@ impl AuthorizationGrant {
274259
scope: Scope::from_iter([OPENID, PROFILE]),
275260
state: Some(Alphanumeric.sample_string(rng, 10)),
276261
nonce: Some(Alphanumeric.sample_string(rng, 10)),
277-
max_age: None,
278262
response_mode: ResponseMode::Query,
279263
response_type_id_token: false,
280264
created_at: now,
281-
requires_consent: false,
282265
login_hint: Some(String::from("mxid:@example-user:example.com")),
283266
}
284267
}

crates/handlers/src/oauth2/authorization/complete.rs

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use axum::{
1111
use axum_extra::TypedHeader;
1212
use hyper::StatusCode;
1313
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID};
14-
use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device};
14+
use mas_data_model::{AuthorizationGrant, BrowserSession, Client};
1515
use mas_keystore::Keystore;
1616
use mas_policy::{EvaluationResult, Policy};
1717
use mas_router::{PostAuthAction, UrlBuilder};
@@ -149,15 +149,6 @@ pub(crate) async fn get(
149149
let res = callback_destination.go(&templates, &locale, params).await?;
150150
Ok((cookie_jar, res).into_response())
151151
}
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-
}
161152
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
162153
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
163154

@@ -184,12 +175,6 @@ pub enum GrantCompletionError {
184175
#[error("authorization grant is not in a pending state")]
185176
NotPending,
186177

187-
#[error("user needs to reauthenticate")]
188-
RequiresReauth,
189-
190-
#[error("client lacks consent")]
191-
RequiresConsent,
192-
193178
#[error("denied by the policy")]
194179
PolicyViolation(AuthorizationGrant, EvaluationResult),
195180
}
@@ -218,18 +203,6 @@ pub(crate) async fn complete(
218203
return Err(GrantCompletionError::NotPending);
219204
}
220205

221-
// Check if the authentication is fresh enough
222-
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-
233206
// Run through the policy
234207
let res = policy
235208
.evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
@@ -248,23 +221,6 @@ pub(crate) async fn complete(
248221
return Err(GrantCompletionError::PolicyViolation(grant, res));
249222
}
250223

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(&current_consent)
259-
.filter(|scope| Device::from_scope_token(scope).is_none())
260-
.any(|_| true);
261-
262-
// Check if the client lacks consent *or* if consent was explicitly asked
263-
if lacks_consent || grant.requires_consent {
264-
repo.save().await?;
265-
return Err(GrantCompletionError::RequiresConsent);
266-
}
267-
268224
// All good, let's start the session
269225
let session = repo
270226
.oauth2_session()
@@ -281,6 +237,12 @@ pub(crate) async fn complete(
281237

282238
// Did they request an ID token?
283239
if grant.response_type_id_token {
240+
// Fetch the last authentication
241+
let last_authentication = repo
242+
.browser_session()
243+
.get_last_authentication(browser_session)
244+
.await?;
245+
284246
params.id_token = Some(generate_id_token(
285247
rng,
286248
clock,
@@ -290,7 +252,7 @@ pub(crate) async fn complete(
290252
Some(&grant),
291253
browser_session,
292254
None,
293-
Some(&valid_authentication),
255+
last_authentication.as_ref(),
294256
)?);
295257
}
296258

crates/handlers/src/oauth2/authorization/mod.rs

Lines changed: 18 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,17 @@
66

77
use axum::{
88
extract::{Form, State},
9-
response::{Html, IntoResponse, Response},
9+
response::{IntoResponse, Response},
1010
};
11-
use axum_extra::TypedHeader;
1211
use hyper::StatusCode;
13-
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID};
12+
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, sentry::SentryEventID};
1413
use mas_data_model::{AuthorizationCode, Pkce};
15-
use mas_keystore::Keystore;
16-
use mas_policy::Policy;
1714
use mas_router::{PostAuthAction, UrlBuilder};
1815
use mas_storage::{
1916
BoxClock, BoxRepository, BoxRng,
2017
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
2118
};
22-
use mas_templates::{PolicyViolationContext, TemplateContext, Templates};
19+
use mas_templates::Templates;
2320
use oauth2_types::{
2421
errors::{ClientError, ClientErrorCode},
2522
pkce,
@@ -29,9 +26,8 @@ use oauth2_types::{
2926
use rand::{Rng, distributions::Alphanumeric};
3027
use serde::Deserialize;
3128
use thiserror::Error;
32-
use tracing::warn;
3329

34-
use self::{callback::CallbackDestination, complete::GrantCompletionError};
30+
use self::callback::CallbackDestination;
3531
use crate::{BoundActivityTracker, PreferredLanguage, impl_from_error_for_route};
3632

3733
mod callback;
@@ -134,10 +130,7 @@ pub(crate) async fn get(
134130
clock: BoxClock,
135131
PreferredLanguage(locale): PreferredLanguage,
136132
State(templates): State<Templates>,
137-
State(key_store): State<Keystore>,
138133
State(url_builder): State<UrlBuilder>,
139-
policy: Policy,
140-
user_agent: Option<TypedHeader<headers::UserAgent>>,
141134
activity_tracker: BoundActivityTracker,
142135
mut repo: BoxRepository,
143136
cookie_jar: CookieJar,
@@ -166,9 +159,6 @@ pub(crate) async fn get(
166159

167160
// Get the session info from the cookie
168161
let (session_info, cookie_jar) = cookie_jar.session_info();
169-
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
170-
171-
let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string());
172162

173163
// One day, we will have try blocks
174164
let res: Result<Response, RouteError> = ({
@@ -235,8 +225,8 @@ pub(crate) async fn get(
235225
.await?);
236226
}
237227

238-
// Fail early if prompt=none and there is no active session
239-
if prompt.contains(&Prompt::None) && maybe_session.is_none() {
228+
// Fail early if prompt=none; we never let it go through
229+
if prompt.contains(&Prompt::None) {
240230
return Ok(callback_destination
241231
.go(
242232
&templates,
@@ -287,8 +277,6 @@ pub(crate) async fn get(
287277
None
288278
};
289279

290-
let requires_consent = prompt.contains(&Prompt::Consent);
291-
292280
let grant = repo
293281
.oauth2_authorization_grant()
294282
.add(
@@ -300,151 +288,43 @@ pub(crate) async fn get(
300288
code,
301289
params.auth.state.clone(),
302290
params.auth.nonce,
303-
params.auth.max_age,
304291
response_mode,
305292
response_type.has_id_token(),
306-
requires_consent,
307293
params.auth.login_hint,
308294
)
309295
.await?;
310296
let continue_grant = PostAuthAction::continue_grant(grant.id);
311297

312298
let res = match maybe_session {
313-
// Cases where there is no active session, redirect to the relevant page
314-
None if prompt.contains(&Prompt::None) => {
315-
// This case should already be handled earlier
316-
unreachable!();
317-
}
318299
None if prompt.contains(&Prompt::Create) => {
319300
// Client asked for a registration, show the registration prompt
320301
repo.save().await?;
321302

322-
url_builder.redirect(&mas_router::Register::and_then(continue_grant))
303+
url_builder
304+
.redirect(&mas_router::Register::and_then(continue_grant))
323305
.into_response()
324306
}
307+
325308
None => {
326309
// Other cases where we don't have a session, ask for a login
327310
repo.save().await?;
328311

329-
url_builder.redirect(&mas_router::Login::and_then(continue_grant))
312+
url_builder
313+
.redirect(&mas_router::Login::and_then(continue_grant))
330314
.into_response()
331315
}
332316

333-
// Special case when we already have a session but prompt=login|select_account
334-
Some(session)
335-
if prompt.contains(&Prompt::Login)
336-
|| prompt.contains(&Prompt::SelectAccount) =>
337-
{
338-
// TODO: better pages here
317+
Some(user_session) => {
318+
// TODO: better support for prompt=create when we have a session
339319
repo.save().await?;
340320

341-
activity_tracker.record_browser_session(&clock, &session).await;
342-
343-
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant))
321+
activity_tracker
322+
.record_browser_session(&clock, &user_session)
323+
.await;
324+
url_builder
325+
.redirect(&mas_router::Consent(grant.id))
344326
.into_response()
345327
}
346-
347-
// Else, we immediately try to complete the authorization grant
348-
Some(user_session) if prompt.contains(&Prompt::None) => {
349-
activity_tracker.record_browser_session(&clock, &user_session).await;
350-
351-
// With prompt=none, we should get back to the client immediately
352-
match self::complete::complete(
353-
&mut rng,
354-
&clock,
355-
&activity_tracker,
356-
user_agent,
357-
repo,
358-
key_store,
359-
policy,
360-
&url_builder,
361-
grant,
362-
&client,
363-
&user_session,
364-
)
365-
.await
366-
{
367-
Ok(params) => callback_destination.go(&templates, &locale, params).await?,
368-
Err(GrantCompletionError::RequiresConsent) => {
369-
callback_destination
370-
.go(
371-
&templates,
372-
&locale,
373-
ClientError::from(ClientErrorCode::ConsentRequired),
374-
)
375-
.await?
376-
}
377-
Err(GrantCompletionError::RequiresReauth) => {
378-
callback_destination
379-
.go(
380-
&templates,
381-
&locale,
382-
ClientError::from(ClientErrorCode::InteractionRequired),
383-
)
384-
.await?
385-
}
386-
Err(GrantCompletionError::PolicyViolation(_grant, _res)) => {
387-
callback_destination
388-
.go(&templates, &locale, ClientError::from(ClientErrorCode::AccessDenied))
389-
.await?
390-
}
391-
Err(GrantCompletionError::Internal(e)) => {
392-
return Err(RouteError::Internal(e))
393-
}
394-
Err(e @ GrantCompletionError::NotPending) => {
395-
// This should never happen
396-
return Err(RouteError::Internal(Box::new(e)));
397-
}
398-
}
399-
}
400-
Some(user_session) => {
401-
activity_tracker.record_browser_session(&clock, &user_session).await;
402-
403-
let grant_id = grant.id;
404-
// Else, we show the relevant reauth/consent page if necessary
405-
match self::complete::complete(
406-
&mut rng,
407-
&clock,
408-
&activity_tracker,
409-
user_agent,
410-
repo,
411-
key_store,
412-
policy,
413-
&url_builder,
414-
grant,
415-
&client,
416-
&user_session,
417-
)
418-
.await
419-
{
420-
Ok(params) => callback_destination.go(&templates, &locale, params).await?,
421-
Err(GrantCompletionError::RequiresConsent) => {
422-
url_builder.redirect(&mas_router::Consent(grant_id)).into_response()
423-
}
424-
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
425-
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
426-
427-
let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
428-
.with_session(user_session)
429-
.with_csrf(csrf_token.form_value())
430-
.with_language(locale);
431-
432-
let content = templates.render_policy_violation(&ctx)?;
433-
Html(content).into_response()
434-
}
435-
Err(GrantCompletionError::RequiresReauth) => {
436-
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant))
437-
.into_response()
438-
}
439-
Err(GrantCompletionError::Internal(e)) => {
440-
return Err(RouteError::Internal(e))
441-
}
442-
Err(e @ GrantCompletionError::NotPending) => {
443-
// This should never happen
444-
return Err(RouteError::Internal(Box::new(e)));
445-
}
446-
}
447-
}
448328
};
449329

450330
Ok(res)

0 commit comments

Comments
 (0)