You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-09 04:22:45 +03:00
Show and log the policy violations better
This commit is contained in:
@@ -16,23 +16,24 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::PrivateCookieJar;
|
use axum_extra::extract::PrivateCookieJar;
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_axum_utils::SessionInfoExt;
|
use mas_axum_utils::{csrf::CsrfExt, SessionInfoExt};
|
||||||
use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device};
|
use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device};
|
||||||
use mas_keystore::{Encrypter, Keystore};
|
use mas_keystore::{Encrypter, Keystore};
|
||||||
use mas_policy::PolicyFactory;
|
use mas_policy::{EvaluationResult, PolicyFactory};
|
||||||
use mas_router::{PostAuthAction, Route, UrlBuilder};
|
use mas_router::{PostAuthAction, Route, UrlBuilder};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
|
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
|
||||||
user::BrowserSessionRepository,
|
user::BrowserSessionRepository,
|
||||||
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
|
BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
|
||||||
};
|
};
|
||||||
use mas_templates::Templates;
|
use mas_templates::{PolicyViolationContext, TemplateContext, Templates};
|
||||||
use oauth2_types::requests::AuthorizationResponse;
|
use oauth2_types::requests::AuthorizationResponse;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::warn;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use super::callback::CallbackDestination;
|
use super::callback::CallbackDestination;
|
||||||
@@ -74,6 +75,7 @@ impl IntoResponse for RouteError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||||
|
impl_from_error_for_route!(mas_templates::TemplateError);
|
||||||
impl_from_error_for_route!(mas_policy::LoadError);
|
impl_from_error_for_route!(mas_policy::LoadError);
|
||||||
impl_from_error_for_route!(mas_policy::InstanciateError);
|
impl_from_error_for_route!(mas_policy::InstanciateError);
|
||||||
impl_from_error_for_route!(mas_policy::EvaluationError);
|
impl_from_error_for_route!(mas_policy::EvaluationError);
|
||||||
@@ -87,7 +89,7 @@ impl_from_error_for_route!(super::callback::CallbackDestinationError);
|
|||||||
err,
|
err,
|
||||||
)]
|
)]
|
||||||
pub(crate) async fn get(
|
pub(crate) async fn get(
|
||||||
rng: BoxRng,
|
mut rng: BoxRng,
|
||||||
clock: BoxClock,
|
clock: BoxClock,
|
||||||
State(policy_factory): State<Arc<PolicyFactory>>,
|
State(policy_factory): State<Arc<PolicyFactory>>,
|
||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
@@ -123,15 +125,15 @@ pub(crate) async fn get(
|
|||||||
.ok_or(RouteError::NoSuchClient)?;
|
.ok_or(RouteError::NoSuchClient)?;
|
||||||
|
|
||||||
match complete(
|
match complete(
|
||||||
rng,
|
&mut rng,
|
||||||
clock,
|
&clock,
|
||||||
repo,
|
repo,
|
||||||
key_store,
|
key_store,
|
||||||
&policy_factory,
|
&policy_factory,
|
||||||
url_builder,
|
url_builder,
|
||||||
grant,
|
grant,
|
||||||
client,
|
&client,
|
||||||
session,
|
&session,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -144,10 +146,22 @@ pub(crate) async fn get(
|
|||||||
mas_router::Reauth::and_then(continue_grant).go(),
|
mas_router::Reauth::and_then(continue_grant).go(),
|
||||||
)
|
)
|
||||||
.into_response()),
|
.into_response()),
|
||||||
Err(GrantCompletionError::RequiresConsent | GrantCompletionError::PolicyViolation) => {
|
Err(GrantCompletionError::RequiresConsent) => {
|
||||||
let next = mas_router::Consent(grant_id);
|
let next = mas_router::Consent(grant_id);
|
||||||
Ok((cookie_jar, next.go()).into_response())
|
Ok((cookie_jar, next.go()).into_response())
|
||||||
}
|
}
|
||||||
|
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
|
||||||
|
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
|
||||||
|
|
||||||
|
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||||
|
let ctx = PolicyViolationContext::new(grant, client)
|
||||||
|
.with_session(session)
|
||||||
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
|
let content = templates.render_policy_violation(&ctx).await?;
|
||||||
|
|
||||||
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
|
}
|
||||||
Err(GrantCompletionError::NotPending) => Err(RouteError::NotPending),
|
Err(GrantCompletionError::NotPending) => Err(RouteError::NotPending),
|
||||||
Err(GrantCompletionError::Internal(e)) => Err(RouteError::Internal(e)),
|
Err(GrantCompletionError::Internal(e)) => Err(RouteError::Internal(e)),
|
||||||
}
|
}
|
||||||
@@ -168,7 +182,7 @@ pub enum GrantCompletionError {
|
|||||||
RequiresConsent,
|
RequiresConsent,
|
||||||
|
|
||||||
#[error("denied by the policy")]
|
#[error("denied by the policy")]
|
||||||
PolicyViolation,
|
PolicyViolation(AuthorizationGrant, EvaluationResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from_error_for_route!(GrantCompletionError: mas_storage::RepositoryError);
|
impl_from_error_for_route!(GrantCompletionError: mas_storage::RepositoryError);
|
||||||
@@ -179,15 +193,15 @@ impl_from_error_for_route!(GrantCompletionError: mas_policy::EvaluationError);
|
|||||||
impl_from_error_for_route!(GrantCompletionError: super::super::IdTokenSignatureError);
|
impl_from_error_for_route!(GrantCompletionError: super::super::IdTokenSignatureError);
|
||||||
|
|
||||||
pub(crate) async fn complete(
|
pub(crate) async fn complete(
|
||||||
mut rng: BoxRng,
|
rng: &mut (impl rand::RngCore + rand::CryptoRng + Send),
|
||||||
clock: BoxClock,
|
clock: &impl Clock,
|
||||||
mut repo: BoxRepository,
|
mut repo: BoxRepository,
|
||||||
key_store: Keystore,
|
key_store: Keystore,
|
||||||
policy_factory: &PolicyFactory,
|
policy_factory: &PolicyFactory,
|
||||||
url_builder: UrlBuilder,
|
url_builder: UrlBuilder,
|
||||||
grant: AuthorizationGrant,
|
grant: AuthorizationGrant,
|
||||||
client: Client,
|
client: &Client,
|
||||||
browser_session: BrowserSession,
|
browser_session: &BrowserSession,
|
||||||
) -> Result<AuthorizationResponse, GrantCompletionError> {
|
) -> Result<AuthorizationResponse, GrantCompletionError> {
|
||||||
// Verify that the grant is in a pending stage
|
// Verify that the grant is in a pending stage
|
||||||
if !grant.stage.is_pending() {
|
if !grant.stage.is_pending() {
|
||||||
@@ -197,7 +211,7 @@ pub(crate) async fn complete(
|
|||||||
// Check if the authentication is fresh enough
|
// Check if the authentication is fresh enough
|
||||||
let authentication = repo
|
let authentication = repo
|
||||||
.browser_session()
|
.browser_session()
|
||||||
.get_last_authentication(&browser_session)
|
.get_last_authentication(browser_session)
|
||||||
.await?;
|
.await?;
|
||||||
let authentication = authentication.filter(|auth| auth.created_at > grant.max_auth_time());
|
let authentication = authentication.filter(|auth| auth.created_at > grant.max_auth_time());
|
||||||
|
|
||||||
@@ -209,16 +223,16 @@ pub(crate) async fn complete(
|
|||||||
// Run through the policy
|
// Run through the policy
|
||||||
let mut policy = policy_factory.instantiate().await?;
|
let mut policy = policy_factory.instantiate().await?;
|
||||||
let res = policy
|
let res = policy
|
||||||
.evaluate_authorization_grant(&grant, &client, &browser_session.user)
|
.evaluate_authorization_grant(&grant, client, &browser_session.user)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !res.valid() {
|
if !res.valid() {
|
||||||
return Err(GrantCompletionError::PolicyViolation);
|
return Err(GrantCompletionError::PolicyViolation(grant, res));
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_consent = repo
|
let current_consent = repo
|
||||||
.oauth2_client()
|
.oauth2_client()
|
||||||
.get_consent_for_user(&client, &browser_session.user)
|
.get_consent_for_user(client, &browser_session.user)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let lacks_consent = grant
|
let lacks_consent = grant
|
||||||
@@ -236,18 +250,12 @@ pub(crate) async fn complete(
|
|||||||
// All good, let's start the session
|
// All good, let's start the session
|
||||||
let session = repo
|
let session = repo
|
||||||
.oauth2_session()
|
.oauth2_session()
|
||||||
.add(
|
.add(rng, clock, client, browser_session, grant.scope.clone())
|
||||||
&mut rng,
|
|
||||||
&clock,
|
|
||||||
&client,
|
|
||||||
&browser_session,
|
|
||||||
grant.scope.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let grant = repo
|
let grant = repo
|
||||||
.oauth2_authorization_grant()
|
.oauth2_authorization_grant()
|
||||||
.fulfill(&clock, &session, grant)
|
.fulfill(clock, &session, grant)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Yep! Let's complete the auth now
|
// Yep! Let's complete the auth now
|
||||||
@@ -256,13 +264,13 @@ pub(crate) async fn complete(
|
|||||||
// Did they request an ID token?
|
// Did they request an ID token?
|
||||||
if grant.response_type_id_token {
|
if grant.response_type_id_token {
|
||||||
params.id_token = Some(generate_id_token(
|
params.id_token = Some(generate_id_token(
|
||||||
&mut rng,
|
rng,
|
||||||
&clock,
|
clock,
|
||||||
&url_builder,
|
&url_builder,
|
||||||
&key_store,
|
&key_store,
|
||||||
&client,
|
client,
|
||||||
&grant,
|
&grant,
|
||||||
&browser_session,
|
browser_session,
|
||||||
None,
|
None,
|
||||||
Some(&valid_authentication),
|
Some(&valid_authentication),
|
||||||
)?);
|
)?);
|
||||||
|
@@ -16,11 +16,11 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Form, State},
|
extract::{Form, State},
|
||||||
response::{IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::PrivateCookieJar;
|
use axum_extra::extract::PrivateCookieJar;
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_axum_utils::SessionInfoExt;
|
use mas_axum_utils::{csrf::CsrfExt, SessionInfoExt};
|
||||||
use mas_data_model::{AuthorizationCode, Pkce};
|
use mas_data_model::{AuthorizationCode, Pkce};
|
||||||
use mas_keystore::{Encrypter, Keystore};
|
use mas_keystore::{Encrypter, Keystore};
|
||||||
use mas_policy::PolicyFactory;
|
use mas_policy::PolicyFactory;
|
||||||
@@ -29,7 +29,7 @@ use mas_storage::{
|
|||||||
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
|
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
|
||||||
BoxClock, BoxRepository, BoxRng,
|
BoxClock, BoxRepository, BoxRng,
|
||||||
};
|
};
|
||||||
use mas_templates::Templates;
|
use mas_templates::{PolicyViolationContext, TemplateContext, Templates};
|
||||||
use oauth2_types::{
|
use oauth2_types::{
|
||||||
errors::{ClientError, ClientErrorCode},
|
errors::{ClientError, ClientErrorCode},
|
||||||
pkce,
|
pkce,
|
||||||
@@ -39,6 +39,7 @@ use oauth2_types::{
|
|||||||
use rand::{distributions::Alphanumeric, Rng};
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
use self::{callback::CallbackDestination, complete::GrantCompletionError};
|
use self::{callback::CallbackDestination, complete::GrantCompletionError};
|
||||||
use crate::impl_from_error_for_route;
|
use crate::impl_from_error_for_route;
|
||||||
@@ -91,6 +92,7 @@ impl IntoResponse for RouteError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||||
|
impl_from_error_for_route!(mas_templates::TemplateError);
|
||||||
impl_from_error_for_route!(self::callback::CallbackDestinationError);
|
impl_from_error_for_route!(self::callback::CallbackDestinationError);
|
||||||
impl_from_error_for_route!(mas_policy::LoadError);
|
impl_from_error_for_route!(mas_policy::LoadError);
|
||||||
impl_from_error_for_route!(mas_policy::InstanciateError);
|
impl_from_error_for_route!(mas_policy::InstanciateError);
|
||||||
@@ -170,6 +172,7 @@ pub(crate) async fn get(
|
|||||||
|
|
||||||
// Get the session info from the cookie
|
// Get the session info from the cookie
|
||||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||||
|
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||||
|
|
||||||
// One day, we will have try blocks
|
// One day, we will have try blocks
|
||||||
let res: Result<Response, RouteError> = ({
|
let res: Result<Response, RouteError> = ({
|
||||||
@@ -340,15 +343,15 @@ pub(crate) async fn get(
|
|||||||
Some(user_session) if prompt.contains(&Prompt::None) => {
|
Some(user_session) if prompt.contains(&Prompt::None) => {
|
||||||
// With prompt=none, we should get back to the client immediately
|
// With prompt=none, we should get back to the client immediately
|
||||||
match self::complete::complete(
|
match self::complete::complete(
|
||||||
rng,
|
&mut rng,
|
||||||
clock,
|
&clock,
|
||||||
repo,
|
repo,
|
||||||
key_store,
|
key_store,
|
||||||
&policy_factory,
|
&policy_factory,
|
||||||
url_builder,
|
url_builder,
|
||||||
grant,
|
grant,
|
||||||
client,
|
&client,
|
||||||
user_session,
|
&user_session,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -369,7 +372,7 @@ pub(crate) async fn get(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
Err(GrantCompletionError::PolicyViolation) => {
|
Err(GrantCompletionError::PolicyViolation(_grant, _res)) => {
|
||||||
callback_destination
|
callback_destination
|
||||||
.go(&templates, ClientError::from(ClientErrorCode::AccessDenied))
|
.go(&templates, ClientError::from(ClientErrorCode::AccessDenied))
|
||||||
.await?
|
.await?
|
||||||
@@ -387,29 +390,32 @@ pub(crate) async fn get(
|
|||||||
let grant_id = grant.id;
|
let grant_id = grant.id;
|
||||||
// Else, we show the relevant reauth/consent page if necessary
|
// Else, we show the relevant reauth/consent page if necessary
|
||||||
match self::complete::complete(
|
match self::complete::complete(
|
||||||
rng,
|
&mut rng,
|
||||||
clock,
|
&clock,
|
||||||
repo,
|
repo,
|
||||||
key_store,
|
key_store,
|
||||||
&policy_factory,
|
&policy_factory,
|
||||||
url_builder,
|
url_builder,
|
||||||
grant,
|
grant,
|
||||||
client,
|
&client,
|
||||||
user_session,
|
&user_session,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(params) => callback_destination.go(&templates, params).await?,
|
Ok(params) => callback_destination.go(&templates, params).await?,
|
||||||
Err(
|
Err(GrantCompletionError::RequiresConsent) => {
|
||||||
GrantCompletionError::RequiresConsent
|
|
||||||
| GrantCompletionError::PolicyViolation,
|
|
||||||
) => {
|
|
||||||
// We're redirecting to the consent URI in both 'consent required' and
|
|
||||||
// 'policy violation' cases, because we reevaluate the policy on this
|
|
||||||
// page, and show the error accordingly
|
|
||||||
// XXX: is this the right approach?
|
|
||||||
mas_router::Consent(grant_id).go().into_response()
|
mas_router::Consent(grant_id).go().into_response()
|
||||||
}
|
}
|
||||||
|
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
|
||||||
|
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
|
||||||
|
|
||||||
|
let ctx = PolicyViolationContext::new(grant, client)
|
||||||
|
.with_session(user_session)
|
||||||
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
|
let content = templates.render_policy_violation(&ctx).await?;
|
||||||
|
Html(content).into_response()
|
||||||
|
}
|
||||||
Err(GrantCompletionError::RequiresReauth) => {
|
Err(GrantCompletionError::RequiresReauth) => {
|
||||||
mas_router::Reauth::and_then(continue_grant)
|
mas_router::Reauth::and_then(continue_grant)
|
||||||
.go()
|
.go()
|
||||||
|
@@ -31,6 +31,7 @@ use mas_storage::{
|
|||||||
Repository, RepositoryAccess, RepositoryTransaction,
|
Repository, RepositoryAccess, RepositoryTransaction,
|
||||||
};
|
};
|
||||||
use sqlx::{PgPool, Postgres, Transaction};
|
use sqlx::{PgPool, Postgres, Transaction};
|
||||||
|
use tracing::Instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
compat::{
|
compat::{
|
||||||
@@ -78,11 +79,21 @@ impl RepositoryTransaction for PgRepository {
|
|||||||
type Error = DatabaseError;
|
type Error = DatabaseError;
|
||||||
|
|
||||||
fn save(self: Box<Self>) -> BoxFuture<'static, Result<(), Self::Error>> {
|
fn save(self: Box<Self>) -> BoxFuture<'static, Result<(), Self::Error>> {
|
||||||
self.txn.commit().map_err(DatabaseError::from).boxed()
|
let span = tracing::info_span!("db.save");
|
||||||
|
self.txn
|
||||||
|
.commit()
|
||||||
|
.map_err(DatabaseError::from)
|
||||||
|
.instrument(span)
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(self: Box<Self>) -> BoxFuture<'static, Result<(), Self::Error>> {
|
fn cancel(self: Box<Self>) -> BoxFuture<'static, Result<(), Self::Error>> {
|
||||||
self.txn.rollback().map_err(DatabaseError::from).boxed()
|
let span = tracing::info_span!("db.cancel");
|
||||||
|
self.txn
|
||||||
|
.rollback()
|
||||||
|
.map_err(DatabaseError::from)
|
||||||
|
.instrument(span)
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user