diff --git a/Cargo.lock b/Cargo.lock index ccc4e2e1..310bc5f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,18 @@ dependencies = [ "mime", ] +[[package]] +name = "axum-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63bcb0d395bc5dd286e61aada9fc48201eb70e232f006f9d6c330c9db2f256f5" +dependencies = [ + "heck 0.4.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.64" @@ -2071,6 +2083,7 @@ dependencies = [ "anyhow", "argon2", "axum", + "axum-macros", "chrono", "crc", "data-encoding", diff --git a/crates/axum-utils/src/client_authorization.rs b/crates/axum-utils/src/client_authorization.rs index f66eddc9..2e75856f 100644 --- a/crates/axum-utils/src/client_authorization.rs +++ b/crates/axum-utils/src/client_authorization.rs @@ -132,8 +132,8 @@ impl Credentials { .ok_or(CredentialsVerificationError::InvalidClientConfig)?; let store: Either = jwks_key_store(jwks); - jwt.verify(header, &store) - .await + let fut = jwt.verify(header, &store); + fut.await .map_err(|_| CredentialsVerificationError::InvalidAssertionSignature)?; } @@ -152,8 +152,8 @@ impl Credentials { .map_err(|_e| CredentialsVerificationError::DecryptionError)?; let store = SharedSecret::new(&decrypted_client_secret); - jwt.verify(header, &store) - .await + let fut = jwt.verify(header, &store); + fut.await .map_err(|_| CredentialsVerificationError::InvalidAssertionSignature)?; } @@ -181,8 +181,8 @@ pub enum CredentialsVerificationError { #[derive(Debug, PartialEq, Eq)] pub struct ClientAuthorization { - credentials: Credentials, - form: Option, + pub credentials: Credentials, + pub form: Option, } #[derive(Debug)] diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 0d4c6dde..a8fe13ec 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -24,6 +24,7 @@ warp = "0.3.2" hyper = { version = "0.14.17", features = ["full"] } tower = "0.4.12" axum = "0.4.8" +axum-macros = "0.2.0" # Emails lettre = { version = "0.10.0-rc.4", default-features = false, features = ["builder"] } diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 3fe7fe56..b42acf58 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -90,6 +90,10 @@ where self::oauth2::userinfo::get, ), ) + .route( + "/oauth2/introspect", + post(self::oauth2::introspection::post), + ) .fallback(mas_static_files::Assets) .layer(Extension(pool.clone())) .layer(Extension(templates.clone())) diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 9ed65d67..453d2703 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -1,4 +1,4 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. +// Copyright 2021, 2022 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,44 +12,96 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::{Encrypter, HttpConfig}; -use mas_data_model::{Client, TokenType}; +use axum::{extract::Extension, response::IntoResponse, Json}; +use hyper::StatusCode; +use mas_axum_utils::client_authorization::{ClientAuthorization, CredentialsVerificationError}; +use mas_config::Encrypter; +use mas_data_model::{TokenFormatError, TokenType}; use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; -use mas_storage::{ - oauth2::{ - access_token::lookup_active_access_token, refresh_token::lookup_active_refresh_token, - }, - PostgresqlBackend, -}; -use mas_warp_utils::{ - errors::WrapError, - filters::{self, client::client_authentication, database::connection, url_builder::UrlBuilder}, +use mas_storage::oauth2::{ + access_token::{lookup_active_access_token, AccessTokenLookupError}, + client::ClientFetchError, + refresh_token::{lookup_active_refresh_token, RefreshTokenLookupError}, }; use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse}; -use sqlx::{pool::PoolConnection, PgPool, Postgres}; -use tracing::{info, warn}; -use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; +use sqlx::PgPool; -pub fn filter( - pool: &PgPool, - encrypter: &Encrypter, - http_config: &HttpConfig, -) -> BoxedFilter<(Box,)> { - let audience = UrlBuilder::from(http_config) - .oauth_introspection_endpoint() - .to_string(); +pub enum RouteError { + Internal(Box), + ClientNotFound, + NotAllowed, + UnknownToken, + BadRequest, + ClientCredentialsVerification(CredentialsVerificationError), +} - warp::path!("oauth2" / "introspect") - .and(filters::trace::name("POST /oauth2/introspect")) - .and( - warp::post() - .and(connection(pool)) - .and(client_authentication(pool, encrypter, audience)) - .and_then(introspect) - .recover(recover) - .unify(), - ) - .boxed() +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + match self { + Self::Internal(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + Self::ClientNotFound => (StatusCode::UNAUTHORIZED, "client not found").into_response(), + Self::UnknownToken => Json(INACTIVE).into_response(), + Self::NotAllowed => ( + StatusCode::UNAUTHORIZED, + "client can't use the introspection endpoint", + ) + .into_response(), + Self::BadRequest => StatusCode::BAD_REQUEST.into_response(), + Self::ClientCredentialsVerification(_c) => ( + StatusCode::UNAUTHORIZED, + "could not verify client credentials", + ) + .into_response(), + } + } +} + +impl From for RouteError { + fn from(e: sqlx::Error) -> Self { + Self::Internal(Box::new(e)) + } +} + +impl From for RouteError { + fn from(_e: TokenFormatError) -> Self { + Self::UnknownToken + } +} + +impl From for RouteError { + fn from(e: ClientFetchError) -> Self { + if e.not_found() { + Self::ClientNotFound + } else { + Self::Internal(Box::new(e)) + } + } +} + +impl From for RouteError { + fn from(e: AccessTokenLookupError) -> Self { + if e.not_found() { + Self::UnknownToken + } else { + Self::Internal(Box::new(e)) + } + } +} + +impl From for RouteError { + fn from(e: RefreshTokenLookupError) -> Self { + if e.not_found() { + Self::UnknownToken + } else { + Self::Internal(Box::new(e)) + } + } +} + +impl From for RouteError { + fn from(e: CredentialsVerificationError) -> Self { + Self::ClientCredentialsVerification(e) + } } const INACTIVE: IntrospectionResponse = IntrospectionResponse { @@ -67,33 +119,44 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse { jti: None, }; -async fn introspect( - mut conn: PoolConnection, - auth: OAuthClientAuthenticationMethod, - client: Client, - params: IntrospectionRequest, -) -> Result, Rejection> { - // Token introspection is only allowed by confidential clients - if auth == OAuthClientAuthenticationMethod::None { - warn!(?client, "Client tried to introspect"); - // TODO: have a nice error here - return Ok(Box::new(warp::reply::json(&INACTIVE))); - } +pub(crate) async fn post( + Extension(pool): Extension, + Extension(encrypter): Extension, + client_authorization: ClientAuthorization, +) -> Result { + let mut conn = pool.acquire().await?; - let token = ¶ms.token; - let token_type = TokenType::check(token).wrap_error()?; - if let Some(hint) = params.token_type_hint { + let client = client_authorization.credentials.fetch(&mut conn).await?; + + let method = match client.token_endpoint_auth_method { + None | Some(OAuthClientAuthenticationMethod::None) => { + return Err(RouteError::NotAllowed); + } + Some(c) => c, + }; + + client_authorization + .credentials + .verify(&encrypter, method, &client) + .await?; + + let form = if let Some(form) = client_authorization.form { + form + } else { + return Err(RouteError::BadRequest); + }; + + let token = &form.token; + let token_type = TokenType::check(token)?; + if let Some(hint) = form.token_type_hint { if token_type != hint { - info!("Token type hint did not match"); - return Ok(Box::new(warp::reply::json(&INACTIVE))); + return Err(RouteError::UnknownToken); } } let reply = match token_type { TokenType::AccessToken => { - let (token, session) = lookup_active_access_token(&mut conn, token) - .await - .wrap_error()?; + let (token, session) = lookup_active_access_token(&mut conn, token).await?; let exp = token.exp(); IntrospectionResponse { @@ -112,9 +175,7 @@ async fn introspect( } } TokenType::RefreshToken => { - let (token, session) = lookup_active_refresh_token(&mut conn, token) - .await - .wrap_error()?; + let (token, session) = lookup_active_refresh_token(&mut conn, token).await?; IntrospectionResponse { active: true, @@ -133,13 +194,5 @@ async fn introspect( } }; - Ok(Box::new(warp::reply::json(&reply))) -} - -async fn recover(rejection: Rejection) -> Result, Rejection> { - if rejection.is_not_found() { - Err(rejection) - } else { - Ok(Box::new(warp::reply::json(&INACTIVE))) - } + Ok(Json(reply)) } diff --git a/crates/handlers/src/oauth2/mod.rs b/crates/handlers/src/oauth2/mod.rs index bfc68ffb..a225b50b 100644 --- a/crates/handlers/src/oauth2/mod.rs +++ b/crates/handlers/src/oauth2/mod.rs @@ -14,7 +14,7 @@ // pub mod authorization; pub mod discovery; -// pub mod introspection; +pub mod introspection; pub mod keys; // pub mod token; pub mod userinfo; diff --git a/crates/jose/src/jwt.rs b/crates/jose/src/jwt.rs index 63b9aa6b..6b698ad7 100644 --- a/crates/jose/src/jwt.rs +++ b/crates/jose/src/jwt.rs @@ -213,10 +213,7 @@ impl JsonWebTokenParts { Ok(decoded) } - pub fn verify(&self, header: &JwtHeader, store: &S) -> S::Future - where - S::Error: std::error::Error + Send + Sync + 'static, - { + pub fn verify(&self, header: &JwtHeader, store: &S) -> S::Future { store.verify(header, self.payload.as_bytes(), &self.signature) }