1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Axum migration: /oauth2/introspection

This commit is contained in:
Quentin Gliech
2022-04-05 08:21:31 +02:00
parent ed49624c3a
commit 0f7484beee
7 changed files with 145 additions and 77 deletions

13
Cargo.lock generated
View File

@ -531,6 +531,18 @@ dependencies = [
"mime", "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]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.64" version = "0.3.64"
@ -2071,6 +2083,7 @@ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
"axum", "axum",
"axum-macros",
"chrono", "chrono",
"crc", "crc",
"data-encoding", "data-encoding",

View File

@ -132,8 +132,8 @@ impl Credentials {
.ok_or(CredentialsVerificationError::InvalidClientConfig)?; .ok_or(CredentialsVerificationError::InvalidClientConfig)?;
let store: Either<StaticJwksStore, DynamicJwksStore> = jwks_key_store(jwks); let store: Either<StaticJwksStore, DynamicJwksStore> = jwks_key_store(jwks);
jwt.verify(header, &store) let fut = jwt.verify(header, &store);
.await fut.await
.map_err(|_| CredentialsVerificationError::InvalidAssertionSignature)?; .map_err(|_| CredentialsVerificationError::InvalidAssertionSignature)?;
} }
@ -152,8 +152,8 @@ impl Credentials {
.map_err(|_e| CredentialsVerificationError::DecryptionError)?; .map_err(|_e| CredentialsVerificationError::DecryptionError)?;
let store = SharedSecret::new(&decrypted_client_secret); let store = SharedSecret::new(&decrypted_client_secret);
jwt.verify(header, &store) let fut = jwt.verify(header, &store);
.await fut.await
.map_err(|_| CredentialsVerificationError::InvalidAssertionSignature)?; .map_err(|_| CredentialsVerificationError::InvalidAssertionSignature)?;
} }
@ -181,8 +181,8 @@ pub enum CredentialsVerificationError {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct ClientAuthorization<F = ()> { pub struct ClientAuthorization<F = ()> {
credentials: Credentials, pub credentials: Credentials,
form: Option<F>, pub form: Option<F>,
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -24,6 +24,7 @@ warp = "0.3.2"
hyper = { version = "0.14.17", features = ["full"] } hyper = { version = "0.14.17", features = ["full"] }
tower = "0.4.12" tower = "0.4.12"
axum = "0.4.8" axum = "0.4.8"
axum-macros = "0.2.0"
# Emails # Emails
lettre = { version = "0.10.0-rc.4", default-features = false, features = ["builder"] } lettre = { version = "0.10.0-rc.4", default-features = false, features = ["builder"] }

View File

@ -90,6 +90,10 @@ where
self::oauth2::userinfo::get, self::oauth2::userinfo::get,
), ),
) )
.route(
"/oauth2/introspect",
post(self::oauth2::introspection::post),
)
.fallback(mas_static_files::Assets) .fallback(mas_static_files::Assets)
.layer(Extension(pool.clone())) .layer(Extension(pool.clone()))
.layer(Extension(templates.clone())) .layer(Extension(templates.clone()))

View File

@ -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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with 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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use mas_config::{Encrypter, HttpConfig}; use axum::{extract::Extension, response::IntoResponse, Json};
use mas_data_model::{Client, TokenType}; 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_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint};
use mas_storage::{ use mas_storage::oauth2::{
oauth2::{ access_token::{lookup_active_access_token, AccessTokenLookupError},
access_token::lookup_active_access_token, refresh_token::lookup_active_refresh_token, client::ClientFetchError,
}, refresh_token::{lookup_active_refresh_token, RefreshTokenLookupError},
PostgresqlBackend,
};
use mas_warp_utils::{
errors::WrapError,
filters::{self, client::client_authentication, database::connection, url_builder::UrlBuilder},
}; };
use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse}; use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse};
use sqlx::{pool::PoolConnection, PgPool, Postgres}; use sqlx::PgPool;
use tracing::{info, warn};
use warp::{filters::BoxedFilter, Filter, Rejection, Reply};
pub fn filter( pub enum RouteError {
pool: &PgPool, Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
encrypter: &Encrypter, ClientNotFound,
http_config: &HttpConfig, NotAllowed,
) -> BoxedFilter<(Box<dyn Reply>,)> { UnknownToken,
let audience = UrlBuilder::from(http_config) BadRequest,
.oauth_introspection_endpoint() ClientCredentialsVerification(CredentialsVerificationError),
.to_string(); }
warp::path!("oauth2" / "introspect") impl IntoResponse for RouteError {
.and(filters::trace::name("POST /oauth2/introspect")) fn into_response(self) -> axum::response::Response {
.and( match self {
warp::post() Self::Internal(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
.and(connection(pool)) Self::ClientNotFound => (StatusCode::UNAUTHORIZED, "client not found").into_response(),
.and(client_authentication(pool, encrypter, audience)) Self::UnknownToken => Json(INACTIVE).into_response(),
.and_then(introspect) Self::NotAllowed => (
.recover(recover) StatusCode::UNAUTHORIZED,
.unify(), "client can't use the introspection endpoint",
) )
.boxed() .into_response(),
Self::BadRequest => StatusCode::BAD_REQUEST.into_response(),
Self::ClientCredentialsVerification(_c) => (
StatusCode::UNAUTHORIZED,
"could not verify client credentials",
)
.into_response(),
}
}
}
impl From<sqlx::Error> for RouteError {
fn from(e: sqlx::Error) -> Self {
Self::Internal(Box::new(e))
}
}
impl From<TokenFormatError> for RouteError {
fn from(_e: TokenFormatError) -> Self {
Self::UnknownToken
}
}
impl From<ClientFetchError> for RouteError {
fn from(e: ClientFetchError) -> Self {
if e.not_found() {
Self::ClientNotFound
} else {
Self::Internal(Box::new(e))
}
}
}
impl From<AccessTokenLookupError> for RouteError {
fn from(e: AccessTokenLookupError) -> Self {
if e.not_found() {
Self::UnknownToken
} else {
Self::Internal(Box::new(e))
}
}
}
impl From<RefreshTokenLookupError> for RouteError {
fn from(e: RefreshTokenLookupError) -> Self {
if e.not_found() {
Self::UnknownToken
} else {
Self::Internal(Box::new(e))
}
}
}
impl From<CredentialsVerificationError> for RouteError {
fn from(e: CredentialsVerificationError) -> Self {
Self::ClientCredentialsVerification(e)
}
} }
const INACTIVE: IntrospectionResponse = IntrospectionResponse { const INACTIVE: IntrospectionResponse = IntrospectionResponse {
@ -67,33 +119,44 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse {
jti: None, jti: None,
}; };
async fn introspect( pub(crate) async fn post(
mut conn: PoolConnection<Postgres>, Extension(pool): Extension<PgPool>,
auth: OAuthClientAuthenticationMethod, Extension(encrypter): Extension<Encrypter>,
client: Client<PostgresqlBackend>, client_authorization: ClientAuthorization<IntrospectionRequest>,
params: IntrospectionRequest, ) -> Result<impl IntoResponse, RouteError> {
) -> Result<Box<dyn Reply>, Rejection> { let mut conn = pool.acquire().await?;
// 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)));
}
let token = &params.token; let client = client_authorization.credentials.fetch(&mut conn).await?;
let token_type = TokenType::check(token).wrap_error()?;
if let Some(hint) = params.token_type_hint { 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 { if token_type != hint {
info!("Token type hint did not match"); return Err(RouteError::UnknownToken);
return Ok(Box::new(warp::reply::json(&INACTIVE)));
} }
} }
let reply = match token_type { let reply = match token_type {
TokenType::AccessToken => { TokenType::AccessToken => {
let (token, session) = lookup_active_access_token(&mut conn, token) let (token, session) = lookup_active_access_token(&mut conn, token).await?;
.await
.wrap_error()?;
let exp = token.exp(); let exp = token.exp();
IntrospectionResponse { IntrospectionResponse {
@ -112,9 +175,7 @@ async fn introspect(
} }
} }
TokenType::RefreshToken => { TokenType::RefreshToken => {
let (token, session) = lookup_active_refresh_token(&mut conn, token) let (token, session) = lookup_active_refresh_token(&mut conn, token).await?;
.await
.wrap_error()?;
IntrospectionResponse { IntrospectionResponse {
active: true, active: true,
@ -133,13 +194,5 @@ async fn introspect(
} }
}; };
Ok(Box::new(warp::reply::json(&reply))) Ok(Json(reply))
}
async fn recover(rejection: Rejection) -> Result<Box<dyn Reply>, Rejection> {
if rejection.is_not_found() {
Err(rejection)
} else {
Ok(Box::new(warp::reply::json(&INACTIVE)))
}
} }

View File

@ -14,7 +14,7 @@
// pub mod authorization; // pub mod authorization;
pub mod discovery; pub mod discovery;
// pub mod introspection; pub mod introspection;
pub mod keys; pub mod keys;
// pub mod token; // pub mod token;
pub mod userinfo; pub mod userinfo;

View File

@ -213,10 +213,7 @@ impl JsonWebTokenParts {
Ok(decoded) Ok(decoded)
} }
pub fn verify<S: VerifyingKeystore>(&self, header: &JwtHeader, store: &S) -> S::Future pub fn verify<S: VerifyingKeystore>(&self, header: &JwtHeader, store: &S) -> S::Future {
where
S::Error: std::error::Error + Send + Sync + 'static,
{
store.verify(header, self.payload.as_bytes(), &self.signature) store.verify(header, self.payload.as_bytes(), &self.signature)
} }