diff options
author | Vika <vika@fireburn.ru> | 2023-07-29 21:59:56 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2023-07-29 21:59:56 +0300 |
commit | 0617663b249f9ca488e5de652108b17d67fbaf45 (patch) | |
tree | 11564b6c8fa37bf9203a0a4cc1c4e9cc088cb1a5 /kittybox-rs/indieauth/src/lib.rs | |
parent | 26c2b79f6a6380ae3224e9309b9f3352f5717bd7 (diff) | |
download | kittybox-0617663b249f9ca488e5de652108b17d67fbaf45.tar.zst |
Moved the entire Kittybox tree into the root
Diffstat (limited to 'kittybox-rs/indieauth/src/lib.rs')
-rw-r--r-- | kittybox-rs/indieauth/src/lib.rs | 773 |
1 files changed, 0 insertions, 773 deletions
diff --git a/kittybox-rs/indieauth/src/lib.rs b/kittybox-rs/indieauth/src/lib.rs deleted file mode 100644 index a60cc42..0000000 --- a/kittybox-rs/indieauth/src/lib.rs +++ /dev/null @@ -1,773 +0,0 @@ -#![deny(missing_docs)] -#![forbid(rustdoc::broken_intra_doc_links)] -//! A library of useful structs and helpers to implement [IndieAuth -//! version 20220212][indieauth]. -//! -//! This crate is completely network-agnostic, which means it can be -//! used with both sync and async web frameworks, and even on the -//! client side to implement identity consumers. -//! -//! ## Integration with web frameworks -//! -//! For convenience, helpers for [`axum`], the web framework Kittybox -//! happens to use, are provided. Enable the `axum` feature to use -//! them. -//! -//! The author is happy to accept patches to add more -//! framework-specific helpers. -//! -//! [indieauth]: https://indieauth.spec.indieweb.org/20220212/ -//! [`axum`]: https://github.com/tokio-rs/axum -use serde::{Serialize, Deserialize}; -use url::Url; - -mod scopes; -pub use self::scopes::{Scope, Scopes}; -mod pkce; -pub use self::pkce::{PKCEMethod, PKCEVerifier, PKCEChallenge}; - -/// Authentication methods supported by the introspection endpoint. -/// Note that authentication at the introspection endpoint is -/// mandatory. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -pub enum IntrospectionEndpointAuthMethod { - /// `Authorization` header with a `Bearer` token. - Bearer, - /// A token passed as part of a POST request. - #[serde(rename = "snake_case")] - ClientSecretPost, - /// Username and password passed using HTTP Basic authentication. - #[serde(rename = "snake_case")] - ClientSecretBasic, - /// TLS client auth with a certificate signed by a valid CA. - #[serde(rename = "snake_case")] - TlsClientAuth, - /// TLS client auth with a self-signed certificate. - #[serde(rename = "snake_case")] - SelfSignedTlsClientAuth -} - -/// Authentication methods supported by the revocation endpoint. -/// -/// The intent of the IndieAuth revocation endpoints is to quickly -/// revoke leaked tokens. As it requires posession of a token, no -/// authentication is neccesary to protect tokens. A well-intentioned -/// person discovering a leaked token could quickly revoke it without -/// disturbing anyone. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RevocationEndpointAuthMethod { - /// No authentication is required to access an endpoint declaring - /// this value. - None -} - -/// The response types supported by the authorization endpoint. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ResponseType { - /// An authorization code will be issued if this response type is - /// requested. - Code -} -// TODO serde_variant -impl ResponseType { - /// Return the response type as it would appear in serialized form. - pub fn as_str(&self) -> &'static str { - match self { - ResponseType::Code => "code", - } - } -} - -/// Grant types that are described in the IndieAuth spec. -/// -/// This type is strictly for usage in the [`Metadata`] response. For -/// grant requests and responses, see [`GrantRequest`] and -/// [`GrantResponse`]. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum GrantType { - /// The authorization code grant, allowing to exchange an - /// authorization code for a confirmation of identity or an access - /// token. - AuthorizationCode, - /// 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 -} - -/// OAuth 2.0 Authorization Server Metadata in application to the IndieAuth protocol. -/// -/// Your metadata endpoint should return this as a response. -/// -/// ```rust -/// use kittybox_indieauth::{ -/// Metadata, IntrospectionEndpointAuthMethod, RevocationEndpointAuthMethod, -/// ResponseType, Scope, GrantType, PKCEMethod -/// }; -/// -/// let metadata = Metadata { -/// issuer: "https://indieauth.example.com/".parse().unwrap(), -/// authorization_endpoint: "https://indieauth.example.com/auth".parse().unwrap(), -/// token_endpoint: "https://indieauth.example.com/token".parse().unwrap(), -/// introspection_endpoint: "https://indieauth.example.com/introspection".parse().unwrap(), -/// introspection_endpoint_auth_methods_supported: Some(vec![IntrospectionEndpointAuthMethod::Bearer]), -/// revocation_endpoint: Some("https://indieauth.example.com/revoke".parse().unwrap()), -/// revocation_endpoint_auth_methods_supported: Some(vec![RevocationEndpointAuthMethod::None]), -/// scopes_supported: Some(vec![Scope::Create, Scope::Update, Scope::custom("manage_tokens")]), -/// response_types_supported: Some(vec![ResponseType::Code]), -/// grant_types_supported: Some(vec![GrantType::AuthorizationCode, GrantType::RefreshToken]), -/// service_documentation: Some("https://indieauth.spec.indieweb.org/".parse().unwrap()), -/// code_challenge_methods_supported: vec![PKCEMethod::S256], -/// authorization_response_iss_parameter_supported: Some(true), -/// userinfo_endpoint: Some("https://indieauth.example.com/userinfo".parse().unwrap()) -/// }; -/// ``` -#[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>>, - /// The Revocation Endpoint - #[serde(skip_serializing_if = "Option::is_none")] - pub revocation_endpoint: Option<Url>, - /// JSON array containing the value - /// [`RevocationEndpointAuthMethod::None`]. If a revocation endpoint - /// is provided, this property should also be provided with the - /// value `vec![RevocationEndpointAuthMethod::None]`, since the - /// omission of this value defaults to `client_secret_basic` - /// according to [RFC8414]. - /// - /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414 - #[serde(skip_serializing_if = "Option::is_none")] - pub revocation_endpoint_auth_methods_supported: Option<Vec<RevocationEndpointAuthMethod>>, - /// JSON array containing scope values supported by the IndieAuth - /// server. Servers MAY choose not to advertise some supported - /// scope values even when this parameter is used. - // 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<Vec<Scope>>, - /// JSON array containing the response_type values supported. This - /// differs from [RFC8414] in that this parameter is OPTIONAL and - /// that, if omitted, the default is [`ResponseType::Code`]. - /// - /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414 - #[serde(skip_serializing_if = "Option::is_none")] - pub response_types_supported: Option<Vec<ResponseType>>, - /// JSON array containing grant type values supported. If omitted, - /// the default value differs from [RFC8414] and is - /// `authorization_code`. - /// - /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414 - #[serde(skip_serializing_if = "Option::is_none")] - pub grant_types_supported: Option<Vec<GrantType>>, - /// URL of a page containing human-readable information that - /// developers might need to know when using the server. This - /// might be a link to the IndieAuth spec or something more - /// personal to your implementation. - #[serde(skip_serializing_if = "Option::is_none")] - pub service_documentation: Option<Url>, - /// JSON array containing the methods supported for PKCE. This - /// parameter differs from [RFC8414] in that it is not optional as - /// PKCE is *REQUIRED*. - /// - /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414 - pub code_challenge_methods_supported: Vec<PKCEMethod>, - /// Boolean parameter indicating whether the authorization server - /// provides the iss parameter. If omitted, the default value is - /// false. As the iss parameter is REQUIRED, this is provided for - /// compatibility with OAuth 2.0 servers implementing the - /// parameter. - #[serde(skip_serializing_if = "Option::is_none")] - pub authorization_response_iss_parameter_supported: Option<bool>, - /// The User Info Endpoint - #[serde(skip_serializing_if = "Option::is_none")] - pub userinfo_endpoint: Option<Url> -} - -#[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()) - .into_response() - } -} - -/// User profile to be returned from the userinfo endpoint and when -/// the `profile` scope was requested. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Profile { - /// User's chosen name. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option<String>, - /// User's profile page. Fetching it may reveal an `h-card`. - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option<Url>, - /// User's profile picture suitable to represent them. - #[serde(skip_serializing_if = "Option::is_none")] - pub photo: Option<Url>, - /// 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> -} - -#[cfg(feature = "axum")] -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()) - .into_response() - } -} - -/// A state string comprised of alphanumeric characters to protect -/// from CSRF attacks. -/// -/// There is no reason to inspect the string itself except to ensure -/// it hasn't been tampered with. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct State(String); -impl State { - /// Generate a random state string of 128 bytes in length. - pub 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()) - } -} -impl AsRef<str> for State { - fn as_ref(&self) -> &str { - self.0.as_str() - } -} - -/// The authorization request that should be affixed to the URL of an -/// authorization endpoint to start the IndieAuth ceremony. -/// -/// ```rust -/// use kittybox_indieauth::{ -/// AuthorizationRequest, ResponseType, State, -/// Scopes, Scope, -/// PKCEChallenge, PKCEVerifier, PKCEMethod -/// }; -/// -/// // Save that for later, it'll come in handy -/// let verifier = PKCEVerifier::new(); -/// -/// let request = AuthorizationRequest { -/// response_type: ResponseType::Code, -/// client_id: "https://kittybox.fireburn.ru/companion/native".parse().unwrap(), -/// redirect_uri: "https://kittybox.fireburn.ru/companion/native/redirect".parse().unwrap(), -/// state: State::new(), -/// code_challenge: PKCEChallenge::new(&verifier, PKCEMethod::default()), -/// scope: Some(Scopes::new(vec![Scope::Create, Scope::Update, Scope::Delete, Scope::Media])), -/// me: Some("https://fireburn.ru/".parse().unwrap()) -/// }; -/// -/// let mut url: url::Url = "https://fireburn.ru/.kittybox/indieauth/auth" -/// .parse() -/// .unwrap(); -/// -/// url.set_query(Some(&serde_urlencoded::to_string(request).unwrap())); -/// -/// // Open a user's browser to navigate to the authorization endpoint page... -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuthorizationRequest { - /// The response type expected to this request. - pub response_type: ResponseType, - /// The homepage of the client. It must be fetched to show - /// metadata and check the redirect URI's authenticity. - pub client_id: Url, - /// The URI that the user will be redirected to in case they - /// approve the authentication request. A query string containing - /// the response is affixed to it. - pub redirect_uri: Url, - /// A random state to protect from CSRF attacks. The server should - /// return this string unmodified. - pub state: State, - /// A PKCE challenge neccesary to protect from authorization code - /// injection and CSRF attacks. - #[serde(flatten)] - pub code_challenge: PKCEChallenge, - /// An array of scopes that are requested for a token. If no - /// scopes are provided, a token will not be issued. - #[serde(skip_serializing_if = "Option::is_none")] - pub scope: Option<Scopes>, - /// The URL that user entered. The authorization endpoint MAY use - /// it as a hint of which user is attempting to sign in, and to - /// indicate which profile URL the client is expecting in the - /// resulting profile URL response or access token response. - #[serde(skip_serializing_if = "Option::is_none")] - pub me: Option<Url> -} - -/// The authorization response that must be appended to the -/// [`AuthorizationRequest::redirect_uri`]'s query string. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuthorizationResponse { - /// The authorization code generated by the authorization - /// endpoint. The code MUST expire shortly after it is issued to - /// mitigate the risk of leaks, and MUST be valid for only one - /// use. A maximum lifetime of 10 minutes is recommended. See - /// [OAuth 2.0 Section 4.1.2][oauth2-sec-4.1.2] for additional - /// requirements on the authorization code. - /// - /// [oauth2-sec-4.1.2]: https://tools.ietf.org/html/rfc6749#section-4.1.2 - pub code: String, - /// The state parameter from the [AuthorizationRequest], - /// unmodified. - pub state: State, - /// The issuer identifier for client validation. - /// - /// Clients MUST verify this matches the [`Metadata::issuer`] - /// parameter provided by the Server [Metadata] endpoint during - /// Discovery as outlined in [OAuth 2.0 Authorization Server - /// Issuer Identification][oauth2-iss]. If the value does not - /// match the expected issuer identifier, clients MUST reject the - /// authorization response and MUST NOT proceed with the - /// authorization grant. For error responses, clients MUST NOT - /// assume that the error originates from the intended - /// authorization server. - /// - /// [oauth2-iss]: https://www.ietf.org/archive/id/draft-ietf-oauth-iss-auth-resp-02.html - pub iss: Url -} - -/// A grant request that continues the IndieAuth ceremony. -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "grant_type")] -#[serde(rename_all = "snake_case")] -pub enum GrantRequest { - /// Use an authorization code to receive identity verification - /// and/or an access token. - AuthorizationCode { - /// The code from [`AuthorizationResponse`]. - code: String, - /// Client ID that this grant belongs to. - client_id: Url, - /// Redirect URI that was used to receive the grant. - redirect_uri: Url, - /// The PKCE code verifier that was used to create the code - /// challenge. - code_verifier: PKCEVerifier - }, - /// Use a refresh token to get a fresh access token and a new - /// matching refresh token. - RefreshToken { - /// The refresh token that was issued before. - refresh_token: String, - /// The client ID to which the token belongs to. - client_id: url::Url, - /// A list of scopes, not exceeding the already-granted scope, - /// that can be passed to further restrict the scopes on the - /// new token. - /// - /// This cannot be used to gain new scopes -- you need to - /// start over if you need new scopes from the user. - scope: Option<Scopes> - } -} - -/// Token type, as described in [RFC6749][]. -/// -/// [RFC6749]: https://www.rfc-editor.org/rfc/rfc6749#section-7.1 -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TokenType { - /// A Bearer token described in [RFC6750][]. As far as the author - /// of this library is concerned, this is the only type that - /// IndieAuth uses. - /// - /// [RFC6750]: https://www.rfc-editor.org/rfc/rfc6750 - Bearer -} - -/// The response to a successful [`GrantRequest`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum GrantResponse { - /// An access token response, containing an access token, a refresh - /// token (if the identity provider supports them) and the profile - /// (if access was granted to the profile data). - AccessToken { - /// The URL for the user this token corresponds to. - me: Url, - /// Token type. Required by OAuth2, not mentioned in - /// IndieAuth. Confirmed as erroneous. - token_type: TokenType, - /// Scopes. REQUIRED if different from what was - /// requested. Absence from IndieAuth spec confirmed as - /// erroneous. - scope: Option<Scopes>, - /// The user's profile information, if it was requested. - #[serde(skip_serializing_if = "Option::is_none")] - profile: Option<Profile>, - /// The access token that can be used to access protected resources. - access_token: String, - /// The duration in which the access token expires, represented in seconds. - // TODO replace with std::time::Duration - #[serde(skip_serializing_if = "Option::is_none")] - expires_in: Option<u64>, - /// The refresh token, if it was issued. - #[serde(skip_serializing_if = "Option::is_none")] - 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 { - /// The authenticated user's URL. - me: Url, - /// The user's profile information, if it was requested. - #[serde(skip_serializing_if = "Option::is_none")] - profile: Option<Profile> - } -} - -#[cfg(feature = "axum")] -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()) - .into_response() - } -} - -/// 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)] -#[allow(missing_docs)] -pub enum RequestMaybeAuthorizationEndpoint { - Authorization(AuthorizationRequest), - Grant(GrantRequest) -} - -/// A token introspection request that can be handled by the token -/// introspection endpoint. -/// -/// Note that this request doesn't contain authentication data, which -/// is commonly transmitted out-of-band (e.g. via the `Authorization` -/// header). -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenIntrospectionRequest { - /// The token for which data was requested. - pub token: String -} - -/// Data for a token that will be returned by the introspection -/// endpoint (and can also be used internally by the resource server -/// if it is part of a monolith with the identity provider). -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenData { - /// The user this token corresponds to. - pub me: Url, - /// The client ID for the client that this token was issued to. - pub client_id: Url, - /// Scope that was granted to this token. - pub scope: Scopes, - /// The expiration date for this token, measured in seconds from - /// the Unix time epoch (1970-01-01 00:00:00). - // TODO replace these two with std::time::SystemTime - #[serde(skip_serializing_if = "Option::is_none")] - pub exp: Option<u64>, - /// 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> -} - -impl TokenData { - /// Check if the token in question expired. - 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() - } - - /// 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) - }) - } - /// 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) - }) - } - - /// Check if a certain scope is allowed for this token. - pub fn check_scope(&self, scope: &Scope) -> bool { - self.scope.has(scope) - } -} - -// I don't like this type, because it could've been represented -// internally by Option<TokenData>. But the IndieAuth standard -// requires the "active" field to be present. I can't do anything -// about it. -/// The introspection response that the introspection endpoint must -/// return. -/// -/// It is recommended to use the [`From`][`std::convert::From`] trait -/// to convert from `Option<TokenData>` for ergonomics. -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenIntrospectionResponse { - active: bool, - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - 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 } - } - /// Indicate that this token is valid, and provide data about it. - pub fn active(data: TokenData) -> Self { - Self { active: true, data: Some(data) } - } - /// Check if the endpoint reports this token as valid. - pub fn is_active(&self) -> bool { - self.active - } - - /// Get data contained in the response, if the token is valid. - 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<Option<TokenData>> for TokenIntrospectionResponse { - fn from(data: Option<TokenData>) -> Self { - Self { active: data.is_some(), data } - } -} -impl From<TokenIntrospectionResponse> for Option<TokenData> { - fn from(response: TokenIntrospectionResponse) -> Option<TokenData> { - response.data - } -} - -#[cfg(feature = "axum")] -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()) - .into_response() - } -} - -/// A request for revoking a token. There is no response beyond `HTTP -/// 200 OK`. -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenRevocationRequest { - /// The token that needs to be revoked in case it is valid. - 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 { - /// The provided token was invalid. - InvalidToken, - /// The scope on the token was insufficient to perform the - /// requested operation. - InsufficientScope, -} - -/// Various kinds of errors that could occur when performing the -/// IndieAuth ceremony. -#[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<str> 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()) - } -} - -/// An error that can be returned when performing the IndieAuth ceremony. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Error { - /// Type of an error. - #[serde(rename = "error")] - pub kind: ErrorKind, - /// Human-friendly description of an error, suitable for a - /// developer to read while debugging. - #[serde(rename = "error_description")] - pub msg: Option<String>, - /// An URL to documentation describing what went wrong and how to - /// fix it. - pub error_uri: Option<url::Url> -} - -impl From<ErrorKind> 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(feature = "axum")] -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()) - .into_response() - } -} - -#[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() - ) - } -} |