Skip to content

Record auth related metrics #4301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 7, 2025
35 changes: 33 additions & 2 deletions crates/handlers/src/compat/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

use std::sync::Arc;
use std::sync::{Arc, LazyLock};

use axum::{
Json,
Expand All @@ -27,6 +27,7 @@ use mas_storage::{
},
user::{UserPasswordRepository, UserRepository},
};
use opentelemetry::{Key, KeyValue, metrics::Counter};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use serde_with::{DurationMilliSeconds, serde_as, skip_serializing_none};
Expand All @@ -35,10 +36,20 @@ use zeroize::Zeroizing;

use super::MatrixError;
use crate::{
BoundActivityTracker, Limiter, RequesterFingerprint, impl_from_error_for_route,
BoundActivityTracker, Limiter, METER, RequesterFingerprint, impl_from_error_for_route,
passwords::PasswordManager, rate_limit::PasswordCheckLimitedError,
};

static LOGIN_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.compat.login_request")
.with_description("How many compatibility login requests have happened")
.with_unit("{request}")
.build()
});
const TYPE: Key = Key::from_static_str("type");
const RESULT: Key = Key::from_static_str("result");

#[derive(Debug, Serialize)]
#[serde(tag = "type")]
enum LoginType {
Expand Down Expand Up @@ -123,6 +134,16 @@ pub enum Credentials {
Unsupported,
}

impl Credentials {
fn login_type(&self) -> &'static str {
match self {
Self::Password { .. } => "m.login.password",
Self::Token { .. } => "m.login.token",
Self::Unsupported => "unsupported",
}
}
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Identifier {
Expand Down Expand Up @@ -192,6 +213,7 @@ impl_from_error_for_route!(mas_storage::RepositoryError);
impl IntoResponse for RouteError {
fn into_response(self) -> axum::response::Response {
let event_id = sentry::capture_error(&self);
LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
let response = match self {
Self::Internal(_) | Self::SessionNotFound | Self::ProvisionDeviceFailed(_) => {
MatrixError {
Expand Down Expand Up @@ -278,6 +300,7 @@ pub(crate) async fn post(
WithRejection(Json(input), _): WithRejection<Json<RequestBody>, RouteError>,
) -> Result<impl IntoResponse, RouteError> {
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
let login_type = input.credentials.login_type();
let (mut session, user) = match (password_manager.is_enabled(), input.credentials) {
(
true,
Expand Down Expand Up @@ -360,6 +383,14 @@ pub(crate) async fn post(
.record_compat_session(&clock, &session)
.await;

LOGIN_COUNTER.add(
1,
&[
KeyValue::new(TYPE, login_type),
KeyValue::new(RESULT, "success"),
],
);

Ok(Json(ResponseBody {
access_token: access_token.token,
device_id: session.device,
Expand Down
17 changes: 16 additions & 1 deletion crates/handlers/src/compat/logout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

use std::sync::LazyLock;

use axum::{Json, response::IntoResponse};
use axum_extra::typed_header::TypedHeader;
use headers::{Authorization, authorization::Bearer};
Expand All @@ -15,10 +17,20 @@ use mas_storage::{
compat::{CompatAccessTokenRepository, CompatSessionRepository},
queue::{QueueJobRepositoryExt as _, SyncDevicesJob},
};
use opentelemetry::{Key, KeyValue, metrics::Counter};
use thiserror::Error;

use super::MatrixError;
use crate::{BoundActivityTracker, impl_from_error_for_route};
use crate::{BoundActivityTracker, METER, impl_from_error_for_route};

static LOGOUT_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.compat.logout_request")
.with_description("How many compatibility logout request have happened")
.with_unit("{request}")
.build()
});
const RESULT: Key = Key::from_static_str("result");

#[derive(Error, Debug)]
pub enum RouteError {
Expand All @@ -40,6 +52,7 @@ impl_from_error_for_route!(mas_storage::RepositoryError);
impl IntoResponse for RouteError {
fn into_response(self) -> axum::response::Response {
let event_id = sentry::capture_error(&self);
LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
let response = match self {
Self::Internal(_) => MatrixError {
errcode: "M_UNKNOWN",
Expand Down Expand Up @@ -113,5 +126,7 @@ pub(crate) async fn post(

repo.save().await?;

LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "success")]);

Ok(Json(serde_json::json!({})))
}
56 changes: 54 additions & 2 deletions crates/handlers/src/oauth2/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

use std::sync::LazyLock;

use axum::{Json, extract::State, http::HeaderValue, response::IntoResponse};
use hyper::{HeaderMap, StatusCode};
use mas_axum_utils::{
Expand All @@ -24,9 +26,21 @@ use oauth2_types::{
requests::{IntrospectionRequest, IntrospectionResponse},
scope::ScopeToken,
};
use opentelemetry::{Key, KeyValue, metrics::Counter};
use thiserror::Error;

use crate::{ActivityTracker, impl_from_error_for_route};
use crate::{ActivityTracker, METER, impl_from_error_for_route};

static INTROSPECTION_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.oauth2.introspection_request")
.with_description("Number of OAuth 2.0 introspection requests")
.with_unit("{request}")
.build()
});

const KIND: Key = Key::from_static_str("kind");
const ACTIVE: Key = Key::from_static_str("active");

#[derive(Debug, Error)]
pub enum RouteError {
Expand Down Expand Up @@ -118,14 +132,20 @@ impl IntoResponse for RouteError {
),
)
.into_response(),

Self::UnknownToken(_)
| Self::UnexpectedTokenType
| Self::InvalidToken(_)
| Self::InvalidUser
| Self::InvalidCompatSession
| Self::InvalidOAuthSession
| Self::InvalidTokenFormat(_)
| Self::CantEncodeDeviceID(_) => Json(INACTIVE).into_response(),
| Self::CantEncodeDeviceID(_) => {
INTROSPECTION_COUNTER.add(1, &[KeyValue::new(ACTIVE.clone(), false)]);

Json(INACTIVE).into_response()
}

Self::NotAllowed => (
StatusCode::UNAUTHORIZED,
Json(ClientError::from(ClientErrorCode::AccessDenied)),
Expand Down Expand Up @@ -275,6 +295,14 @@ pub(crate) async fn post(
.record_oauth2_session(&clock, &session, ip)
.await;

INTROSPECTION_COUNTER.add(
1,
&[
KeyValue::new(KIND, "oauth2_access_token"),
KeyValue::new(ACTIVE, true),
],
);

IntrospectionResponse {
active: true,
scope: Some(session.scope),
Expand Down Expand Up @@ -338,6 +366,14 @@ pub(crate) async fn post(
.record_oauth2_session(&clock, &session, ip)
.await;

INTROSPECTION_COUNTER.add(
1,
&[
KeyValue::new(KIND, "oauth2_refresh_token"),
KeyValue::new(ACTIVE, true),
],
);

IntrospectionResponse {
active: true,
scope: Some(session.scope),
Expand Down Expand Up @@ -412,6 +448,14 @@ pub(crate) async fn post(
.record_compat_session(&clock, &session, ip)
.await;

INTROSPECTION_COUNTER.add(
1,
&[
KeyValue::new(KIND, "compat_access_token"),
KeyValue::new(ACTIVE, true),
],
);

IntrospectionResponse {
active: true,
scope: Some(scope),
Expand Down Expand Up @@ -488,6 +532,14 @@ pub(crate) async fn post(
.record_compat_session(&clock, &session, ip)
.await;

INTROSPECTION_COUNTER.add(
1,
&[
KeyValue::new(KIND, "compat_refresh_token"),
KeyValue::new(ACTIVE, true),
],
);

IntrospectionResponse {
active: true,
scope: Some(scope),
Expand Down
19 changes: 18 additions & 1 deletion crates/handlers/src/oauth2/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

use std::sync::LazyLock;

use axum::{Json, extract::State, response::IntoResponse};
use axum_extra::TypedHeader;
use hyper::StatusCode;
Expand All @@ -19,6 +21,7 @@ use oauth2_types::{
VerifiedClientMetadata,
},
};
use opentelemetry::{Key, KeyValue, metrics::Counter};
use psl::Psl;
use rand::distributions::{Alphanumeric, DistString};
use serde::Serialize;
Expand All @@ -27,7 +30,16 @@ use thiserror::Error;
use tracing::info;
use url::Url;

use crate::{BoundActivityTracker, impl_from_error_for_route};
use crate::{BoundActivityTracker, METER, impl_from_error_for_route};

static REGISTRATION_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.oauth2.registration_request")
.with_description("Number of OAuth2 registration requests")
.with_unit("{request}")
.build()
});
const RESULT: Key = Key::from_static_str("result");

#[derive(Debug, Error)]
pub(crate) enum RouteError {
Expand Down Expand Up @@ -56,6 +68,9 @@ impl_from_error_for_route!(serde_json::Error);
impl IntoResponse for RouteError {
fn into_response(self) -> axum::response::Response {
let event_id = sentry::capture_error(&self);

REGISTRATION_COUNTER.add(1, &[KeyValue::new(RESULT, "denied")]);

let response = match self {
Self::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Expand Down Expand Up @@ -303,6 +318,7 @@ pub(crate) async fn post(

let client = if let Some(client) = existing_client {
tracing::info!(%client.id, "Reusing existing client");
REGISTRATION_COUNTER.add(1, &[KeyValue::new(RESULT, "reused")]);
client
} else {
let client = repo
Expand Down Expand Up @@ -335,6 +351,7 @@ pub(crate) async fn post(
)
.await?;
tracing::info!(%client.id, "Registered new client");
REGISTRATION_COUNTER.add(1, &[KeyValue::new(RESULT, "created")]);
client
};

Expand Down
27 changes: 25 additions & 2 deletions crates/handlers/src/oauth2/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

use std::sync::Arc;
use std::sync::{Arc, LazyLock};

use axum::{Json, extract::State, response::IntoResponse};
use axum_extra::typed_header::TypedHeader;
Expand Down Expand Up @@ -40,12 +40,23 @@ use oauth2_types::{
},
scope,
};
use opentelemetry::{Key, KeyValue, metrics::Counter};
use thiserror::Error;
use tracing::{debug, info};
use ulid::Ulid;

use super::{generate_id_token, generate_token_pair};
use crate::{BoundActivityTracker, impl_from_error_for_route};
use crate::{BoundActivityTracker, METER, impl_from_error_for_route};

static TOKEN_REQUEST_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.oauth2.token_request")
.with_description("How many OAuth 2.0 token requests have gone through")
.with_unit("{request}")
.build()
});
const GRANT_TYPE: Key = Key::from_static_str("grant_type");
const RESULT: Key = Key::from_static_str("successful");

#[derive(Debug, Error)]
pub(crate) enum RouteError {
Expand Down Expand Up @@ -136,6 +147,8 @@ impl IntoResponse for RouteError {
fn into_response(self) -> axum::response::Response {
let event_id = sentry::capture_error(&self);

TOKEN_REQUEST_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);

let response = match self {
Self::Internal(_)
| Self::NoSuchBrowserSession
Expand Down Expand Up @@ -254,6 +267,8 @@ pub(crate) async fn post(

let form = client_authorization.form.ok_or(RouteError::BadRequest)?;

let grant_type = form.grant_type();

let (reply, repo) = match form {
AccessTokenRequest::AuthorizationCode(grant) => {
authorization_code_grant(
Expand Down Expand Up @@ -321,6 +336,14 @@ pub(crate) async fn post(

repo.save().await?;

TOKEN_REQUEST_COUNTER.add(
1,
&[
KeyValue::new(GRANT_TYPE, grant_type),
KeyValue::new(RESULT, "success"),
],
);

let mut headers = HeaderMap::new();
headers.typed_insert(CacheControl::new().with_no_store());
headers.typed_insert(Pragma::no_cache());
Expand Down
Loading
Loading