You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Support for token revocation
This commit is contained in:
@ -180,6 +180,10 @@ where
|
||||
mas_router::OAuth2Introspection::route(),
|
||||
post(self::oauth2::introspection::post),
|
||||
)
|
||||
.route(
|
||||
mas_router::OAuth2Revocation::route(),
|
||||
post(self::oauth2::revoke::post),
|
||||
)
|
||||
.route(
|
||||
mas_router::OAuth2TokenEndpoint::route(),
|
||||
post(self::oauth2::token::post),
|
||||
|
@ -53,6 +53,7 @@ pub(crate) async fn get(
|
||||
let token_endpoint = Some(url_builder.oauth_token_endpoint());
|
||||
let jwks_uri = Some(url_builder.jwks_uri());
|
||||
let introspection_endpoint = Some(url_builder.oauth_introspection_endpoint());
|
||||
let revocation_endpoint = Some(url_builder.oauth_revocation_endpoint());
|
||||
let userinfo_endpoint = Some(url_builder.oidc_userinfo_endpoint());
|
||||
let registration_endpoint = Some(url_builder.oauth_registration_endpoint());
|
||||
|
||||
@ -76,6 +77,10 @@ pub(crate) async fn get(
|
||||
let token_endpoint_auth_signing_alg_values_supported =
|
||||
client_auth_signing_alg_values_supported.clone();
|
||||
|
||||
let revocation_endpoint_auth_methods_supported = client_auth_methods_supported.clone();
|
||||
let revocation_endpoint_auth_signing_alg_values_supported =
|
||||
client_auth_signing_alg_values_supported.clone();
|
||||
|
||||
let introspection_endpoint_auth_methods_supported =
|
||||
client_auth_methods_supported.map(|v| v.into_iter().map(Into::into).collect());
|
||||
let introspection_endpoint_auth_signing_alg_values_supported =
|
||||
@ -125,6 +130,9 @@ pub(crate) async fn get(
|
||||
grant_types_supported,
|
||||
token_endpoint_auth_methods_supported,
|
||||
token_endpoint_auth_signing_alg_values_supported,
|
||||
revocation_endpoint,
|
||||
revocation_endpoint_auth_methods_supported,
|
||||
revocation_endpoint_auth_signing_alg_values_supported,
|
||||
introspection_endpoint,
|
||||
introspection_endpoint_auth_methods_supported,
|
||||
introspection_endpoint_auth_signing_alg_values_supported,
|
||||
|
@ -18,6 +18,7 @@ pub mod discovery;
|
||||
pub mod introspection;
|
||||
pub mod keys;
|
||||
pub mod registration;
|
||||
pub mod revoke;
|
||||
pub mod token;
|
||||
pub mod userinfo;
|
||||
pub mod webfinger;
|
||||
|
200
crates/handlers/src/oauth2/revoke.rs
Normal file
200
crates/handlers/src/oauth2/revoke.rs
Normal file
@ -0,0 +1,200 @@
|
||||
// Copyright 2023 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.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{
|
||||
client_authorization::{ClientAuthorization, CredentialsVerificationError},
|
||||
http_client_factory::HttpClientFactory,
|
||||
};
|
||||
use mas_data_model::TokenType;
|
||||
use mas_iana::oauth::OAuthTokenTypeHint;
|
||||
use mas_keystore::Encrypter;
|
||||
use mas_storage::{BoxClock, BoxRepository};
|
||||
use oauth2_types::{
|
||||
errors::{ClientError, ClientErrorCode},
|
||||
requests::RevocationRequest,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::impl_from_error_for_route;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("bad request")]
|
||||
BadRequest,
|
||||
|
||||
#[error("client not found")]
|
||||
ClientNotFound,
|
||||
|
||||
#[error("client not allowed")]
|
||||
ClientNotAllowed,
|
||||
|
||||
#[error("could not verify client credentials")]
|
||||
ClientCredentialsVerification(#[from] CredentialsVerificationError),
|
||||
|
||||
#[error("client is unauthorized")]
|
||||
UnauthorizedClient,
|
||||
|
||||
#[error("unsupported token type")]
|
||||
UnsupportedTokenType,
|
||||
|
||||
#[error("unknown token")]
|
||||
UnknownToken,
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
sentry::capture_error(&self);
|
||||
match self {
|
||||
Self::Internal(_) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ClientError::from(ClientErrorCode::ServerError)),
|
||||
)
|
||||
.into_response(),
|
||||
|
||||
Self::BadRequest => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ClientError::from(ClientErrorCode::InvalidRequest)),
|
||||
)
|
||||
.into_response(),
|
||||
|
||||
Self::ClientNotFound | Self::ClientCredentialsVerification(_) => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ClientError::from(ClientErrorCode::InvalidClient)),
|
||||
)
|
||||
.into_response(),
|
||||
|
||||
Self::ClientNotAllowed | Self::UnauthorizedClient => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ClientError::from(ClientErrorCode::UnauthorizedClient)),
|
||||
)
|
||||
.into_response(),
|
||||
|
||||
Self::UnsupportedTokenType => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ClientError::from(ClientErrorCode::UnsupportedTokenType)),
|
||||
)
|
||||
.into_response(),
|
||||
|
||||
// If the token is unknown, we still return a 200 OK response.
|
||||
Self::UnknownToken => StatusCode::OK.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
|
||||
impl From<mas_data_model::TokenFormatError> for RouteError {
|
||||
fn from(_e: mas_data_model::TokenFormatError) -> Self {
|
||||
Self::UnknownToken
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.oauth2.revoke.post",
|
||||
fields(client.id = client_authorization.client_id()),
|
||||
skip_all,
|
||||
err,
|
||||
)]
|
||||
pub(crate) async fn post(
|
||||
clock: BoxClock,
|
||||
State(http_client_factory): State<HttpClientFactory>,
|
||||
mut repo: BoxRepository,
|
||||
State(encrypter): State<Encrypter>,
|
||||
client_authorization: ClientAuthorization<RevocationRequest>,
|
||||
) -> Result<impl IntoResponse, RouteError> {
|
||||
let client = client_authorization
|
||||
.credentials
|
||||
.fetch(&mut repo)
|
||||
.await?
|
||||
.ok_or(RouteError::ClientNotFound)?;
|
||||
|
||||
let method = client
|
||||
.token_endpoint_auth_method
|
||||
.as_ref()
|
||||
.ok_or(RouteError::ClientNotAllowed)?;
|
||||
|
||||
client_authorization
|
||||
.credentials
|
||||
.verify(&http_client_factory, &encrypter, method, &client)
|
||||
.await?;
|
||||
|
||||
let Some(form) = client_authorization.form else {
|
||||
return Err(RouteError::BadRequest);
|
||||
};
|
||||
|
||||
let token_type = TokenType::check(&form.token)?;
|
||||
|
||||
// Find the ID of the session to end.
|
||||
let session_id = match (form.token_type_hint, token_type) {
|
||||
(Some(OAuthTokenTypeHint::AccessToken) | None, TokenType::AccessToken) => {
|
||||
let access_token = repo
|
||||
.oauth2_access_token()
|
||||
.find_by_token(&form.token)
|
||||
.await?
|
||||
.ok_or(RouteError::UnknownToken)?;
|
||||
|
||||
if !access_token.is_valid(clock.now()) {
|
||||
return Err(RouteError::UnknownToken);
|
||||
}
|
||||
access_token.session_id
|
||||
}
|
||||
|
||||
(Some(OAuthTokenTypeHint::RefreshToken) | None, TokenType::RefreshToken) => {
|
||||
let refresh_token = repo
|
||||
.oauth2_refresh_token()
|
||||
.find_by_token(&form.token)
|
||||
.await?
|
||||
.ok_or(RouteError::UnknownToken)?;
|
||||
|
||||
if !refresh_token.is_valid() {
|
||||
return Err(RouteError::UnknownToken);
|
||||
}
|
||||
|
||||
refresh_token.session_id
|
||||
}
|
||||
|
||||
// This case can happen if there is a mismatch between the token type hint and the guessed
|
||||
// token type or if the token was a compat access/refresh token. In those cases, we return
|
||||
// an unknown token error.
|
||||
(Some(OAuthTokenTypeHint::AccessToken | OAuthTokenTypeHint::RefreshToken) | None, _) => {
|
||||
return Err(RouteError::UnknownToken)
|
||||
}
|
||||
|
||||
(Some(_), _) => return Err(RouteError::UnsupportedTokenType),
|
||||
};
|
||||
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.lookup(session_id)
|
||||
.await?
|
||||
.ok_or(RouteError::UnknownToken)?;
|
||||
|
||||
// Check that the client ending the session is the same as the client that
|
||||
// created it.
|
||||
if client.id != session.client_id {
|
||||
return Err(RouteError::UnauthorizedClient);
|
||||
}
|
||||
|
||||
// Now that we checked eveyrthing, we can end the session.
|
||||
repo.oauth2_session().finish(&clock, session).await?;
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -265,6 +265,15 @@ pub enum ClientErrorCode {
|
||||
/// From [RFC8628](https://www.rfc-editor.org/rfc/rfc8628#section-3.5).
|
||||
ExpiredToken,
|
||||
|
||||
/// `unsupported_token_type`
|
||||
///
|
||||
/// The authorization server does not support the revocation of the
|
||||
/// presented token type. That is, the client tried to revoke an access
|
||||
/// token on a server not supporting this feature.
|
||||
///
|
||||
/// From [RFC7009](https://www.rfc-editor.org/rfc/rfc7009#section-2.2.1).
|
||||
UnsupportedTokenType,
|
||||
|
||||
/// Another error code.
|
||||
#[display("{0}")]
|
||||
Unknown(String),
|
||||
@ -353,6 +362,9 @@ impl ClientErrorCode {
|
||||
ClientErrorCode::ExpiredToken => {
|
||||
"The \"device_code\" has expired, and the device authorization session has concluded"
|
||||
}
|
||||
ClientErrorCode::UnsupportedTokenType => {
|
||||
"The authorization server does not support the revocation of the presented token type."
|
||||
},
|
||||
ClientErrorCode::Unknown(_) => "",
|
||||
}
|
||||
}
|
||||
|
@ -716,6 +716,27 @@ pub struct IntrospectionResponse {
|
||||
pub jti: Option<String>,
|
||||
}
|
||||
|
||||
/// A request to the [Revocation Endpoint].
|
||||
///
|
||||
/// [Revocation Endpoint]: https://www.rfc-editor.org/rfc/rfc7009#section-2
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct RevocationRequest {
|
||||
/// The value of the token.
|
||||
pub token: String,
|
||||
|
||||
/// A hint about the type of the token submitted for introspection.
|
||||
pub token_type_hint: Option<OAuthTokenTypeHint>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RevocationRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RevocationRequest")
|
||||
.field("token_type_hint", &self.token_type_hint)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// A successful response from the [Pushed Authorization Request Endpoint].
|
||||
///
|
||||
/// Note that there is no request type because it is by definition the same as
|
||||
|
@ -91,7 +91,7 @@ impl SimpleRoute for OidcUserinfo {
|
||||
const PATH: &'static str = "/oauth2/userinfo";
|
||||
}
|
||||
|
||||
/// `POST /oauth2/userinfo`
|
||||
/// `POST /oauth2/introspect`
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct OAuth2Introspection;
|
||||
|
||||
@ -99,6 +99,14 @@ impl SimpleRoute for OAuth2Introspection {
|
||||
const PATH: &'static str = "/oauth2/introspect";
|
||||
}
|
||||
|
||||
/// `POST /oauth2/revoke`
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct OAuth2Revocation;
|
||||
|
||||
impl SimpleRoute for OAuth2Revocation {
|
||||
const PATH: &'static str = "/oauth2/revoke";
|
||||
}
|
||||
|
||||
/// `POST /oauth2/token`
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct OAuth2TokenEndpoint;
|
||||
|
@ -75,6 +75,12 @@ impl UrlBuilder {
|
||||
self.url_for(&crate::endpoints::OAuth2Introspection)
|
||||
}
|
||||
|
||||
/// OAuth 2.0 revocation endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_revocation_endpoint(&self) -> Url {
|
||||
self.url_for(&crate::endpoints::OAuth2Revocation)
|
||||
}
|
||||
|
||||
/// OAuth 2.0 client registration endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_registration_endpoint(&self) -> Url {
|
||||
|
Reference in New Issue
Block a user