diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 7c1a6d97a..7711f53aa 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -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> = 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, RouteError>, ) -> Result { 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, diff --git a/crates/handlers/src/compat/logout.rs b/crates/handlers/src/compat/logout.rs index 20df8e682..557dbaef9 100644 --- a/crates/handlers/src/compat/logout.rs +++ b/crates/handlers/src/compat/logout.rs @@ -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> = 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!({}))) } diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 608871e54..a97c62ab6 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -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> = 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), diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index 71812fac6..0b1f3515d 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -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> = 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 }; diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index d76c81f1b..4ed02ef8d 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -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> = 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()); diff --git a/crates/handlers/src/upstream_oauth2/callback.rs b/crates/handlers/src/upstream_oauth2/callback.rs index b0421c33d..766631d96 100644 --- a/crates/handlers/src/upstream_oauth2/callback.rs +++ b/crates/handlers/src/upstream_oauth2/callback.rs @@ -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> = 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 diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index dddf47fa8..cacba650a 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -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> = 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> = 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?; diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 292b3ff66..36b66c1ea 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -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> = 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; diff --git a/crates/handlers/src/views/register/steps/finish.rs b/crates/handlers/src/views/register/steps/finish.rs index 3edb6fbfd..7c73825cc 100644 --- a/crates/handlers/src/views/register/steps/finish.rs +++ b/crates/handlers/src/views/register/steps/finish.rs @@ -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> = 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 { diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index 631b33309..36ee36da6 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -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