diff --git a/crates/oauth2-types/src/errors.rs b/crates/oauth2-types/src/errors.rs index 60e8d4db..2dd5e632 100644 --- a/crates/oauth2-types/src/errors.rs +++ b/crates/oauth2-types/src/errors.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Error types returned by an authorization server. + use std::borrow::Cow; use parse_display::{Display, FromStr}; @@ -56,6 +58,7 @@ impl From for ClientError { } } +/// Client error codes defined in OAuth2.0, OpenID Connect and their extensions. #[derive(Debug, Clone, PartialEq, Eq, Display, FromStr, SerializeDisplay, DeserializeFromStr)] #[display(style = "snake_case")] pub enum ClientErrorCode { diff --git a/crates/oauth2-types/src/lib.rs b/crates/oauth2-types/src/lib.rs index d8a3c674..1fb0df1a 100644 --- a/crates/oauth2-types/src/lib.rs +++ b/crates/oauth2-types/src/lib.rs @@ -12,8 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! [OAuth 2.0] and [OpenID Connect] types. +//! +//! This is part of the [Matrix Authentication Service] project. +//! +//! [OAuth 2.0]: https://oauth.net/2/ +//! [OpenID Connect]: https://openid.net/connect/ +//! [Matrix Authentication Service]: https://github.com/matrix-org/matrix-authentication-service + #![forbid(unsafe_code)] -#![deny(clippy::all, clippy::str_to_string, rustdoc::broken_intra_doc_links)] +#![deny( + clippy::all, + clippy::str_to_string, + rustdoc::broken_intra_doc_links, + missing_docs +)] #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] @@ -26,6 +39,7 @@ pub mod response_type; pub mod scope; pub mod webfinger; +/// Traits intended for blanket imports. pub mod prelude { pub use crate::pkce::CodeChallengeMethodExt; } diff --git a/crates/oauth2-types/src/oidc.rs b/crates/oauth2-types/src/oidc.rs index ec389b04..e73fa712 100644 --- a/crates/oauth2-types/src/oidc.rs +++ b/crates/oauth2-types/src/oidc.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Types to interact with the [OpenID Connect] specification. +//! +//! [OpenID Connect]: https://openid.net/connect/ + use std::{ops::Deref, str::FromStr}; use language_tags::LanguageTag; @@ -107,43 +111,63 @@ impl From for AuthenticationMethodOrAccessTokenType { } } +/// The kind of an application. #[derive( SerializeDisplay, DeserializeFromStr, Clone, Copy, PartialEq, Eq, Hash, Debug, Display, FromStr, )] #[display(style = "lowercase")] pub enum ApplicationType { + /// A web application. Web, + + /// A native application. Native, } +/// Subject Identifier types. +/// +/// A Subject Identifier is a locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client. #[derive( SerializeDisplay, DeserializeFromStr, Clone, Copy, PartialEq, Eq, Hash, Debug, Display, FromStr, )] #[display(style = "lowercase")] pub enum SubjectType { + /// This provides the same `sub` (subject) value to all Clients. Public, + + /// This provides a different `sub` value to each Client, so as not to enable Clients to correlate the End-User's activities without permission. Pairwise, } +/// Claim types. #[derive( SerializeDisplay, DeserializeFromStr, Clone, Copy, PartialEq, Eq, Hash, Debug, Display, FromStr, )] #[display(style = "lowercase")] pub enum ClaimType { + /// Claims that are directly asserted by the OpenID Provider. Normal, + + /// Claims that are asserted by a Claims Provider other than the OpenID Provider but are returned by OpenID Provider. Aggregated, + + /// Claims that are asserted by a Claims Provider other than the OpenID Provider but are returned as references by the OpenID Provider. Distributed, } +/// The default value of `response_modes_supported` if it is not set. pub static DEFAULT_RESPONSE_MODES_SUPPORTED: &[ResponseMode] = &[ResponseMode::Query, ResponseMode::Fragment]; +/// The default value of `grant_types_supported` if it is not set. pub static DEFAULT_GRANT_TYPES_SUPPORTED: &[GrantType] = &[GrantType::AuthorizationCode, GrantType::Implicit]; +/// The default value of `token_endpoint_auth_methods_supported` if it is not set. pub static DEFAULT_AUTH_METHODS_SUPPORTED: &[OAuthClientAuthenticationMethod] = &[OAuthClientAuthenticationMethod::ClientSecretBasic]; +/// The default value of `claim_types_supported` if it is not set. pub static DEFAULT_CLAIM_TYPES_SUPPORTED: &[ClaimType] = &[ClaimType::Normal]; /// Authorization server metadata, as described by the [IANA registry]. diff --git a/crates/oauth2-types/src/pkce.rs b/crates/oauth2-types/src/pkce.rs index c677fdde..407bb131 100644 --- a/crates/oauth2-types/src/pkce.rs +++ b/crates/oauth2-types/src/pkce.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Types for the [Proof Key for Code Exchange]. +//! +//! [Proof Key for Code Exchange]: https://www.rfc-editor.org/rfc/rfc7636 + use std::borrow::Cow; use data_encoding::BASE64URL_NOPAD; @@ -20,20 +24,26 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; +/// Errors that can occur when verifying a code challenge. #[derive(Debug, Error, PartialEq, Eq)] pub enum CodeChallengeError { + /// The code verifier should be at least 43 characters long. #[error("code_verifier should be at least 43 characters long")] TooShort, + /// The code verifier should be at most 128 characters long. #[error("code_verifier should be at most 128 characters long")] TooLong, + /// The code verifier contains invalid characters. #[error("code_verifier contains invalid characters")] InvalidCharacters, + /// The challenge verification failed. #[error("challenge verification failed")] VerificationFailed, + /// The challenge method is unsupported. #[error("unknown challenge method")] UnknownChallengeMethod, } @@ -57,6 +67,7 @@ fn validate_verifier(verifier: &str) -> Result<(), CodeChallengeError> { Ok(()) } +/// Helper trait to compute and verify code challenges. pub trait CodeChallengeMethodExt { /// Compute the challenge for a given verifier /// @@ -105,14 +116,20 @@ impl CodeChallengeMethodExt for PkceCodeChallengeMethod { } } +/// The code challenge data added to an authorization request. #[derive(Clone, Serialize, Deserialize)] pub struct AuthorizationRequest { + /// The code challenge method. pub code_challenge_method: PkceCodeChallengeMethod, + + /// The code challenge computed from the verifier and the method. pub code_challenge: String, } +/// The code challenge data added to a token request. #[derive(Clone, Serialize, Deserialize)] pub struct TokenRequest { + /// The code challenge verifier. pub code_challenge_verifier: String, } diff --git a/crates/oauth2-types/src/registration/mod.rs b/crates/oauth2-types/src/registration/mod.rs index d2436d19..18aa24fa 100644 --- a/crates/oauth2-types/src/registration/mod.rs +++ b/crates/oauth2-types/src/registration/mod.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Types for [Dynamic Client Registration]. +//! +//! [Dynamic Client Registration]: https://openid.net/specs/openid-connect-registration-1_0.html + use std::{collections::HashMap, ops::Deref}; use chrono::{DateTime, Duration, Utc}; @@ -35,18 +39,24 @@ use crate::{ mod client_metadata_serde; use client_metadata_serde::ClientMetadataSerdeHelper; +/// The default value of `response_types` if it is not set. pub const DEFAULT_RESPONSE_TYPES: [OAuthAuthorizationEndpointResponseType; 1] = [OAuthAuthorizationEndpointResponseType::Code]; +/// The default value of `grant_types` if it is not set. pub const DEFAULT_GRANT_TYPES: &[GrantType] = &[GrantType::AuthorizationCode]; +/// The default value of `application_type` if it is not set. pub const DEFAULT_APPLICATION_TYPE: ApplicationType = ApplicationType::Web; +/// The default value of `token_endpoint_auth_method` if it is not set. pub const DEFAULT_TOKEN_AUTH_METHOD: &OAuthClientAuthenticationMethod = &OAuthClientAuthenticationMethod::ClientSecretBasic; +/// The default value of `id_token_signed_response_alg` if it is not set. pub const DEFAULT_SIGNING_ALGORITHM: &JsonWebSignatureAlg = &JsonWebSignatureAlg::Rs256; +/// The default value of `id_token_encrypted_response_enc` if it is not set. pub const DEFAULT_ENCRYPTION_ENC_ALGORITHM: &JsonWebEncryptionEnc = &JsonWebEncryptionEnc::A128CbcHs256; @@ -841,19 +851,26 @@ pub enum ClientMetadataVerificationError { MissingEncryptionAlg(&'static str), } +/// The issuer response to dynamic client registration. #[serde_as] #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct ClientRegistrationResponse { + /// A unique client identifier. pub client_id: String, + /// A client secret, if the `token_endpoint_auth_method` requires one. #[serde(default)] pub client_secret: Option, + /// Time at which the Client Identifier was issued. #[serde(default)] #[serde_as(as = "Option>")] pub client_id_issued_at: Option>, + /// Time at which the client_secret will expire or 0 if it will not expire. + /// + /// Required if `client_secret` is issued. #[serde(default)] #[serde_as(as = "Option>")] pub client_secret_expires_at: Option>, diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index adca763e..faa2b65d 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Requests and response types to interact with the [OAuth 2.0] specification. +//! +//! [OAuth 2.0]: https://oauth.net/2/ + use std::{collections::HashSet, fmt, hash::Hash, num::NonZeroU32}; use chrono::{DateTime, Duration, Utc}; @@ -323,7 +327,10 @@ impl fmt::Debug for AuthorizationRequest { #[skip_serializing_none] #[derive(Serialize, Deserialize, Default, Clone)] pub struct AuthorizationResponse { + /// The authorization code generated by the authorization server. pub code: Option, + + /// Other fields of the response. #[serde(flatten)] pub response: R, } @@ -345,6 +352,7 @@ pub struct DeviceAuthorizationRequest { pub scope: Option, } +/// The default value of the `interval` between polling requests, if it is not set. pub const DEFAULT_DEVICE_AUTHORIZATION_INTERVAL_SECONDS: i64 = 5; /// A successful response from the [Device Authorization Endpoint]. @@ -384,7 +392,7 @@ pub struct DeviceAuthorizationResponse { } impl DeviceAuthorizationResponse { - ///The minimum amount of time in seconds that the client should wait + /// The minimum amount of time in seconds that the client should wait /// between polling requests to the token endpoint. /// /// Defaults to [`DEFAULT_DEVICE_AUTHORIZATION_INTERVAL_SECONDS`]. @@ -535,11 +543,20 @@ pub enum GrantType { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(tag = "grant_type", rename_all = "snake_case")] pub enum AccessTokenRequest { + /// A request in the Authorization Code flow. AuthorizationCode(AuthorizationCodeGrant), + + /// A request to refresh an access token. RefreshToken(RefreshTokenGrant), + + /// A request in the Client Credentials flow. ClientCredentials(ClientCredentialsGrant), + + /// A request in the Device Code flow. #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")] DeviceCode(DeviceCodeGrant), + + /// An unsupported request. #[serde(skip, other)] Unsupported, } diff --git a/crates/oauth2-types/src/response_type.rs b/crates/oauth2-types/src/response_type.rs index 1aab1639..a3d287a5 100644 --- a/crates/oauth2-types/src/response_type.rs +++ b/crates/oauth2-types/src/response_type.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! [Response types] in the OpenID Connect specification. +//! +//! [Response types]: https://openid.net/specs/openid-connect-core-1_0.html#Authentication + #![allow(clippy::module_name_repetitions)] use std::{collections::BTreeSet, fmt, iter::FromIterator, str::FromStr}; diff --git a/crates/oauth2-types/src/scope.rs b/crates/oauth2-types/src/scope.rs index 294f2dee..232920b2 100644 --- a/crates/oauth2-types/src/scope.rs +++ b/crates/oauth2-types/src/scope.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Types to define an [access token's scope]. +//! +//! [access token's scope]: https://www.rfc-editor.org/rfc/rfc6749#section-3.3 + #![allow(clippy::module_name_repetitions)] use std::{borrow::Cow, collections::BTreeSet, iter::FromIterator, ops::Deref, str::FromStr}; @@ -20,10 +24,12 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use thiserror::Error; +/// The error type returned when a scope is invalid. #[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord, Hash)] #[error("Invalid scope format")] pub struct InvalidScope; +/// A scope token or scope value. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ScopeToken(Cow<'static, str>); @@ -36,11 +42,34 @@ impl ScopeToken { } } +/// `openid`. +/// +/// Must be included in OpenID Connect requests. pub const OPENID: ScopeToken = ScopeToken::from_static("openid"); + +/// `profile`. +/// +/// Requests access to the End-User's default profile Claims. pub const PROFILE: ScopeToken = ScopeToken::from_static("profile"); + +/// `email`. +/// +/// Requests access to the `email` and `email_verified` Claims. pub const EMAIL: ScopeToken = ScopeToken::from_static("email"); + +/// `address`. +/// +/// Requests access to the `address` Claim. pub const ADDRESS: ScopeToken = ScopeToken::from_static("address"); + +/// `phone`. +/// +/// Requests access to the `phone_number` and `phone_number_verified` Claims. pub const PHONE: ScopeToken = ScopeToken::from_static("phone"); + +/// `offline_access`. +/// +/// Requests that an OAuth 2.0 Refresh Token be issued that can be used to obtain an Access Token that grants access to the End-User's Userinfo Endpoint even when the End-User is not present (not logged in). pub const OFFLINE_ACCESS: ScopeToken = ScopeToken::from_static("offline_access"); // As per RFC6749 appendix A: @@ -81,6 +110,7 @@ impl ToString for ScopeToken { } } +/// A scope. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Scope(BTreeSet); @@ -108,17 +138,20 @@ impl FromStr for Scope { } impl Scope { + /// Whether this `Scope` is empty. #[must_use] pub fn is_empty(&self) -> bool { // This should never be the case? self.0.is_empty() } + /// The number of tokens in the `Scope`. #[must_use] pub fn len(&self) -> usize { self.0.len() } + /// Whether this `Scope` contains the given value. #[must_use] pub fn contains(&self, token: &str) -> bool { ScopeToken::from_str(token) @@ -126,6 +159,9 @@ impl Scope { .unwrap_or(false) } + /// Inserts the given token in this `Scope`. + /// + /// Returns whether the token was newly inserted. pub fn insert(&mut self, value: ScopeToken) -> bool { self.0.insert(value) } diff --git a/crates/oauth2-types/src/webfinger.rs b/crates/oauth2-types/src/webfinger.rs index bfc9001b..dbebbbe6 100644 --- a/crates/oauth2-types/src/webfinger.rs +++ b/crates/oauth2-types/src/webfinger.rs @@ -12,16 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Types for provider discovery using [Webfinger]. +//! +//! [Webfinger]: https://www.rfc-editor.org/rfc/rfc7033 + use serde::{Deserialize, Serialize}; use url::Url; +/// The response of the Webfinger endpoint. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct WebFingerResponse { + /// A URI that identifies the entity described by the response. subject: String, + + /// Links that describe the subject. links: Vec, } impl WebFingerResponse { + /// Creates a new `WebFingerResponse` with the given subject. #[must_use] pub const fn new(subject: String) -> Self { Self { @@ -30,26 +39,34 @@ impl WebFingerResponse { } } + /// Adds the given link to this `WebFingerResponse`. #[must_use] pub fn with_link(mut self, link: WebFingerLink) -> Self { self.links.push(link); self } + /// Adds the given issuer to this `WebFingerResponse`. #[must_use] pub fn with_issuer(self, issuer: Url) -> Self { self.with_link(WebFingerLink::issuer(issuer)) } } +/// A link in a Webfinger response. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(tag = "rel")] pub enum WebFingerLink { + /// An OpenID Connect issuer. #[serde(rename = "http://openid.net/specs/connect/1.0/issuer")] - OidcIssuer { href: Url }, + OidcIssuer { + /// The URL of the issuer. + href: Url, + }, } impl WebFingerLink { + /// Creates a new `WebFingerLink` for an OpenID Connect issuer. #[must_use] pub const fn issuer(href: Url) -> Self { Self::OidcIssuer { href }