You've already forked authentication-service
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:
@@ -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?;
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user