about summary refs log tree commit diff
path: root/indieauth/src/lib.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2025-04-09 23:31:02 +0300
committerVika <vika@fireburn.ru>2025-04-09 23:31:57 +0300
commit8826d9446e6c492db2243b9921e59ce496027bef (patch)
tree63738aa9001cb73b11cb0e974e93129bcdf1adbb /indieauth/src/lib.rs
parent519cadfbb298f50cbf819dde757037ab56e2863e (diff)
downloadkittybox-8826d9446e6c492db2243b9921e59ce496027bef.tar.zst
cargo fmt
Change-Id: I80e81ebba3f0cdf8c094451c9fe3ee4126b8c888
Diffstat (limited to 'indieauth/src/lib.rs')
-rw-r--r--indieauth/src/lib.rs252
1 files changed, 158 insertions, 94 deletions
diff --git a/indieauth/src/lib.rs b/indieauth/src/lib.rs
index 459d943..1582318 100644
--- a/indieauth/src/lib.rs
+++ b/indieauth/src/lib.rs
@@ -20,13 +20,13 @@
 //! [`axum`]: https://github.com/tokio-rs/axum
 use std::borrow::Cow;
 
-use serde::{Serialize, Deserialize};
+use serde::{Deserialize, Serialize};
 use url::Url;
 
 mod scopes;
 pub use self::scopes::{Scope, Scopes};
 mod pkce;
-pub use self::pkce::{PKCEMethod, PKCEVerifier, PKCEChallenge};
+pub use self::pkce::{PKCEChallenge, PKCEMethod, PKCEVerifier};
 
 // Re-export rand crate just to be sure.
 pub use rand;
@@ -48,7 +48,7 @@ pub enum IntrospectionEndpointAuthMethod {
     /// TLS client auth with a certificate signed by a valid CA.
     TlsClientAuth,
     /// TLS client auth with a self-signed certificate.
-    SelfSignedTlsClientAuth
+    SelfSignedTlsClientAuth,
 }
 
 /// Authentication methods supported by the revocation endpoint.
@@ -64,7 +64,7 @@ pub enum IntrospectionEndpointAuthMethod {
 pub enum RevocationEndpointAuthMethod {
     /// No authentication is required to access an endpoint declaring
     /// this value.
-    None
+    None,
 }
 
 /// The response types supported by the authorization endpoint.
@@ -80,7 +80,7 @@ pub enum ResponseType {
     /// This response type requires a valid access token.
     ///
     /// [AutoAuth spec]: https://github.com/sknebel/AutoAuth/blob/master/AutoAuth.md#allowing-external-clients-to-obtain-tokens
-    ExternalToken
+    ExternalToken,
 }
 // TODO serde_variant
 impl ResponseType {
@@ -108,7 +108,7 @@ pub enum GrantType {
     /// The refresh token grant, allowing to exchange a refresh token
     /// for a fresh access token and a new refresh token, to
     /// facilitate long-term access.
-    RefreshToken
+    RefreshToken,
 }
 
 /// OAuth 2.0 Authorization Server Metadata in application to the IndieAuth protocol.
@@ -220,7 +220,7 @@ pub struct Metadata {
     /// registration.
     #[serde(skip_serializing_if = "ref_identity")]
     #[serde(default = "Default::default")]
-    pub client_id_metadata_document_supported: bool
+    pub client_id_metadata_document_supported: bool,
 }
 
 impl std::fmt::Debug for Metadata {
@@ -230,31 +230,59 @@ impl std::fmt::Debug for Metadata {
             .field("authorization_endpoint", &self.issuer.as_str())
             .field("token_endpoint", &self.issuer.as_str())
             .field("introspection_endpoint", &self.issuer.as_str())
-            .field("introspection_endpoint_auth_methods_supported", &self.introspection_endpoint_auth_methods_supported)
-            .field("revocation_endpoint", &self.revocation_endpoint.as_ref().map(Url::as_str))
-            .field("revocation_endpoint_auth_methods_supported", &self.revocation_endpoint_auth_methods_supported)
+            .field(
+                "introspection_endpoint_auth_methods_supported",
+                &self.introspection_endpoint_auth_methods_supported,
+            )
+            .field(
+                "revocation_endpoint",
+                &self.revocation_endpoint.as_ref().map(Url::as_str),
+            )
+            .field(
+                "revocation_endpoint_auth_methods_supported",
+                &self.revocation_endpoint_auth_methods_supported,
+            )
             .field("scopes_supported", &self.scopes_supported)
             .field("response_types_supported", &self.response_types_supported)
             .field("grant_types_supported", &self.grant_types_supported)
-            .field("service_documentation", &self.service_documentation.as_ref().map(Url::as_str))
-            .field("code_challenge_methods_supported", &self.code_challenge_methods_supported)
-            .field("authorization_response_iss_parameter_supported", &self.authorization_response_iss_parameter_supported)
-            .field("userinfo_endpoint", &self.userinfo_endpoint.as_ref().map(Url::as_str))
-            .field("client_id_metadata_document_supported", &self.client_id_metadata_document_supported)
+            .field(
+                "service_documentation",
+                &self.service_documentation.as_ref().map(Url::as_str),
+            )
+            .field(
+                "code_challenge_methods_supported",
+                &self.code_challenge_methods_supported,
+            )
+            .field(
+                "authorization_response_iss_parameter_supported",
+                &self.authorization_response_iss_parameter_supported,
+            )
+            .field(
+                "userinfo_endpoint",
+                &self.userinfo_endpoint.as_ref().map(Url::as_str),
+            )
+            .field(
+                "client_id_metadata_document_supported",
+                &self.client_id_metadata_document_supported,
+            )
             .finish()
     }
 }
 
-fn ref_identity(v: &bool) -> bool { *v }
+fn ref_identity(v: &bool) -> bool {
+    *v
+}
 
 #[cfg(feature = "axum")]
 impl axum_core::response::IntoResponse for Metadata {
     fn into_response(self) -> axum_core::response::Response {
         use http::StatusCode;
 
-        (StatusCode::OK,
-         [("Content-Type", "application/json")],
-         serde_json::to_vec(&self).unwrap())
+        (
+            StatusCode::OK,
+            [("Content-Type", "application/json")],
+            serde_json::to_vec(&self).unwrap(),
+        )
             .into_response()
     }
 }
@@ -306,7 +334,7 @@ pub struct ClientMetadata {
     pub software_version: Option<Cow<'static, str>>,
     /// URI for the homepage of this client's owners
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub homepage_uri: Option<Url>
+    pub homepage_uri: Option<Url>,
 }
 
 /// Error that occurs when creating [`ClientMetadata`] with mismatched `client_id` and `client_uri`.
@@ -328,12 +356,15 @@ impl ClientMetadata {
     /// Returns `()` if the `client_uri` is not a prefix of `client_id` as required by the IndieAuth
     /// spec.
     pub fn new(client_id: url::Url, client_uri: url::Url) -> Result<Self, ClientIdMismatch> {
-        if client_id.as_str().as_bytes()[..client_uri.as_str().len()] != *client_uri.as_str().as_bytes() {
+        if client_id.as_str().as_bytes()[..client_uri.as_str().len()]
+            != *client_uri.as_str().as_bytes()
+        {
             return Err(ClientIdMismatch);
         }
 
         Ok(Self {
-            client_id, client_uri,
+            client_id,
+            client_uri,
             client_name: None,
             logo_uri: None,
             redirect_uris: None,
@@ -363,14 +394,15 @@ impl axum_core::response::IntoResponse for ClientMetadata {
     fn into_response(self) -> axum_core::response::Response {
         use http::StatusCode;
 
-        (StatusCode::OK,
-         [("Content-Type", "application/json")],
-         serde_json::to_vec(&self).unwrap())
+        (
+            StatusCode::OK,
+            [("Content-Type", "application/json")],
+            serde_json::to_vec(&self).unwrap(),
+        )
             .into_response()
     }
 }
 
-
 /// User profile to be returned from the userinfo endpoint and when
 /// the `profile` scope was requested.
 #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -387,7 +419,7 @@ pub struct Profile {
     /// User's email, if they've chosen to reveal it. This is guarded
     /// by the `email` scope.
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub email: Option<String>
+    pub email: Option<String>,
 }
 
 #[cfg(feature = "axum")]
@@ -395,9 +427,11 @@ impl axum_core::response::IntoResponse for Profile {
     fn into_response(self) -> axum_core::response::Response {
         use http::StatusCode;
 
-        (StatusCode::OK,
-         [("Content-Type", "application/json")],
-         serde_json::to_vec(&self).unwrap())
+        (
+            StatusCode::OK,
+            [("Content-Type", "application/json")],
+            serde_json::to_vec(&self).unwrap(),
+        )
             .into_response()
     }
 }
@@ -422,13 +456,13 @@ impl State {
     /// Generate a random state string of 128 bytes in length, using
     /// the provided random number generator.
     pub fn from_rng(rng: &mut (impl rand::CryptoRng + rand::Rng)) -> Self {
-        use rand::{Rng, distributions::Alphanumeric};
+        use rand::{distributions::Alphanumeric, Rng};
 
-        let bytes = rng.sample_iter(&Alphanumeric)
+        let bytes = rng
+            .sample_iter(&Alphanumeric)
             .take(128)
             .collect::<Vec<u8>>();
         Self(String::from_utf8(bytes).unwrap())
-
     }
 }
 impl AsRef<str> for State {
@@ -511,21 +545,23 @@ impl AuthorizationRequest {
             ("response_type", Cow::Borrowed(self.response_type.as_str())),
             ("client_id", Cow::Borrowed(self.client_id.as_str())),
             ("redirect_uri", Cow::Borrowed(self.redirect_uri.as_str())),
-            ("code_challenge", Cow::Borrowed(self.code_challenge.as_str())),
-            ("code_challenge_method", Cow::Borrowed(self.code_challenge.method().as_str())),
-            ("state", Cow::Borrowed(self.state.as_ref()))
+            (
+                "code_challenge",
+                Cow::Borrowed(self.code_challenge.as_str()),
+            ),
+            (
+                "code_challenge_method",
+                Cow::Borrowed(self.code_challenge.method().as_str()),
+            ),
+            ("state", Cow::Borrowed(self.state.as_ref())),
         ];
 
         if let Some(ref scope) = self.scope {
-            v.push(
-                ("scope", Cow::Owned(scope.to_string()))
-            );
+            v.push(("scope", Cow::Owned(scope.to_string())));
         }
 
         if let Some(ref me) = self.me {
-            v.push(
-                ("me", Cow::Borrowed(me.as_str()))
-            );
+            v.push(("me", Cow::Borrowed(me.as_str())));
         }
 
         v
@@ -558,17 +594,22 @@ pub struct AutoAuthRequest {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct AutoAuthCallbackData {
     state: State,
-    callback_url: Url
+    callback_url: Url,
 }
 
 #[inline(always)]
-fn deserialize_secs<'de, D: serde::de::Deserializer<'de>>(d: D) -> Result<std::time::Duration, D::Error> {
+fn deserialize_secs<'de, D: serde::de::Deserializer<'de>>(
+    d: D,
+) -> Result<std::time::Duration, D::Error> {
     use serde::Deserialize;
     Ok(std::time::Duration::from_secs(u64::deserialize(d)?))
 }
 
 #[inline(always)]
-fn serialize_secs<S: serde::ser::Serializer>(d: &std::time::Duration, s: S) -> Result<S::Ok, S::Error> {
+fn serialize_secs<S: serde::ser::Serializer>(
+    d: &std::time::Duration,
+    s: S,
+) -> Result<S::Ok, S::Error> {
     s.serialize_u64(std::time::Duration::as_secs(d))
 }
 
@@ -578,7 +619,7 @@ pub struct AutoAuthPollingResponse {
     request_id: State,
     #[serde(serialize_with = "serialize_secs")]
     #[serde(deserialize_with = "deserialize_secs")]
-    interval: std::time::Duration
+    interval: std::time::Duration,
 }
 
 /// The authorization response that must be appended to the
@@ -610,10 +651,9 @@ pub struct AuthorizationResponse {
     /// authorization server.
     ///
     /// [oauth2-iss]: https://www.ietf.org/archive/id/draft-ietf-oauth-iss-auth-resp-02.html
-    pub iss: Url
+    pub iss: Url,
 }
 
-
 /// A special grant request that is used in the AutoAuth ceremony.
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub struct AutoAuthCodeGrant {
@@ -636,7 +676,7 @@ pub struct AutoAuthCodeGrant {
     callback_url: Url,
     /// The user's URL. Will be used to confirm the authorization
     /// endpoint's authority.
-    me: Url
+    me: Url,
 }
 
 /// A grant request that continues the IndieAuth ceremony.
@@ -655,7 +695,7 @@ pub enum GrantRequest {
         redirect_uri: Url,
         /// The PKCE code verifier that was used to create the code
         /// challenge.
-        code_verifier: PKCEVerifier
+        code_verifier: PKCEVerifier,
     },
     /// Use a refresh token to get a fresh access token and a new
     /// matching refresh token.
@@ -670,8 +710,8 @@ pub enum GrantRequest {
         ///
         /// This cannot be used to gain new scopes -- you need to
         /// start over if you need new scopes from the user.
-        scope: Option<Scopes>
-    }
+        scope: Option<Scopes>,
+    },
 }
 
 /// Token type, as described in [RFC6749][].
@@ -685,7 +725,7 @@ pub enum TokenType {
     /// IndieAuth uses.
     ///
     /// [RFC6750]: https://www.rfc-editor.org/rfc/rfc6750
-    Bearer
+    Bearer,
 }
 
 /// The response to a successful [`GrantRequest`].
@@ -722,14 +762,14 @@ pub enum GrantResponse {
         profile: Option<Profile>,
         /// The refresh token, if it was issued.
         #[serde(skip_serializing_if = "Option::is_none")]
-        refresh_token: Option<String>
+        refresh_token: Option<String>,
     },
     /// A profile URL response, that only contains the profile URL and
     /// the profile, if it was requested.
     ///
     /// This is suitable for confirming the identity of the user, but
     /// no more than that.
-    ProfileUrl(ProfileUrl)
+    ProfileUrl(ProfileUrl),
 }
 
 /// The contents of a profile URL response.
@@ -739,7 +779,7 @@ pub struct ProfileUrl {
     pub me: Url,
     /// The user's profile information, if it was requested.
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub profile: Option<Profile>
+    pub profile: Option<Profile>,
 }
 
 #[cfg(feature = "axum")]
@@ -747,12 +787,15 @@ impl axum_core::response::IntoResponse for GrantResponse {
     fn into_response(self) -> axum_core::response::Response {
         use http::StatusCode;
 
-        (StatusCode::OK,
-         [("Content-Type", "application/json"),
-          ("Cache-Control", "no-store"),
-          ("Pragma", "no-cache")
-         ],
-         serde_json::to_vec(&self).unwrap())
+        (
+            StatusCode::OK,
+            [
+                ("Content-Type", "application/json"),
+                ("Cache-Control", "no-store"),
+                ("Pragma", "no-cache"),
+            ],
+            serde_json::to_vec(&self).unwrap(),
+        )
             .into_response()
     }
 }
@@ -766,7 +809,7 @@ impl axum_core::response::IntoResponse for GrantResponse {
 pub enum RequestMaybeAuthorizationEndpoint {
     Authorization(AuthorizationRequest),
     Grant(GrantRequest),
-    AutoAuth(AutoAuthCodeGrant)
+    AutoAuth(AutoAuthCodeGrant),
 }
 
 /// A token introspection request that can be handled by the token
@@ -778,7 +821,7 @@ pub enum RequestMaybeAuthorizationEndpoint {
 #[derive(Debug, Serialize, Deserialize)]
 pub struct TokenIntrospectionRequest {
     /// The token for which data was requested.
-    pub token: String
+    pub token: String,
 }
 
 /// Data for a token that will be returned by the introspection
@@ -800,7 +843,7 @@ pub struct TokenData {
     /// The issue date, represented in the same format as the
     /// [`exp`][TokenData::exp] field.
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub iat: Option<u64>
+    pub iat: Option<u64>,
 }
 
 impl TokenData {
@@ -809,24 +852,25 @@ impl TokenData {
         use std::time::{Duration, SystemTime, UNIX_EPOCH};
 
         self.exp
-            .map(|exp| SystemTime::now()
-                 .duration_since(UNIX_EPOCH)
-                 .unwrap_or(Duration::ZERO)
-                 .as_secs() >= exp)
+            .map(|exp| {
+                SystemTime::now()
+                    .duration_since(UNIX_EPOCH)
+                    .unwrap_or(Duration::ZERO)
+                    .as_secs()
+                    >= exp
+            })
             .unwrap_or_default()
     }
 
     /// Return a timestamp at which the token is not considered valid anymore.
     pub fn expires_at(&self) -> Option<std::time::SystemTime> {
-        self.exp.map(|time| {
-            std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)
-        })
+        self.exp
+            .map(|time| std::time::UNIX_EPOCH + std::time::Duration::from_secs(time))
     }
     /// Return a timestamp describing when the token was issued.
     pub fn issued_at(&self) -> Option<std::time::SystemTime> {
-        self.iat.map(|time| {
-            std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)
-        })
+        self.iat
+            .map(|time| std::time::UNIX_EPOCH + std::time::Duration::from_secs(time))
     }
 
     /// Check if a certain scope is allowed for this token.
@@ -849,18 +893,24 @@ pub struct TokenIntrospectionResponse {
     active: bool,
     #[serde(flatten)]
     #[serde(skip_serializing_if = "Option::is_none")]
-    data: Option<TokenData>
+    data: Option<TokenData>,
 }
 // These wrappers and impls should take care of making use of this
 // type as painless as possible.
 impl TokenIntrospectionResponse {
     /// Indicate that this token is not valid.
     pub fn inactive() -> Self {
-        Self { active: false, data: None }
+        Self {
+            active: false,
+            data: None,
+        }
     }
     /// Indicate that this token is valid, and provide data about it.
     pub fn active(data: TokenData) -> Self {
-        Self { active: true, data: Some(data) }
+        Self {
+            active: true,
+            data: Some(data),
+        }
     }
     /// Check if the endpoint reports this token as valid.
     pub fn is_active(&self) -> bool {
@@ -870,7 +920,7 @@ impl TokenIntrospectionResponse {
     /// Get data contained in the response, if the token is valid.
     pub fn data(&self) -> Option<&TokenData> {
         if !self.active {
-            return None
+            return None;
         }
         self.data.as_ref()
     }
@@ -882,7 +932,10 @@ impl Default for TokenIntrospectionResponse {
 }
 impl From<Option<TokenData>> for TokenIntrospectionResponse {
     fn from(data: Option<TokenData>) -> Self {
-        Self { active: data.is_some(), data }
+        Self {
+            active: data.is_some(),
+            data,
+        }
     }
 }
 impl From<TokenIntrospectionResponse> for Option<TokenData> {
@@ -896,9 +949,11 @@ impl axum_core::response::IntoResponse for TokenIntrospectionResponse {
     fn into_response(self) -> axum_core::response::Response {
         use http::StatusCode;
 
-        (StatusCode::OK,
-         [("Content-Type", "application/json")],
-         serde_json::to_vec(&self).unwrap())
+        (
+            StatusCode::OK,
+            [("Content-Type", "application/json")],
+            serde_json::to_vec(&self).unwrap(),
+        )
             .into_response()
     }
 }
@@ -908,7 +963,7 @@ impl axum_core::response::IntoResponse for TokenIntrospectionResponse {
 #[derive(Debug, Serialize, Deserialize)]
 pub struct TokenRevocationRequest {
     /// The token that needs to be revoked in case it is valid.
-    pub token: String
+    pub token: String,
 }
 
 /// Types of errors that a resource server (IndieAuth consumer) can
@@ -969,7 +1024,6 @@ pub enum ErrorKind {
     /// AutoAuth/OAuth2 Device Flow: Access was denied by the
     /// authorization endpoint.
     AccessDenied,
-
 }
 // TODO consider relying on serde_variant for these conversions
 impl AsRef<str> for ErrorKind {
@@ -1005,13 +1059,15 @@ pub struct Error {
     pub msg: Option<String>,
     /// An URL to documentation describing what went wrong and how to
     /// fix it.
-    pub error_uri: Option<url::Url>
+    pub error_uri: Option<url::Url>,
 }
 
 impl From<ErrorKind> for Error {
     fn from(kind: ErrorKind) -> Error {
         Error {
-            kind, msg: None, error_uri: None
+            kind,
+            msg: None,
+            error_uri: None,
         }
     }
 }
@@ -1037,9 +1093,11 @@ impl axum_core::response::IntoResponse for self::Error {
     fn into_response(self) -> axum_core::response::Response {
         use http::StatusCode;
 
-        (StatusCode::BAD_REQUEST,
-         [("Content-Type", "application/json")],
-         serde_json::to_vec(&self).unwrap())
+        (
+            StatusCode::BAD_REQUEST,
+            [("Content-Type", "application/json")],
+            serde_json::to_vec(&self).unwrap(),
+        )
             .into_response()
     }
 }
@@ -1052,17 +1110,23 @@ mod tests {
     fn test_serialize_deserialize_grant_request() {
         let authorization_code: GrantRequest = GrantRequest::AuthorizationCode {
             client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
-            redirect_uri: "https://kittybox.fireburn.ru/.kittybox/login/redirect".parse().unwrap(),
+            redirect_uri: "https://kittybox.fireburn.ru/.kittybox/login/redirect"
+                .parse()
+                .unwrap(),
             code_verifier: PKCEVerifier("helloworld".to_string()),
-            code: "hithere".to_owned()
+            code: "hithere".to_owned(),
         };
         let serialized = serde_urlencoded::to_string([
             ("grant_type", "authorization_code"),
             ("code", "hithere"),
             ("client_id", "https://kittybox.fireburn.ru/"),
-            ("redirect_uri", "https://kittybox.fireburn.ru/.kittybox/login/redirect"),
+            (
+                "redirect_uri",
+                "https://kittybox.fireburn.ru/.kittybox/login/redirect",
+            ),
             ("code_verifier", "helloworld"),
-        ]).unwrap();
+        ])
+        .unwrap();
 
         let deserialized = serde_urlencoded::from_str(&serialized).unwrap();