Record auth related metrics (#4301)

This commit is contained in:
Quentin Gliech
2025-04-07 08:49:28 +02:00
committed by GitHub
10 changed files with 256 additions and 15 deletions
+33 -2
View File
@@ -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,
@@ -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};
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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,
@@ -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,
+16 -1
View File
@@ -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};
@@ -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 {
@@ -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",
@@ -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!({})))
}
+54 -2
View File
@@ -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::{
@@ -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 {
@@ -118,6 +132,7 @@ impl IntoResponse for RouteError {
),
)
.into_response(),
Self::UnknownToken(_)
| Self::UnexpectedTokenType
| Self::InvalidToken(_)
@@ -125,7 +140,12 @@ impl IntoResponse for RouteError {
| 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)),
@@ -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),
@@ -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),
@@ -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),
@@ -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),
+18 -1
View File
@@ -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;
@@ -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;
@@ -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 {
@@ -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,
@@ -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
@@ -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
};
+25 -2
View File
@@ -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;
@@ -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 {
@@ -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
@@ -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(
@@ -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());
@@ -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::{
Form,
extract::{Path, State},
@@ -26,6 +28,7 @@ use mas_storage::{
};
use mas_templates::{FormPostContext, Templates};
use oauth2_types::{errors::ClientErrorCode, requests::AccessTokenRequest};
use opentelemetry::{Key, KeyValue, metrics::Counter};
use serde::{Deserialize, Serialize};
use serde_json::json;
use thiserror::Error;
@@ -37,7 +40,18 @@ use super::{
client_credentials_for_provider,
template::{AttributeMappingContext, environment},
};
use crate::{PreferredLanguage, impl_from_error_for_route, upstream_oauth2::cache::MetadataCache};
use crate::{
METER, PreferredLanguage, impl_from_error_for_route, upstream_oauth2::cache::MetadataCache,
};
static CALLBACK_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.upstream_oauth2.callback")
.with_description("Number of requests to the upstream OAuth2 callback endpoint")
.build()
});
const PROVIDER: Key = Key::from_static_str("provider");
const RESULT: Key = Key::from_static_str("result");
#[derive(Serialize, Deserialize)]
pub struct Params {
@@ -216,6 +230,14 @@ pub(crate) async fn handler(
}
if let Some(error) = params.error {
CALLBACK_COUNTER.add(
1,
&[
KeyValue::new(PROVIDER, provider_id.to_string()),
KeyValue::new(RESULT, "error"),
],
);
return Err(RouteError::ClientError {
error,
error_description: params.error_description.clone(),
@@ -256,6 +278,14 @@ pub(crate) async fn handler(
return Err(RouteError::MissingCode);
};
CALLBACK_COUNTER.add(
1,
&[
KeyValue::new(PROVIDER, provider_id.to_string()),
KeyValue::new(RESULT, "success"),
],
);
let mut lazy_metadata = LazyProviderInfos::new(&metadata_cache, &provider, &client);
// Figure out the client credentials
+29 -2
View File
@@ -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::{
Form,
@@ -35,6 +35,7 @@ use mas_templates::{
ToFormState, UpstreamExistingLinkContext, UpstreamRegister, UpstreamSuggestLink,
};
use minijinja::Environment;
use opentelemetry::{Key, KeyValue, metrics::Counter};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::warn;
@@ -45,10 +46,26 @@ use super::{
template::{AttributeMappingContext, environment},
};
use crate::{
BoundActivityTracker, PreferredLanguage, SiteConfig, impl_from_error_for_route,
BoundActivityTracker, METER, PreferredLanguage, SiteConfig, impl_from_error_for_route,
views::shared::OptionalPostAuthAction,
};
static LOGIN_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.upstream_oauth2.login")
.with_description("Successful upstream OAuth 2.0 login to existing accounts")
.with_unit("{login}")
.build()
});
static REGISTRATION_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.upstream_oauth2.registration")
.with_description("Successful upstream OAuth 2.0 registration")
.with_unit("{registration}")
.build()
});
const PROVIDER: Key = Key::from_static_str("provider");
const DEFAULT_LOCALPART_TEMPLATE: &str = "{{ user.preferred_username }}";
const DEFAULT_DISPLAYNAME_TEMPLATE: &str = "{{ user.name }}";
const DEFAULT_EMAIL_TEMPLATE: &str = "{{ user.email }}";
@@ -340,6 +357,14 @@ pub(crate) async fn get(
repo.save().await?;
LOGIN_COUNTER.add(
1,
&[KeyValue::new(
PROVIDER,
upstream_session.provider_id.to_string(),
)],
);
post_auth_action.go_next(&url_builder).into_response()
}
@@ -805,6 +830,8 @@ pub(crate) async fn post(
.into_response());
}
REGISTRATION_COUNTER.add(1, &[KeyValue::new(PROVIDER, provider.id.to_string())]);
// Now we can create the user
let user = repo.user().add(&mut rng, &clock, username).await?;
+21 -2
View File
@@ -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::{
extract::{Form, Query, State},
@@ -30,17 +30,27 @@ use mas_templates::{
AccountInactiveContext, FieldError, FormError, FormState, LoginContext, LoginFormField,
PostAuthContext, PostAuthContextInner, TemplateContext, Templates, ToFormState,
};
use opentelemetry::{Key, KeyValue, metrics::Counter};
use rand::Rng;
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use super::shared::OptionalPostAuthAction;
use crate::{
BoundActivityTracker, Limiter, PreferredLanguage, RequesterFingerprint, SiteConfig,
BoundActivityTracker, Limiter, METER, PreferredLanguage, RequesterFingerprint, SiteConfig,
passwords::PasswordManager,
session::{SessionOrFallback, load_session_or_fallback},
};
static PASSWORD_LOGIN_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.user.password_login_attempt")
.with_description("Number of password login attempts")
.with_unit("{attempt}")
.build()
});
const RESULT: Key = Key::from_static_str("result");
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LoginForm {
username: String,
@@ -156,6 +166,7 @@ pub(crate) async fn post(
}
if !form_state.is_valid() {
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
return render(
locale,
cookie_jar,
@@ -178,6 +189,7 @@ pub(crate) async fn post(
// First, lookup the user
let Some(user) = repo.user().find_by_username(username).await? else {
let form_state = form_state.with_error_on_form(FormError::InvalidCredentials);
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
return render(
locale,
cookie_jar,
@@ -196,6 +208,7 @@ pub(crate) async fn post(
if let Err(e) = limiter.check_password(requester, &user) {
tracing::warn!(error = &e as &dyn std::error::Error);
let form_state = form_state.with_error_on_form(FormError::RateLimitExceeded);
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
return render(
locale,
cookie_jar,
@@ -215,6 +228,7 @@ pub(crate) async fn post(
// There is no password for this user, but we don't want to disclose that. Show
// a generic 'invalid credentials' error instead
let form_state = form_state.with_error_on_form(FormError::InvalidCredentials);
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
return render(
locale,
cookie_jar,
@@ -257,6 +271,7 @@ pub(crate) async fn post(
Ok(None) => user_password,
Err(_) => {
let form_state = form_state.with_error_on_form(FormError::InvalidCredentials);
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
return render(
locale,
cookie_jar,
@@ -275,6 +290,7 @@ pub(crate) async fn post(
// Now that we have checked the user password, we now want to show an error if
// the user is locked or deactivated
if user.deactivated_at.is_some() {
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let ctx = AccountInactiveContext::new(user)
.with_csrf(csrf_token.form_value())
@@ -284,6 +300,7 @@ pub(crate) async fn post(
}
if user.locked_at.is_some() {
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let ctx = AccountInactiveContext::new(user)
.with_csrf(csrf_token.form_value())
@@ -309,6 +326,8 @@ pub(crate) async fn post(
repo.save().await?;
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "success")]);
activity_tracker
.record_browser_session(&clock, &user_session)
.await;
@@ -3,7 +3,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 anyhow::Context as _;
use axum::{
@@ -22,10 +22,21 @@ use mas_storage::{
user::UserEmailFilter,
};
use mas_templates::{RegisterStepsEmailInUseContext, TemplateContext as _, Templates};
use opentelemetry::metrics::Counter;
use ulid::Ulid;
use super::super::cookie::UserRegistrationSessions;
use crate::{BoundActivityTracker, PreferredLanguage, views::shared::OptionalPostAuthAction};
use crate::{
BoundActivityTracker, METER, PreferredLanguage, views::shared::OptionalPostAuthAction,
};
static PASSWORD_REGISTER_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
METER
.u64_counter("mas.user.password_registration")
.with_description("Number of password registrations")
.with_unit("{registration}")
.build()
});
#[tracing::instrument(
name = "handlers.views.register.steps.finish.get",
@@ -203,6 +214,8 @@ pub(crate) async fn get(
repo.browser_session()
.authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
.await?;
PASSWORD_REGISTER_COUNTER.add(1, &[]);
}
if let Some(terms_url) = registration.terms_url {
+14
View File
@@ -638,6 +638,20 @@ pub enum AccessTokenRequest {
Unsupported,
}
impl AccessTokenRequest {
/// Returns the string representation of the grant type of the request.
#[must_use]
pub fn grant_type(&self) -> &'static str {
match self {
Self::AuthorizationCode(_) => "authorization_code",
Self::RefreshToken(_) => "refresh_token",
Self::ClientCredentials(_) => "client_credentials",
Self::DeviceCode(_) => "urn:ietf:params:oauth:grant-type:device_code",
Self::Unsupported => "unsupported",
}
}
}
/// A successful response from the [Token Endpoint].
///
/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2