1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-20 12:02:22 +03:00

Authorization grant policy (#288)

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
This commit is contained in:
Quentin Gliech
2022-07-21 11:18:59 -05:00
committed by GitHub
parent a263330ea5
commit ba6a382f2c
12 changed files with 319 additions and 21 deletions

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use anyhow::anyhow;
use axum::{
extract::Path,
@@ -24,6 +26,7 @@ use hyper::StatusCode;
use mas_axum_utils::SessionInfoExt;
use mas_config::Encrypter;
use mas_data_model::{AuthorizationGrant, BrowserSession, TokenType};
use mas_policy::PolicyFactory;
use mas_router::{PostAuthAction, Route};
use mas_storage::{
oauth2::{
@@ -105,6 +108,7 @@ impl From<CallbackDestinationError> for RouteError {
}
pub(crate) async fn get(
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
@@ -129,7 +133,7 @@ pub(crate) async fn get(
return Ok((cookie_jar, mas_router::Login::and_then(continue_grant).go()).into_response());
};
match complete(grant, session, txn).await {
match complete(grant, session, &policy_factory, txn).await {
Ok(params) => {
let res = callback_destination.go(&templates, params).await?;
Ok((cookie_jar, res).into_response())
@@ -139,7 +143,7 @@ pub(crate) async fn get(
mas_router::Reauth::and_then(continue_grant).go(),
)
.into_response()),
Err(GrantCompletionError::RequiresConsent) => {
Err(GrantCompletionError::RequiresConsent | GrantCompletionError::PolicyViolation) => {
let next = mas_router::Consent(grant_id);
Ok((cookie_jar, next.go()).into_response())
}
@@ -165,6 +169,9 @@ pub enum GrantCompletionError {
#[error("client lacks consent")]
RequiresConsent,
#[error("denied by the policy")]
PolicyViolation,
}
impl From<sqlx::Error> for GrantCompletionError {
@@ -182,6 +189,7 @@ impl From<InvalidRedirectUriError> for GrantCompletionError {
pub(crate) async fn complete(
grant: AuthorizationGrant<PostgresqlBackend>,
browser_session: BrowserSession<PostgresqlBackend>,
policy_factory: &PolicyFactory,
mut txn: Transaction<'_, Postgres>,
) -> Result<AuthorizationResponse<Option<AccessTokenResponse>>, GrantCompletionError> {
// Verify that the grant is in a pending stage
@@ -195,6 +203,16 @@ pub(crate) async fn complete(
return Err(GrantCompletionError::RequiresReauth);
}
// Run through the policy
let mut policy = policy_factory.instantiate().await?;
let res = policy
.evaluate_authorization_grant(&grant, &browser_session.user)
.await?;
if !res.valid() {
return Err(GrantCompletionError::PolicyViolation);
}
let current_consent =
fetch_client_consent(&mut txn, &browser_session.user, &grant.client).await?;

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use anyhow::{anyhow, Context};
use axum::{
extract::{Extension, Form},
@@ -23,6 +25,7 @@ use mas_axum_utils::SessionInfoExt;
use mas_config::Encrypter;
use mas_data_model::{AuthorizationCode, Pkce};
use mas_iana::oauth::OAuthAuthorizationEndpointResponseType;
use mas_policy::PolicyFactory;
use mas_router::{PostAuthAction, Route};
use mas_storage::oauth2::{
authorization_grant::new_authorization_grant,
@@ -31,7 +34,7 @@ use mas_storage::oauth2::{
use mas_templates::Templates;
use oauth2_types::{
errors::{
CONSENT_REQUIRED, INTERACTION_REQUIRED, INVALID_REQUEST, LOGIN_REQUIRED,
ACCESS_DENIED, CONSENT_REQUIRED, INTERACTION_REQUIRED, INVALID_REQUEST, LOGIN_REQUIRED,
REGISTRATION_NOT_SUPPORTED, REQUEST_NOT_SUPPORTED, REQUEST_URI_NOT_SUPPORTED, SERVER_ERROR,
UNAUTHORIZED_CLIENT,
},
@@ -157,6 +160,7 @@ fn resolve_response_mode(
#[allow(clippy::too_many_lines)]
pub(crate) async fn get(
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
@@ -306,7 +310,8 @@ pub(crate) async fn get(
// Else, we immediately try to complete the authorization grant
(Some(user_session), Some(Prompt::None)) => {
// With prompt=none, we should get back to the client immediately
match self::complete::complete(grant, user_session, txn).await {
match self::complete::complete(grant, user_session, &policy_factory, txn).await
{
Ok(params) => callback_destination.go(&templates, params).await?,
Err(GrantCompletionError::RequiresConsent) => {
callback_destination
@@ -318,6 +323,9 @@ pub(crate) async fn get(
.go(&templates, INTERACTION_REQUIRED)
.await?
}
Err(GrantCompletionError::PolicyViolation) => {
callback_destination.go(&templates, ACCESS_DENIED).await?
}
Err(GrantCompletionError::Anyhow(a)) => return Err(RouteError::Anyhow(a)),
Err(GrantCompletionError::Internal(e)) => {
return Err(RouteError::Internal(e))
@@ -331,9 +339,17 @@ pub(crate) async fn get(
(Some(user_session), _) => {
let grant_id = grant.data;
// Else, we show the relevant reauth/consent page if necessary
match self::complete::complete(grant, user_session, txn).await {
match self::complete::complete(grant, user_session, &policy_factory, txn).await
{
Ok(params) => callback_destination.go(&templates, params).await?,
Err(GrantCompletionError::RequiresConsent) => {
Err(
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()
}
Err(GrantCompletionError::RequiresReauth) => {

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use anyhow::Context;
use axum::{
extract::{Extension, Form, Path},
@@ -25,12 +27,13 @@ use mas_axum_utils::{
};
use mas_config::Encrypter;
use mas_data_model::AuthorizationGrantStage;
use mas_policy::PolicyFactory;
use mas_router::{PostAuthAction, Route};
use mas_storage::oauth2::{
authorization_grant::{get_grant_by_id, give_consent_to_grant},
consent::insert_client_consent,
};
use mas_templates::{ConsentContext, TemplateContext, Templates};
use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
use sqlx::PgPool;
use thiserror::Error;
@@ -47,6 +50,7 @@ impl IntoResponse for RouteError {
}
pub(crate) async fn get(
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
@@ -73,16 +77,34 @@ pub(crate) async fn get(
if let Some(session) = maybe_session {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
let ctx = ConsentContext::new(grant, PostAuthAction::continue_grant(grant_id))
.with_session(session)
.with_csrf(csrf_token.form_value());
let mut policy = policy_factory.instantiate().await?;
let res = policy
.evaluate_authorization_grant(&grant, &session.user)
.await?;
let content = templates
.render_consent(&ctx)
.await
.context("failed to render template")?;
if res.valid() {
let ctx = ConsentContext::new(grant, PostAuthAction::continue_grant(grant_id))
.with_session(session)
.with_csrf(csrf_token.form_value());
Ok((cookie_jar, Html(content)).into_response())
let content = templates
.render_consent(&ctx)
.await
.context("failed to render template")?;
Ok((cookie_jar, Html(content)).into_response())
} else {
let ctx = PolicyViolationContext::new(grant, PostAuthAction::continue_grant(grant_id))
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates
.render_policy_violation(&ctx)
.await
.context("failed to render template")?;
Ok((cookie_jar, Html(content)).into_response())
}
} else {
let login = mas_router::Login::and_continue_grant(grant_id);
Ok((cookie_jar, login.go()).into_response())
@@ -90,6 +112,7 @@ pub(crate) async fn get(
}
pub(crate) async fn post(
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
Path(grant_id): Path<i64>,
@@ -121,6 +144,15 @@ pub(crate) async fn post(
return Ok((cookie_jar, login.go()).into_response());
};
let mut policy = policy_factory.instantiate().await?;
let res = policy
.evaluate_authorization_grant(&grant, &session.user)
.await?;
if !res.valid() {
return Err(anyhow::anyhow!("policy violation").into());
}
// Do not consent for the "urn:matrix:device:*" scope
let scope_without_device = grant
.scope