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>, #[serde(skip_serializing_if = "Option::is_none")] pub revocation_endpoint: Option, #[serde(skip_serializing_if = "Option::is_none")] pub revocation_endpoint_auth_methods_supported: Option>, // Note: Scopes isn't used here because this field should be // serialized as a list, not as a string #[serde(skip_serializing_if = "Option::is_none")] pub scopes_supported: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub response_types_supported: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub grant_types_supported: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub service_documentation: Option, pub code_challenge_methods_supported: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub authorization_response_iss_parameter_supported: Option, #[serde(skip_serializing_if = "Option::is_none")] pub userinfo_endpoint: Option } #[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 } #[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::>(); 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, #[serde(skip_serializing_if = "Option::is_none")] pub me: Option } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthorizationResponse { pub code: String, pub state: State, pub iss: Url } #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] #[serde(tag = "grant_type")] #[serde(rename_all = "snake_case")] pub enum GrantRequest { AuthorizationCode { code: String, client_id: Url, redirect_uri: Url, code_verifier: PKCEVerifier }, RefreshToken { refresh_token: String, client_id: url::Url, scope: Option } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum GrantResponse { AccessToken { me: Url, #[serde(skip_serializing_if = "Option::is_none")] profile: Option, access_token: String, #[serde(skip_serializing_if = "Option::is_none")] expires_in: Option, #[serde(skip_serializing_if = "Option::is_none")] refresh_token: Option }, ProfileUrl { me: Url, #[serde(skip_serializing_if = "Option::is_none")] profile: Option } } #[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, #[serde(skip_serializing_if = "Option::is_none")] pub iat: Option } 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 { self.exp.map(|time| { std::time::UNIX_EPOCH + std::time::Duration::from_secs(time) }) } pub fn issued_at(&self) -> Option { self.iat.map(|time| { std::time::UNIX_EPOCH + std::time::Duration::from_secs(time) }) } } // I don't like this type, because it could've been represented // internally by Option. But the IndieAuth standard // requires the "active" field to be present. I can't do anything // about it. #[derive(Debug, Serialize, Deserialize)] pub struct TokenIntrospectionResponse { active: bool, #[serde(flatten)] #[serde(skip_serializing_if = "Option::is_none")] data: Option } // These wrappers and impls should take care of making use of this // type as painless as possible. 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() } } impl From> for TokenIntrospectionResponse { fn from(data: Option) -> Self { Self { active: data.is_some(), data } } } impl From for Option { fn from(response: TokenIntrospectionResponse) -> Option { response.data } } #[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"}) ); } #[test] 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(), code_verifier: PKCEVerifier("helloworld".to_string()), 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"), ("code_verifier", "helloworld"), ]).unwrap(); let deserialized = serde_urlencoded::from_str(&serialized).unwrap(); assert_eq!(authorization_code, deserialized); assert_eq!( serialized, serde_urlencoded::to_string(authorization_code).unwrap() ) } }