about summary refs log tree commit diff
path: root/kittybox-rs/indieauth/src/lib.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-07-10 19:35:10 +0300
committerVika <vika@fireburn.ru>2022-07-10 19:35:10 +0300
commit9ca0e358dc95e7358815886b061288f04a7d29af (patch)
treebc19ac6071d75f1ae58069e577fc02692aa87c8b /kittybox-rs/indieauth/src/lib.rs
parent0ee2072d40dc0b88a7f475a25dfc790d49546a5f (diff)
downloadkittybox-9ca0e358dc95e7358815886b061288f04a7d29af.tar.zst
kittybox-indieauth: init
This crate is the base framework-agnostic implementation of all data
structures and methods required for IndieAuth protocol. Anything that
can deserialize HTTP request payloads with serde can utilize this
crate.

This is a good candidate to independently release on crates.io when
the interface becomes stable enough.
Diffstat (limited to 'kittybox-rs/indieauth/src/lib.rs')
-rw-r--r--kittybox-rs/indieauth/src/lib.rs257
1 files changed, 257 insertions, 0 deletions
diff --git a/kittybox-rs/indieauth/src/lib.rs b/kittybox-rs/indieauth/src/lib.rs
new file mode 100644
index 0000000..23b3923
--- /dev/null
+++ b/kittybox-rs/indieauth/src/lib.rs
@@ -0,0 +1,257 @@
+use serde::{Serialize, Deserialize};
+use url::Url;
+
+mod scopes;
+pub use self::scopes::{Scope, Scopes};
+mod pkce;
+pub use self::pkce::{PKCEMethod, PKCEVerifier, PKCEChallenge};
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+pub enum IntrospectionEndpointAuthMethod {
+    Bearer,
+    #[serde(rename = "snake_case")]
+    ClientSecretPost,
+    #[serde(rename = "snake_case")]
+    ClientSecretBasic,
+    #[serde(rename = "snake_case")]
+    TlsClientAuth,
+    #[serde(rename = "snake_case")]
+    SelfSignedTlsClientAuth
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum RevocationEndpointAuthMethod {
+    None
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ResponseType {
+    Code
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum GrantType {
+    AuthorizationCode,
+    RefreshToken
+}
+
+/// OAuth 2.0 Authorization Server Metadata in application to the IndieAuth protocol.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Metadata {
+    /// The server's issuer identifier. The issuer identifier is a URL
+    /// that uses the "https" scheme and has no query or fragment
+    /// components. The identifier MUST be a prefix of the
+    /// `indieauth-metadata` URL.
+    pub issuer: Url,
+    /// The Authorization Endpoint
+    pub authorization_endpoint: Url,
+    /// The Token Endpoint
+    pub token_endpoint: Url,
+    /// The Introspection Endpoint
+    pub introspection_endpoint: Url,
+    /// JSON array containing a list of client authentication methods supported by this introspection endpoint.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub introspection_endpoint_auth_methods_supported: Option<Vec<IntrospectionEndpointAuthMethod>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub revocation_endpoint: Option<Url>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub revocation_endpoint_auth_methods_supported: Option<Vec<RevocationEndpointAuthMethod>>,
+    // Note: Scopes isn't used here because this field should be
+    // serialized as a list, not as a string
+    pub scopes_supported: Vec<Scope>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub response_types_supported: Option<Vec<ResponseType>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub grant_types_supported: Option<Vec<GrantType>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub service_documentation: Option<Url>,
+    pub code_challenge_methods_supported: Vec<PKCEMethod>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub authorization_response_iss_parameter_supported: Option<bool>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub userinfo_endpoint: Option<Url>
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Profile {
+    pub name: String,
+    pub url: Url,
+    pub photo: Url,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub email: Option<String>
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+pub struct State(String);
+impl State {
+    fn new() -> Self {
+        use rand::{Rng, distributions::Alphanumeric};
+        let bytes = rand::thread_rng()
+            .sample_iter(&Alphanumeric)
+            .take(128)
+            .collect::<Vec<u8>>();
+        Self(String::from_utf8(bytes).unwrap())
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AuthorizationRequest {
+    pub response_type: ResponseType,
+    pub client_id: Url,
+    pub redirect_uri: Url,
+    pub state: State,
+    #[serde(flatten)]
+    pub code_challenge: PKCEChallenge,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub scope: Option<Scopes>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub me: Option<Url>
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct AuthorizationResponse {
+    pub code: String,
+    pub state: State,
+    iss: Url
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct GrantRequest {
+    grant_type: GrantType,
+    code: String,
+    client_id: Url,
+    redirect_uri: Url,
+    code_verifier: PKCEVerifier
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+enum GrantResponse {
+    AccessToken {
+        me: Url,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        profile: Option<Profile>,
+        access_token: String,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        expires_in: Option<usize>,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        refresh_token: Option<String>
+    },
+    ProfileUrl {
+        me: Url,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        profile: Option<Profile>
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenIntrospectionRequest {
+    pub token: String
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenData {
+    pub me: Url,
+    pub client_id: Url,
+    pub scope: Scopes,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub exp: Option<u64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub iat: Option<u64>
+}
+
+impl TokenData {
+    pub fn expired(&self) -> bool {
+        use std::time::{Duration, SystemTime, UNIX_EPOCH};
+        
+        self.exp
+            .map(|exp| SystemTime::now()
+                 .duration_since(UNIX_EPOCH)
+                 .unwrap_or(Duration::ZERO)
+                 .as_secs() >= exp)
+            .unwrap_or_default()
+    }
+
+    pub fn expires_at(&self) -> Option<std::time::SystemTime> {
+        self.exp.map(|time| {
+            std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)
+        })
+    }
+    
+    pub fn issued_at(&self) -> Option<std::time::SystemTime> {
+        self.iat.map(|time| {
+            std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)
+        })
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenIntrospectionResponse {
+    active: bool,
+    #[serde(flatten)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    data: Option<TokenData>
+}
+impl TokenIntrospectionResponse {
+    pub fn inactive() -> Self {
+        Self { active: false, data: None }
+    }
+    pub fn active(data: TokenData) -> Self {
+        Self { active: true, data: Some(data) }
+    }
+
+    pub fn is_active(&self) -> bool {
+        self.active
+    }
+
+    pub fn data(&self) -> Option<&TokenData> {
+        if !self.active {
+            return None
+        }
+        self.data.as_ref()
+    }
+}
+impl Default for TokenIntrospectionResponse {
+    fn default() -> Self {
+        Self::inactive()
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenRevocationRequest {
+    pub token: String
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+#[serde(tag = "error")]
+pub enum IndieAuthError {
+    InvalidRequest,
+    InvalidToken,
+    InsufficientScope,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_json::json;
+
+    #[test]
+    fn test_serialize_indieauth_error() {
+        assert_eq!(
+            serde_json::to_value(IndieAuthError::InvalidRequest).unwrap(),
+            json!({"error": "invalid_request"})
+        );
+        assert_eq!(
+            serde_json::to_value(IndieAuthError::InvalidToken).unwrap(),
+            json!({"error": "invalid_token"})
+        );
+        assert_eq!(
+            serde_json::to_value(IndieAuthError::InsufficientScope).unwrap(),
+            json!({"error": "insufficient_scope"})
+        );
+    }
+}