about summary refs log tree commit diff
path: root/kittybox-rs/indieauth
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2023-07-29 21:59:56 +0300
committerVika <vika@fireburn.ru>2023-07-29 21:59:56 +0300
commit0617663b249f9ca488e5de652108b17d67fbaf45 (patch)
tree11564b6c8fa37bf9203a0a4cc1c4e9cc088cb1a5 /kittybox-rs/indieauth
parent26c2b79f6a6380ae3224e9309b9f3352f5717bd7 (diff)
Moved the entire Kittybox tree into the root
Diffstat (limited to 'kittybox-rs/indieauth')
-rw-r--r--kittybox-rs/indieauth/Cargo.toml31
-rw-r--r--kittybox-rs/indieauth/src/lib.rs773
-rw-r--r--kittybox-rs/indieauth/src/pkce.rs132
-rw-r--r--kittybox-rs/indieauth/src/scopes.rs208
4 files changed, 0 insertions, 1144 deletions
diff --git a/kittybox-rs/indieauth/Cargo.toml b/kittybox-rs/indieauth/Cargo.toml
deleted file mode 100644
index d6bc1fe..0000000
--- a/kittybox-rs/indieauth/Cargo.toml
+++ /dev/null
@@ -1,31 +0,0 @@
-[package]
-name = "kittybox-indieauth"
-version = "0.1.0"
-edition = "2021"
-
-[features]
-default = []
-axum = ["axum-core", "serde_json", "http"]
-
-[dev-dependencies]
-serde_json = "^1.0.64"       # A JSON serialization file format
-serde_urlencoded = "^0.7.0"  # `x-www-form-urlencoded` meets Serde
-[dependencies]
-rand = "^0.8.5"              # Utilities for random number generation
-data-encoding = "^2.3.2"     # Efficient and customizable data-encoding functions like base64, base32, and hex
-sha2 = "^0.10.7"              # SHA-2 series of algorithms for Rust
-[dependencies.url]           # URL library for Rust, based on the WHATWG URL Standard
-version = "^2.2.1"
-features = ["serde"]
-[dependencies.serde]         # A generic serialization/deserialization framework
-version = "^1.0.170"
-features = ["derive"]
-[dependencies.axum-core]
-version = "^0.3.4"
-optional = true
-[dependencies.serde_json]
-version = "^1.0.64"
-optional = true
-[dependencies.http]
-version = "^0.2.7"
-optional = true
\ No newline at end of file
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()
-        )
-    }
-}
diff --git a/kittybox-rs/indieauth/src/pkce.rs b/kittybox-rs/indieauth/src/pkce.rs
deleted file mode 100644
index bf8d1a0..0000000
--- a/kittybox-rs/indieauth/src/pkce.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-use serde::{Serialize, Deserialize};
-use rand::{Rng, distributions::Alphanumeric};
-use sha2::{Sha256, Digest};
-use data_encoding::BASE64URL;
-
-/// Methods to use for PKCE challenges.
-#[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize, /*Default*/)]
-pub enum PKCEMethod {
-    /// Base64-encoded SHA256 hash of an ASCII string.
-    //#[default]
-    S256,
-    /// Plain string by itself. Please don't use this.
-    #[serde(rename = "snake_case")]
-    Plain
-}
-// manual impl until Rust 1.62 hits nixos-unstable
-impl Default for PKCEMethod {
-    fn default() -> Self { PKCEMethod::S256 }
-}
-impl PKCEMethod {
-    /// Return a string representing a PKCE method as it would be serialized.
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            PKCEMethod::S256 => "S256",
-            PKCEMethod::Plain => "plain"
-        }
-    }
-}
-/// A PKCE verifier string that should be kept in secret until the end
-/// of the authentication ceremony, where it is revealed to prove that
-/// the one who uses the grant is the same entity who it was given to.
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct PKCEVerifier(pub(super) String);
-
-impl AsRef<str> for PKCEVerifier {
-    fn as_ref(&self) -> &str {
-        self.0.as_str()
-    }
-}
-impl ToString for PKCEVerifier {
-    fn to_string(&self) -> String {
-        self.0.clone()
-    }
-}
-
-impl PKCEVerifier {
-    /// Generate a new PKCE verifier string of 128 bytes in length.
-    #[allow(clippy::new_without_default)]
-    pub fn new() -> Self {
-        let bytes = rand::thread_rng()
-            .sample_iter(&Alphanumeric)
-            .take(128)
-            .collect::<Vec<u8>>();
-        Self(String::from_utf8(bytes).unwrap())
-    }
-}
-
-/// A PKCE challenge as described in [RFC7636].
-///
-/// [RFC7636]: https://tools.ietf.org/html/rfc7636
-#[derive(Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
-pub struct PKCEChallenge {
-    code_challenge: String,
-    #[serde(rename = "code_challenge_method")]
-    method: PKCEMethod
-}
-
-impl PKCEChallenge {
-    /// Create a new challenge from a [PKCEVerifier] using a certain
-    /// [PKCEMethod].
-    pub fn new(code_verifier: &PKCEVerifier, method: PKCEMethod) -> Self {
-        Self {
-            code_challenge: match method {
-                PKCEMethod::S256 => {
-                    let mut hasher = Sha256::new();
-                    hasher.update(code_verifier.as_ref());
-                    let mut challenge = BASE64URL.encode(&hasher.finalize());
-                    challenge.retain(|c| c != '=');
-
-                    challenge
-                },
-                PKCEMethod::Plain => code_verifier.to_string(),
-            },
-            method
-        }
-    }
-
-    /// Verify that the [PKCEVerifier] corresponds to this challenge,
-    /// by creating a second challenge string and comparing it against
-    /// this challenge data.
-    ///
-    /// ```rust
-    /// use kittybox_indieauth::{PKCEVerifier, PKCEMethod, PKCEChallenge};
-    ///
-    /// let verifier = PKCEVerifier::new();
-    /// let challenge = PKCEChallenge::new(&verifier, PKCEMethod::default());
-    /// // Meanwhile, at the token endpoint, in the end of the ceremony...
-    /// // ...the challenge gets retrieved from the stored data and verified
-    /// assert!(challenge.verify(verifier))
-    /// ```
-    #[must_use]
-    pub fn verify(&self, code_verifier: PKCEVerifier) -> bool {
-        Self::new(&code_verifier, self.method) == *self
-    }
-
-    /// Return a reference to the code challenge string.
-    pub fn as_str(&self) -> &str {
-        self.code_challenge.as_str()
-    }
-
-    /// Return the method used to create this challenge.
-    pub fn method(&self) -> PKCEMethod {
-        self.method
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::{PKCEMethod, PKCEVerifier, PKCEChallenge};
-
-    #[test]
-    /// A snapshot test generated using [Aaron Parecki's PKCE
-    /// tools](https://example-app.com/pkce) that checks for a
-    /// conforming challenge.
-    fn test_pkce_challenge_verification() {
-        let verifier = PKCEVerifier("ec03310e4e90f7bc988af05384060c3c1afeae4bb4d0f648c5c06b63".to_owned());
-
-        let challenge = PKCEChallenge::new(&verifier, PKCEMethod::S256);
-
-        assert_eq!(challenge.as_str(), "aB8OG20Rh8UoQ9gFhI0YvPkx4dDW2MBspBKGXL6j6Wg");
-    }
-}
diff --git a/kittybox-rs/indieauth/src/scopes.rs b/kittybox-rs/indieauth/src/scopes.rs
deleted file mode 100644
index d74878e..0000000
--- a/kittybox-rs/indieauth/src/scopes.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use std::str::FromStr;
-
-use serde::{
-    Serialize, Serializer,
-    Deserialize,
-    de::{
-        Deserializer, Visitor,
-        Error as DeserializeError
-    }
-};
-
-/// Various scopes that can be requested through IndieAuth.
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum Scope {
-    /// Allows to create posts using Micropub.
-    Create,
-    /// Allows to edit posts using Micropub.
-    Update,
-    /// Allows to delete posts using Micropub.
-    Delete,
-    /// Allows to upload blobs to the media endpoint.
-    Media,
-    /// Allows to read feeds via Microsub.
-    Read,
-    /// Allows to manage follows via Microsub.
-    Follow,
-    /// Allows to mute and unmute users in feeds via Microsub.
-    Mute,
-    /// Allows to block and unblock users.
-    Block,
-    /// Allows to create and manage feeds via Microsub.
-    Channels,
-    /// Allows to request profile information (except email, see Email)
-    Profile,
-    /// Allows to receive email in the profile information.
-    Email,
-    /// Custom scope not included above.
-    Custom(String)
-}
-impl Scope {
-    /// Create a custom scope from a string slice.
-    pub fn custom(scope: &str) -> Scope {
-        Scope::Custom(scope.to_string())
-    }
-}
-
-// TODO consider relying on serde_variant for these conversions
-impl AsRef<str> for Scope {
-    fn as_ref(&self) -> &str {
-        use Scope::*;
-        match self {
-            Create => "create",
-            Update => "update",
-            Delete => "delete",
-            Media => "media",
-            Read => "read",
-            Follow => "follow",
-            Mute => "mute",
-            Block => "block",
-            Channels => "channels",
-            Profile => "profile",
-            Email => "email",
-            Custom(s) => s.as_ref()
-        }
-    }
-}
-impl From<&str> for Scope {
-    fn from(scope: &str) -> Self {
-        match scope {
-            "create"   => Scope::Create,
-            "update"   => Scope::Update,
-            "delete"   => Scope::Delete,
-            "media"    => Scope::Media,
-            "read"     => Scope::Read,
-            "follow"   => Scope::Follow,
-            "mute"     => Scope::Mute,
-            "block"    => Scope::Block,
-            "channels" => Scope::Channels,
-            "profile"  => Scope::Profile,
-            "email"    => Scope::Email,
-            other      => Scope::custom(other)
-        }
-    }
-}
-impl FromStr for Scope {
-    type Err = std::convert::Infallible;
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(s.into())
-    }
-}
-
-/// A list of scopes that serializes to a space-separated string instead of a list.
-///
-/// OAuth2 is weird, don't ask me why it's a thing.
-#[derive(PartialEq, Eq, Debug, Clone)]
-pub struct Scopes(Vec<Scope>);
-impl Scopes {
-    /// Create a list of scopes from a vector of scopes.
-    pub fn new(scopes: Vec<Scope>) -> Self {
-        Self(scopes)
-    }
-    /// Ensure a certain scope is listed in the scope list.
-    pub fn has(&self, scope: &Scope) -> bool {
-        self.0.iter().any(|s| s == scope)
-    }
-    /// Ensure all of the requested scopes are in the list.
-    pub fn has_all(&self, scopes: &[Scope]) -> bool {
-        scopes.iter()
-            .map(|s1| self.iter().any(|s2| s1 == s2))
-            .all(|s| s)
-    }
-    /// Transform this into an iterator over individual scopes.
-    pub fn iter(&self) -> std::slice::Iter<'_, Scope> {
-        self.0.iter()
-    }
-}
-impl AsRef<[Scope]> for Scopes {
-    fn as_ref(&self) -> &[Scope] {
-        self.0.as_ref()
-    }
-}
-impl ToString for Scopes {
-    fn to_string(&self) -> String {
-        self.0.iter()
-            .map(|s| s.as_ref())
-            .fold(String::new(), |a, s| if a.is_empty() {
-                s.to_string()
-            } else {
-                a + " " + s
-            })
-    }
-}
-impl FromStr for Scopes {
-    type Err = std::convert::Infallible;
-
-    fn from_str(value: &str) -> Result<Self, Self::Err> {
-        Ok(Self(value.split_ascii_whitespace()
-                .map(Scope::from)
-                .collect::<Vec<Scope>>()))
-    }
-}
-impl Serialize for Scopes {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer
-    {
-        serializer.serialize_str(&self.to_string())
-    }
-}
-struct ScopeVisitor;
-impl<'de> Visitor<'de> for ScopeVisitor {
-    type Value = Scopes;
-
-    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-        formatter.write_str("a string of space-separated OAuth2 scopes")
-    }
-
-    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
-    where
-        E: DeserializeError
-    {
-        Ok(Scopes::from_str(value).unwrap())
-    }
-}
-impl<'de> Deserialize<'de> for Scopes {
-    
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>
-    {
-        deserializer.deserialize_str(ScopeVisitor)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_serde_vec_scope() {
-        let scopes = vec![
-            Scope::Create, Scope::Update, Scope::Delete,
-            Scope::Media,
-            Scope::custom("kittybox_internal_access")
-        ];
-
-        let scope_serialized = serde_json::to_value(
-            Scopes::new(scopes.clone())
-        ).unwrap();
-        let scope_str = scope_serialized.as_str().unwrap();
-        assert_eq!(scope_str, "create update delete media kittybox_internal_access");
-
-        assert!(serde_json::from_value::<Scopes>(scope_serialized).unwrap().has_all(&scopes))        
-    }
-
-    #[test]
-    fn test_scope_has_all() {
-        let scopes = Scopes(vec![
-            Scope::Create, Scope::Update, Scope::custom("draft")
-        ]);
-
-        assert!(scopes.has_all(&[Scope::Create, Scope::custom("draft")]));
-        
-        assert!(!scopes.has_all(&[Scope::Read, Scope::custom("kittybox_internal_access")]));
-    }
-
-}