diff options
Diffstat (limited to 'kittybox-rs/indieauth/src/lib.rs')
-rw-r--r-- | kittybox-rs/indieauth/src/lib.rs | 257 |
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"}) + ); + } +} |