about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--kittybox-rs/indieauth/src/lib.rs296
-rw-r--r--kittybox-rs/indieauth/src/pkce.rs50
-rw-r--r--kittybox-rs/indieauth/src/scopes.rs10
3 files changed, 333 insertions, 23 deletions
diff --git a/kittybox-rs/indieauth/src/lib.rs b/kittybox-rs/indieauth/src/lib.rs
index 5896ebb..826da4e 100644
--- a/kittybox-rs/indieauth/src/lib.rs
+++ b/kittybox-rs/indieauth/src/lib.rs
@@ -1,3 +1,23 @@
+#![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;
 
@@ -6,39 +26,96 @@ 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
 }
 
+/// 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
@@ -52,35 +129,92 @@ pub struct Metadata {
     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.
+    /// 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.
     pub name: String,
+    /// User's profile page. Fetching it may reveal an `h-card`.
     pub url: Url,
+    /// User's profile picture suitable to represent them.
     pub photo: 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>
 }
@@ -97,9 +231,15 @@ impl axum_core::response::IntoResponse for Profile {
     }
 }
 
+/// 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.
     fn new() -> Self {
         use rand::{Rng, distributions::Alphanumeric};
         let bytes = rand::thread_rng()
@@ -110,60 +250,165 @@ impl State {
     }
 }
 
+/// 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,
+///     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();
+///
+/// authorization_endpoint.set_query(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>
     }
 }
 
+/// 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,
+        /// 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>
     }
@@ -189,29 +434,48 @@ impl axum_core::response::IntoResponse for GrantResponse {
 /// 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};
         
@@ -223,12 +487,13 @@ impl TokenData {
             .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)
@@ -240,6 +505,11 @@ impl TokenData {
 // 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,
@@ -250,17 +520,20 @@ pub struct TokenIntrospectionResponse {
 // 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
@@ -296,9 +569,11 @@ impl axum_core::response::IntoResponse for TokenIntrospectionResponse {
     }
 }
 
-
+/// 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
 }
 
@@ -307,11 +582,15 @@ pub struct TokenRevocationRequest {
 #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub enum ResourceErrorKind {
-    InvalidRequest,
+    /// 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 {
@@ -367,13 +646,18 @@ impl std::fmt::Display for ErrorKind {
     }
 }
 
-
+/// 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>
 }
 
diff --git a/kittybox-rs/indieauth/src/pkce.rs b/kittybox-rs/indieauth/src/pkce.rs
index 6dabcc3..249917e 100644
--- a/kittybox-rs/indieauth/src/pkce.rs
+++ b/kittybox-rs/indieauth/src/pkce.rs
@@ -3,12 +3,22 @@ use rand::{Rng, distributions::Alphanumeric};
 use sha2::{Sha256, Digest};
 use data_encoding::BASE64URL;
 
-#[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize)]
+/// 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.
     Plain
 }
-
+// manual impl until Rust 1.62 hits nixos-unstable
+impl Default for PKCEMethod {
+    fn default() -> Self { PKCEMethod::S256 }
+}
+/// 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);
 
@@ -24,7 +34,9 @@ impl ToString for PKCEVerifier {
 }
 
 impl PKCEVerifier {
-    fn new() -> Self {
+    /// 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)
@@ -33,6 +45,9 @@ impl PKCEVerifier {
     }
 }
 
+/// 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,
@@ -40,6 +55,8 @@ pub struct PKCEChallenge {
 }
 
 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 {
@@ -54,22 +71,21 @@ impl PKCEChallenge {
         }
     }
 
+    /// 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.clone(), 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
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_pkce() {
-        let verifier = PKCEVerifier::new();
-        let challenge = PKCEChallenge::new(verifier.clone(), PKCEMethod::S256);
-
-        assert!(challenge.verify(verifier));
-    }
-
-}
diff --git a/kittybox-rs/indieauth/src/scopes.rs b/kittybox-rs/indieauth/src/scopes.rs
index 18ebfbd..ae039a6 100644
--- a/kittybox-rs/indieauth/src/scopes.rs
+++ b/kittybox-rs/indieauth/src/scopes.rs
@@ -9,6 +9,7 @@ use serde::{
     }
 };
 
+/// Various scopes that can be requested through IndieAuth.
 #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub enum Scope {
@@ -38,6 +39,7 @@ pub enum Scope {
     Custom(String)
 }
 impl Scope {
+    /// Create a custom scope from a string slice.
     pub fn custom(scope: &str) -> Scope {
         Scope::Custom(scope.to_string())
     }
@@ -81,20 +83,28 @@ impl From<&str> for Scope {
         }
     }
 }
+
+/// 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()
     }