diff --git a/UPGRADE.md b/UPGRADE.md index 988b717..834fcd2 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -14,7 +14,7 @@ with Rust releases older than 6 months will no longer be considered SemVer break not result in a new major version number for this crate. MSRV changes will coincide with minor version updates and will not happen in patch releases. -### Add typestate const generics to `Client` +### Add typestate generic types to `Client` Each auth flow depends on one or more server endpoints. For example, the authorization code flow depends on both an authorization endpoint and a token endpoint, while the @@ -29,28 +29,43 @@ time, which endpoints' setters (e.g., `set_auth_uri()`) have been called. Auth f an endpoint cannot be used without first calling the corresponding setter, which is enforced by the compiler's type checker. This guarantees that certain errors will not arise at runtime. +In addition to unconditional setters (e.g., `set_auth_uri()`), each +endpoint has a corresponding conditional setter (e.g., `set_auth_uri_option()`) that sets a +conditional typestate (`EndpointMaybeSet`). When the conditional typestate is set, endpoints can +be used via fallible methods that return `Err(ConfigurationError::MissingUrl(_))` if an endpoint +has not been set. This is useful in dynamic scenarios such as +[OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), in which +it cannot be determined until runtime whether an endpoint is configured. + +There are three possible typestates, each implementing the `EndpointState` trait: +* `EndpointNotSet`: the corresponding endpoint has **not** been set and cannot be used. +* `EndpointSet`: the corresponding endpoint **has** been set and is ready to be used. +* `EndpointMaybeSet`: the corresponding endpoint **may have** been set and can be used via fallible + methods that return `Result<_, ConfigurationError>`. + The following code changes are required to support the new interface: 1. Update calls to [`Client::new()`](https://docs.rs/oauth2/latest/oauth2/struct.Client.html#method.new) to use the single-argument constructor (which accepts only a `ClientId`). Use the `set_auth_uri()`, - `set_token_uri()`, and `set_client_secret()` methods to set the optional authorization endpoint, + `set_token_uri()`, and `set_client_secret()` methods to set the authorization endpoint, token endpoint, and client secret, respectively, if applicable to your application's auth flows. 2. If required by your usage of the `Client` or `BasicClient` types (i.e., if you see related compiler errors), add the following generic parameters: ```rust - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, ``` For example, if you store a `BasicClient` within another data type, you may need to annotate it - as `BasicClient` if it has both an authorization endpoint and a + as `BasicClient` if it + has both an authorization endpoint and a token endpoint set. Compiler error messages will likely guide you to the appropriate combination - of Boolean values. + of typestates. If, instead of using `BasicClient`, you are directly using `Client` with a different set of type - parameters, you will need to append the five Boolean typestate parameters. For example, replace: + parameters, you will need to append the five generic typestate parameters. For example, replace: ```rust type SpecialClient = Client< BasicErrorResponse, @@ -64,11 +79,11 @@ The following code changes are required to support the new interface: with: ```rust type SpecialClient< - const HAS_AUTH_URL: bool = false, - const HAS_DEVICE_AUTH_URL: bool = false, - const HAS_INTROSPECTION_URL: bool = false, - const HAS_REVOCATION_URL: bool = false, - const HAS_TOKEN_URL: bool = false, + HasAuthUrl = EndpointNotSet, + HasDeviceAuthUrl = EndpointNotSet, + HasIntrospectionUrl = EndpointNotSet, + HasRevocationUrl = EndpointNotSet, + HasTokenUrl = EndpointNotSet, > = Client< BasicErrorResponse, SpecialTokenResponse, @@ -76,16 +91,16 @@ The following code changes are required to support the new interface: BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, >; ``` - The default values (`= false`) are optional but often helpful since they will allow you to - instantiate a client using `SpecialClient::new()` instead of having to specify - `SpecialClient::::new()`. + The default values (`= EndpointNotSet`) are optional but often helpful since they will allow you + to instantiate a client using `SpecialClient::new()` instead of having to specify + `SpecialClient::::new()`. ### Rename endpoint getters and setters for consistency diff --git a/examples/wunderlist.rs b/examples/wunderlist.rs index dd98c67..d676d43 100644 --- a/examples/wunderlist.rs +++ b/examples/wunderlist.rs @@ -18,12 +18,11 @@ use oauth2::basic::{ BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, BasicTokenType, }; -use oauth2::helpers; use oauth2::reqwest::reqwest; use oauth2::{ AccessToken, AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, - EmptyExtraTokenFields, ExtraTokenFields, RedirectUrl, RefreshToken, Scope, TokenResponse, - TokenUrl, + EmptyExtraTokenFields, EndpointNotSet, ExtraTokenFields, RedirectUrl, RefreshToken, Scope, + TokenResponse, TokenUrl, }; use oauth2::{StandardRevocableToken, TokenType}; use serde::{Deserialize, Serialize}; @@ -36,11 +35,11 @@ use std::time::Duration; type SpecialTokenResponse = NonStandardTokenResponse; type SpecialClient< - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl = EndpointNotSet, + HasDeviceAuthUrl = EndpointNotSet, + HasIntrospectionUrl = EndpointNotSet, + HasRevocationUrl = EndpointNotSet, + HasTokenUrl = EndpointNotSet, > = Client< BasicErrorResponse, SpecialTokenResponse, @@ -48,11 +47,11 @@ type SpecialClient< BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, >; fn default_token_type() -> Option { @@ -78,8 +77,8 @@ pub struct NonStandardTokenResponse { #[serde(skip_serializing_if = "Option::is_none")] refresh_token: Option, #[serde(rename = "scope")] - #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] - #[serde(serialize_with = "helpers::serialize_space_delimited_vec")] + #[serde(deserialize_with = "oauth2::helpers::deserialize_space_delimited_vec")] + #[serde(serialize_with = "oauth2::helpers::serialize_space_delimited_vec")] #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] scopes: Option>, diff --git a/src/basic.rs b/src/basic.rs index 0210644..1521f36 100644 --- a/src/basic.rs +++ b/src/basic.rs @@ -1,7 +1,7 @@ use crate::{ revocation::{RevocationErrorResponseType, StandardRevocableToken}, - Client, EmptyExtraTokenFields, ErrorResponseType, RequestTokenError, StandardErrorResponse, - StandardTokenIntrospectionResponse, StandardTokenResponse, TokenType, + Client, EmptyExtraTokenFields, EndpointNotSet, ErrorResponseType, RequestTokenError, + StandardErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenType, }; use std::fmt::Error as FormatterError; @@ -9,11 +9,11 @@ use std::fmt::{Debug, Display, Formatter}; /// Basic OAuth2 client specialization, suitable for most applications. pub type BasicClient< - const HAS_AUTH_URL: bool = false, - const HAS_DEVICE_AUTH_URL: bool = false, - const HAS_INTROSPECTION_URL: bool = false, - const HAS_REVOCATION_URL: bool = false, - const HAS_TOKEN_URL: bool = false, + HasAuthUrl = EndpointNotSet, + HasDeviceAuthUrl = EndpointNotSet, + HasIntrospectionUrl = EndpointNotSet, + HasRevocationUrl = EndpointNotSet, + HasTokenUrl = EndpointNotSet, > = Client< BasicErrorResponse, BasicTokenResponse, @@ -21,11 +21,11 @@ pub type BasicClient< BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, >; /// Basic OAuth2 authorization token types. diff --git a/src/client.rs b/src/client.rs index bec0b4c..ee5356a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,11 +8,34 @@ use crate::{ TokenIntrospectionResponse, TokenResponse, TokenType, TokenUrl, }; -use chrono::Utc; - -use std::borrow::Cow; use std::marker::PhantomData; -use std::sync::Arc; + +mod private { + /// Private trait to make `EndpointState` a sealed trait. + pub trait EndpointStateSealed {} +} + +/// [Typestate](https://cliffle.com/blog/rust-typestate/) base trait indicating whether an endpoint +/// has been configured via its corresponding setter. +pub trait EndpointState: private::EndpointStateSealed {} + +/// [Typestate](https://cliffle.com/blog/rust-typestate/) indicating that an endpoint has not been +/// set and cannot be used. +pub struct EndpointNotSet; +impl EndpointState for EndpointNotSet {} +impl private::EndpointStateSealed for EndpointNotSet {} + +/// [Typestate](https://cliffle.com/blog/rust-typestate/) indicating that an endpoint has been set +/// and is ready to be used. +pub struct EndpointSet; +impl EndpointState for EndpointSet {} +impl private::EndpointStateSealed for EndpointSet {} + +/// [Typestate](https://cliffle.com/blog/rust-typestate/) indicating that an endpoint may have been +/// set and can be used via fallible methods. +pub struct EndpointMaybeSet; +impl EndpointState for EndpointMaybeSet {} +impl private::EndpointStateSealed for EndpointMaybeSet {} /// Stores the configuration for an OAuth2 client. /// @@ -20,12 +43,22 @@ use std::sync::Arc; /// [Builder Pattern](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html) together with /// [typestates](https://cliffle.com/blog/rust-typestate/#what-are-typestates) to encode whether /// certain fields have been set that are prerequisites to certain authentication flows. For -/// example, the authorization endpoint must be set via [`Client::set_auth_uri`] before -/// [`Client::authorize_url`] can be called. Each endpoint has a corresponding const generic -/// parameter (e.g., `HAS_AUTH_URL`) used to statically enforce these dependencies. These generics +/// example, the authorization endpoint must be set via [`set_auth_uri()`](Client::set_auth_uri) +/// before [`authorize_url()`](Client::authorize_url) can be called. Each endpoint has a +/// corresponding generic type +/// parameter (e.g., `HasAuthUrl`) used to statically enforce these dependencies. These generics /// are set automatically by the corresponding setter functions, and in most cases user code should /// not need to deal with them directly. /// +/// In addition to unconditional setters (e.g., [`set_auth_uri()`](Client::set_auth_uri)), each +/// endpoint has a corresponding conditional setter (e.g., +/// [`set_auth_uri_option()`](Client::set_auth_uri_option)) that sets a +/// conditional typestate ([`EndpointMaybeSet`]). When the conditional typestate is set, endpoints +/// can be used via fallible methods that return [`ConfigurationError::MissingUrl`] if an +/// endpoint has not been set. This is useful in dynamic scenarios such as +/// [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), in which +/// it cannot be determined until runtime whether an endpoint is configured. +/// /// # Error Types /// /// To enable compile time verification that only the correct and complete set of errors for the `Client` function being @@ -91,11 +124,11 @@ pub struct Client< TIR, RT, TRE, - const HAS_AUTH_URL: bool = false, - const HAS_DEVICE_AUTH_URL: bool = false, - const HAS_INTROSPECTION_URL: bool = false, - const HAS_REVOCATION_URL: bool = false, - const HAS_TOKEN_URL: bool = false, + HasAuthUrl = EndpointNotSet, + HasDeviceAuthUrl = EndpointNotSet, + HasIntrospectionUrl = EndpointNotSet, + HasRevocationUrl = EndpointNotSet, + HasTokenUrl = EndpointNotSet, > where TE: ErrorResponse, TR: TokenResponse, @@ -103,6 +136,11 @@ pub struct Client< TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, { pub(crate) client_id: ClientId, pub(crate) client_secret: Option, @@ -113,9 +151,35 @@ pub struct Client< pub(crate) introspection_url: Option, pub(crate) revocation_url: Option, pub(crate) device_authorization_url: Option, - pub(crate) phantom: PhantomData<(TE, TR, TT, TIR, RT, TRE)>, + #[allow(clippy::type_complexity)] + pub(crate) phantom: PhantomData<( + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + )>, } -impl Client +impl + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + > where TE: ErrorResponse + 'static, TR: TokenResponse, @@ -147,11 +211,11 @@ impl< TIR, RT, TRE, - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > Client< TE, @@ -160,11 +224,11 @@ impl< TIR, RT, TRE, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > where TE: ErrorResponse + 'static, @@ -173,14 +237,19 @@ where TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, { - /// Configures the type of client authentication used for communicating with the authorization + /// Set the type of client authentication used for communicating with the authorization /// server. /// /// The default is to use HTTP Basic authentication, as recommended in /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). Note that - /// if a client secret is omitted (i.e., `client_secret` is set to `None` when calling - /// [`Client::new`]), [`AuthType::RequestBody`] is used regardless of the `auth_type` passed to + /// if a client secret is omitted (i.e., [`set_client_secret()`](Self::set_client_secret) is not + /// called), [`AuthType::RequestBody`] is used regardless of the `auth_type` passed to /// this function. pub fn set_auth_type(mut self, auth_type: AuthType) -> Self { self.auth_type = auth_type; @@ -188,7 +257,7 @@ where self } - /// Sets the authorization endpoint. + /// Set the authorization endpoint. /// /// The client uses the authorization endpoint to obtain authorization from the resource owner /// via user-agent redirection. This URL is used in all standard OAuth2 flows except the @@ -204,11 +273,11 @@ where TIR, RT, TRE, - true, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + EndpointSet, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > { Client { client_id: self.client_id, @@ -220,11 +289,47 @@ where introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, - phantom: self.phantom, + phantom: PhantomData, } } - /// Sets the client secret. + /// Conditionally set the authorization endpoint. + /// + /// The client uses the authorization endpoint to obtain authorization from the resource owner + /// via user-agent redirection. This URL is used in all standard OAuth2 flows except the + /// [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) + /// and the [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4). + pub fn set_auth_uri_option( + self, + auth_url: Option, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + EndpointMaybeSet, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > { + Client { + client_id: self.client_id, + client_secret: self.client_secret, + auth_url, + auth_type: self.auth_type, + token_url: self.token_url, + redirect_url: self.redirect_url, + introspection_url: self.introspection_url, + revocation_url: self.revocation_url, + device_authorization_url: self.device_authorization_url, + phantom: PhantomData, + } + } + + /// Set the client secret. /// /// A client secret is generally used for confidential (i.e., server-side) OAuth2 clients and /// omitted from public (browser or native app) OAuth2 clients (see @@ -235,8 +340,10 @@ where self } - /// Sets the device authorization URL used by the device authorization endpoint. - /// Used for Device Code Flow, as per [RFC 8628](https://tools.ietf.org/html/rfc8628). + /// Set the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint used + /// for the Device Authorization Flow. + /// + /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn set_device_authorization_url( self, device_authorization_url: DeviceAuthorizationUrl, @@ -247,11 +354,11 @@ where TIR, RT, TRE, - HAS_AUTH_URL, - true, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + EndpointSet, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > { Client { client_id: self.client_id, @@ -263,12 +370,47 @@ where introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: Some(device_authorization_url), - phantom: self.phantom, + phantom: PhantomData, } } - /// Sets the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) - /// introspection endpoint. + /// Conditionally set the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization + /// endpoint used for the Device Authorization Flow. + /// + /// See [`exchange_device_code()`](Self::exchange_device_code). + pub fn set_device_authorization_url_option( + self, + device_authorization_url: Option, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + EndpointMaybeSet, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > { + Client { + client_id: self.client_id, + client_secret: self.client_secret, + auth_url: self.auth_url, + auth_type: self.auth_type, + token_url: self.token_url, + redirect_url: self.redirect_url, + introspection_url: self.introspection_url, + revocation_url: self.revocation_url, + device_authorization_url, + phantom: PhantomData, + } + } + + /// Set the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. + /// + /// See [`introspect()`](Self::introspect). pub fn set_introspection_url( self, introspection_url: IntrospectionUrl, @@ -279,11 +421,11 @@ where TIR, RT, TRE, - HAS_TOKEN_URL, - HAS_DEVICE_AUTH_URL, - true, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasTokenUrl, + HasDeviceAuthUrl, + EndpointSet, + HasRevocationUrl, + HasTokenUrl, > { Client { client_id: self.client_id, @@ -295,20 +437,54 @@ where introspection_url: Some(introspection_url), revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, - phantom: self.phantom, + phantom: PhantomData, } } - /// Sets the redirect URL used by the authorization endpoint. + /// Conditionally set the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection + /// endpoint. + /// + /// See [`introspect()`](Self::introspect). + pub fn set_introspection_url_option( + self, + introspection_url: Option, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasTokenUrl, + HasDeviceAuthUrl, + EndpointMaybeSet, + HasRevocationUrl, + HasTokenUrl, + > { + Client { + client_id: self.client_id, + client_secret: self.client_secret, + auth_url: self.auth_url, + auth_type: self.auth_type, + token_url: self.token_url, + redirect_url: self.redirect_url, + introspection_url, + revocation_url: self.revocation_url, + device_authorization_url: self.device_authorization_url, + phantom: PhantomData, + } + } + + /// Set the redirect URL used by the authorization endpoint. pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self { self.redirect_url = Some(redirect_url); self } - /// Sets the revocation URL for contacting the revocation endpoint ([RFC 7009](https://tools.ietf.org/html/rfc7009)). + /// Set the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. /// - /// See: [`revoke_token()`](Self::revoke_token()) + /// See [`revoke_token()`](Self::revoke_token()). pub fn set_revocation_url( self, revocation_url: RevocationUrl, @@ -319,11 +495,11 @@ where TIR, RT, TRE, - HAS_TOKEN_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - true, - HAS_TOKEN_URL, + HasTokenUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + EndpointSet, + HasTokenUrl, > { Client { client_id: self.client_id, @@ -335,11 +511,45 @@ where introspection_url: self.introspection_url, revocation_url: Some(revocation_url), device_authorization_url: self.device_authorization_url, - phantom: self.phantom, + phantom: PhantomData, } } - /// Sets the token endpoint. + /// Conditionally set the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation + /// endpoint. + /// + /// See [`revoke_token()`](Self::revoke_token()). + pub fn set_revocation_url_option( + self, + revocation_url: Option, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasTokenUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + EndpointMaybeSet, + HasTokenUrl, + > { + Client { + client_id: self.client_id, + client_secret: self.client_secret, + auth_url: self.auth_url, + auth_type: self.auth_type, + token_url: self.token_url, + redirect_url: self.redirect_url, + introspection_url: self.introspection_url, + revocation_url, + device_authorization_url: self.device_authorization_url, + phantom: PhantomData, + } + } + + /// Set the token endpoint. /// /// The client uses the token endpoint to exchange an authorization code for an access token, /// typically with client authentication. This URL is used in @@ -355,11 +565,11 @@ where TIR, RT, TRE, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - true, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + EndpointSet, > { Client { client_id: self.client_id, @@ -371,28 +581,64 @@ where introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, - phantom: self.phantom, + phantom: PhantomData, + } + } + + /// Conditionally set the token endpoint. + /// + /// The client uses the token endpoint to exchange an authorization code for an access token, + /// typically with client authentication. This URL is used in + /// all standard OAuth2 flows except the + /// [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2). + pub fn set_token_uri_option( + self, + token_url: Option, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + EndpointMaybeSet, + > { + Client { + client_id: self.client_id, + client_secret: self.client_secret, + auth_url: self.auth_url, + auth_type: self.auth_type, + token_url, + redirect_url: self.redirect_url, + introspection_url: self.introspection_url, + revocation_url: self.revocation_url, + device_authorization_url: self.device_authorization_url, + phantom: PhantomData, } } - /// Returns the Client ID. + /// Return the Client ID. pub fn client_id(&self) -> &ClientId { &self.client_id } - /// Returns the type of client authentication used for communicating with the authorization + /// Return the type of client authentication used for communicating with the authorization /// server. pub fn auth_type(&self) -> &AuthType { &self.auth_type } - /// Returns the redirect URL used by the authorization endpoint. + /// Return the redirect URL used by the authorization endpoint. pub fn redirect_uri(&self) -> Option<&RedirectUrl> { self.redirect_url.as_ref() } } -// Methods requiring an authorization endpoint. +/// Methods requiring an authorization endpoint. impl< TE, TR, @@ -400,10 +646,10 @@ impl< TIR, RT, TRE, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > Client< TE, @@ -412,11 +658,11 @@ impl< TIR, RT, TRE, - true, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + EndpointSet, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > where TE: ErrorResponse + 'static, @@ -425,14 +671,21 @@ where TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, { - /// Returns the authorization endpoint. + /// Return the authorization endpoint. pub fn auth_uri(&self) -> &AuthUrl { - // This is enforced statically via the HAS_AUTH_URL const generic. + // This is enforced statically via the HasAuthUrl generic type. self.auth_url.as_ref().expect("should have auth_url") } - /// Generates an authorization URL for a new authorization request. + /// Generate an authorization URL for a new authorization request. + /// + /// Requires [`set_auth_uri()`](Self::set_auth_uri) to have been previously + /// called to set the authorization endpoint. /// /// # Arguments /// @@ -452,21 +705,85 @@ where where S: FnOnce() -> CsrfToken, { - AuthorizationRequest { - // This is enforced statically via the HAS_AUTH_URL const generic. - auth_url: self.auth_uri(), - client_id: &self.client_id, - extra_params: Vec::new(), - pkce_challenge: None, - redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed), - response_type: "code".into(), - scopes: Vec::new(), - state: state_fn(), - } + self.authorize_url_impl(self.auth_uri(), state_fn) + } +} + +/// Methods with a possibly-set authorization endpoint. +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + EndpointMaybeSet, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + /// Return the authorization endpoint. + pub fn auth_uri(&self) -> Option<&AuthUrl> { + self.auth_url.as_ref() + } + + /// Generate an authorization URL for a new authorization request. + /// + /// Requires [`set_auth_uri_option()`](Self::set_auth_uri_option) to have been previously + /// called to set the authorization endpoint. + /// + /// # Arguments + /// + /// * `state_fn` - A function that returns an opaque value used by the client to maintain state + /// between the request and callback. The authorization server includes this value when + /// redirecting the user-agent back to the client. + /// + /// # Security Warning + /// + /// Callers should use a fresh, unpredictable `state` for each authorization request and verify + /// that this value matches the `state` parameter passed by the authorization server to the + /// redirect URI. Doing so mitigates + /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) + /// attacks. To disable CSRF protections (NOT recommended), use `insecure::authorize_url` + /// instead. + pub fn authorize_url(&self, state_fn: S) -> Result + where + S: FnOnce() -> CsrfToken, + { + Ok(self.authorize_url_impl( + self.auth_uri() + .ok_or(ConfigurationError::MissingUrl("authorization"))?, + state_fn, + )) } } -// Methods requiring a token endpoint. +/// Methods requiring a token endpoint. impl< TE, TR, @@ -474,10 +791,10 @@ impl< TIR, RT, TRE, - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, > Client< TE, @@ -486,11 +803,11 @@ impl< TIR, RT, TRE, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - true, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + EndpointSet, > where TE: ErrorResponse + 'static, @@ -499,124 +816,85 @@ where TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, { - /// Requests an access token for the *client credentials* grant type. + /// Request an access token using the + /// [Client Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). /// - /// See . + /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously + /// called to set the token endpoint. pub fn exchange_client_credentials(&self) -> ClientCredentialsTokenRequest { - ClientCredentialsTokenRequest { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - extra_params: Vec::new(), - scopes: Vec::new(), - // This is enforced statically via the HAS_TOKEN_URL const generic. - token_url: self.token_url.as_ref().expect("should have token_url"), - _phantom: PhantomData, - } + self.exchange_client_credentials_impl(self.token_uri()) } - /// Exchanges a code produced by a successful authorization process with an access token. + /// Exchange a code returned during the + /// [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) + /// for an access token. /// /// Acquires ownership of the `code` because authorization codes may only be used once to /// retrieve an access token from the authorization server. /// - /// See . + /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously + /// called to set the token endpoint. pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest { - CodeTokenRequest { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - code, - extra_params: Vec::new(), - pkce_verifier: None, - // This is enforced statically via the HAS_TOKEN_URL const generic. - token_url: self.token_url.as_ref().expect("should have token_url"), - redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed), - _phantom: PhantomData, - } + self.exchange_code_impl(self.token_uri(), code) } - /// Perform a device access token request as per - /// . - pub fn exchange_device_access_token<'a, 'b, 'c, EF>( + /// Exchange an [RFC 8628](https://tools.ietf.org/html/rfc8628#section-3.2) Device Authorization + /// Response returned by [`exchange_device_code()`](Self::exchange_device_code) for an access + /// token. + /// + /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously + /// called to set the token endpoint. + pub fn exchange_device_access_token<'a, EF>( &'a self, - auth_response: &'b DeviceAuthorizationResponse, - ) -> DeviceAccessTokenRequest<'b, 'c, TR, TT, EF> + auth_response: &'a DeviceAuthorizationResponse, + ) -> DeviceAccessTokenRequest<'a, 'static, TR, TT, EF> where - 'a: 'b, EF: ExtraDeviceAuthorizationFields, { - DeviceAccessTokenRequest { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - extra_params: Vec::new(), - // This is enforced statically via the HAS_TOKEN_URL const generic. - token_url: self.token_url.as_ref().expect("should have token_url"), - dev_auth_resp: auth_response, - time_fn: Arc::new(Utc::now), - max_backoff_interval: None, - _phantom: PhantomData, - } + self.exchange_device_access_token_impl(self.token_uri(), auth_response) } - /// Requests an access token for the *password* grant type. + /// Request an access token using the + /// [Resource Owner Password Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3). /// - /// See . - pub fn exchange_password<'a, 'b>( + /// Requires + /// [`set_token_uri()`](Self::set_token_uri) to have + /// been previously called to set the token endpoint. + pub fn exchange_password<'a>( &'a self, - username: &'b ResourceOwnerUsername, - password: &'b ResourceOwnerPassword, - ) -> PasswordTokenRequest<'b, TE, TR, TT> - where - 'a: 'b, - { - PasswordTokenRequest::<'b> { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - username, - password, - extra_params: Vec::new(), - scopes: Vec::new(), - // This is enforced statically via the HAS_TOKEN_URL const generic. - token_url: self.token_url.as_ref().expect("should have token_url"), - _phantom: PhantomData, - } + username: &'a ResourceOwnerUsername, + password: &'a ResourceOwnerPassword, + ) -> PasswordTokenRequest<'a, TE, TR, TT> { + self.exchange_password_impl(self.token_uri(), username, password) } - /// Exchanges a refresh token for an access token + /// Exchange a refresh token for an access token. /// /// See . - pub fn exchange_refresh_token<'a, 'b>( + /// + /// Requires + /// [`set_token_uri()`](Self::set_token_uri) to have + /// been previously called to set the token endpoint. + pub fn exchange_refresh_token<'a>( &'a self, - refresh_token: &'b RefreshToken, - ) -> RefreshTokenRequest<'b, TE, TR, TT> - where - 'a: 'b, - { - RefreshTokenRequest { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - extra_params: Vec::new(), - refresh_token, - scopes: Vec::new(), - // This is enforced statically via the HAS_TOKEN_URL const generic. - token_url: self.token_url.as_ref().expect("should have token_url"), - _phantom: PhantomData, - } + refresh_token: &'a RefreshToken, + ) -> RefreshTokenRequest<'a, TE, TR, TT> { + self.exchange_refresh_token_impl(self.token_uri(), refresh_token) } - /// Returns the token endpoint. + /// Return the token endpoint. pub fn token_uri(&self) -> &TokenUrl { - // This is enforced statically via the HAS_TOKEN_URL const generic. + // This is enforced statically via the HasTokenUrl generic type. self.token_url.as_ref().expect("should have token_url") } } -// Methods requiring a device authorization endpoint. +/// Methods with a possibly-set token endpoint. impl< TE, TR, @@ -624,10 +902,10 @@ impl< TIR, RT, TRE, - const HAS_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, > Client< TE, @@ -636,11 +914,11 @@ impl< TIR, RT, TRE, - HAS_AUTH_URL, - true, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + EndpointMaybeSet, > where TE: ErrorResponse + 'static, @@ -649,47 +927,219 @@ where TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, { - /// Perform a device authorization request as per - /// . - pub fn exchange_device_code(&self) -> DeviceAuthorizationRequest { - DeviceAuthorizationRequest { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - extra_params: Vec::new(), - scopes: Vec::new(), - // This is enforced statically via the HAS_DEVICE_AUTH_URL const generic. - device_authorization_url: self - .device_authorization_url + /// Request an access token using the + /// [Client Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). + /// + /// Requires [`set_token_uri_option()`](Self::set_token_uri_option) to have been previously + /// called to set the token endpoint. + pub fn exchange_client_credentials( + &self, + ) -> Result, ConfigurationError> { + Ok(self.exchange_client_credentials_impl( + self.token_url .as_ref() - .expect("should have device_authorization_url"), - _phantom: PhantomData, - } + .ok_or(ConfigurationError::MissingUrl("token"))?, + )) } - /// Returns the device authorization URL used by the device authorization endpoint. + /// Exchange a code returned during the + /// [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) + /// for an access token. + /// + /// Acquires ownership of the `code` because authorization codes may only be used once to + /// retrieve an access token from the authorization server. + /// + /// Requires [`set_token_uri_option()`](Self::set_token_uri_option) to have been previously + /// called to set the token endpoint. + pub fn exchange_code( + &self, + code: AuthorizationCode, + ) -> Result, ConfigurationError> { + Ok(self.exchange_code_impl( + self.token_url + .as_ref() + .ok_or(ConfigurationError::MissingUrl("token"))?, + code, + )) + } + + /// Exchange an [RFC 8628](https://tools.ietf.org/html/rfc8628#section-3.2) Device Authorization + /// Response returned by [`exchange_device_code()`](Self::exchange_device_code) for an access + /// token. + /// + /// Requires [`set_token_uri_option()`](Self::set_token_uri_option) to have been previously + /// called to set the token endpoint. + pub fn exchange_device_access_token<'a, EF>( + &'a self, + auth_response: &'a DeviceAuthorizationResponse, + ) -> Result, ConfigurationError> + where + EF: ExtraDeviceAuthorizationFields, + { + Ok(self.exchange_device_access_token_impl( + self.token_url + .as_ref() + .ok_or(ConfigurationError::MissingUrl("token"))?, + auth_response, + )) + } + + /// Request an access token using the + /// [Resource Owner Password Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3). + /// + /// Requires + /// [`set_token_uri_option()`](Self::set_token_uri_option) to have + /// been previously called to set the token endpoint. + pub fn exchange_password<'a>( + &'a self, + username: &'a ResourceOwnerUsername, + password: &'a ResourceOwnerPassword, + ) -> Result, ConfigurationError> { + Ok(self.exchange_password_impl( + self.token_url + .as_ref() + .ok_or(ConfigurationError::MissingUrl("token"))?, + username, + password, + )) + } + + /// Exchange a refresh token for an access token. + /// + /// See . + /// + /// Requires + /// [`set_token_uri_option()`](Self::set_token_uri_option) to have + /// been previously called to set the token endpoint. + pub fn exchange_refresh_token<'a>( + &'a self, + refresh_token: &'a RefreshToken, + ) -> Result, ConfigurationError> { + Ok(self.exchange_refresh_token_impl( + self.token_url + .as_ref() + .ok_or(ConfigurationError::MissingUrl("token"))?, + refresh_token, + )) + } + + /// Return the token endpoint. + pub fn token_uri(&self) -> Option<&TokenUrl> { + self.token_url.as_ref() + } +} + +/// Methods requiring a device authorization endpoint. +impl + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + EndpointSet, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + /// Begin the [RFC 8628](https://tools.ietf.org/html/rfc8628) Device Authorization Flow and + /// retrieve a Device Authorization Response. + /// + /// Requires + /// [`set_device_authorization_url()`](Self::set_device_authorization_url) to have + /// been previously called to set the device authorization endpoint. + /// + /// See [`exchange_device_access_token()`](Self::exchange_device_access_token). + pub fn exchange_device_code(&self) -> DeviceAuthorizationRequest { + self.exchange_device_code_impl(self.device_authorization_url()) + } + + /// Return the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint + /// used for the Device Authorization Flow. + /// + /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn device_authorization_url(&self) -> &DeviceAuthorizationUrl { - // This is enforced statically via the HAS_DEVICE_AUTH_URL const generic. + // This is enforced statically via the HasDeviceAuthUrl generic type. self.device_authorization_url .as_ref() .expect("should have device_authorization_url") } } -// Methods requiring an introspection endpoint. -impl< +/// Methods with a possibly-set device authorization endpoint. +impl + Client< TE, TR, TT, TIR, RT, TRE, - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl, + EndpointMaybeSet, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + /// Begin the [RFC 8628](https://tools.ietf.org/html/rfc8628) Device Authorization Flow. + /// + /// Requires + /// [`set_device_authorization_url_option()`](Self::set_device_authorization_url_option) to have + /// been previously called to set the device authorization endpoint. + /// + /// See [`exchange_device_access_token()`](Self::exchange_device_access_token). + pub fn exchange_device_code( + &self, + ) -> Result, ConfigurationError> { + Ok(self.exchange_device_code_impl( + self.device_authorization_url + .as_ref() + .ok_or(ConfigurationError::MissingUrl("device authorization"))?, + )) + } + + /// Return the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint + /// used for the Device Authorization Flow. + /// + /// See [`exchange_device_code()`](Self::exchange_device_code). + pub fn device_authorization_url(&self) -> Option<&DeviceAuthorizationUrl> { + self.device_authorization_url.as_ref() + } +} + +/// Methods requiring an introspection endpoint. +impl Client< TE, TR, @@ -697,11 +1147,11 @@ impl< TIR, RT, TRE, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - true, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + EndpointSet, + HasRevocationUrl, + HasTokenUrl, > where TE: ErrorResponse + 'static, @@ -710,55 +1160,84 @@ where TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, { - /// Query the authorization server [`RFC 7662 compatible`](https://tools.ietf.org/html/rfc7662) introspection - /// endpoint to determine the set of metadata for a previously received token. + /// Retrieve metadata for an access token using the + /// [`RFC 7662`](https://tools.ietf.org/html/rfc7662) introspection endpoint. /// /// Requires [`set_introspection_url()`](Self::set_introspection_url) to have been previously - /// called to set the introspection endpoint URL. + /// called to set the introspection endpoint. pub fn introspect<'a>( &'a self, token: &'a AccessToken, - ) -> Result, ConfigurationError> { - Ok(IntrospectionRequest { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - extra_params: Vec::new(), - // This is enforced statically via the HAS_INTROSPECTION_URL const generic. - introspection_url: self - .introspection_url - .as_ref() - .expect("should have introspection_url"), - token, - token_type_hint: None, - _phantom: PhantomData, - }) + ) -> IntrospectionRequest<'a, TE, TIR, TT> { + self.introspect_impl(self.introspection_url(), token) } - /// Returns the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) - /// introspection endpoint. + /// Return the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. pub fn introspection_url(&self) -> &IntrospectionUrl { - // This is enforced statically via the HAS_INTROSPECTION_URL const generic. + // This is enforced statically via the HasIntrospectionUrl generic type. self.introspection_url .as_ref() .expect("should have introspection_url") } } -// Methods requiring a revocation endpoint. -impl< +/// Methods with a possibly-set introspection endpoint. +impl + Client< TE, TR, TT, TIR, RT, TRE, - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl, + HasDeviceAuthUrl, + EndpointMaybeSet, + HasRevocationUrl, + HasTokenUrl, > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + /// Retrieve metadata for an access token using the + /// [`RFC 7662`](https://tools.ietf.org/html/rfc7662) introspection endpoint. + /// + /// Requires [`set_introspection_url_option()`](Self::set_introspection_url_option) to have been + /// previously called to set the introspection endpoint. + pub fn introspect<'a>( + &'a self, + token: &'a AccessToken, + ) -> Result, ConfigurationError> { + Ok(self.introspect_impl( + self.introspection_url + .as_ref() + .ok_or(ConfigurationError::MissingUrl("introspection"))?, + token, + )) + } + + /// Return the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. + pub fn introspection_url(&self) -> Option<&IntrospectionUrl> { + self.introspection_url.as_ref() + } +} + +/// Methods requiring a revocation endpoint. +impl Client< TE, TR, @@ -766,11 +1245,11 @@ impl< TIR, RT, TRE, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - true, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + EndpointSet, + HasTokenUrl, > where TE: ErrorResponse + 'static, @@ -779,52 +1258,82 @@ where TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasTokenUrl: EndpointState, { - /// Attempts to revoke the given previously received token using an - /// [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009) compatible - /// endpoint. + /// Revoke an access or refresh token using the [RFC 7009](https://tools.ietf.org/html/rfc7009) + /// revocation endpoint. /// /// Requires [`set_revocation_url()`](Self::set_revocation_url) to have been previously - /// called to set the revocation endpoint URL. + /// called to set the revocation endpoint. pub fn revoke_token( &self, token: RT, ) -> Result, ConfigurationError> { - // https://tools.ietf.org/html/rfc7009#section-2 states: - // "The client requests the revocation of a particular token by making an - // HTTP POST request to the token revocation endpoint URL. This URL - // MUST conform to the rules given in [RFC6749], Section 3.1. Clients - // MUST verify that the URL is an HTTPS URL." - - // This is enforced statically via the HAS_REVOCATION_URL const generic. - let revocation_url = self - .revocation_url - .as_ref() - .expect("should have revocation_url"); - - if revocation_url.url().scheme() != "https" { - return Err(ConfigurationError::InsecureUrl("revocation")); - } - - Ok(RevocationRequest { - auth_type: &self.auth_type, - client_id: &self.client_id, - client_secret: self.client_secret.as_ref(), - extra_params: Vec::new(), - revocation_url, - token, - _phantom: PhantomData, - }) + self.revoke_token_impl(self.revocation_url(), token) } - /// Returns the revocation URL for contacting the revocation endpoint - /// ([RFC 7009](https://tools.ietf.org/html/rfc7009)). + /// Return the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. /// - /// See: [`revoke_token()`](Self::revoke_token()) + /// See [`revoke_token()`](Self::revoke_token()). pub fn revocation_url(&self) -> &RevocationUrl { - // This is enforced statically via the HAS_REVOCATION_URL const generic. + // This is enforced statically via the HasRevocationUrl generic type. self.revocation_url .as_ref() .expect("should have revocation_url") } } + +/// Methods with a possible-set revocation endpoint. +impl + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + EndpointMaybeSet, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + /// Revoke an access or refresh token using the [RFC 7009](https://tools.ietf.org/html/rfc7009) + /// revocation endpoint. + /// + /// Requires [`set_revocation_url_option()`](Self::set_revocation_url_option) to have been + /// previously called to set the revocation endpoint. + pub fn revoke_token( + &self, + token: RT, + ) -> Result, ConfigurationError> { + self.revoke_token_impl( + self.revocation_url + .as_ref() + .ok_or(ConfigurationError::MissingUrl("revocation"))?, + token, + ) + } + + /// Return the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. + /// + /// See [`revoke_token()`](Self::revoke_token()). + pub fn revocation_url(&self) -> Option<&RevocationUrl> { + self.revocation_url.as_ref() + } +} diff --git a/src/code.rs b/src/code.rs index 31f8f0a..288b022 100644 --- a/src/code.rs +++ b/src/code.rs @@ -1,9 +1,73 @@ -use crate::{AuthUrl, ClientId, CsrfToken, PkceCodeChallenge, RedirectUrl, ResponseType, Scope}; +use crate::{ + AuthUrl, Client, ClientId, CsrfToken, EndpointState, ErrorResponse, PkceCodeChallenge, + RedirectUrl, ResponseType, RevocableToken, Scope, TokenIntrospectionResponse, TokenResponse, + TokenType, +}; use url::Url; use std::borrow::Cow; +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + pub(crate) fn authorize_url_impl<'a, S>( + &'a self, + auth_url: &'a AuthUrl, + state_fn: S, + ) -> AuthorizationRequest<'a> + where + S: FnOnce() -> CsrfToken, + { + AuthorizationRequest { + auth_url, + client_id: &self.client_id, + extra_params: Vec::new(), + pkce_challenge: None, + redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed), + response_type: "code".into(), + scopes: Vec::new(), + state: state_fn(), + } + } +} + /// A request to the authorization endpoint #[derive(Debug)] pub struct AuthorizationRequest<'a> { diff --git a/src/devicecode.rs b/src/devicecode.rs index caf086f..63141ab 100644 --- a/src/devicecode.rs +++ b/src/devicecode.rs @@ -2,10 +2,10 @@ use crate::basic::BasicErrorResponseType; use crate::endpoint::{endpoint_request, endpoint_response}; use crate::types::VerificationUriComplete; use crate::{ - AsyncHttpClient, AuthType, ClientId, ClientSecret, DeviceAuthorizationUrl, DeviceCode, - EndUserVerificationUrl, ErrorResponse, ErrorResponseType, HttpRequest, HttpResponse, - RequestTokenError, Scope, StandardErrorResponse, SyncHttpClient, TokenRequestFuture, - TokenResponse, TokenType, TokenUrl, UserCode, + AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, DeviceAuthorizationUrl, DeviceCode, + EndUserVerificationUrl, EndpointState, ErrorResponse, ErrorResponseType, HttpRequest, + HttpResponse, RequestTokenError, RevocableToken, Scope, StandardErrorResponse, SyncHttpClient, + TokenIntrospectionResponse, TokenRequestFuture, TokenResponse, TokenType, TokenUrl, UserCode, }; use chrono::{DateTime, Utc}; @@ -22,6 +22,82 @@ use std::pin::Pin; use std::sync::Arc; use std::time::Duration; +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + pub(crate) fn exchange_device_code_impl<'a>( + &'a self, + device_authorization_url: &'a DeviceAuthorizationUrl, + ) -> DeviceAuthorizationRequest<'a, TE> { + DeviceAuthorizationRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + extra_params: Vec::new(), + scopes: Vec::new(), + device_authorization_url, + _phantom: PhantomData, + } + } + + pub(crate) fn exchange_device_access_token_impl<'a, EF>( + &'a self, + token_url: &'a TokenUrl, + auth_response: &'a DeviceAuthorizationResponse, + ) -> DeviceAccessTokenRequest<'a, 'static, TR, TT, EF> + where + EF: ExtraDeviceAuthorizationFields, + { + DeviceAccessTokenRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + extra_params: Vec::new(), + token_url, + dev_auth_resp: auth_response, + time_fn: Arc::new(Utc::now), + max_backoff_interval: None, + _phantom: PhantomData, + } + } +} + /// Future returned by [`DeviceAuthorizationRequest::request_async`]. pub type DeviceAuthorizationRequestFuture<'c, C, EF, TE> = TokenRequestFuture<'c, >::Error, TE, DeviceAuthorizationResponse>; @@ -143,7 +219,7 @@ where pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) token_url: &'a TokenUrl, pub(crate) dev_auth_resp: &'a DeviceAuthorizationResponse, - pub(crate) time_fn: Arc DateTime + 'b + Send + Sync>, + pub(crate) time_fn: Arc DateTime + Send + Sync + 'b>, pub(crate) max_backoff_interval: Option, pub(crate) _phantom: PhantomData<(TR, TT, EF)>, } @@ -179,12 +255,21 @@ where /// Specifies a function for returning the current time. /// /// This function is used while polling the authorization server. - pub fn set_time_fn(mut self, time_fn: T) -> Self + pub fn set_time_fn<'t, T>(self, time_fn: T) -> DeviceAccessTokenRequest<'a, 't, TR, TT, EF> where - T: Fn() -> DateTime + 'b + Send + Sync, + T: Fn() -> DateTime + Send + Sync + 't, { - self.time_fn = Arc::new(time_fn); - self + DeviceAccessTokenRequest { + auth_type: self.auth_type, + client_id: self.client_id, + client_secret: self.client_secret, + extra_params: self.extra_params, + token_url: self.token_url, + dev_auth_resp: self.dev_auth_resp, + time_fn: Arc::new(time_fn), + max_backoff_interval: self.max_backoff_interval, + _phantom: PhantomData, + } } /// Sets the upper limit of the sleep interval to use for polling the token endpoint when the @@ -419,7 +504,7 @@ where /// it into their user agent. /// /// The `verification_url` alias here is a deviation from the RFC, as - /// implementations of device code flow predate RFC 8628. + /// implementations of device authorization flow predate RFC 8628. #[serde(alias = "verification_url")] verification_uri: EndUserVerificationUrl, diff --git a/src/endpoint.rs b/src/endpoint.rs index 84f8768..ffdddf0 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -34,8 +34,8 @@ impl<'c, E, F, T> AsyncHttpClient<'c> for T where E: Error + 'static, F: Future> + 'c, - // We can't implement this for FnOnce because the device code flow requires clients to support - // multiple calls. + // We can't implement this for FnOnce because the device authorization flow requires clients to + // supportmultiple calls. T: Fn(HttpRequest) -> F, { type Error = E; @@ -59,8 +59,8 @@ pub trait SyncHttpClient { impl SyncHttpClient for T where E: Error + 'static, - // We can't implement this for FnOnce because the device code flow requires clients to support - // multiple calls. + // We can't implement this for FnOnce because the device authorization flow requires clients to + // support multiple calls. T: Fn(HttpRequest) -> Result, { type Error = E; diff --git a/src/introspection.rs b/src/introspection.rs index 8f4e7d2..99a82a9 100644 --- a/src/introspection.rs +++ b/src/introspection.rs @@ -1,8 +1,9 @@ use crate::endpoint::{endpoint_request, endpoint_response}; use crate::{ - AccessToken, AsyncHttpClient, AuthType, ClientId, ClientSecret, ErrorResponse, - ExtraTokenFields, HttpRequest, IntrospectionUrl, RequestTokenError, Scope, SyncHttpClient, - TokenRequestFuture, TokenType, + AccessToken, AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, ConfigurationError, + EndpointState, ErrorResponse, ExtraTokenFields, HttpRequest, IntrospectionUrl, + RequestTokenError, RevocableToken, Scope, SyncHttpClient, TokenRequestFuture, TokenResponse, + TokenType, }; use chrono::serde::ts_seconds_option; @@ -16,6 +17,63 @@ use std::fmt::Debug; use std::marker::PhantomData; use std::pin::Pin; +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + pub(crate) fn introspect_impl<'a>( + &'a self, + introspection_url: &'a IntrospectionUrl, + token: &'a AccessToken, + ) -> IntrospectionRequest<'a, TE, TIR, TT> { + IntrospectionRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + extra_params: Vec::new(), + introspection_url, + token, + token_type_hint: None, + _phantom: PhantomData, + } + } +} + /// A request to introspect an access token. /// /// See . diff --git a/src/lib.rs b/src/lib.rs index 45c9be0..f534f37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ //! * [Implicit Grant](#implicit-grant) //! * [Resource Owner Password Credentials Grant](#resource-owner-password-credentials-grant) //! * [Client Credentials Grant](#client-credentials-grant) -//! * [Device Code Flow](#device-code-flow) +//! * [Device Authorization Flow](#device-authorization-flow) //! * [Other examples](#other-examples) //! * [Contributed Examples](#contributed-examples) //! @@ -389,9 +389,9 @@ //! # } //! ``` //! -//! # Device Code Flow +//! # Device Authorization Flow //! -//! Device Code Flow allows users to sign in on browserless or input-constrained +//! Device Authorization Flow allows users to sign in on browserless or input-constrained //! devices. This is a two-stage process; first a user-code and verification //! URL are obtained by using the `Client::exchange_client_credentials` //! method. Those are displayed to the user, then are used in a second client @@ -455,7 +455,7 @@ //! //! - [Google](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/google.rs) (includes token revocation) //! - [Github](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/github.rs) -//! - [Microsoft Device Code Flow (async)](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/microsoft_devicecode.rs) +//! - [Microsoft Device Authorization Flow (async)](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/microsoft_devicecode.rs) //! - [Microsoft Graph](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/msgraph.rs) //! - [Wunderlist](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/wunderlist.rs) //! @@ -480,7 +480,7 @@ pub mod curl; #[cfg(all(feature = "curl", target_arch = "wasm32"))] compile_error!("wasm32 is not supported with the `curl` feature. Use the `reqwest` backend or a custom backend for wasm32 support"); -/// Device Code Flow OAuth2 implementation +/// Device Authorization Flow OAuth2 implementation /// ([RFC 8628](https://tools.ietf.org/html/rfc8628)). mod devicecode; @@ -514,7 +514,7 @@ mod types; #[cfg(feature = "ureq")] pub mod ureq; -pub use crate::client::Client; +pub use crate::client::{Client, EndpointMaybeSet, EndpointNotSet, EndpointSet, EndpointState}; pub use crate::code::AuthorizationRequest; pub use crate::devicecode::{ DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationRequestFuture, @@ -556,6 +556,9 @@ const CONTENT_TYPE_FORMENCODED: &str = "application/x-www-form-urlencoded"; #[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum ConfigurationError { + /// The endpoint URL is not set. + #[error("No {0} endpoint URL specified")] + MissingUrl(&'static str), /// The endpoint URL to be contacted MUST be HTTPS. #[error("Scheme for {0} endpoint URL must be HTTPS")] InsecureUrl(&'static str), diff --git a/src/revocation.rs b/src/revocation.rs index 6ab95ee..a76abc2 100644 --- a/src/revocation.rs +++ b/src/revocation.rs @@ -1,9 +1,10 @@ use crate::basic::BasicErrorResponseType; use crate::endpoint::{endpoint_request, endpoint_response, endpoint_response_status_only}; use crate::{ - AccessToken, AsyncHttpClient, AuthType, ClientId, ClientSecret, ErrorResponse, - ErrorResponseType, HttpRequest, RefreshToken, RequestTokenError, RevocationUrl, SyncHttpClient, - TokenRequestFuture, + AccessToken, AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, ConfigurationError, + EndpointState, ErrorResponse, ErrorResponseType, HttpRequest, RefreshToken, RequestTokenError, + RevocationUrl, SyncHttpClient, TokenIntrospectionResponse, TokenRequestFuture, TokenResponse, + TokenType, }; use serde::{Deserialize, Serialize}; @@ -15,6 +16,71 @@ use std::fmt::{Debug, Display, Formatter}; use std::marker::PhantomData; use std::pin::Pin; +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + pub(crate) fn revoke_token_impl<'a>( + &'a self, + revocation_url: &'a RevocationUrl, + token: RT, + ) -> Result, ConfigurationError> { + // https://tools.ietf.org/html/rfc7009#section-2 states: + // "The client requests the revocation of a particular token by making an + // HTTP POST request to the token revocation endpoint URL. This URL + // MUST conform to the rules given in [RFC6749], Section 3.1. Clients + // MUST verify that the URL is an HTTPS URL." + if revocation_url.url().scheme() != "https" { + return Err(ConfigurationError::InsecureUrl("revocation")); + } + + Ok(RevocationRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + extra_params: Vec::new(), + revocation_url, + token, + _phantom: PhantomData, + }) + } +} + /// A revocable token. /// /// Implement this trait to indicate support for token revocation per [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009#section-2.2). @@ -316,6 +382,17 @@ mod tests { use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::{HeaderValue, Response, StatusCode}; + #[test] + fn test_token_revocation_with_missing_url() { + let client = new_client().set_revocation_url_option(None); + + let result = client + .revoke_token(AccessToken::new("access_token_123".to_string()).into()) + .unwrap_err(); + + assert_eq!(result.to_string(), "No revocation endpoint URL specified"); + } + #[test] fn test_token_revocation_with_non_https_url() { let client = new_client(); @@ -326,7 +403,7 @@ mod tests { .unwrap_err(); assert_eq!( - format!("{}", result), + result.to_string(), "Scheme for revocation endpoint URL must be HTTPS" ); } diff --git a/src/tests.rs b/src/tests.rs index c76a7ee..708ead2 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,12 +6,12 @@ use crate::{ ClientCredentialsTokenRequest, ClientId, ClientSecret, CodeTokenRequest, CsrfToken, DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationUrl, DeviceCode, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, EmptyExtraDeviceAuthorizationFields, - EmptyExtraTokenFields, EndUserVerificationUrl, HttpRequest, HttpResponse, PasswordTokenRequest, - PkceCodeChallenge, PkceCodeChallengeMethod, PkceCodeVerifier, RedirectUrl, RefreshToken, - RefreshTokenRequest, RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, - ResponseType, Scope, StandardDeviceAuthorizationResponse, StandardErrorResponse, - StandardRevocableToken, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenUrl, - UserCode, + EmptyExtraTokenFields, EndUserVerificationUrl, EndpointNotSet, EndpointSet, HttpRequest, + HttpResponse, PasswordTokenRequest, PkceCodeChallenge, PkceCodeChallengeMethod, + PkceCodeVerifier, RedirectUrl, RefreshToken, RefreshTokenRequest, RequestTokenError, + ResourceOwnerPassword, ResourceOwnerUsername, ResponseType, Scope, + StandardDeviceAuthorizationResponse, StandardErrorResponse, StandardRevocableToken, + StandardTokenIntrospectionResponse, StandardTokenResponse, TokenUrl, UserCode, }; use http::header::HeaderName; @@ -19,7 +19,8 @@ use http::HeaderValue; use thiserror::Error; use url::Url; -pub(crate) fn new_client() -> BasicClient { +pub(crate) fn new_client( +) -> BasicClient { BasicClient::new(ClientId::new("aaa".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(TokenUrl::new("https://example.com/token".to_string()).unwrap()) @@ -86,11 +87,11 @@ pub(crate) mod colorful_extension { use std::fmt::{Debug, Display, Formatter}; pub type ColorfulClient< - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > = Client< StandardErrorResponse, StandardTokenResponse, @@ -98,11 +99,11 @@ pub(crate) mod colorful_extension { StandardTokenIntrospectionResponse, ColorfulRevocableToken, StandardErrorResponse, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, >; #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -254,11 +255,11 @@ fn test_send_sync_impl() { StandardTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, - false, - false, - false, - false, - false, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, >, >(); is_sync_and_send::< diff --git a/src/token/mod.rs b/src/token/mod.rs index b521a02..9e57851 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -1,8 +1,9 @@ use crate::endpoint::{endpoint_request, endpoint_response}; use crate::{ - AccessToken, AsyncHttpClient, AuthType, AuthorizationCode, ClientId, ClientSecret, - ErrorResponse, HttpRequest, PkceCodeVerifier, RedirectUrl, RefreshToken, RequestTokenError, - ResourceOwnerPassword, ResourceOwnerUsername, Scope, SyncHttpClient, TokenUrl, + AccessToken, AsyncHttpClient, AuthType, AuthorizationCode, Client, ClientId, ClientSecret, + EndpointState, ErrorResponse, HttpRequest, PkceCodeVerifier, RedirectUrl, RefreshToken, + RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, RevocableToken, Scope, + SyncHttpClient, TokenIntrospectionResponse, TokenUrl, }; use serde::de::DeserializeOwned; @@ -19,6 +20,115 @@ use std::time::Duration; #[cfg(test)] mod tests; +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, + HasAuthUrl: EndpointState, + HasDeviceAuthUrl: EndpointState, + HasIntrospectionUrl: EndpointState, + HasRevocationUrl: EndpointState, + HasTokenUrl: EndpointState, +{ + pub(crate) fn exchange_client_credentials_impl<'a>( + &'a self, + token_url: &'a TokenUrl, + ) -> ClientCredentialsTokenRequest<'a, TE, TR, TT> { + ClientCredentialsTokenRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + extra_params: Vec::new(), + scopes: Vec::new(), + token_url, + _phantom: PhantomData, + } + } + + pub(crate) fn exchange_code_impl<'a>( + &'a self, + token_url: &'a TokenUrl, + code: AuthorizationCode, + ) -> CodeTokenRequest<'a, TE, TR, TT> { + CodeTokenRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + code, + extra_params: Vec::new(), + pkce_verifier: None, + token_url, + redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed), + _phantom: PhantomData, + } + } + + pub(crate) fn exchange_password_impl<'a>( + &'a self, + token_url: &'a TokenUrl, + username: &'a ResourceOwnerUsername, + password: &'a ResourceOwnerPassword, + ) -> PasswordTokenRequest<'a, TE, TR, TT> { + PasswordTokenRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + username, + password, + extra_params: Vec::new(), + scopes: Vec::new(), + token_url, + _phantom: PhantomData, + } + } + + pub(crate) fn exchange_refresh_token_impl<'a>( + &'a self, + token_url: &'a TokenUrl, + refresh_token: &'a RefreshToken, + ) -> RefreshTokenRequest<'a, TE, TR, TT> { + RefreshTokenRequest { + auth_type: &self.auth_type, + client_id: &self.client_id, + client_secret: self.client_secret.as_ref(), + extra_params: Vec::new(), + refresh_token, + scopes: Vec::new(), + token_url, + _phantom: PhantomData, + } + } +} + /// Future returned by `request_async` methods. pub type TokenRequestFuture<'c, RE, TE, TR> = dyn Future>> + 'c; diff --git a/src/token/tests.rs b/src/token/tests.rs index 9255e16..8e6e91a 100644 --- a/src/token/tests.rs +++ b/src/token/tests.rs @@ -1154,11 +1154,11 @@ mod custom_errors { impl ErrorResponse for CustomErrorResponse {} pub type CustomErrorClient< - const HAS_AUTH_URL: bool, - const HAS_DEVICE_AUTH_URL: bool, - const HAS_INTROSPECTION_URL: bool, - const HAS_REVOCATION_URL: bool, - const HAS_TOKEN_URL: bool, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, > = Client< CustomErrorResponse, StandardTokenResponse, @@ -1166,11 +1166,11 @@ mod custom_errors { StandardTokenIntrospectionResponse, ColorfulRevocableToken, CustomErrorResponse, - HAS_AUTH_URL, - HAS_DEVICE_AUTH_URL, - HAS_INTROSPECTION_URL, - HAS_REVOCATION_URL, - HAS_TOKEN_URL, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, >; }