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::<