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 } } /// Describes requests that the authorization endpoint might want to handle. /// /// This type mostly exists for ease-of-use with serde. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum RequestMaybeAuthorizationEndpoint { Authorization(AuthorizationRequest), Grant(GrantRequest) } #[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 } /// Types of errors that a resource server (IndieAuth consumer) can /// throw when authentication goes wrong. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ResourceErrorKind { InvalidRequest, InvalidToken, InsufficientScope, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ErrorKind { /// The request is missing a required parameter, includes an /// unsupported parameter value (other than grant type), repeats a /// parameter, includes multiple credentials, utilizes more than /// one mechanism for authenticating the client, or is otherwise /// malformed. InvalidRequest, /// Client authentication failed (e.g., unknown client, no client /// authentication included, or unsupported authentication /// method). The authorization server MAY return an HTTP 401 /// (Unauthorized) status code to indicate which HTTP /// authentication schemes are supported. If the client attempted /// to authenticate via the "Authorization" request header field, /// the authorization server MUST respond with an HTTP 401 /// (Unauthorized) status code and include the "WWW-Authenticate" /// response header field matching the authentication scheme used /// by the client. InvalidClient, /// The provided authorization grant (e.g., authorization /// code, resource owner credentials) or refresh token is /// invalid, expired, revoked, does not match the redirection /// URI used in the authorization request, or was issued to /// another client. InvalidGrant, /// The authenticated client is not authorized to use this /// authorization grant type. UnauthorizedClient, /// The authorization grant type is not supported by the /// authorization server. UnsupportedGrantType, /// The requested scope is invalid, unknown, malformed, or /// exceeds the scope granted by the resource owner. InvalidScope } // TODO consider relying on serde_variant for these conversions impl AsRef for ErrorKind { fn as_ref(&self) -> &str { match self { ErrorKind::InvalidRequest => "invalid_request", ErrorKind::InvalidClient => "invalid_client", ErrorKind::InvalidGrant => "invalid_grant", ErrorKind::UnauthorizedClient => "unauthorized_client", ErrorKind::UnsupportedGrantType => "unsupported_grant_type", ErrorKind::InvalidScope => "invalid_scope", } } } impl std::fmt::Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_ref()) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Error { #[serde(rename = "error")] pub kind: ErrorKind, #[serde(rename = "error_description")] pub msg: Option, pub error_uri: Option } impl From for Error { fn from(kind: ErrorKind) -> Error { Error { kind, msg: None, error_uri: None } } } impl std::error::Error for self::Error {} impl std::fmt::Display for self::Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "IndieAuth error ({})", self.kind)?; if let Some(msg) = self.msg.as_deref() { write!(f, ": {}", msg)?; } if let Some(error_uri) = &self.error_uri { write!(f, " (see `{}` for more info)", error_uri)?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[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() ) } }