From 1d1f4d17ecdf2a3feb565eb1789cc8649cac7705 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Tue, 20 Feb 2024 21:57:11 -0800 Subject: [PATCH] Track Client endpoints statically via typestates Currently, the authorization endpoint is required, and all other endpoints are optional. This both causes problems for authentication flows that don't require an authorization endpoint (and for which the server may not implement one; see #135), and introduces fallibility into methods that depend on endpoints having been set previously. This change makes all endpoints optional, and each endpoint has a corresponding setter method as part of the Builder Pattern. Each endpoint has a corresponding const generic parameter within the `Client` that tracks whether that endpoint has been set. Each method that depends on an endpoint is implemented only for `Client` instances that have previously called the corresponding setter, which is enforced at compile time. BREAKING CHANGE: The `Client::new()` method now only accepts a client ID. The client secret, authorization endpoint, and token endpoint have been moved to `set_client_secret`, `set_auth_url`, and `set_token_url` methods, respectively. Also, the additional const generics added to `Client` and `BasicClient` will need to be specified at each call site that specifies any of the generic parameters. --- examples/github.rs | 20 +- examples/github_async.rs | 20 +- examples/google.rs | 30 +- examples/google_devicecode.rs | 15 +- examples/letterboxd.rs | 10 +- examples/microsoft_devicecode_common_user.rs | 22 +- examples/microsoft_devicecode_tenant_user.rs | 24 +- examples/msgraph.rs | 28 +- examples/wunderlist.rs | 33 +- src/basic.rs | 13 +- src/lib.rs | 865 +++++++++++++------ src/tests.rs | 183 ++-- 12 files changed, 797 insertions(+), 466 deletions(-) diff --git a/examples/github.rs b/examples/github.rs index 1c8a4f2..904ce46 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -40,17 +40,15 @@ fn main() { .expect("Invalid token endpoint URL"); // Set up the config for the Github OAuth2 process. - let client = BasicClient::new( - github_client_id, - Some(github_client_secret), - auth_url, - Some(token_url), - ) - // This example will be running its own server at localhost:8080. - // See below for the server implementation. - .set_redirect_uri( - RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), - ); + let client = BasicClient::new(github_client_id) + .set_client_secret(github_client_secret) + .set_auth_url(auth_url) + .set_token_url(token_url) + // This example will be running its own server at localhost:8080. + // See below for the server implementation. + .set_redirect_uri( + RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), + ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client diff --git a/examples/github_async.rs b/examples/github_async.rs index f768622..4add907 100644 --- a/examples/github_async.rs +++ b/examples/github_async.rs @@ -41,17 +41,15 @@ async fn main() { .expect("Invalid token endpoint URL"); // Set up the config for the Github OAuth2 process. - let client = BasicClient::new( - github_client_id, - Some(github_client_secret), - auth_url, - Some(token_url), - ) - // This example will be running its own server at localhost:8080. - // See below for the server implementation. - .set_redirect_uri( - RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), - ); + let client = BasicClient::new(github_client_id) + .set_client_secret(github_client_secret) + .set_auth_url(auth_url) + .set_token_url(token_url) + // This example will be running its own server at localhost:8080. + // See below for the server implementation. + .set_redirect_uri( + RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), + ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client diff --git a/examples/google.rs b/examples/google.rs index d12643c..fce2d0c 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -40,22 +40,20 @@ fn main() { .expect("Invalid token endpoint URL"); // Set up the config for the Google OAuth2 process. - let client = BasicClient::new( - google_client_id, - Some(google_client_secret), - auth_url, - Some(token_url), - ) - // This example will be running its own server at localhost:8080. - // See below for the server implementation. - .set_redirect_uri( - RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), - ) - // Google supports OAuth 2.0 Token Revocation (RFC-7009) - .set_revocation_uri( - RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string()) - .expect("Invalid revocation endpoint URL"), - ); + let client = BasicClient::new(google_client_id) + .set_client_secret(google_client_secret) + .set_auth_url(auth_url) + .set_token_url(token_url) + // This example will be running its own server at localhost:8080. + // See below for the server implementation. + .set_redirect_uri( + RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), + ) + // Google supports OAuth 2.0 Token Revocation (RFC-7009) + .set_revocation_uri( + RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string()) + .expect("Invalid revocation endpoint URL"), + ); // Google supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/). // Create a PKCE code verifier and SHA-256 encode it as a code challenge. diff --git a/examples/google_devicecode.rs b/examples/google_devicecode.rs index d246c98..0a38a92 100644 --- a/examples/google_devicecode.rs +++ b/examples/google_devicecode.rs @@ -49,19 +49,16 @@ fn main() { // // Google's OAuth endpoint expects the client_id to be in the request body, // so ensure that option is set. - let device_client = BasicClient::new( - google_client_id, - Some(google_client_secret), - auth_url, - Some(token_url), - ) - .set_device_authorization_url(device_auth_url) - .set_auth_type(AuthType::RequestBody); + let device_client = BasicClient::new(google_client_id) + .set_client_secret(google_client_secret) + .set_auth_url(auth_url) + .set_token_url(token_url) + .set_device_authorization_url(device_auth_url) + .set_auth_type(AuthType::RequestBody); // Request the set of codes from the Device Authorization endpoint. let details: StoringDeviceAuthorizationResponse = device_client .exchange_device_code() - .unwrap() .add_scope(Scope::new("profile".to_string())) .request(http_client) .expect("Failed to request codes from device auth endpoint"); diff --git a/examples/letterboxd.rs b/examples/letterboxd.rs index 31c0175..9b7737e 100644 --- a/examples/letterboxd.rs +++ b/examples/letterboxd.rs @@ -41,12 +41,10 @@ fn main() -> Result<(), anyhow::Error> { let token_url = TokenUrl::new("https://api.letterboxd.com/api/v0/auth/token".to_string())?; // Set up the config for the Letterboxd OAuth2 process. - let client = BasicClient::new( - letterboxd_client_id.clone(), - Some(letterboxd_client_secret.clone()), - auth_url, - Some(token_url), - ); + let client = BasicClient::new(letterboxd_client_id.clone()) + .set_client_secret(letterboxd_client_secret.clone()) + .set_auth_url(auth_url) + .set_token_url(token_url); // Resource Owner flow uses username and password for authentication let letterboxd_username = ResourceOwnerUsername::new( diff --git a/examples/microsoft_devicecode_common_user.rs b/examples/microsoft_devicecode_common_user.rs index 2eca95f..fa3ddfd 100644 --- a/examples/microsoft_devicecode_common_user.rs +++ b/examples/microsoft_devicecode_common_user.rs @@ -7,21 +7,19 @@ use std::error::Error; #[tokio::main] async fn main() -> Result<(), Box> { - let device_auth_url = DeviceAuthorizationUrl::new( - "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode".to_string(), - )?; - let client = BasicClient::new( - ClientId::new("client_id".to_string()), - None, - AuthUrl::new("https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string())?, - Some(TokenUrl::new( + let client = BasicClient::new(ClientId::new("client_id".to_string())) + .set_auth_url(AuthUrl::new( + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string(), + )?) + .set_token_url(TokenUrl::new( "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string(), - )?), - ) - .set_device_authorization_url(device_auth_url); + )?) + .set_device_authorization_url(DeviceAuthorizationUrl::new( + "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode".to_string(), + )?); let details: StandardDeviceAuthorizationResponse = client - .exchange_device_code()? + .exchange_device_code() .add_scope(Scope::new("read".to_string())) .request_async(async_http_client) .await?; diff --git a/examples/microsoft_devicecode_tenant_user.rs b/examples/microsoft_devicecode_tenant_user.rs index 8638f57..56a0ef0 100644 --- a/examples/microsoft_devicecode_tenant_user.rs +++ b/examples/microsoft_devicecode_tenant_user.rs @@ -11,26 +11,22 @@ const TENANT_ID: &str = "{tenant}"; #[tokio::main] async fn main() -> Result<(), Box> { - let device_auth_url = DeviceAuthorizationUrl::new(format!( - "https://login.microsoftonline.com/{}/oauth2/v2.0/devicecode", - TENANT_ID - ))?; - let client = BasicClient::new( - ClientId::new("client_id".to_string()), - None, - AuthUrl::new(format!( + let client = BasicClient::new(ClientId::new("client_id".to_string())) + .set_auth_url(AuthUrl::new(format!( "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize", TENANT_ID - ))?, - Some(TokenUrl::new(format!( + ))?) + .set_token_url(TokenUrl::new(format!( "https://login.microsoftonline.com/{}/oauth2/v2.0/token", TENANT_ID - ))?), - ) - .set_device_authorization_url(device_auth_url); + ))?) + .set_device_authorization_url(DeviceAuthorizationUrl::new(format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/devicecode", + TENANT_ID + ))?); let details: StandardDeviceAuthorizationResponse = client - .exchange_device_code()? + .exchange_device_code() .add_scope(Scope::new("read".to_string())) .request_async(async_http_client) .await?; diff --git a/examples/msgraph.rs b/examples/msgraph.rs index e0cc4b0..d1e4237 100644 --- a/examples/msgraph.rs +++ b/examples/msgraph.rs @@ -49,21 +49,19 @@ fn main() { .expect("Invalid token endpoint URL"); // Set up the config for the Microsoft Graph OAuth2 process. - let client = BasicClient::new( - graph_client_id, - Some(graph_client_secret), - auth_url, - Some(token_url), - ) - // Microsoft Graph requires client_id and client_secret in URL rather than - // using Basic authentication. - .set_auth_type(AuthType::RequestBody) - // This example will be running its own server at localhost:3003. - // See below for the server implementation. - .set_redirect_uri( - RedirectUrl::new("http://localhost:3003/redirect".to_string()) - .expect("Invalid redirect URL"), - ); + let client = BasicClient::new(graph_client_id) + .set_client_secret(graph_client_secret) + .set_auth_url(auth_url) + .set_token_url(token_url) + // Microsoft Graph requires client_id and client_secret in URL rather than + // using Basic authentication. + .set_auth_type(AuthType::RequestBody) + // This example will be running its own server at localhost:3003. + // See below for the server implementation. + .set_redirect_uri( + RedirectUrl::new("http://localhost:3003/redirect".to_string()) + .expect("Invalid redirect URL"), + ); // Microsoft Graph supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/). // Create a PKCE code verifier and SHA-256 encode it as a code challenge. diff --git a/examples/wunderlist.rs b/examples/wunderlist.rs index 976e728..635f649 100644 --- a/examples/wunderlist.rs +++ b/examples/wunderlist.rs @@ -39,13 +39,24 @@ use std::net::TcpListener; use std::time::Duration; type SpecialTokenResponse = NonStandardTokenResponse; -type SpecialClient = Client< +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, +> = Client< BasicErrorResponse, SpecialTokenResponse, BasicTokenType, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, >; fn default_token_type() -> Option { @@ -151,17 +162,15 @@ fn main() { .expect("Invalid token endpoint URL"); // Set up the config for the Wunderlist OAuth2 process. - let client = SpecialClient::new( - wunder_client_id, - Some(wunderlist_client_secret), - auth_url, - Some(token_url), - ) - // This example will be running its own server at localhost:8080. - // See below for the server implementation. - .set_redirect_uri( - RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), - ); + let client = SpecialClient::new(wunder_client_id) + .set_client_secret(wunderlist_client_secret) + .set_auth_url(auth_url) + .set_token_url(token_url) + // This example will be running its own server at localhost:8080. + // See below for the server implementation. + .set_redirect_uri( + RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), + ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random).url(); diff --git a/src/basic.rs b/src/basic.rs index 93fb92f..56db6b4 100644 --- a/src/basic.rs +++ b/src/basic.rs @@ -13,13 +13,24 @@ use crate::{ /// /// Basic OAuth2 client specialization, suitable for most applications. /// -pub type BasicClient = Client< +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, +> = Client< BasicErrorResponse, BasicTokenResponse, BasicTokenType, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, >; /// diff --git a/src/lib.rs b/src/lib.rs index c9d3099..d82341b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,13 +116,10 @@ //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! // Create an OAuth2 client by specifying the client ID, client secret, authorization URL and //! // token URL. -//! let client = -//! BasicClient::new( -//! ClientId::new("client_id".to_string()), -//! Some(ClientSecret::new("client_secret".to_string())), -//! AuthUrl::new("http://authorize".to_string())?, -//! Some(TokenUrl::new("http://token".to_string())?) -//! ) +//! let client = BasicClient::new(ClientId::new("client_id".to_string())) +//! .set_client_secret(ClientSecret::new("client_secret".to_string())) +//! .set_auth_url(AuthUrl::new("http://authorize".to_string())?) +//! .set_token_url(TokenUrl::new("http://token".to_string())?) //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?); //! @@ -187,13 +184,10 @@ //! # async fn err_wrapper() -> Result<(), anyhow::Error> { //! // Create an OAuth2 client by specifying the client ID, client secret, authorization URL and //! // token URL. -//! let client = -//! BasicClient::new( -//! ClientId::new("client_id".to_string()), -//! Some(ClientSecret::new("client_secret".to_string())), -//! AuthUrl::new("http://authorize".to_string())?, -//! Some(TokenUrl::new("http://token".to_string())?) -//! ) +//! let client = BasicClient::new(ClientId::new("client_id".to_string())) +//! .set_client_secret(ClientSecret::new("client_secret".to_string())) +//! .set_auth_url(AuthUrl::new("http://authorize".to_string())?) +//! .set_token_url(TokenUrl::new("http://token".to_string())?) //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?); //! @@ -253,13 +247,9 @@ //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { -//! let client = -//! BasicClient::new( -//! ClientId::new("client_id".to_string()), -//! Some(ClientSecret::new("client_secret".to_string())), -//! AuthUrl::new("http://authorize".to_string())?, -//! None -//! ); +//! let client = BasicClient::new(ClientId::new("client_id".to_string())) +//! .set_client_secret(ClientSecret::new("client_secret".to_string())) +//! .set_auth_url(AuthUrl::new("http://authorize".to_string())?); //! //! // Generate the full authorization URL. //! let (auth_url, csrf_token) = client @@ -303,13 +293,10 @@ //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { -//! let client = -//! BasicClient::new( -//! ClientId::new("client_id".to_string()), -//! Some(ClientSecret::new("client_secret".to_string())), -//! AuthUrl::new("http://authorize".to_string())?, -//! Some(TokenUrl::new("http://token".to_string())?) -//! ); +//! let client = BasicClient::new(ClientId::new("client_id".to_string())) +//! .set_client_secret(ClientSecret::new("client_secret".to_string())) +//! .set_auth_url(AuthUrl::new("http://authorize".to_string())?) +//! .set_token_url(TokenUrl::new("http://token".to_string())?); //! //! let token_result = //! client @@ -345,13 +332,10 @@ //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { -//! let client = -//! BasicClient::new( -//! ClientId::new("client_id".to_string()), -//! Some(ClientSecret::new("client_secret".to_string())), -//! AuthUrl::new("http://authorize".to_string())?, -//! Some(TokenUrl::new("http://token".to_string())?), -//! ); +//! let client = BasicClient::new(ClientId::new("client_id".to_string())) +//! .set_client_secret(ClientSecret::new("client_secret".to_string())) +//! .set_auth_url(AuthUrl::new("http://authorize".to_string())?) +//! .set_token_url(TokenUrl::new("http://token".to_string())?); //! //! let token_result = client //! .exchange_client_credentials() @@ -389,17 +373,14 @@ //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let device_auth_url = DeviceAuthorizationUrl::new("http://deviceauth".to_string())?; -//! let client = -//! BasicClient::new( -//! ClientId::new("client_id".to_string()), -//! Some(ClientSecret::new("client_secret".to_string())), -//! AuthUrl::new("http://authorize".to_string())?, -//! Some(TokenUrl::new("http://token".to_string())?), -//! ) +//! let client = BasicClient::new(ClientId::new("client_id".to_string())) +//! .set_client_secret(ClientSecret::new("client_secret".to_string())) +//! .set_auth_url(AuthUrl::new("http://authorize".to_string())?) +//! .set_token_url(TokenUrl::new("http://token".to_string())?) //! .set_device_authorization_url(device_auth_url); //! //! let details: StandardDeviceAuthorizationResponse = client -//! .exchange_device_code()? +//! .exchange_device_code() //! .add_scope(Scope::new("read".to_string())) //! .request(http_client)?; //! @@ -534,11 +515,6 @@ const CONTENT_TYPE_FORMENCODED: &str = "application/x-www-form-urlencoded"; #[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum ConfigurationError { - /// - /// The endpoint URL tp be contacted is missing. - /// - #[error("No {0} endpoint URL specified")] - MissingUrl(&'static str), /// /// The endpoint URL to be contacted MUST be HTTPS. /// @@ -565,6 +541,16 @@ pub enum AuthType { /// /// Stores the configuration for an OAuth2 client. /// +/// This type implements the +/// [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_url`] 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 +/// are set automatically by the corresponding setter functions, and in most cases user code should +/// not need to deal with them directly. +/// /// # Error Types /// /// To enable compile time verification that only the correct and complete set of errors for the `Client` function being @@ -581,13 +567,11 @@ pub enum AuthType { /// # use http::status::StatusCode; /// # use http::header::{HeaderValue, CONTENT_TYPE}; /// # use oauth2::{*, basic::*}; -/// # let client = BasicClient::new( -/// # ClientId::new("aaa".to_string()), -/// # Some(ClientSecret::new("bbb".to_string())), -/// # AuthUrl::new("https://example.com/auth".to_string()).unwrap(), -/// # Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), -/// # ) -/// # .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); +/// # let client = BasicClient::new(ClientId::new("aaa".to_string())) +/// # .set_client_secret(ClientSecret::new("bbb".to_string())) +/// # .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) +/// # .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()) +/// # .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); /// # /// # #[derive(Debug, Error)] /// # enum FakeError { @@ -621,9 +605,23 @@ pub enum AuthType { /// RevocationErrorResponseType::UnsupportedTokenType))); /// ``` /// +/// # Examples +/// +/// See the [crate] root documentation for usage examples. #[derive(Clone, Debug)] -pub struct Client -where +pub struct Client< + TE, + TR, + TT, + 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, +> where TE: ErrorResponse, TR: TokenResponse, TT: TokenType, @@ -633,7 +631,7 @@ where { client_id: ClientId, client_secret: Option, - auth_url: AuthUrl, + auth_url: Option, auth_type: AuthType, token_url: Option, redirect_url: Option, @@ -642,8 +640,7 @@ where device_authorization_url: Option, phantom: PhantomData<(TE, TR, TT, TIR, RT, TRE)>, } - -impl Client +impl Client where TE: ErrorResponse + 'static, TR: TokenResponse, @@ -653,37 +650,15 @@ where TRE: ErrorResponse + 'static, { /// - /// Initializes an OAuth2 client with the fields common to most OAuth2 flows. - /// - /// # Arguments + /// Initializes an OAuth2 client with the specified client ID. /// - /// * `client_id` - Client ID - /// * `client_secret` - Optional client secret. A client secret is generally used for private - /// (server-side) OAuth2 clients and omitted from public (client-side or native app) OAuth2 - /// clients (see [RFC 8252](https://tools.ietf.org/html/rfc8252)). - /// * `auth_url` - Authorization endpoint: used by the client 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). - /// * `token_url` - Token endpoint: used by the client to exchange an authorization grant - /// (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). If this value is set - /// to `None`, the `exchange_*` methods will return `Err(RequestTokenError::Other(_))`. - /// - pub fn new( - client_id: ClientId, - client_secret: Option, - auth_url: AuthUrl, - token_url: Option, - ) -> Self { - Client { + pub fn new(client_id: ClientId) -> Self { + Self { client_id, - client_secret, - auth_url, + client_secret: None, + auth_url: None, auth_type: AuthType::BasicAuth, - token_url, + token_url: None, redirect_url: None, introspection_url: None, revocation_url: None, @@ -691,7 +666,41 @@ where phantom: PhantomData, } } - +} +impl< + TE, + TR, + TT, + 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, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, +{ /// /// Configures the type of client authentication used for communicating with the authorization /// server. @@ -709,20 +718,127 @@ where } /// - /// Sets the redirect URL used by the authorization endpoint. + /// Sets the authorization endpoint. /// - pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self { - self.redirect_url = Some(redirect_url); + /// 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_url( + self, + auth_url: AuthUrl, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + true, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, + > { + Client { + client_id: self.client_id, + client_secret: self.client_secret, + auth_url: Some(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: self.phantom, + } + } + + /// Sets 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 + /// [RFC 8252](https://tools.ietf.org/html/rfc8252)). + pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self { + self.client_secret = Some(client_secret); self } + /// + /// Sets the 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). + /// + pub fn set_device_authorization_url( + self, + device_authorization_url: DeviceAuthorizationUrl, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_AUTH_URL, + true, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, + > { + 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: Some(device_authorization_url), + phantom: self.phantom, + } + } + /// /// Sets the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) /// introspection endpoint. /// - pub fn set_introspection_uri(mut self, introspection_url: IntrospectionUrl) -> Self { - self.introspection_url = Some(introspection_url); + pub fn set_introspection_uri( + self, + introspection_url: IntrospectionUrl, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_TOKEN_URL, + HAS_DEVICE_AUTH_URL, + true, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, + > { + 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: Some(introspection_url), + revocation_url: self.revocation_url, + device_authorization_url: self.device_authorization_url, + phantom: self.phantom, + } + } + + /// + /// Sets 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 } @@ -732,23 +848,135 @@ where /// /// See: [`revoke_token()`](Self::revoke_token()) /// - pub fn set_revocation_uri(mut self, revocation_url: RevocationUrl) -> Self { - self.revocation_url = Some(revocation_url); + pub fn set_revocation_uri( + self, + revocation_url: RevocationUrl, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_TOKEN_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + true, + HAS_TOKEN_URL, + > { + 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: Some(revocation_url), + device_authorization_url: self.device_authorization_url, + phantom: self.phantom, + } + } - self + /// Sets 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_url( + self, + token_url: TokenUrl, + ) -> Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + true, + > { + Client { + client_id: self.client_id, + client_secret: self.client_secret, + auth_url: self.auth_url, + auth_type: self.auth_type, + token_url: Some(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: self.phantom, + } } /// - /// Sets the 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). + /// Returns the Client ID. /// - pub fn set_device_authorization_url( - mut self, - device_authorization_url: DeviceAuthorizationUrl, - ) -> Self { - self.device_authorization_url = Some(device_authorization_url); + pub fn client_id(&self) -> &ClientId { + &self.client_id + } - self + /// + /// Returns 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. + /// + pub fn redirect_url(&self) -> Option<&RedirectUrl> { + self.redirect_url.as_ref() + } +} + +// Methods requiring an authorization endpoint. +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + const HAS_DEVICE_AUTH_URL: bool, + const HAS_INTROSPECTION_URL: bool, + const HAS_REVOCATION_URL: bool, + const HAS_TOKEN_URL: bool, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + true, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, +{ + /// + /// Returns the authorization endpoint. + /// + pub fn auth_url(&self) -> &AuthUrl { + // This is enforced statically via the HAS_AUTH_URL const generic. + self.auth_url.as_ref().expect("should have auth_url") } /// @@ -774,7 +1002,8 @@ where S: FnOnce() -> CsrfToken, { AuthorizationRequest { - auth_url: &self.auth_url, + // This is enforced statically via the HAS_AUTH_URL const generic. + auth_url: self.auth_url(), client_id: &self.client_id, extra_params: Vec::new(), pkce_challenge: None, @@ -784,6 +1013,59 @@ where state: state_fn(), } } +} + +// Methods requiring a token endpoint. +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + const HAS_AUTH_URL: bool, + const HAS_DEVICE_AUTH_URL: bool, + const HAS_INTROSPECTION_URL: bool, + const HAS_REVOCATION_URL: bool, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + true, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, +{ + /// + /// Requests an access token for the *client credentials* grant type. + /// + /// See . + /// + 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, + } + } /// /// Exchanges a code produced by a successful authorization process with an access token. @@ -801,51 +1083,62 @@ where code, extra_params: Vec::new(), pkce_verifier: None, - token_url: self.token_url.as_ref(), + // 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, } } /// - /// Requests an access token for the *password* grant type. - /// - /// See . + /// Perform a device access token request as per + /// . /// - pub fn exchange_password<'a, 'b>( + pub fn exchange_device_access_token<'a, 'b, 'c, EF>( &'a self, - username: &'b ResourceOwnerUsername, - password: &'b ResourceOwnerPassword, - ) -> PasswordTokenRequest<'b, TE, TR, TT> + auth_response: &'b DeviceAuthorizationResponse, + ) -> DeviceAccessTokenRequest<'b, 'c, TR, TT, EF> where 'a: 'b, + EF: ExtraDeviceAuthorizationFields, { - PasswordTokenRequest::<'b> { + DeviceAccessTokenRequest { 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: self.token_url.as_ref(), + // 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, } } /// - /// Requests an access token for the *client credentials* grant type. + /// Requests an access token for the *password* grant type. /// - /// See . + /// See . /// - pub fn exchange_client_credentials(&self) -> ClientCredentialsTokenRequest { - ClientCredentialsTokenRequest { + pub fn exchange_password<'a, 'b>( + &'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(), - token_url: self.token_url.as_ref(), + // 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, } } @@ -869,66 +1162,126 @@ where extra_params: Vec::new(), refresh_token, scopes: Vec::new(), - token_url: self.token_url.as_ref(), + // 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, } } + /// + /// Returns the token endpoint. + /// + pub fn token_url(&self) -> &TokenUrl { + // This is enforced statically via the HAS_TOKEN_URL const generic. + self.token_url.as_ref().expect("should have token_url") + } +} + +// Methods requiring a device authorization endpoint. +impl< + TE, + TR, + TT, + TIR, + RT, + TRE, + const HAS_AUTH_URL: bool, + const HAS_INTROSPECTION_URL: bool, + const HAS_REVOCATION_URL: bool, + const HAS_TOKEN_URL: bool, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_AUTH_URL, + true, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, +{ /// /// Perform a device authorization request as per /// . /// - pub fn exchange_device_code( - &self, - ) -> Result, ConfigurationError> { - Ok(DeviceAuthorizationRequest { + 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 .as_ref() - .ok_or(ConfigurationError::MissingUrl("device authorization_url"))?, + .expect("should have device_authorization_url"), _phantom: PhantomData, - }) + } } /// - /// Perform a device access token request as per - /// . + /// Returns the device authorization URL used by the device authorization endpoint. /// - pub fn exchange_device_access_token<'a, 'b, 'c, EF>( - &'a self, - auth_response: &'b DeviceAuthorizationResponse, - ) -> DeviceAccessTokenRequest<'b, 'c, 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(), - token_url: self.token_url.as_ref(), - dev_auth_resp: auth_response, - time_fn: Arc::new(Utc::now), - max_backoff_interval: None, - _phantom: PhantomData, - } + pub fn device_authorization_url(&self) -> &DeviceAuthorizationUrl { + // This is enforced statically via the HAS_DEVICE_AUTH_URL const generic. + self.device_authorization_url + .as_ref() + .expect("should have device_authorization_url") } +} +// Methods requiring an introspection endpoint. +impl< + 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, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + true, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, +{ /// /// 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. /// - /// Requires that [`set_introspection_uri()`](Self::set_introspection_uri()) have already been called to set the - /// introspection endpoint URL. - /// - /// Attempting to submit the generated request without calling [`set_introspection_uri()`](Self::set_introspection_uri()) - /// first will result in an error. + /// Requires [`set_introspection_uri()`](Self::set_introspection_uri) to have been previously + /// called to set the introspection endpoint URL. /// pub fn introspect<'a>( &'a self, @@ -939,10 +1292,11 @@ where 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() - .ok_or(ConfigurationError::MissingUrl("introspection"))?, + .expect("should have introspection_url"), token, token_type_hint: None, _phantom: PhantomData, @@ -950,14 +1304,58 @@ where } /// - /// 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. + /// Returns the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) + /// introspection endpoint. /// - /// Requires that [`set_revocation_uri()`](Self::set_revocation_uri()) have already been called to set the - /// revocation endpoint URL. + pub fn introspection_url(&self) -> &IntrospectionUrl { + // This is enforced statically via the HAS_INTROSPECTION_URL const generic. + self.introspection_url + .as_ref() + .expect("should have introspection_url") + } +} + +// Methods requiring a revocation endpoint. +impl< + 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, + > + Client< + TE, + TR, + TT, + TIR, + RT, + TRE, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + true, + HAS_TOKEN_URL, + > +where + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, +{ /// - /// Attempting to submit the generated request without calling [`set_revocation_uri()`](Self::set_revocation_uri()) - /// first will result in an error. + /// 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. + /// + /// Requires [`set_revocation_uri()`](Self::set_revocation_uri) to have been previously + /// called to set the revocation endpoint URL. /// pub fn revoke_token( &self, @@ -968,11 +1366,16 @@ where // 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." - let revocation_url = match self.revocation_url.as_ref() { - Some(url) if url.url().scheme() == "https" => Ok(url), - Some(_) => Err(ConfigurationError::InsecureUrl("revocation")), - None => Err(ConfigurationError::MissingUrl("revocation")), - }?; + + // 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, @@ -986,63 +1389,16 @@ where } /// - /// Returns the Client ID. - /// - pub fn client_id(&self) -> &ClientId { - &self.client_id - } - - /// - /// Returns the authorization endpoint. - /// - pub fn auth_url(&self) -> &AuthUrl { - &self.auth_url - } - - /// - /// Returns the type of client authentication used for communicating with the authorization - /// server. - /// - pub fn auth_type(&self) -> &AuthType { - &self.auth_type - } - - /// - /// Returns the token endpoint. - /// - pub fn token_url(&self) -> Option<&TokenUrl> { - self.token_url.as_ref() - } - - /// - /// Returns the redirect URL used by the authorization endpoint. - /// - pub fn redirect_url(&self) -> Option<&RedirectUrl> { - self.redirect_url.as_ref() - } - - /// - /// Returns the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) - /// introspection endpoint. - /// - pub fn introspection_url(&self) -> Option<&IntrospectionUrl> { - self.introspection_url.as_ref() - } - - /// - /// Returns the revocation URL for contacting the revocation endpoint ([RFC 7009](https://tools.ietf.org/html/rfc7009)). + /// Returns the revocation URL for contacting the revocation endpoint + /// ([RFC 7009](https://tools.ietf.org/html/rfc7009)). /// /// See: [`revoke_token()`](Self::revoke_token()) /// - pub fn revocation_url(&self) -> Option<&RevocationUrl> { - self.revocation_url.as_ref() - } - - /// - /// Returns the the device authorization URL used by the device authorization endpoint. - /// - pub fn device_authorization_url(&self) -> Option<&DeviceAuthorizationUrl> { - self.device_authorization_url.as_ref() + pub fn revocation_url(&self) -> &RevocationUrl { + // This is enforced statically via the HAS_REVOCATION_URL const generic. + self.revocation_url + .as_ref() + .expect("should have revocation_url") } } @@ -1235,7 +1591,7 @@ where code: AuthorizationCode, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pkce_verifier: Option, - token_url: Option<&'a TokenUrl>, + token_url: &'a TokenUrl, redirect_url: Option>, _phantom: PhantomData<(TE, TR, TT)>, } @@ -1273,8 +1629,8 @@ where /// Completes the [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636) /// (PKCE) protocol flow. /// - /// This method must be called if `set_pkce_challenge` was used during the authorization - /// request. + /// This method must be called if [`AuthorizationRequest::set_pkce_challenge`] was used during + /// the authorization request. /// pub fn set_pkce_verifier(mut self, pkce_verifier: PkceCodeVerifier) -> Self { self.pkce_verifier = Some(pkce_verifier); @@ -1289,10 +1645,7 @@ where self } - fn prepare_request(self) -> Result> - where - RE: Error + 'static, - { + fn prepare_request(self) -> HttpRequest { let mut params = vec![ ("grant_type", "authorization_code"), ("code", self.code.secret()), @@ -1301,18 +1654,16 @@ where params.push(("code_verifier", pkce_verifier.secret())); } - Ok(endpoint_request( + endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, self.redirect_url, None, - self.token_url - .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? - .url(), + self.token_url.url(), params, - )) + ) } /// @@ -1323,7 +1674,7 @@ where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, { - endpoint_response(http_client(self.prepare_request()?)?) + endpoint_response(http_client(self.prepare_request())?) } /// @@ -1338,8 +1689,7 @@ where F: Future>, RE: Error + 'static, { - let http_request = self.prepare_request()?; - let http_response = http_client(http_request).await?; + let http_response = http_client(self.prepare_request()).await?; endpoint_response(http_response) } } @@ -1362,7 +1712,7 @@ where extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, refresh_token: &'a RefreshToken, scopes: Vec>, - token_url: Option<&'a TokenUrl>, + token_url: &'a TokenUrl, _phantom: PhantomData<(TE, TR, TT)>, } impl<'a, TE, TR, TT> RefreshTokenRequest<'a, TE, TR, TT> @@ -1452,9 +1802,7 @@ where &self.extra_params, None, Some(&self.scopes), - self.token_url - .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? - .url(), + self.token_url.url(), vec![ ("grant_type", "refresh_token"), ("refresh_token", self.refresh_token.secret()), @@ -1482,7 +1830,7 @@ where username: &'a ResourceOwnerUsername, password: &'a ResourceOwnerPassword, scopes: Vec>, - token_url: Option<&'a TokenUrl>, + token_url: &'a TokenUrl, _phantom: PhantomData<(TE, TR, TT)>, } impl<'a, TE, TR, TT> PasswordTokenRequest<'a, TE, TR, TT> @@ -1573,9 +1921,7 @@ where &self.extra_params, None, Some(&self.scopes), - self.token_url - .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? - .url(), + self.token_url.url(), vec![ ("grant_type", "password"), ("username", self.username), @@ -1602,7 +1948,7 @@ where client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, scopes: Vec>, - token_url: Option<&'a TokenUrl>, + token_url: &'a TokenUrl, _phantom: PhantomData<(TE, TR, TT)>, } impl<'a, TE, TR, TT> ClientCredentialsTokenRequest<'a, TE, TR, TT> @@ -1693,9 +2039,7 @@ where &self.extra_params, None, Some(&self.scopes), - self.token_url - .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? - .url(), + self.token_url.url(), vec![("grant_type", "client_credentials")], )) } @@ -2240,7 +2584,7 @@ where client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, - token_url: Option<&'a TokenUrl>, + token_url: &'a TokenUrl, dev_auth_resp: &'a DeviceAuthorizationResponse, time_fn: Arc DateTime + 'b + Send + Sync>, max_backoff_interval: Option, @@ -2331,7 +2675,7 @@ where )); } - match self.process_response(http_client(self.prepare_request()?), interval) { + match self.process_response(http_client(self.prepare_request()), interval) { DeviceAccessTokenPollResult::ContinueWithNewPollInterval(new_interval) => { interval = new_interval } @@ -2376,7 +2720,7 @@ where )); } - match self.process_response(http_client(self.prepare_request()?).await, interval) { + match self.process_response(http_client(self.prepare_request()).await, interval) { DeviceAccessTokenPollResult::ContinueWithNewPollInterval(new_interval) => { interval = new_interval } @@ -2388,27 +2732,20 @@ where } } - fn prepare_request( - &self, - ) -> Result> - where - RE: Error + 'static, - { - Ok(endpoint_request( + fn prepare_request(&self) -> HttpRequest { + endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, None, - self.token_url - .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? - .url(), + self.token_url.url(), vec![ ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), ("device_code", self.dev_auth_resp.device_code().secret()), ], - )) + ) } fn process_response( diff --git a/src/tests.rs b/src/tests.rs index 274abac..43f389b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,3 +1,4 @@ +use chrono::TimeZone; use http::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::status::StatusCode; use revocation::RevocationErrorResponseType; @@ -10,15 +11,12 @@ use crate::revocation::StandardRevocableToken; use super::basic::*; use super::devicecode::*; use super::*; -use chrono::TimeZone; -fn new_client() -> BasicClient { - BasicClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ) +fn new_client() -> BasicClient { + BasicClient::new(ClientId::new("aaa".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()) + .set_client_secret(ClientSecret::new("bbb".to_string())) } fn mock_http_client( @@ -155,12 +153,10 @@ fn test_authorize_url_implicit() { #[test] fn test_authorize_url_with_param() { - let client = BasicClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth?foo=bar".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ); + let client = BasicClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth?foo=bar".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) @@ -309,12 +305,11 @@ where #[test] fn test_exchange_code_successful_with_minimal_json_response() { - let client = BasicClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ); + let client = BasicClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()); + let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( @@ -411,13 +406,12 @@ fn test_exchange_code_successful_with_complete_json_response() { #[test] fn test_exchange_client_credentials_with_basic_auth() { - let client = BasicClient::new( - ClientId::new("aaa/;&".to_string()), - Some(ClientSecret::new("bbb/;&".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ) - .set_auth_type(AuthType::BasicAuth); + let client = BasicClient::new(ClientId::new("aaa/;&".to_string())) + .set_client_secret(ClientSecret::new("bbb/;&".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()) + .set_auth_type(AuthType::BasicAuth); + let token = client .exchange_client_credentials() .request(mock_http_client( @@ -457,13 +451,11 @@ fn test_exchange_client_credentials_with_basic_auth() { #[test] fn test_exchange_client_credentials_with_basic_auth_but_no_client_secret() { - let client = BasicClient::new( - ClientId::new("aaa/;&".to_string()), - None, - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ) - .set_auth_type(AuthType::BasicAuth); + let client = BasicClient::new(ClientId::new("aaa/;&".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()) + .set_auth_type(AuthType::BasicAuth); + let token = client .exchange_client_credentials() .request(mock_http_client( @@ -1095,12 +1087,9 @@ fn test_exchange_code_with_unexpected_content_type() { #[test] fn test_exchange_code_with_invalid_token_type() { - let client = BasicClient::new( - ClientId::new("aaa".to_string()), - None, - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ); + let client = BasicClient::new(ClientId::new("aaa".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) @@ -1191,12 +1180,11 @@ fn test_exchange_code_with_400_status_code() { #[test] fn test_exchange_code_fails_gracefully_on_transport_error() { - let client = BasicClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://auth".to_string()).unwrap(), - Some(TokenUrl::new("https://token".to_string()).unwrap()), - ); + let client = BasicClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://token".to_string()).unwrap()); + let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(|_| Err(FakeError::Err)); @@ -1216,13 +1204,24 @@ mod colorful_extension { use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; - pub type ColorfulClient = Client< + 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, + > = Client< StandardErrorResponse, StandardTokenResponse, ColorfulTokenType, StandardTokenIntrospectionResponse, ColorfulRevocableToken, StandardErrorResponse, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, >; #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -1308,12 +1307,11 @@ mod colorful_extension { #[test] fn test_extension_successful_with_minimal_json_response() { use self::colorful_extension::*; - let client = ColorfulClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ); + let client = ColorfulClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()); + let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( @@ -1361,13 +1359,12 @@ fn test_extension_successful_with_minimal_json_response() { #[test] fn test_extension_successful_with_complete_json_response() { use self::colorful_extension::*; - let client = ColorfulClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ) - .set_auth_type(AuthType::RequestBody); + let client = ColorfulClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()) + .set_auth_type(AuthType::RequestBody); + let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( @@ -1431,12 +1428,11 @@ fn test_extension_successful_with_complete_json_response() { #[test] fn test_extension_with_simple_json_error() { use self::colorful_extension::*; - let client = ColorfulClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ); + let client = ColorfulClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()); + let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( @@ -1530,25 +1526,34 @@ mod custom_errors { impl ErrorResponse for CustomErrorResponse {} - pub type CustomErrorClient = Client< + 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, + > = Client< CustomErrorResponse, StandardTokenResponse, ColorfulTokenType, StandardTokenIntrospectionResponse, ColorfulRevocableToken, CustomErrorResponse, + HAS_AUTH_URL, + HAS_DEVICE_AUTH_URL, + HAS_INTROSPECTION_URL, + HAS_REVOCATION_URL, + HAS_TOKEN_URL, >; } #[test] fn test_extension_with_custom_json_error() { use self::custom_errors::*; - let client = CustomErrorClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ); + let client = CustomErrorClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) @@ -1809,20 +1814,6 @@ fn test_token_introspection_successful_with_basic_auth_full_response() { ); } -#[test] -fn test_token_revocation_with_missing_url() { - let client = new_client(); - - let result = client - .revoke_token(AccessToken::new("access_token_123".to_string()).into()) - .unwrap_err(); - - assert_eq!( - format!("{}", result), - "No revocation endpoint URL specified" - ); -} - #[test] fn test_token_revocation_with_non_https_url() { let client = new_client(); @@ -1997,13 +1988,11 @@ fn test_token_revocation_with_refresh_token() { #[test] fn test_extension_token_revocation_successful() { use self::colorful_extension::*; - let client = ColorfulClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - AuthUrl::new("https://example.com/auth".to_string()).unwrap(), - Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), - ) - .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); + let client = ColorfulClient::new(ClientId::new("aaa".to_string())) + .set_client_secret(ClientSecret::new("bbb".to_string())) + .set_auth_url(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) + .set_token_url(TokenUrl::new("https://example.com/token".to_string()).unwrap()) + .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); client .revoke_token(ColorfulRevocableToken::Red( @@ -2057,7 +2046,6 @@ fn new_device_auth_details(expires_in: u32) -> StandardDeviceAuthorizationRespon let client = new_client().set_device_authorization_url(device_auth_url.clone()); client .exchange_device_code() - .unwrap() .add_extra_param("foo", "bar") .add_scope(Scope::new("openid".to_string())) .request(mock_http_client( @@ -2553,6 +2541,11 @@ fn test_send_sync_impl() { StandardTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, + false, + false, + false, + false, + false, >, >(); is_sync_and_send::<