From df2f52a56582162c1ca675879fbcb3dfbd9da0c2 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Sun, 1 Apr 2018 22:12:12 -0700 Subject: [PATCH 01/11] Add stronger typing using the NewType pattern --- Cargo.toml | 1 - examples/github.rs | 137 ++++++++++------ examples/google.rs | 119 ++++++++------ src/lib.rs | 380 +++++++++++++++++++++++++++++++++++---------- tests/lib.rs | 343 ++++++++++++++++++---------------------- 5 files changed, 605 insertions(+), 375 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 94789c5c..ef36194a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ repository = "https://github.com/alexcrichton/oauth2-rs" curl = "0.4.0" failure = "0.1" failure_derive = "0.1" -log = "0.3" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" diff --git a/examples/github.rs b/examples/github.rs index cc3fea28..9494de8c 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -18,6 +18,7 @@ extern crate oauth2; extern crate rand; extern crate url; +use oauth2::*; use oauth2::basic::BasicClient; use rand::{thread_rng, Rng}; use std::env; @@ -26,85 +27,119 @@ use std::io::{BufRead, BufReader, Write}; use url::Url; fn main() { - let github_client_id = env::var("GITHUB_CLIENT_ID").expect("Missing the GITHUB_CLIENT_ID environment variable."); - let github_client_secret = env::var("GITHUB_CLIENT_SECRET").expect("Missing the GITHUB_CLIENT_SECRET environment variable."); - let auth_url = "https://github.com/login/oauth/authorize"; - let token_url = "https://github.com/login/oauth/access_token"; + let github_client_id = + ClientId::new( + env::var("GITHUB_CLIENT_ID") + .expect("Missing the GITHUB_CLIENT_ID environment variable.") + ); + let github_client_secret = + ClientSecret::new( + env::var("GITHUB_CLIENT_SECRET") + .expect("Missing the GITHUB_CLIENT_SECRET environment variable.") + ); + let auth_url = + AuthUrl::new( + Url::parse("https://github.com/login/oauth/authorize") + .expect("Invalid authorization endpoint URL") + ); + let token_url = + TokenUrl::new( + Url::parse("https://github.com/login/oauth/access_token") + .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, token_url) - .expect("failed to create client") - // This example is requesting access to the user's public repos and email. - .add_scope("public_repo") - .add_scope("user:email") + .add_scope(Scope::new("public_repo".to_string())) + .add_scope(Scope::new("user:email".to_string())) // This example will be running its own server at localhost:8080. // See below for the server implementation. - .set_redirect_url("http://localhost:8080"); + .set_redirect_url( + RedirectUrl::new( + Url::parse("http://localhost:8080") + .expect("Invalid redirect URL") + ) + ); let mut rng = thread_rng(); // Generate a 128-bit random string for CSRF protection (each time!). let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); - let csrf_state = base64::encode(&random_bytes); + let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); // Generate the authorization URL to which we'll redirect the user. - let authorize_url = client.authorize_url(csrf_state.clone()); + let authorize_url = client.authorize_url(&csrf_state); println!("Open this URL in your browser:\n{}\n", authorize_url.to_string()); - // These variables will store the code & state retrieved during the authorization process. - let mut code = String::new(); - let mut state = String::new(); - // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); for stream in listener.incoming() { - match stream { - Ok(mut stream) => { - { - let mut reader = BufReader::new(&stream); + if let Ok(mut stream) = stream { + let code; + let state; + { + let mut reader = BufReader::new(&stream); - let mut request_line = String::new(); - reader.read_line(&mut request_line).unwrap(); + let mut request_line = String::new(); + reader.read_line(&mut request_line).unwrap(); - let redirect_url = request_line.split_whitespace().nth(1).unwrap(); - let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); + let redirect_url = request_line.split_whitespace().nth(1).unwrap(); + let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); - let code_pair = url.query_pairs().find(|pair| { - let &(ref key, _) = pair; - key == "code" - }).unwrap(); + let code_pair = url.query_pairs().find(|pair| { + let &(ref key, _) = pair; + key == "code" + }).unwrap(); - let (_, value) = code_pair; - code = value.into_owned(); + let (_, value) = code_pair; + code = AuthorizationCode::new(value.into_owned()); - let state_pair = url.query_pairs().find(|pair| { - let &(ref key, _) = pair; - key == "state" - }).unwrap(); + let state_pair = url.query_pairs().find(|pair| { + let &(ref key, _) = pair; + key == "state" + }).unwrap(); - let (_, value) = state_pair; - state = value.into_owned(); - } - - let message = "Go back to your terminal :)"; - let response = format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message); - stream.write_all(response.as_bytes()).unwrap(); + let (_, value) = state_pair; + state = CsrfToken::new(value.into_owned()); + } - // The server will terminate itself after collecting the first code. - break; + let message = "Go back to your terminal :)"; + let response = + format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message); + stream.write_all(response.as_bytes()).unwrap(); + + println!("Github returned the following code:\n{}\n", code.secret()); + println!( + "Github returned the following state:\n{} (expected `{}`)\n", + state.secret(), + csrf_state.secret() + ); + + // Exchange the code with a token. + let token_res = client.exchange_code(code); + + println!("Github returned the following token:\n{:?}\n", token_res); + + if let Ok(token) = token_res { + // NB: Github returns a single comma-separated "scope" parameter instead of multiple + // space-separated scopes. Github-specific clients can parse this scope into + // multiple scopes by splitting at the commas. Note that it's not safe for the + // library to do this by default because RFC 6749 allows scopes to contain commas. + let scopes_raw = token.scopes().clone().unwrap_or_else(|| Vec::new()); + let scopes = + scopes_raw + .iter() + .map(|comma_separated| comma_separated.split(",")) + .flat_map(|inner_scopes| inner_scopes) + .collect::>(); + println!("Github returned the following scopes:\n{:?}\n", scopes); } - Err(_) => {}, + + // The server will terminate itself after collecting the first code. + break; } }; - - println!("Github returned the following code:\n{}\n", code); - println!("Github returned the following state:\n{} (expected `{}`)\n", state, csrf_state); - - // Exchange the code with a token. - let token = client.exchange_code(code); - - println!("Github returned the following token:\n{:?}\n", token); } diff --git a/examples/google.rs b/examples/google.rs index c9294b61..9e31214e 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -18,6 +18,7 @@ extern crate oauth2; extern crate rand; extern crate url; +use oauth2::*; use oauth2::basic::BasicClient; use rand::{thread_rng, Rng}; use std::env; @@ -26,84 +27,104 @@ use std::io::{BufRead, BufReader, Write}; use url::Url; fn main() { - let google_client_id = env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable."); - let google_client_secret = env::var("GOOGLE_CLIENT_SECRET").expect("Missing the GOOGLE_CLIENT_SECRET environment variable."); - let auth_url = "https://accounts.google.com/o/oauth2/v2/auth"; - let token_url = "https://www.googleapis.com/oauth2/v3/token"; + let google_client_id = + ClientId::new( + env::var("GOOGLE_CLIENT_ID") + .expect("Missing the GOOGLE_CLIENT_ID environment variable.") + ); + let google_client_secret = + ClientSecret::new( + env::var("GOOGLE_CLIENT_SECRET") + .expect("Missing the GOOGLE_CLIENT_SECRET environment variable.") + ); + let auth_url = + AuthUrl::new( + Url::parse("https://accounts.google.com/o/oauth2/v2/auth") + .expect("Invalid authorization endpoint URL") + ); + let token_url = + TokenUrl::new( + Url::parse("https://www.googleapis.com/oauth2/v3/token") + .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, token_url) - .expect("failed to create client") // This example is requesting access to the "calendar" features and the user's profile. - .add_scope("https://www.googleapis.com/auth/calendar") - .add_scope("https://www.googleapis.com/auth/plus.me") + .add_scope(Scope::new("https://www.googleapis.com/auth/calendar".to_string())) + .add_scope(Scope::new("https://www.googleapis.com/auth/plus.me".to_string())) // This example will be running its own server at localhost:8080. // See below for the server implementation. - .set_redirect_url("http://localhost:8080"); + .set_redirect_url( + RedirectUrl::new( + Url::parse("http://localhost:8080") + .expect("Invalid redirect URL") + ) + ); let mut rng = thread_rng(); // Generate a 128-bit random string for CSRF protection (each time!). let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); - let csrf_state = base64::encode(&random_bytes); + let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); // Generate the authorization URL to which we'll redirect the user. - let authorize_url = client.authorize_url(csrf_state.clone()); + let authorize_url = client.authorize_url(&csrf_state); println!("Open this URL in your browser:\n{}\n", authorize_url.to_string()); - // These variables will store the code & state retrieved during the authorization process. - let mut code = String::new(); - let mut state = String::new(); - // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); for stream in listener.incoming() { - match stream { - Ok(mut stream) => { - { - let mut reader = BufReader::new(&stream); + if let Ok(mut stream) = stream { + let code; + let state; + { + let mut reader = BufReader::new(&stream); - let mut request_line = String::new(); - reader.read_line(&mut request_line).unwrap(); + let mut request_line = String::new(); + reader.read_line(&mut request_line).unwrap(); - let redirect_url = request_line.split_whitespace().nth(1).unwrap(); - let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); + let redirect_url = request_line.split_whitespace().nth(1).unwrap(); + let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); - let code_pair = url.query_pairs().find(|pair| { - let &(ref key, _) = pair; - key == "code" - }).unwrap(); + let code_pair = url.query_pairs().find(|pair| { + let &(ref key, _) = pair; + key == "code" + }).unwrap(); - let (_, value) = code_pair; - code = value.into_owned(); + let (_, value) = code_pair; + code = AuthorizationCode::new(value.into_owned()); - let state_pair = url.query_pairs().find(|pair| { - let &(ref key, _) = pair; - key == "state" - }).unwrap(); + let state_pair = url.query_pairs().find(|pair| { + let &(ref key, _) = pair; + key == "state" + }).unwrap(); - let (_, value) = state_pair; - state = value.into_owned(); - } + let (_, value) = state_pair; + state = CsrfToken::new(value.into_owned()); + } - let message = "Go back to your terminal :)"; - let response = format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message); - stream.write_all(response.as_bytes()).unwrap(); + let message = "Go back to your terminal :)"; + let response = + format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message); + stream.write_all(response.as_bytes()).unwrap(); - // The server will terminate itself after collecting the first code. - break; - } - Err(_) => {}, - } - }; + println!("Google returned the following code:\n{}\n", code.secret()); + println!( + "Google returned the following state:\n{} (expected `{}`)\n", + state.secret(), + csrf_state.secret() + ); - println!("Google returned the following code:\n{}\n", code); - println!("Google returned the following state:\n{} (expected `{}`)\n", state, csrf_state); + // Exchange the code with a token. + let token = client.exchange_code(code); - // Exchange the code with a token. - let token = client.exchange_code(code); + println!("Google returned the following token:\n{:?}\n", token); - println!("Google returned the following token:\n{:?}\n", token); + // The server will terminate itself after collecting the first code. + break; + } + }; } diff --git a/src/lib.rs b/src/lib.rs index 13272c3d..85862e78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,26 +13,34 @@ //! extern crate base64; //! extern crate oauth2; //! extern crate rand; +//! extern crate url; //! -//! use oauth2::basic::BasicClient; +//! use oauth2::*; +//! use oauth2::basic::*; //! use rand::{thread_rng, Rng}; +//! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { //! // Create an OAuth2 client by specifying the client ID, client secret, authorization URL and //! // token URL. //! let client = -//! BasicClient::new("client_id", Some("client_secret"), "http://authorize", "http://token")? +//! BasicClient::new( +//! ClientId::new("client_id".to_string()), +//! Some(ClientSecret::new("client_secret".to_string())), +//! AuthUrl::new(Url::parse("http://authorize")?), +//! TokenUrl::new(Url::parse("http://token")?) +//! ) //! // Set the desired scopes. -//! .add_scope("read") -//! .add_scope("write") +//! .add_scope(Scope::new("read".to_string())) +//! .add_scope(Scope::new("write".to_string())) //! //! // Set the URL the user will be redirected to after the authorization process. -//! .set_redirect_url("http://redirect"); +//! .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect")?)); //! //! let mut rng = thread_rng(); //! // Generate a 128-bit random string for CSRF protection (each time!). //! let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); -//! let csrf_state = base64::encode(&random_bytes); +//! let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); //! //! // Generate the full authorization URL. //! // This is the URL you should redirect the user to, in order to trigger the authorization @@ -44,7 +52,8 @@ //! // parameter returned by the server matches `csrf_state`. //! //! // Now you can trade it for an access token. -//! let token_result = client.exchange_code("some authorization code".to_string()); +//! let token_result = +//! client.exchange_code(AuthorizationCode::new("some authorization code".to_string())); //! //! // Unwrapping token_result will either produce a Token or a RequestTokenError. //! # Ok(()) @@ -64,18 +73,26 @@ //! extern crate base64; //! extern crate oauth2; //! extern crate rand; +//! extern crate url; //! -//! use oauth2::basic::BasicClient; +//! use oauth2::*; +//! use oauth2::basic::*; //! use rand::{thread_rng, Rng}; +//! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { //! let client = -//! BasicClient::new("client_id", Some("client_secret"), "http://authorize", "http://token")?; +//! BasicClient::new( +//! ClientId::new("client_id".to_string()), +//! Some(ClientSecret::new("client_secret".to_string())), +//! AuthUrl::new(Url::parse("http://authorize")?), +//! TokenUrl::new(Url::parse("http://token")?) +//! ); //! //! let mut rng = thread_rng(); //! // Generate a 128-bit random string for CSRF protection (each time!). //! let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); -//! let csrf_state = base64::encode(&random_bytes); +//! let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); //! //! // Generate the full authorization URL. //! // This is the URL you should redirect the user to, in order to trigger the authorization @@ -94,17 +111,35 @@ //! ## Example //! //! ``` -//! use oauth2::basic::BasicClient; +//! extern crate base64; +//! extern crate oauth2; +//! extern crate rand; +//! extern crate url; +//! +//! use oauth2::*; +//! use oauth2::basic::*; +//! use rand::{thread_rng, Rng}; +//! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { //! let client = -//! BasicClient::new("client_id", Some("client_secret"), "http://authorize", "http://token")? -//! .add_scope("read") -//! .set_redirect_url("http://redirect"); +//! BasicClient::new( +//! ClientId::new("client_id".to_string()), +//! Some(ClientSecret::new("client_secret".to_string())), +//! AuthUrl::new(Url::parse("http://authorize")?), +//! TokenUrl::new(Url::parse("http://token")?) +//! ) +//! .add_scope(Scope::new("read".to_string())) +//! .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect")?)); //! -//! let token_result = client.exchange_password("user", "pass"); +//! let token_result = +//! client.exchange_password( +//! &EndUserUsername::new("user".to_string()), +//! &EndUserPassword::new("pass".to_string()) +//! ); //! # Ok(()) //! # } +//! # fn main() {} //! ``` //! //! # Client Credentials Grant @@ -115,17 +150,28 @@ //! ## Example: //! //! ``` -//! use oauth2::basic::BasicClient; +//! extern crate oauth2; +//! extern crate url; +//! +//! use oauth2::*; +//! use oauth2::basic::*; +//! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { //! let client = -//! BasicClient::new("client_id", Some("client_secret"), "http://authorize", "http://token")? -//! .add_scope("read") -//! .set_redirect_url("http://redirect"); +//! BasicClient::new( +//! ClientId::new("client_id".to_string()), +//! Some(ClientSecret::new("client_secret".to_string())), +//! AuthUrl::new(Url::parse("http://authorize")?), +//! TokenUrl::new(Url::parse("http://token")?) +//! ) +//! .add_scope(Scope::new("read".to_string())) +//! .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect")?)); //! //! let token_result = client.exchange_client_credentials(); //! # Ok(()) //! # } +//! # fn main() {} //! ``` //! //! # Other examples @@ -148,10 +194,10 @@ use curl::easy::Easy; use serde::Serialize; use serde::de::DeserializeOwned; use std::io::Read; -use std::convert::{Into, AsRef}; use std::fmt::{Debug, Display, Formatter}; use std::fmt::Error as FormatterError; use std::marker::PhantomData; +use std::ops::Deref; use std::time::Duration; use url::Url; @@ -172,18 +218,188 @@ pub enum AuthType { BasicAuth, } +pub trait NewType : Clone + Debug + Deref + PartialEq { + fn new(val: T) -> Self; +} + +pub trait SecretNewType { + fn new(val: T) -> Self where Self: Sized; + fn secret(&self) -> &T; +} +// FIXME: make sure there's a test for this +impl Debug for SecretNewType { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, "[redacted]") + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ClientId(String); +impl NewType for ClientId { + fn new(s: String) -> Self { + ClientId(s) + } +} +impl Deref for ClientId { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ClientSecret(String); +impl SecretNewType for ClientSecret { + fn new(s: String) -> Self { + ClientSecret(s) + } + fn secret(&self) -> &String { &self.0 } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct AuthUrl(Url); +impl NewType for AuthUrl { + fn new(s: Url) -> Self { + AuthUrl(s) + } +} +impl Deref for AuthUrl { + type Target = Url; + fn deref(&self) -> &Url { + &self.0 + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TokenUrl(Url); +impl NewType for TokenUrl { + fn new(s: Url) -> Self { + TokenUrl(s) + } +} +impl Deref for TokenUrl { + type Target = Url; + fn deref(&self) -> &Url { + &self.0 + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Scope(String); +impl NewType for Scope { + fn new(s: String) -> Self { + Scope(s) + } +} +impl Deref for Scope { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } +} +impl AsRef for Scope { + fn as_ref(&self) -> &str { &self } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RedirectUrl(Url); +impl NewType for RedirectUrl { + fn new(s: Url) -> Self { + RedirectUrl(s) + } +} +impl Deref for RedirectUrl { + type Target = Url; + fn deref(&self) -> &Url { + &self.0 + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct CsrfToken(String); +impl SecretNewType for CsrfToken { + fn new(s: String) -> Self { + CsrfToken(s) + } + fn secret(&self) -> &String { &self.0 } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ResponseType(String); +impl NewType for ResponseType { + fn new(s: String) -> Self { + ResponseType(s) + } +} +impl Deref for ResponseType { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct AuthorizationCode(String); +impl SecretNewType for AuthorizationCode { + fn new(s: String) -> Self { + AuthorizationCode(s) + } + fn secret(&self) -> &String { &self.0 } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct RefreshToken(String); +impl SecretNewType for RefreshToken { + fn new(s: String) -> Self { + RefreshToken(s) + } + fn secret(&self) -> &String { &self.0 } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct AccessToken(String); +impl SecretNewType for AccessToken { + fn new(s: String) -> Self { + AccessToken(s) + } + fn secret(&self) -> &String { &self.0 } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct EndUserUsername(String); +impl NewType for EndUserUsername { + fn new(s: String) -> Self { + EndUserUsername(s) + } +} +impl Deref for EndUserUsername { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct EndUserPassword(String); +impl SecretNewType for EndUserPassword { + fn new(s: String) -> Self { + EndUserPassword(s) + } + fn secret(&self) -> &String { &self.0 } +} + /// /// Stores the configuration for an OAuth2 client. /// #[derive(Clone, Debug)] pub struct Client, TE: ErrorResponseType> { - client_id: String, - client_secret: Option, - auth_url: Url, + client_id: ClientId, + client_secret: Option, + auth_url: AuthUrl, auth_type: AuthType, - token_url: Url, - scopes: Vec, - redirect_url: Option, + token_url: TokenUrl, + scopes: Vec, + redirect_url: Option, phantom_tt: PhantomData, phantom_t: PhantomData, phantom_te: PhantomData, @@ -209,30 +425,32 @@ impl, TE: ErrorResponseType> Client { /// all standard OAuth2 flows except the /// [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2). /// - pub fn new(client_id: I, client_secret: Option, auth_url: A, token_url: U) - -> Result - where I: Into, S: Into, A: AsRef, U: AsRef { + pub fn new( + client_id: ClientId, + client_secret: Option, + auth_url: AuthUrl, + token_url: TokenUrl + ) -> Self { let client = Client { - client_id: client_id.into(), - client_secret: client_secret.map(|s| s.into()), - auth_url: Url::parse(auth_url.as_ref())?, + client_id: client_id, + client_secret: client_secret, + auth_url: auth_url, auth_type: AuthType::BasicAuth, - token_url: Url::parse(token_url.as_ref())?, + token_url: token_url, scopes: Vec::new(), redirect_url: None, phantom_tt: PhantomData, phantom_t: PhantomData, phantom_te: PhantomData, }; - Ok(client) + client } /// /// Appends a new scope to the authorization URL. /// - pub fn add_scope(mut self, scope: S) -> Self - where S: Into { - self.scopes.push(scope.into()); + pub fn add_scope(mut self, scope: Scope) -> Self { + self.scopes.push(scope); self } @@ -253,9 +471,8 @@ impl, TE: ErrorResponseType> Client { /// /// Sets the the redirect URL used by the authorization endpoint. /// - pub fn set_redirect_url(mut self, redirect_url: R) -> Self - where R: Into { - self.redirect_url = Some(redirect_url.into()); + pub fn set_redirect_url(mut self, redirect_url: RedirectUrl) -> Self { + self.redirect_url = Some(redirect_url); self } @@ -280,8 +497,8 @@ impl, TE: ErrorResponseType> Client { /// attacks. To disable CSRF protections (NOT recommended), use `insecure::authorize_url` /// instead. /// - pub fn authorize_url(&self, state: String) -> Url { - self.authorize_url_impl("code", Some(&state), None) + pub fn authorize_url(&self, state: &CsrfToken) -> Url { + self.authorize_url_impl("code", Some(state), None) } /// @@ -303,8 +520,8 @@ impl, TE: ErrorResponseType> Client { /// attacks. To disable CSRF protections (NOT recommended), use /// `insecure::authorize_url_implicit` instead. /// - pub fn authorize_url_implicit(&self, state: String) -> Url { - self.authorize_url_impl("token", Some(&state), None) + pub fn authorize_url_implicit(&self, state: &CsrfToken) -> Url { + self.authorize_url_impl("token", Some(state), None) } /// @@ -327,7 +544,7 @@ impl, TE: ErrorResponseType> Client { /// [RFC 6749](https://tools.ietf.org/html/rfc6749). pub fn authorize_url_extension( &self, - response_type: &str, + response_type: &ResponseType, extra_params: &[(&str, &str)] ) -> Url { self.authorize_url_impl(response_type, None, Some(extra_params)) @@ -336,31 +553,30 @@ impl, TE: ErrorResponseType> Client { fn authorize_url_impl( &self, response_type: &str, - state_opt: Option<&String>, + state_opt: Option<&CsrfToken>, extra_params_opt: Option<&[(&str, &str)]> ) -> Url { - let scopes = self.scopes.join(" "); - let response_type_str = response_type.to_string(); + let scopes = self.scopes.iter().map(|s| s.to_string()).collect::>().join(" "); + let response_type_str = response_type.clone(); - let mut pairs = vec![ + let mut pairs: Vec<(&str, &str) > = vec![ ("response_type", &response_type_str), ("client_id", &self.client_id), ]; if let Some(ref redirect_url) = self.redirect_url { - pairs.push(("redirect_uri", redirect_url)); + pairs.push(("redirect_uri", redirect_url.as_str())); } if !scopes.is_empty() { pairs.push(("scope", &scopes)); } - if let Some(state) = state_opt { - pairs.push(("state", state)); + pairs.push(("state", state.secret())); } - let mut url = self.auth_url.clone(); + let mut url: Url = (*self.auth_url).clone(); url.query_pairs_mut().extend_pairs( pairs.iter().map(|&(k, v)| { (k, &v[..]) }) @@ -383,12 +599,12 @@ impl, TE: ErrorResponseType> Client { /// /// See https://tools.ietf.org/html/rfc6749#section-4.1.3 /// - pub fn exchange_code(&self, code: String) -> Result> { + pub fn exchange_code(&self, code: AuthorizationCode) -> Result> { // Make Clippy happy since we're intentionally taking ownership. let code_owned = code; let params = vec![ ("grant_type", "authorization_code"), - ("code", &code_owned) + ("code", code_owned.secret()) ]; self.request_token(params) @@ -399,12 +615,12 @@ impl, TE: ErrorResponseType> Client { /// /// See https://tools.ietf.org/html/rfc6749#section-4.3.2 /// - pub fn exchange_password(&self, username: &str, password: &str) + pub fn exchange_password(&self, username: &EndUserUsername, password: &EndUserPassword) -> Result> { let params = vec![ ("grant_type", "password"), ("username", username), - ("password", password), + ("password", password.secret()), ]; self.request_token(params) @@ -418,7 +634,12 @@ impl, TE: ErrorResponseType> Client { pub fn exchange_client_credentials(&self) -> Result> { // Generate the space-delimited scopes String before initializing params so that it has // a long enough lifetime. - let scopes_opt = if !self.scopes.is_empty() { Some(self.scopes.join(" ")) } else { None }; + let scopes_opt = + if !self.scopes.is_empty() { + Some(self.scopes.iter().map(|s| s.to_string()).collect::>().join(" ")) + } else { + None + }; let mut params: Vec<(&str, &str)> = vec![("grant_type", "client_credentials")]; @@ -433,10 +654,12 @@ impl, TE: ErrorResponseType> Client { /// /// See https://tools.ietf.org/html/rfc6749#section-6 /// - pub fn exchange_refresh_token(&self, refresh_token: &str) -> Result> { - let params = vec![ + pub fn exchange_refresh_token( + &self, refresh_token: &RefreshToken + ) -> Result> { + let params: Vec<(&str, &str)> = vec![ ("grant_type", "refresh_token"), - ("refresh_token", refresh_token), + ("refresh_token", refresh_token.secret()), ]; self.request_token(params) @@ -452,19 +675,19 @@ impl, TE: ErrorResponseType> Client { AuthType::RequestBody => { params.push(("client_id", &self.client_id)); if let Some(ref client_secret) = self.client_secret { - params.push(("client_secret", client_secret)); + params.push(("client_secret", client_secret.secret())); } } AuthType::BasicAuth => { easy.username(&self.client_id)?; if let Some(ref client_secret) = self.client_secret { - easy.password(client_secret)?; + easy.password(client_secret.secret())?; } } } if let Some(ref redirect_url) = self.redirect_url { - params.push(("redirect_uri", redirect_url)); + params.push(("redirect_uri", redirect_url.as_str())); } let form = @@ -594,7 +817,7 @@ pub trait Token : Debug + DeserializeOwned + PartialEq + Serialize /// /// REQUIRED. The access token issued by the authorization server. /// - fn access_token(&self) -> &str; + fn access_token(&self) -> &AccessToken; /// /// REQUIRED. The type of the token issued as described in /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1). @@ -613,7 +836,7 @@ pub trait Token : Debug + DeserializeOwned + PartialEq + Serialize /// authorization grant as described in /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). /// - fn refresh_token(&self) -> &Option; + fn refresh_token(&self) -> &Option; /// /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The /// scipe of the access token as described by @@ -621,7 +844,7 @@ pub trait Token : Debug + DeserializeOwned + PartialEq + Serialize /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from /// the response, this field is `None`. /// - fn scopes(&self) -> &Option>; + fn scopes(&self) -> &Option>; /// /// Factory method to deserialize a `Token` from a JSON response. @@ -770,7 +993,7 @@ pub mod basic { #[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct BasicToken { #[serde(rename = "access_token")] - _access_token: String, + _access_token: AccessToken, #[serde(bound(deserialize = "T: DeserializeOwned"))] #[serde(rename = "token_type")] #[serde(deserialize_with = "helpers::deserialize_untagged_enum_case_insensitive")] @@ -778,20 +1001,20 @@ pub mod basic { #[serde(rename = "expires_in")] _expires_in: Option, #[serde(rename = "refresh_token")] - _refresh_token: Option, + _refresh_token: Option, #[serde(rename = "scope")] #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] #[serde(serialize_with = "helpers::serialize_space_delimited_vec")] #[serde(default)] - _scopes: Option>, + _scopes: Option>, } impl Token for BasicToken { - fn access_token(&self) -> &str { &self._access_token } + fn access_token(&self) -> &AccessToken { &self._access_token } fn token_type(&self) -> &T { &self._token_type } fn expires_in(&self) -> Option { self._expires_in.map(Duration::from_secs) } - fn refresh_token(&self) -> &Option { &self._refresh_token } - fn scopes(&self) -> &Option> { &self._scopes } + fn refresh_token(&self) -> &Option { &self._refresh_token } + fn scopes(&self) -> &Option> { &self._scopes } fn from_json(data: &str) -> Result { serde_json::from_str(data) @@ -1032,13 +1255,14 @@ pub mod helpers { /// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` /// in the case of JSON serialization). /// - pub fn serialize_space_delimited_vec( - string_vec_opt: &Option>, + pub fn serialize_space_delimited_vec<'a, T, S>( + vec_opt: &'a Option>, serializer: S ) -> Result - where S: Serializer { - if let Some(ref string_vec) = *string_vec_opt { - let space_delimited = string_vec.join(" "); + where T: AsRef, S: Serializer { + if let Some(ref vec) = *vec_opt { + let space_delimited = vec.iter().map(|s| s.as_ref()).collect::>().join(" "); + serializer.serialize_str(&space_delimited) } else { serializer.serialize_none() diff --git a/tests/lib.rs b/tests/lib.rs index f375bc89..baacbc2d 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -6,17 +6,32 @@ extern crate serde; extern crate serde_json; use mockito::{mock, SERVER_URL}; -use oauth2::{Token, RequestTokenError}; +use oauth2::*; use oauth2::basic::*; use url::Url; +fn new_client() -> BasicClient { + BasicClient::new( + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://example.com/auth").unwrap()), + TokenUrl::new(Url::parse("http://example.com/token").unwrap()) + ) +} + +fn new_mock_client() -> BasicClient { + BasicClient::new( + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://example.com/auth").unwrap()), + TokenUrl::new(Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap()) + ) +} + #[test] fn test_authorize_url() { - let client = - BasicClient::new("aaa", Some("bbb"), "http://example.com/auth", "http://example.com/token") - .unwrap(); - - let url = client.authorize_url("csrf_token".to_string()); + let client = new_client(); + let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -28,9 +43,7 @@ fn test_authorize_url() { #[test] fn test_authorize_url_insecure() { - let client = - BasicClient::new("aaa", Some("bbb"), "http://example.com/auth", "http://example.com/token") - .unwrap(); + let client = new_client(); let url = oauth2::insecure::authorize_url(&client); @@ -42,11 +55,9 @@ fn test_authorize_url_insecure() { #[test] fn test_authorize_url_implicit() { - let client = - BasicClient::new("aaa", Some("bbb"), "http://example.com/auth", "http://example.com/token") - .unwrap(); + let client = new_client(); - let url = client.authorize_url_implicit("csrf_token".to_string()); + let url = client.authorize_url_implicit(&CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -58,9 +69,7 @@ fn test_authorize_url_implicit() { #[test] fn test_authorize_url_implicit_insecure() { - let client = - BasicClient::new("aaa", Some("bbb"), "http://example.com/auth", "http://example.com/token") - .unwrap(); + let client = new_client(); let url = oauth2::insecure::authorize_url_implicit(&client); @@ -74,13 +83,13 @@ fn test_authorize_url_implicit_insecure() { fn test_authorize_url_with_param() { let client = BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth?foo=bar", - "http://example.com/token" - ).unwrap(); + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://example.com/auth?foo=bar").unwrap()), + TokenUrl::new(Url::parse("http://example.com/token").unwrap()) + ); - let url = client.authorize_url("csrf_token".to_string()); + let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -93,12 +102,11 @@ fn test_authorize_url_with_param() { #[test] fn test_authorize_url_with_scopes() { let client = - BasicClient::new("aaa", Some("bbb"), "http://example.com/auth", "http://example.com/token") - .unwrap() - .add_scope("read") - .add_scope("write"); + new_client() + .add_scope(Scope::new("read".to_string())) + .add_scope(Scope::new("write".to_string())); - let url = client.authorize_url("csrf_token".to_string()); + let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -111,11 +119,12 @@ fn test_authorize_url_with_scopes() { #[test] fn test_authorize_url_with_extension_response_type() { - let client = - BasicClient::new("aaa", Some("bbb"), "http://example.com/auth", "http://example.com/token") - .unwrap(); + let client = new_client(); - let url = client.authorize_url_extension("code token", &vec![("foo", "bar")]); + let url = + client.authorize_url_extension( + &ResponseType::new("code token".to_string()), &vec![("foo", "bar")] + ); assert_eq!( Url::parse("http://example.com/auth?response_type=code+token&client_id=aaa&foo=bar") @@ -127,11 +136,10 @@ fn test_authorize_url_with_extension_response_type() { #[test] fn test_authorize_url_with_redirect_url() { let client = - BasicClient::new("aaa", Some("bbb"), "http://example.com/auth", "http://example.com/token") - .unwrap() - .set_redirect_url("http://localhost/redirect"); + new_client() + .set_redirect_url(RedirectUrl::new(Url::parse("http://localhost/redirect").unwrap())); - let url = client.authorize_url("csrf_token".to_string()); + let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -155,16 +163,16 @@ fn test_exchange_code_successful_with_minimal_json_response() { let client = BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ).unwrap(); - let token = client.exchange_code("ccc".to_string()).unwrap(); + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://example.com/auth").unwrap()), + TokenUrl::new(Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap()) + ); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); @@ -193,23 +201,20 @@ fn test_exchange_code_successful_with_complete_json_response() { .create(); let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap() + new_mock_client() .set_auth_type(oauth2::AuthType::RequestBody); - let token = client.exchange_code("ccc".to_string()).unwrap(); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!( + Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + *token.scopes() + ); assert_eq!(3600, token.expires_in().unwrap().as_secs()); - assert_eq!(Some("foobar".to_string()), *token.refresh_token()); + assert_eq!("foobar", token.refresh_token().clone().unwrap().secret()); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&token).unwrap(); @@ -234,21 +239,18 @@ fn test_exchange_client_credentials_with_basic_auth() { .create(); let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap() + new_mock_client() .set_auth_type(oauth2::AuthType::BasicAuth); let token = client.exchange_client_credentials().unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!( + Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + *token.scopes() + ); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); } @@ -268,23 +270,17 @@ fn test_exchange_client_credentials_with_body_auth_and_scope() { .create(); let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap() + new_mock_client() .set_auth_type(oauth2::AuthType::RequestBody) - .add_scope("read") - .add_scope("write"); + .add_scope(Scope::new("read".to_string())) + .add_scope(Scope::new("write".to_string())); let token = client.exchange_client_credentials().unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); } @@ -301,21 +297,15 @@ fn test_exchange_refresh_token_with_basic_auth() { .create(); let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap() + new_mock_client() .set_auth_type(oauth2::AuthType::BasicAuth); - let token = client.exchange_refresh_token("ccc").unwrap(); + let token = client.exchange_refresh_token(&RefreshToken::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); } @@ -333,21 +323,14 @@ fn test_exchange_refresh_token_with_json_response() { ) .create(); - let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap(); - let token = client.exchange_refresh_token("ccc").unwrap(); + let client = new_mock_client(); + let token = client.exchange_refresh_token(&RefreshToken::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); } @@ -364,21 +347,20 @@ fn test_exchange_password_with_json_response() { ) .create(); - let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) + let client = new_mock_client(); + let token = + client + .exchange_password( + &EndUserUsername::new("user".to_string()), + &EndUserPassword::new("pass".to_string()) + ) .unwrap(); - let token = client.exchange_password("user", "pass").unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); } @@ -389,7 +371,7 @@ fn test_exchange_code_successful_with_redirect_url() { .match_header("Accept", "application/json") .match_body( "grant_type=authorization_code&code=ccc&client_id=aaa&client_secret=bbb&redirect_uri=\ - http%3A%2F%2Fredirect" + http%3A%2F%2Fredirect%2Fhere" ) .with_body( "{\"access_token\": \"12/34\", \"token_type\": \"bearer\", \"scope\": \"read write\"}" @@ -397,23 +379,17 @@ fn test_exchange_code_successful_with_redirect_url() { .create(); let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap() + new_mock_client() .set_auth_type(oauth2::AuthType::RequestBody) - .set_redirect_url("http://redirect"); + .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect/here").unwrap())); - let token = client.exchange_code("ccc".to_string()).unwrap(); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); } @@ -423,30 +399,26 @@ fn test_exchange_code_successful_with_basic_auth() { let mock = mock("POST", "/token") .match_header("Accept", "application/json") .match_header("Authorization", "Basic YWFhOmJiYg==") // base64("aaa:bbb") - .match_body("grant_type=authorization_code&code=ccc&redirect_uri=http%3A%2F%2Fredirect") + .match_body( + "grant_type=authorization_code&code=ccc&redirect_uri=http%3A%2F%2Fredirect%2Fhere" + ) .with_body( "{\"access_token\": \"12/34\", \"token_type\": \"bearer\", \"scope\": \"read write\"}" ) .create(); let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap() + new_mock_client() .set_auth_type(oauth2::AuthType::BasicAuth) - .set_redirect_url("http://redirect"); + .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect/here").unwrap())); - let token = client.exchange_code("ccc".to_string()).unwrap(); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); } @@ -462,14 +434,8 @@ fn test_exchange_code_with_simple_json_error() { .with_body("{\"error\": \"invalid_request\", \"error_description\": \"stuff happened\"}") .create(); - let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ).unwrap(); - let token = client.exchange_code("ccc".to_string()); + let client = new_mock_client(); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())); mock.assert(); @@ -543,14 +509,8 @@ fn test_exchange_code_with_json_parse_error() { .with_body("broken json") .create(); - let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ).unwrap(); - let token = client.exchange_code("ccc".to_string()); + let client = new_mock_client(); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())); mock.assert(); @@ -576,14 +536,8 @@ fn test_exchange_code_with_unexpected_content_type() { .with_body("broken json") .create(); - let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ).unwrap(); - let token = client.exchange_code("ccc".to_string()); + let client = new_mock_client(); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())); mock.assert(); @@ -612,15 +566,14 @@ fn test_exchange_code_with_invalid_token_type() { .create(); let client = - BasicClient::new::<_, &str, _, _>( - "aaa", + BasicClient::new( + ClientId::new("aaa".to_string()), None, - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap(); + AuthUrl::new(Url::parse("http://example.com/auth").unwrap()), + TokenUrl::new(Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap()) + ); - let token = client.exchange_code("ccc".to_string()); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())); mock.assert(); @@ -647,14 +600,8 @@ fn test_exchange_code_with_400_status_code() { .with_status(400) .create(); - let client = - BasicClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ).unwrap(); - let token = client.exchange_code("ccc".to_string()); + let client = new_mock_client(); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())); mock.assert(); @@ -672,8 +619,14 @@ fn test_exchange_code_with_400_status_code() { #[test] fn test_exchange_code_fails_gracefully_on_transport_error() { - let client = BasicClient::new("aaa", Some("bbb"), "http://auth", "http://token").unwrap(); - let token = client.exchange_code("ccc".to_string()); + let client = + BasicClient::new( + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://auth").unwrap()), + TokenUrl::new(Url::parse("http://token").unwrap()) + ); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())); assert!(token.is_err()); @@ -719,11 +672,11 @@ mod colorful_extension { } impl Token for ColorfulToken { - fn access_token(&self) -> &str { &self._basic_token.access_token() } + fn access_token(&self) -> &AccessToken { &self._basic_token.access_token() } fn token_type(&self) -> &ColorfulTokenType { &self._basic_token.token_type() } fn expires_in(&self) -> Option { self._basic_token.expires_in() } - fn refresh_token(&self) -> &Option { &self._basic_token.refresh_token() } - fn scopes(&self) -> &Option> { &self._basic_token.scopes() } + fn refresh_token(&self) -> &Option { &self._basic_token.refresh_token() } + fn scopes(&self) -> &Option> { &self._basic_token.scopes() } fn from_json(data: &str) -> Result { serde_json::from_str(data) @@ -779,16 +732,16 @@ fn test_extension_successful_with_minimal_json_response() { let client = ColorfulClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ).unwrap(); - let token = client.exchange_code("ccc".to_string()).unwrap(); + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://example.com/auth").unwrap()), + TokenUrl::new(Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap()) + ); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(ColorfulTokenType::Green, *token.token_type()); assert_eq!(None, token.expires_in()); assert_eq!(None, *token.refresh_token()); @@ -823,22 +776,20 @@ fn test_extension_successful_with_complete_json_response() { let client = ColorfulClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ) - .unwrap() - .set_auth_type(oauth2::AuthType::RequestBody); - let token = client.exchange_code("ccc".to_string()).unwrap(); + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://example.com/auth").unwrap()), + TokenUrl::new(Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap()) + ).set_auth_type(oauth2::AuthType::RequestBody); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())).unwrap(); mock.assert(); - assert_eq!("12/34", token.access_token()); + assert_eq!("12/34", token.access_token().secret()); assert_eq!(ColorfulTokenType::Red, *token.token_type()); - assert_eq!(Some(vec!["read".to_string(), "write".to_string()]), *token.scopes()); + assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); assert_eq!(3600, token.expires_in().unwrap().as_secs()); - assert_eq!(Some("foobar".to_string()), *token.refresh_token()); + assert_eq!("foobar", token.refresh_token().clone().unwrap().secret()); assert_eq!(Some("round".to_string()), *token.shape()); assert_eq!(12, token.height()); @@ -872,12 +823,12 @@ fn test_extension_with_simple_json_error() { let client = ColorfulClient::new( - "aaa", - Some("bbb"), - "http://example.com/auth", - &(SERVER_URL.to_string() + "/token") - ).unwrap(); - let token = client.exchange_code("ccc".to_string()); + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + AuthUrl::new(Url::parse("http://example.com/auth").unwrap()), + TokenUrl::new(Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap()) + ); + let token = client.exchange_code(AuthorizationCode::new("ccc".to_string())); mock.assert(); From cbd18eb56a6f6a5e444bd2c05efd8c170d850485 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Sun, 1 Apr 2018 22:59:07 -0700 Subject: [PATCH 02/11] Fix secret redaction --- .DS_Store | Bin 0 -> 8196 bytes src/lib.rs | 50 +++++++++++++++++++++++++++++++++++++------------- tests/lib.rs | 6 ++++++ 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..74c534bca020686c445ae4d157515a858366ba43 GIT binary patch literal 8196 zcmeHLYit!o6h5a9n90(1i%45QTqzH+S8OdbZLxB>Ens2u|1jYfiSdt#8Xr;PBaP2!O#J6B<3D43oSE56piuuIChR10&d!;0 zcJ_Sx?arPB08HhqCV*N1VAO@yH4>L-VqWx%T1jLPlSuIZli-34ffZ!ji@ak)j6jS) zj6jS)j6jUQ{{aEovqfVI?E7jqmN5b`0#_vh;{9My7djc~S*7KxgM=&rNSBdWHaey{ zK*ku6PDXlGX&FlARMi7Up%|4IsKRO95bmUtk)Bmrg#)T^z-VQRDioBfQ=B2)0h3C{ zGDaXq;9>++ac{#Em@sjE{hs!HS3GN_5I`0(qNFdH7C}hfSs0^QKTH>b$+YXI$@Qc3 zr0WD>W8>FYQd(BNXfb0#+4b3nYq|EGLIdUvJ9Ub@4O6QKv^dZMCjOfcc zeJ?Z}&(5Cbex%>=EWeQQbDkB*^Xxanaf;=fGd`l%PET)c+LADun_FiR#&lcDtqG&0 zwQXjGvzm2H9o?f7$4^Y3oVn*2g#$+K5~!Mo%d<4TAjx9(!ta6!H^_IY>`R_5X8%nw zd(O2b-Mjbf9ntk%U>~quGj#HHml@i@h+Y*CDBT&~+n=@bGUe)w>1J}|WN&xfUgv}z z^k;lGH{k^Xp*D5cG;e3d2_d@3`$Ic!sI9515Xa*VEvm9@%Kko!i+O_*UbHc91S2&-mtf{SA zu~O3(avjK}ontvCJg(O>HdkGxS8_vJ!}YW5h(&4a^DVKGC1*54ThG{GVeib^B$b(O z)Ec;Mvc7;)QK#6%8@V3vzPssWtI%e?h3OVQ=$dKURi;+n&h$OJKd>rl-9-c581F-d#@RUJ(IRa%Zu!G8k#mJ?AtBNwiv5V-l(`BQK(Ngs*x_X zr#eW$?Qj5wAOjv0;9+V}bJc;+?DSQwg!YA-~ zd;wp=m+=g~iSOXM_#S?WU*h-b3QLMy-lxVzi9gWMe9A@-Y{~E?DZ6vm9i3N(vfoxH zo-Vs~`SoizZfb7rNS(Wt^X}gHM3UT5fYkinhn>0HS80v!Ao2EGcX;VpO{J|^aU1wX>i@GJaItSiSVtRc>= z!s~D~ZorM$fC;=ATdw9Bl7i=x3ZG?UnlaxP : Clone + Debug + Deref + PartialEq { fn new(val: T) -> Self; } -pub trait SecretNewType { +pub trait SecretNewType : Debug { fn new(val: T) -> Self where Self: Sized; fn secret(&self) -> &T; } -// FIXME: make sure there's a test for this -impl Debug for SecretNewType { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - write!(f, "[redacted]") - } -} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ClientId(String); @@ -247,7 +241,7 @@ impl Deref for ClientId { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Deserialize, PartialEq, Serialize)] pub struct ClientSecret(String); impl SecretNewType for ClientSecret { fn new(s: String) -> Self { @@ -255,6 +249,11 @@ impl SecretNewType for ClientSecret { } fn secret(&self) -> &String { &self.0 } } +impl Debug for ClientSecret { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, "ClientSecret([redacted])") + } +} #[derive(Clone, Debug, PartialEq)] pub struct AuthUrl(Url); @@ -315,7 +314,7 @@ impl Deref for RedirectUrl { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub struct CsrfToken(String); impl SecretNewType for CsrfToken { fn new(s: String) -> Self { @@ -323,6 +322,11 @@ impl SecretNewType for CsrfToken { } fn secret(&self) -> &String { &self.0 } } +impl Debug for CsrfToken { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, "CsrfToken([redacted])") + } +} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ResponseType(String); @@ -338,7 +342,7 @@ impl Deref for ResponseType { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Deserialize, PartialEq, Serialize)] pub struct AuthorizationCode(String); impl SecretNewType for AuthorizationCode { fn new(s: String) -> Self { @@ -346,8 +350,13 @@ impl SecretNewType for AuthorizationCode { } fn secret(&self) -> &String { &self.0 } } +impl Debug for AuthorizationCode { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, "AuthorizationCode([redacted])") + } +} -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Deserialize, PartialEq, Serialize)] pub struct RefreshToken(String); impl SecretNewType for RefreshToken { fn new(s: String) -> Self { @@ -355,8 +364,13 @@ impl SecretNewType for RefreshToken { } fn secret(&self) -> &String { &self.0 } } +impl Debug for RefreshToken { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, "RefreshToken[redacted])") + } +} -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Deserialize, PartialEq, Serialize)] pub struct AccessToken(String); impl SecretNewType for AccessToken { fn new(s: String) -> Self { @@ -364,6 +378,11 @@ impl SecretNewType for AccessToken { } fn secret(&self) -> &String { &self.0 } } +impl Debug for AccessToken { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, "AccessToken([redacted])") + } +} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct EndUserUsername(String); @@ -379,7 +398,7 @@ impl Deref for EndUserUsername { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Deserialize, PartialEq, Serialize)] pub struct EndUserPassword(String); impl SecretNewType for EndUserPassword { fn new(s: String) -> Self { @@ -387,6 +406,11 @@ impl SecretNewType for EndUserPassword { } fn secret(&self) -> &String { &self.0 } } +impl Debug for EndUserPassword { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, "EndUserPassword([redacted])") + } +} /// /// Stores the configuration for an OAuth2 client. diff --git a/tests/lib.rs b/tests/lib.rs index baacbc2d..9eee1cd3 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -874,3 +874,9 @@ fn test_extension_with_simple_json_error() { format!("{}", token_err) ); } + +#[test] +fn test_secret_redaction() { + let secret = ClientSecret::new("top_secret".to_string()); + assert_eq!("ClientSecret([redacted])", format!("{:?}", secret)); +} From b7c5417ff3630caeb0e5c09f3078379ceace5753 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Tue, 3 Apr 2018 01:39:53 -0700 Subject: [PATCH 03/11] Fix Clippy lints --- examples/github.rs | 15 +++++---- src/lib.rs | 42 +++++++++++------------ tests/lib.rs | 83 +++++++++++++++++++++++++++++----------------- 3 files changed, 81 insertions(+), 59 deletions(-) diff --git a/examples/github.rs b/examples/github.rs index 9494de8c..f6854f6f 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -128,13 +128,16 @@ fn main() { // space-separated scopes. Github-specific clients can parse this scope into // multiple scopes by splitting at the commas. Note that it's not safe for the // library to do this by default because RFC 6749 allows scopes to contain commas. - let scopes_raw = token.scopes().clone().unwrap_or_else(|| Vec::new()); let scopes = - scopes_raw - .iter() - .map(|comma_separated| comma_separated.split(",")) - .flat_map(|inner_scopes| inner_scopes) - .collect::>(); + if let Some(scopes_vec) = token.scopes() { + scopes_vec + .iter() + .map(|comma_separated| comma_separated.split(",")) + .flat_map(|inner_scopes| inner_scopes) + .collect::>() + } else { + Vec::new() + }; println!("Github returned the following scopes:\n{:?}\n", scopes); } diff --git a/src/lib.rs b/src/lib.rs index 1b775d33..c4b5304a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ //! // Generate the full authorization URL. //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. -//! println!("Browse to: {}", client.authorize_url(csrf_state)); +//! println!("Browse to: {}", client.authorize_url(&csrf_state)); //! //! // Once the user has been redirected to the redirect URL, you'll have access to the //! // authorization code. For security reasons, your code should verify that the `state` @@ -97,7 +97,7 @@ //! // Generate the full authorization URL. //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. -//! println!("Browse to: {}", client.authorize_url_implicit(csrf_state)); +//! println!("Browse to: {}", client.authorize_url_implicit(&csrf_state)); //! # Ok(()) //! # } //! # fn main() {} @@ -297,7 +297,7 @@ impl Deref for Scope { } } impl AsRef for Scope { - fn as_ref(&self) -> &str { &self } + fn as_ref(&self) -> &str { self } } #[derive(Clone, Debug, PartialEq)] @@ -455,19 +455,18 @@ impl, TE: ErrorResponseType> Client { auth_url: AuthUrl, token_url: TokenUrl ) -> Self { - let client = Client { - client_id: client_id, - client_secret: client_secret, - auth_url: auth_url, + Client { + client_id, + client_secret, + auth_url, auth_type: AuthType::BasicAuth, - token_url: token_url, + token_url, scopes: Vec::new(), redirect_url: None, phantom_tt: PhantomData, phantom_t: PhantomData, phantom_te: PhantomData, - }; - client + } } /// @@ -581,10 +580,9 @@ impl, TE: ErrorResponseType> Client { extra_params_opt: Option<&[(&str, &str)]> ) -> Url { let scopes = self.scopes.iter().map(|s| s.to_string()).collect::>().join(" "); - let response_type_str = response_type.clone(); let mut pairs: Vec<(&str, &str) > = vec![ - ("response_type", &response_type_str), + ("response_type", response_type), ("client_id", &self.client_id), ]; @@ -860,7 +858,7 @@ pub trait Token : Debug + DeserializeOwned + PartialEq + Serialize /// authorization grant as described in /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). /// - fn refresh_token(&self) -> &Option; + fn refresh_token(&self) -> Option<&RefreshToken>; /// /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The /// scipe of the access token as described by @@ -868,7 +866,7 @@ pub trait Token : Debug + DeserializeOwned + PartialEq + Serialize /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from /// the response, this field is `None`. /// - fn scopes(&self) -> &Option>; + fn scopes(&self) -> Option<&Vec>; /// /// Factory method to deserialize a `Token` from a JSON response. @@ -919,24 +917,24 @@ impl ErrorResponse { /// OPTIONAL. Human-readable ASCII text providing additional information, used to assist /// the client developer in understanding the error that occurred. /// - pub fn error_description(&self) -> &Option { &self._error_description } + pub fn error_description(&self) -> Option<&String> { self._error_description.as_ref() } /// /// OPTIONAL. A URI identifying a human-readable web page with information about the error, /// used to provide the client developer with additional information about the error. /// - pub fn error_uri(&self) -> &Option { &self._error_uri } + pub fn error_uri(&self) -> Option<&String> { self._error_uri.as_ref() } } impl Display for ErrorResponse { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { let mut formatted = self.error().to_string(); - if let Some(ref error_description) = *self.error_description() { + if let Some(error_description) = self.error_description() { formatted.push_str(": "); formatted.push_str(error_description); } - if let Some(ref error_uri) = *self.error_uri() { + if let Some(error_uri) = self.error_uri() { formatted.push_str(" / See "); formatted.push_str(error_uri); } @@ -1037,8 +1035,8 @@ pub mod basic { fn access_token(&self) -> &AccessToken { &self._access_token } fn token_type(&self) -> &T { &self._token_type } fn expires_in(&self) -> Option { self._expires_in.map(Duration::from_secs) } - fn refresh_token(&self) -> &Option { &self._refresh_token } - fn scopes(&self) -> &Option> { &self._scopes } + fn refresh_token(&self) -> Option<&RefreshToken> { self._refresh_token.as_ref() } + fn scopes(&self) -> Option<&Vec> { self._scopes.as_ref() } fn from_json(data: &str) -> Result { serde_json::from_str(data) @@ -1279,8 +1277,8 @@ pub mod helpers { /// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` /// in the case of JSON serialization). /// - pub fn serialize_space_delimited_vec<'a, T, S>( - vec_opt: &'a Option>, + pub fn serialize_space_delimited_vec( + vec_opt: &Option>, serializer: S ) -> Result where T: AsRef, S: Serializer { diff --git a/tests/lib.rs b/tests/lib.rs index 9eee1cd3..10d80efb 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -175,7 +175,7 @@ fn test_exchange_code_successful_with_minimal_json_response() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&token).unwrap(); @@ -210,8 +210,8 @@ fn test_exchange_code_successful_with_complete_json_response() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( - Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), - *token.scopes() + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() ); assert_eq!(3600, token.expires_in().unwrap().as_secs()); assert_eq!("foobar", token.refresh_token().clone().unwrap().secret()); @@ -248,11 +248,11 @@ fn test_exchange_client_credentials_with_basic_auth() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( - Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), - *token.scopes() + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() ); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); } #[test] @@ -280,9 +280,12 @@ fn test_exchange_client_credentials_with_body_auth_and_scope() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); + assert_eq!( + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() + ); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); } #[test] @@ -305,9 +308,12 @@ fn test_exchange_refresh_token_with_basic_auth() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); + assert_eq!( + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() + ); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); } #[test] @@ -330,9 +336,12 @@ fn test_exchange_refresh_token_with_json_response() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); + assert_eq!( + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() + ); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); } #[test] @@ -360,9 +369,12 @@ fn test_exchange_password_with_json_response() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); + assert_eq!( + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() + ); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); } #[test] @@ -389,9 +401,12 @@ fn test_exchange_code_successful_with_redirect_url() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); + assert_eq!( + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() + ); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); } #[test] @@ -418,9 +433,12 @@ fn test_exchange_code_successful_with_basic_auth() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); - assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); + assert_eq!( + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() + ); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); + assert_eq!(None, token.refresh_token()); } #[test] @@ -445,8 +463,8 @@ fn test_exchange_code_with_simple_json_error() { match &token_err { &RequestTokenError::ServerResponse(ref error_response) => { assert_eq!(BasicErrorResponseType::InvalidRequest, *error_response.error()); - assert_eq!(Some("stuff happened".to_string()), *error_response.error_description()); - assert_eq!(None, *error_response.error_uri()); + assert_eq!(Some(&"stuff happened".to_string()), error_response.error_description()); + assert_eq!(None, error_response.error_uri()); // Test Debug trait for ErrorResponse assert_eq!( @@ -610,8 +628,8 @@ fn test_exchange_code_with_400_status_code() { match token.err().unwrap() { RequestTokenError::ServerResponse(error_response) => { assert_eq!(BasicErrorResponseType::InvalidRequest, *error_response.error()); - assert_eq!(Some("Expired code.".to_string()), *error_response.error_description()); - assert_eq!(None, *error_response.error_uri()); + assert_eq!(Some(&"Expired code.".to_string()), error_response.error_description()); + assert_eq!(None, error_response.error_uri()); }, other => panic!("Unexpected error: {:?}", other), } @@ -667,7 +685,7 @@ mod colorful_extension { _height: u32, } impl ColorfulToken { - pub fn shape(&self) -> &Option { &self._shape } + pub fn shape(&self) -> Option<&String> { self._shape.as_ref() } pub fn height(&self) -> u32 { self._height } } @@ -675,8 +693,8 @@ mod colorful_extension { fn access_token(&self) -> &AccessToken { &self._basic_token.access_token() } fn token_type(&self) -> &ColorfulTokenType { &self._basic_token.token_type() } fn expires_in(&self) -> Option { self._basic_token.expires_in() } - fn refresh_token(&self) -> &Option { &self._basic_token.refresh_token() } - fn scopes(&self) -> &Option> { &self._basic_token.scopes() } + fn refresh_token(&self) -> Option<&RefreshToken> { self._basic_token.refresh_token() } + fn scopes(&self) -> Option<&Vec> { self._basic_token.scopes() } fn from_json(data: &str) -> Result { serde_json::from_str(data) @@ -744,8 +762,8 @@ fn test_extension_successful_with_minimal_json_response() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(ColorfulTokenType::Green, *token.token_type()); assert_eq!(None, token.expires_in()); - assert_eq!(None, *token.refresh_token()); - assert_eq!(None, *token.shape()); + assert_eq!(None, token.refresh_token()); + assert_eq!(None, token.shape()); assert_eq!(10, token.height()); // Ensure that serialization produces an equivalent JSON value. @@ -787,10 +805,13 @@ fn test_extension_successful_with_complete_json_response() { assert_eq!("12/34", token.access_token().secret()); assert_eq!(ColorfulTokenType::Red, *token.token_type()); - assert_eq!(Some(vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), *token.scopes()); + assert_eq!( + Some(&vec![Scope::new("read".to_string()), Scope::new("write".to_string())]), + token.scopes() + ); assert_eq!(3600, token.expires_in().unwrap().as_secs()); assert_eq!("foobar", token.refresh_token().clone().unwrap().secret()); - assert_eq!(Some("round".to_string()), *token.shape()); + assert_eq!(Some(&"round".to_string()), token.shape()); assert_eq!(12, token.height()); // Ensure that serialization produces an equivalent JSON value. @@ -841,8 +862,8 @@ fn test_extension_with_simple_json_error() { ColorfulErrorResponseType::TooLight, *error_response.error() ); - assert_eq!(Some("stuff happened".to_string()), *error_response.error_description()); - assert_eq!(Some("https://errors".to_string()), *error_response.error_uri()); + assert_eq!(Some(&"stuff happened".to_string()), error_response.error_description()); + assert_eq!(Some(&"https://errors".to_string()), error_response.error_uri()); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&error_response).unwrap(); From 2ec5f40da32d95a7844b8e918786cc21533974de Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Tue, 3 Apr 2018 00:26:36 -0700 Subject: [PATCH 04/11] Use macros, add factory for CsrfToken, and remove wildcard imports --- Cargo.toml | 4 +- examples/github.rs | 20 +- examples/google.rs | 19 +- src/lib.rs | 553 ++++++++++++++++++++++++++++----------------- tests/lib.rs | 4 +- 5 files changed, 374 insertions(+), 226 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef36194a..699a074d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,15 +7,15 @@ description = "Bindings for exchanging OAuth 2 tokens" repository = "https://github.com/alexcrichton/oauth2-rs" [dependencies] +base64 = "0.9" curl = "0.4.0" failure = "0.1" failure_derive = "0.1" +rand = "0.4" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" url = "1.0" [dev-dependencies] -base64 = "0.9" mockito = "0.8.2" -rand = "0.4" diff --git a/examples/github.rs b/examples/github.rs index f6854f6f..3dc6799a 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -18,9 +18,19 @@ extern crate oauth2; extern crate rand; extern crate url; -use oauth2::*; +use oauth2::prelude::*; +use oauth2::{ + AuthorizationCode, + AuthUrl, + ClientId, + ClientSecret, + CsrfToken, + RedirectUrl, + Scope, + Token, + TokenUrl, +}; use oauth2::basic::BasicClient; -use rand::{thread_rng, Rng}; use std::env; use std::net::TcpListener; use std::io::{BufRead, BufReader, Write}; @@ -64,10 +74,8 @@ fn main() { ) ); - let mut rng = thread_rng(); - // Generate a 128-bit random string for CSRF protection (each time!). - let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); - let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); + // Generate a new random token for CSRF protection (each time!). + let csrf_state = CsrfToken::new_random(); // Generate the authorization URL to which we'll redirect the user. let authorize_url = client.authorize_url(&csrf_state); diff --git a/examples/google.rs b/examples/google.rs index 9e31214e..00f9c49f 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -18,9 +18,18 @@ extern crate oauth2; extern crate rand; extern crate url; -use oauth2::*; +use oauth2::prelude::*; +use oauth2::{ + AuthorizationCode, + AuthUrl, + ClientId, + ClientSecret, + CsrfToken, + RedirectUrl, + Scope, + TokenUrl, +}; use oauth2::basic::BasicClient; -use rand::{thread_rng, Rng}; use std::env; use std::net::TcpListener; use std::io::{BufRead, BufReader, Write}; @@ -64,10 +73,8 @@ fn main() { ) ); - let mut rng = thread_rng(); - // Generate a 128-bit random string for CSRF protection (each time!). - let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); - let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); + // Generate a new random token for CSRF protection (each time!). + let csrf_state = CsrfToken::new_random(); // Generate the authorization URL to which we'll redirect the user. let authorize_url = client.authorize_url(&csrf_state); diff --git a/src/lib.rs b/src/lib.rs index c4b5304a..1acb75d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,9 +15,18 @@ //! extern crate rand; //! extern crate url; //! -//! use oauth2::*; -//! use oauth2::basic::*; -//! use rand::{thread_rng, Rng}; +//! use oauth2::prelude::*; +//! use oauth2::{ +//! AuthorizationCode, +//! AuthUrl, +//! ClientId, +//! ClientSecret, +//! CsrfToken, +//! RedirectUrl, +//! Scope, +//! TokenUrl +//! }; +//! use oauth2::basic::BasicClient; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { @@ -37,10 +46,8 @@ //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect")?)); //! -//! let mut rng = thread_rng(); -//! // Generate a 128-bit random string for CSRF protection (each time!). -//! let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); -//! let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); +//! // Generate a new random token for CSRF protection (each time!). +//! let csrf_state = CsrfToken::new_random(); //! //! // Generate the full authorization URL. //! // This is the URL you should redirect the user to, in order to trigger the authorization @@ -75,9 +82,17 @@ //! extern crate rand; //! extern crate url; //! -//! use oauth2::*; -//! use oauth2::basic::*; -//! use rand::{thread_rng, Rng}; +//! use oauth2::prelude::*; +//! use oauth2::{ +//! AuthUrl, +//! ClientId, +//! ClientSecret, +//! CsrfToken, +//! RedirectUrl, +//! Scope, +//! TokenUrl +//! }; +//! use oauth2::basic::BasicClient; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { @@ -89,10 +104,8 @@ //! TokenUrl::new(Url::parse("http://token")?) //! ); //! -//! let mut rng = thread_rng(); -//! // Generate a 128-bit random string for CSRF protection (each time!). -//! let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); -//! let csrf_state = CsrfToken::new(base64::encode(&random_bytes)); +//! // Generate a new random token for CSRF protection (each time!). +//! let csrf_state = CsrfToken::new_random(); //! //! // Generate the full authorization URL. //! // This is the URL you should redirect the user to, in order to trigger the authorization @@ -116,9 +129,17 @@ //! extern crate rand; //! extern crate url; //! -//! use oauth2::*; -//! use oauth2::basic::*; -//! use rand::{thread_rng, Rng}; +//! use oauth2::prelude::*; +//! use oauth2::{ +//! AuthUrl, +//! ClientId, +//! ClientSecret, +//! ResourceOwnerPassword, +//! ResourceOwnerUsername, +//! Scope, +//! TokenUrl +//! }; +//! use oauth2::basic::BasicClient; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { @@ -129,13 +150,12 @@ //! AuthUrl::new(Url::parse("http://authorize")?), //! TokenUrl::new(Url::parse("http://token")?) //! ) -//! .add_scope(Scope::new("read".to_string())) -//! .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect")?)); +//! .add_scope(Scope::new("read".to_string())); //! //! let token_result = //! client.exchange_password( -//! &EndUserUsername::new("user".to_string()), -//! &EndUserPassword::new("pass".to_string()) +//! &ResourceOwnerUsername::new("user".to_string()), +//! &ResourceOwnerPassword::new("pass".to_string()) //! ); //! # Ok(()) //! # } @@ -153,8 +173,15 @@ //! extern crate oauth2; //! extern crate url; //! -//! use oauth2::*; -//! use oauth2::basic::*; +//! use oauth2::prelude::*; +//! use oauth2::{ +//! AuthUrl, +//! ClientId, +//! ClientSecret, +//! Scope, +//! TokenUrl +//! }; +//! use oauth2::basic::BasicClient; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), Box> { @@ -165,8 +192,7 @@ //! AuthUrl::new(Url::parse("http://authorize")?), //! TokenUrl::new(Url::parse("http://token")?) //! ) -//! .add_scope(Scope::new("read".to_string())) -//! .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect")?)); +//! .add_scope(Scope::new("read".to_string())); //! //! let token_result = client.exchange_client_credentials(); //! # Ok(()) @@ -182,25 +208,31 @@ //! - [Github](https://github.com/alexcrichton/oauth2-rs/blob/master/examples/github.rs) //! +extern crate base64; extern crate curl; extern crate failure; #[macro_use] extern crate failure_derive; +extern crate rand; extern crate serde; #[macro_use] extern crate serde_derive; extern crate serde_json; extern crate url; -use curl::easy::Easy; -use serde::Serialize; -use serde::de::DeserializeOwned; use std::io::Read; use std::fmt::{Debug, Display, Formatter}; use std::fmt::Error as FormatterError; use std::marker::PhantomData; use std::ops::Deref; use std::time::Duration; + +use curl::easy::Easy; +use rand::{thread_rng, Rng}; +use serde::Serialize; +use serde::de::DeserializeOwned; use url::Url; +use prelude::*; + const CONTENT_TYPE_JSON: &str = "application/json"; /// @@ -218,199 +250,271 @@ pub enum AuthType { BasicAuth, } -pub trait NewType : Clone + Debug + Deref + PartialEq { - fn new(val: T) -> Self; -} - -pub trait SecretNewType : Debug { - fn new(val: T) -> Self where Self: Sized; - fn secret(&self) -> &T; -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct ClientId(String); -impl NewType for ClientId { - fn new(s: String) -> Self { - ClientId(s) - } -} -impl Deref for ClientId { - type Target = String; - fn deref(&self) -> &String { - &self.0 - } -} - -#[derive(Clone, Deserialize, PartialEq, Serialize)] -pub struct ClientSecret(String); -impl SecretNewType for ClientSecret { - fn new(s: String) -> Self { - ClientSecret(s) - } - fn secret(&self) -> &String { &self.0 } -} -impl Debug for ClientSecret { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - write!(f, "ClientSecret([redacted])") - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct AuthUrl(Url); -impl NewType for AuthUrl { - fn new(s: Url) -> Self { - AuthUrl(s) - } -} -impl Deref for AuthUrl { - type Target = Url; - fn deref(&self) -> &Url { - &self.0 - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct TokenUrl(Url); -impl NewType for TokenUrl { - fn new(s: Url) -> Self { - TokenUrl(s) - } -} -impl Deref for TokenUrl { - type Target = Url; - fn deref(&self) -> &Url { - &self.0 - } -} +/// +/// Crate prelude that should be wildcard-imported by crate users. +/// +pub mod prelude { + use std::fmt::Debug; + use std::ops::Deref; -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct Scope(String); -impl NewType for Scope { - fn new(s: String) -> Self { - Scope(s) - } -} -impl Deref for Scope { - type Target = String; - fn deref(&self) -> &String { - &self.0 + /// + /// New type to wrap a more primitive type in a more typesafe manner. + /// + pub trait NewType : Clone + Debug + Deref + PartialEq { + /// + /// Create a new instance to wrap the given `val`. + /// + fn new(val: T) -> Self; } -} -impl AsRef for Scope { - fn as_ref(&self) -> &str { self } -} -#[derive(Clone, Debug, PartialEq)] -pub struct RedirectUrl(Url); -impl NewType for RedirectUrl { - fn new(s: Url) -> Self { - RedirectUrl(s) - } -} -impl Deref for RedirectUrl { - type Target = Url; - fn deref(&self) -> &Url { - &self.0 + /// + /// New type representing a secret value to wrap a more primitive type in a more typesafe + /// manner. + /// + pub trait SecretNewType : Debug { + /// + /// Create a new instance to wrap the given `val`. + /// + fn new(val: T) -> Self where Self: Sized; + /// + /// Get the secret contained within this type. + /// + /// # Security Warning + /// + /// Leaking this value may compromise the security of the OAuth2 flow. + /// + fn secret(&self) -> &T; } } -#[derive(Clone, PartialEq)] -pub struct CsrfToken(String); -impl SecretNewType for CsrfToken { - fn new(s: String) -> Self { - CsrfToken(s) - } - fn secret(&self) -> &String { &self.0 } -} -impl Debug for CsrfToken { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - write!(f, "CsrfToken([redacted])") +macro_rules! new_type { + ( + $(#[$attr:meta])* + $name:ident($type:ty) + ) => { + new_type![ + $(#[$attr])* + $name($type) + concat!( + "Create a new `", + stringify!($name), + "` to wrap the given `", + stringify!($type), + "`." + ) + ]; + }; + ( + $(#[$attr:meta])* + $name:ident($type:ty) + $new_doc:expr + ) => { + $(#[$attr])* + #[derive(Clone, Debug, PartialEq)] + pub struct $name($type); + impl NewType<$type> for $name { + #[doc = $new_doc] + fn new(s: $type) -> Self { + $name(s) + } + } + impl Deref for $name { + type Target = $type; + fn deref(&self) -> &$type { + &self.0 + } + } } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct ResponseType(String); -impl NewType for ResponseType { - fn new(s: String) -> Self { - ResponseType(s) - } -} -impl Deref for ResponseType { - type Target = String; - fn deref(&self) -> &String { - &self.0 - } +macro_rules! new_secret_type { + ( + $(#[$attr:meta])* + $name:ident($type:ty) + ) => { + new_secret_type![ + $(#[$attr])* + $name($type) + impl {} + ]; + }; + ( + $(#[$attr:meta])* + $name:ident($type:ty) + impl { + $($item:tt)* + } + ) => { + new_secret_type![ + $(#[$attr])*, + $name($type), + concat!( + "Create a new `", + stringify!($name), + "` to wrap the given `", + stringify!($type), + "`." + ), + concat!("Get the secret contained within this `", stringify!($name), "`."), + impl { + $($item)* + } + ]; + }; + ( + $(#[$attr:meta])*, + $name:ident($type:ty), + $new_doc:expr, + $secret_doc:expr, + impl { + $($item:tt)* + } + ) => { + $( + #[$attr] + )* + #[derive(Clone, PartialEq)] + pub struct $name($type); + impl $name { + $($item)* + } + impl SecretNewType<$type> for $name { + #[doc = $new_doc] + fn new(s: $type) -> Self { + $name(s) + } + /// + #[doc = $secret_doc] + /// + /// # Security Warning + /// + /// Leaking this value may compromise the security of the OAuth2 flow. + /// + fn secret(&self) -> &$type { &self.0 } + } + impl Debug for $name { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, concat!(stringify!($name), "([redacted])")) + } + } + }; } -#[derive(Clone, Deserialize, PartialEq, Serialize)] -pub struct AuthorizationCode(String); -impl SecretNewType for AuthorizationCode { - fn new(s: String) -> Self { - AuthorizationCode(s) - } - fn secret(&self) -> &String { &self.0 } -} -impl Debug for AuthorizationCode { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - write!(f, "AuthorizationCode([redacted])") - } -} +new_type![ + /// + /// Client identifier issued to the client during the registration process described by + /// [Section 2.2](https://tools.ietf.org/html/rfc6749#section-2.2). + /// + #[derive(Deserialize, Serialize)] + ClientId(String) +]; -#[derive(Clone, Deserialize, PartialEq, Serialize)] -pub struct RefreshToken(String); -impl SecretNewType for RefreshToken { - fn new(s: String) -> Self { - RefreshToken(s) - } - fn secret(&self) -> &String { &self.0 } -} -impl Debug for RefreshToken { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - write!(f, "RefreshToken[redacted])") - } -} +new_type![ + /// + /// URL of the authorization server's authorization endpoint. + /// + AuthUrl(Url) +]; +new_type![ + /// + /// URL of the authorization server's token endpoint. + /// + TokenUrl(Url) +]; +new_type![ + /// + /// URL of the client's redirection endpoint. + /// + RedirectUrl(Url) +]; +new_type![ + /// + /// Authorization endpoint response (grant) type defined in + /// [Section 3.1.1](https://tools.ietf.org/html/rfc6749#section-3.1.1). + /// + #[derive(Deserialize, Serialize)] + ResponseType(String) +]; +new_type![ + /// + /// Resource owner's username used directly as an authorization grant to obtain an access + /// token. + /// + ResourceOwnerUsername(String) +]; -#[derive(Clone, Deserialize, PartialEq, Serialize)] -pub struct AccessToken(String); -impl SecretNewType for AccessToken { - fn new(s: String) -> Self { - AccessToken(s) - } - fn secret(&self) -> &String { &self.0 } -} -impl Debug for AccessToken { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - write!(f, "AccessToken([redacted])") - } +new_type![ + /// + /// Access token scope, as defined by the authorization server. + /// + #[derive(Deserialize, Serialize)] + Scope(String) +]; +impl AsRef for Scope { + fn as_ref(&self) -> &str { self } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct EndUserUsername(String); -impl NewType for EndUserUsername { - fn new(s: String) -> Self { - EndUserUsername(s) - } -} -impl Deref for EndUserUsername { - type Target = String; - fn deref(&self) -> &String { - &self.0 +new_secret_type![ + /// + /// Client password issued to the client during the registration process described by + /// [Section 2.2](https://tools.ietf.org/html/rfc6749#section-2.2). + /// + ClientSecret(String) +]; +new_secret_type![ + /// + /// Value used for [CSRF]((https://tools.ietf.org/html/rfc6749#section-10.12)) protection + /// via the `state` parameter. + /// + CsrfToken(String) + impl { + /// + /// Generate a new random, base64-encoded 128-bit CSRF token. + /// + pub fn new_random() -> Self { + CsrfToken::new_random_len(16) + } + /// + /// Generate a new random, base64-encoded CSRF token of the specified length. + /// + /// # Arguments + /// + /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. + /// + pub fn new_random_len(num_bytes: u32) -> Self { + let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); + CsrfToken::new(base64::encode(&random_bytes)) + } } -} +]; +new_secret_type![ + /// + /// Authorization code returned from the authorization endpoint. + /// + AuthorizationCode(String) +]; +new_secret_type![ + /// + /// Refresh token used to obtain a new access token (if supported by the authorization server). + /// + #[derive(Deserialize, Serialize)] + RefreshToken(String) +]; +new_secret_type![ + /// + /// Access token returned by the token endpoint and used to access protected resources. + /// + #[derive(Deserialize, Serialize)] + AccessToken(String) +]; +new_secret_type![ + /// + /// Resource owner's password used directly as an authorization grant to obtain an access + /// token. + /// + ResourceOwnerPassword(String) +]; -#[derive(Clone, Deserialize, PartialEq, Serialize)] -pub struct EndUserPassword(String); -impl SecretNewType for EndUserPassword { - fn new(s: String) -> Self { - EndUserPassword(s) - } - fn secret(&self) -> &String { &self.0 } -} -impl Debug for EndUserPassword { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - write!(f, "EndUserPassword([redacted])") - } -} /// /// Stores the configuration for an OAuth2 client. @@ -637,8 +741,11 @@ impl, TE: ErrorResponseType> Client { /// /// See https://tools.ietf.org/html/rfc6749#section-4.3.2 /// - pub fn exchange_password(&self, username: &EndUserUsername, password: &EndUserPassword) - -> Result> { + pub fn exchange_password( + &self, + username: &ResourceOwnerUsername, + password: &ResourceOwnerPassword + ) -> Result> { let params = vec![ ("grant_type", "password"), ("username", username), @@ -978,7 +1085,26 @@ pub enum RequestTokenError { /// ([RFC 6749](https://tools.ietf.org/html/rfc6749)). /// pub mod basic { - use super::*; + extern crate serde_json; + + use std::fmt::Error as FormatterError; + use std::fmt::{Debug, Display, Formatter}; + use std::time::Duration; + + use serde::de::DeserializeOwned; + + use super::{ + AccessToken, + Client, + ErrorResponse, + ErrorResponseType, + RefreshToken, + RequestTokenError, + Scope, + Token, + TokenType, + }; + use super::helpers; /// /// Basic OAuth2 client specialization, suitable for most applications. @@ -1127,7 +1253,14 @@ pub mod basic { /// Insecure methods -- not recommended for most applications. /// pub mod insecure { - use super::*; + use url::Url; + + use super::{ + Client, + ErrorResponseType, + Token, + TokenType, + }; /// /// Produces the full authorization URL used by the @@ -1140,7 +1273,7 @@ pub mod insecure { /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) attacks. /// It is highly recommended to use the `Client::authorize_url` function instead. /// - pub fn authorize_url(client: &super::Client) -> Url + pub fn authorize_url(client: &Client) -> Url where TT: TokenType, T: Token, TE: ErrorResponseType { client.authorize_url_impl("code", None, None) } @@ -1155,7 +1288,7 @@ pub mod insecure { /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) attacks. /// It is highly recommended to use the `Client::authorize_url_implicit` function instead. /// - pub fn authorize_url_implicit(client: &super::Client) -> Url + pub fn authorize_url_implicit(client: &Client) -> Url where TT: TokenType, T: Token, TE: ErrorResponseType { client.authorize_url_impl("token", None, None) } diff --git a/tests/lib.rs b/tests/lib.rs index 10d80efb..00d5ab68 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -360,8 +360,8 @@ fn test_exchange_password_with_json_response() { let token = client .exchange_password( - &EndUserUsername::new("user".to_string()), - &EndUserPassword::new("pass".to_string()) + &ResourceOwnerUsername::new("user".to_string()), + &ResourceOwnerPassword::new("pass".to_string()) ) .unwrap(); From 032a5f6971820815be1e84018632852da3b33992 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Tue, 3 Apr 2018 23:50:23 -0700 Subject: [PATCH 05/11] Return CsrfToken as #[must_use] from authorize_url() --- examples/github.rs | 5 +---- examples/google.rs | 5 +---- src/lib.rs | 32 ++++++++++++++++++++------------ tests/lib.rs | 33 +++++++++++++++++++++++++++------ 4 files changed, 49 insertions(+), 26 deletions(-) diff --git a/examples/github.rs b/examples/github.rs index 3dc6799a..cdd26ed7 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -74,11 +74,8 @@ fn main() { ) ); - // Generate a new random token for CSRF protection (each time!). - let csrf_state = CsrfToken::new_random(); - // Generate the authorization URL to which we'll redirect the user. - let authorize_url = client.authorize_url(&csrf_state); + let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random); println!("Open this URL in your browser:\n{}\n", authorize_url.to_string()); diff --git a/examples/google.rs b/examples/google.rs index 00f9c49f..b0fed3b2 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -73,11 +73,8 @@ fn main() { ) ); - // Generate a new random token for CSRF protection (each time!). - let csrf_state = CsrfToken::new_random(); - // Generate the authorization URL to which we'll redirect the user. - let authorize_url = client.authorize_url(&csrf_state); + let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random); println!("Open this URL in your browser:\n{}\n", authorize_url.to_string()); diff --git a/src/lib.rs b/src/lib.rs index 1acb75d0..09ca3f12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,13 +46,12 @@ //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_url(RedirectUrl::new(Url::parse("http://redirect")?)); //! -//! // Generate a new random token for CSRF protection (each time!). -//! let csrf_state = CsrfToken::new_random(); -//! //! // Generate the full authorization URL. +//! let (auth_url, csrf_token) = client.authorize_url(CsrfToken::new_random); +//! //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. -//! println!("Browse to: {}", client.authorize_url(&csrf_state)); +//! println!("Browse to: {}", auth_url); //! //! // Once the user has been redirected to the redirect URL, you'll have access to the //! // authorization code. For security reasons, your code should verify that the `state` @@ -104,13 +103,17 @@ //! TokenUrl::new(Url::parse("http://token")?) //! ); //! -//! // Generate a new random token for CSRF protection (each time!). -//! let csrf_state = CsrfToken::new_random(); -//! //! // Generate the full authorization URL. +//! let (auth_url, csrf_token) = client.authorize_url_implicit(CsrfToken::new_random); +//! //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. -//! println!("Browse to: {}", client.authorize_url_implicit(&csrf_state)); +//! println!("Browse to: {}", auth_url); +//! +//! // Once the user has been redirected to the redirect URL, you'll have the access code. +//! // For security reasons, your code should verify that the `state` parameter returned by the +//! // server matches `csrf_state`. +//! //! # Ok(()) //! # } //! # fn main() {} @@ -466,6 +469,7 @@ new_secret_type![ /// Value used for [CSRF]((https://tools.ietf.org/html/rfc6749#section-10.12)) protection /// via the `state` parameter. /// + #[must_use] CsrfToken(String) impl { /// @@ -624,8 +628,10 @@ impl, TE: ErrorResponseType> Client { /// attacks. To disable CSRF protections (NOT recommended), use `insecure::authorize_url` /// instead. /// - pub fn authorize_url(&self, state: &CsrfToken) -> Url { - self.authorize_url_impl("code", Some(state), None) + pub fn authorize_url(&self, state_fn: F) -> (Url, CsrfToken) + where F: Fn() -> CsrfToken { + let state = state_fn(); + (self.authorize_url_impl("code", Some(&state), None), state) } /// @@ -647,8 +653,10 @@ impl, TE: ErrorResponseType> Client { /// attacks. To disable CSRF protections (NOT recommended), use /// `insecure::authorize_url_implicit` instead. /// - pub fn authorize_url_implicit(&self, state: &CsrfToken) -> Url { - self.authorize_url_impl("token", Some(state), None) + pub fn authorize_url_implicit(&self, state_fn: F) -> (Url, CsrfToken) + where F: Fn() -> CsrfToken { + let state = state_fn(); + (self.authorize_url_impl("token", Some(&state), None), state) } /// diff --git a/tests/lib.rs b/tests/lib.rs index 00d5ab68..6fa96e6f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -6,9 +6,12 @@ extern crate serde; extern crate serde_json; use mockito::{mock, SERVER_URL}; +use url::Url; +use url::form_urlencoded::byte_serialize; + +use oauth2::prelude::*; use oauth2::*; use oauth2::basic::*; -use url::Url; fn new_client() -> BasicClient { BasicClient::new( @@ -31,7 +34,7 @@ fn new_mock_client() -> BasicClient { #[test] fn test_authorize_url() { let client = new_client(); - let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); + let (url, _) = client.authorize_url(|| CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -41,6 +44,24 @@ fn test_authorize_url() { ); } +#[test] +fn test_authorize_random() { + let client = new_client(); + let (url, csrf_state) = client.authorize_url(CsrfToken::new_random); + + assert_eq!( + Url::parse( + &format!( + "http://example.com/auth?response_type=code&client_id=aaa&state={}", + byte_serialize(csrf_state.secret().clone().into_bytes().as_slice()) + .collect::>() + .join("") + ) + ).unwrap(), + url + ); +} + #[test] fn test_authorize_url_insecure() { let client = new_client(); @@ -57,7 +78,7 @@ fn test_authorize_url_insecure() { fn test_authorize_url_implicit() { let client = new_client(); - let url = client.authorize_url_implicit(&CsrfToken::new("csrf_token".to_string())); + let (url, _) = client.authorize_url_implicit(|| CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -89,7 +110,7 @@ fn test_authorize_url_with_param() { TokenUrl::new(Url::parse("http://example.com/token").unwrap()) ); - let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); + let (url, _) = client.authorize_url(|| CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -106,7 +127,7 @@ fn test_authorize_url_with_scopes() { .add_scope(Scope::new("read".to_string())) .add_scope(Scope::new("write".to_string())); - let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); + let (url, _) = client.authorize_url(|| CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( @@ -139,7 +160,7 @@ fn test_authorize_url_with_redirect_url() { new_client() .set_redirect_url(RedirectUrl::new(Url::parse("http://localhost/redirect").unwrap())); - let url = client.authorize_url(&CsrfToken::new("csrf_token".to_string())); + let (url, _) = client.authorize_url(|| CsrfToken::new("csrf_token".to_string())); assert_eq!( Url::parse( From a86206dafda7477a1710c67c215e814fb00df1cf Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Wed, 4 Apr 2018 00:05:51 -0700 Subject: [PATCH 06/11] Export new_type macros --- src/lib.rs | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 09ca3f12..e699b772 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -290,31 +290,60 @@ pub mod prelude { } } -macro_rules! new_type { +#[macro_export] macro_rules! new_type { ( $(#[$attr:meta])* $name:ident($type:ty) ) => { new_type![ - $(#[$attr])* - $name($type) + $(#[$attr])*, + $name($type), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." - ) + ), + impl {} ]; }; ( $(#[$attr:meta])* $name:ident($type:ty) - $new_doc:expr + impl { + $($item:tt)* + } + ) => { + new_type![ + $(#[$attr])*, + $name($type), + concat!( + "Create a new `", + stringify!($name), + "` to wrap the given `", + stringify!($type), + "`." + ), + impl { + $($item)* + } + ]; + }; + ( + $(#[$attr:meta])*, + $name:ident($type:ty), + $new_doc:expr, + impl { + $($item:tt)* + } ) => { $(#[$attr])* #[derive(Clone, Debug, PartialEq)] pub struct $name($type); + impl $name { + $($item)* + } impl NewType<$type> for $name { #[doc = $new_doc] fn new(s: $type) -> Self { @@ -330,7 +359,7 @@ macro_rules! new_type { } } -macro_rules! new_secret_type { +#[macro_export] macro_rules! new_secret_type { ( $(#[$attr:meta])* $name:ident($type:ty) From 3f7a1df2802982e5002ee984cdc6623f46ba7329 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Sun, 8 Apr 2018 23:16:29 -0700 Subject: [PATCH 07/11] Support type attrs in new_type! macro --- src/lib.rs | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e699b772..08e26e52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -291,13 +291,20 @@ pub mod prelude { } #[macro_export] macro_rules! new_type { + // Convenience pattern without an impl. ( $(#[$attr:meta])* - $name:ident($type:ty) + $name:ident( + $(#[$type_attr:meta])* + $type:ty + ) ) => { new_type![ - $(#[$attr])*, - $name($type), + @new_type $(#[$attr])*, + $name( + $(#[$type_attr])* + $type + ), concat!( "Create a new `", stringify!($name), @@ -308,16 +315,23 @@ pub mod prelude { impl {} ]; }; + // Main entry point with an impl. ( $(#[$attr:meta])* - $name:ident($type:ty) + $name:ident( + $(#[$type_attr:meta])* + $type:ty + ) impl { $($item:tt)* } ) => { new_type![ - $(#[$attr])*, - $name($type), + @new_type $(#[$attr])*, + $name( + $(#[$type_attr])* + $type + ), concat!( "Create a new `", stringify!($name), @@ -330,9 +344,13 @@ pub mod prelude { } ]; }; + // Actual implementation, after stringifying the #[doc] attr. ( - $(#[$attr:meta])*, - $name:ident($type:ty), + @new_type $(#[$attr:meta])*, + $name:ident( + $(#[$type_attr:meta])* + $type:ty + ), $new_doc:expr, impl { $($item:tt)* @@ -340,7 +358,10 @@ pub mod prelude { ) => { $(#[$attr])* #[derive(Clone, Debug, PartialEq)] - pub struct $name($type); + pub struct $name( + $(#[$type_attr])* + $type + ); impl $name { $($item)* } From 0092e2ef9fe73f019cf3c31adba98f173a303520 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Mon, 9 Apr 2018 00:13:56 -0700 Subject: [PATCH 08/11] Add URL serializer/deserializer --- src/lib.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 08e26e52..8021b64d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -463,22 +463,43 @@ new_type![ ]; new_type![ + #[derive(Deserialize, Serialize)] /// /// URL of the authorization server's authorization endpoint. /// - AuthUrl(Url) + AuthUrl( + #[serde( + deserialize_with = "helpers::deserialize_url", + serialize_with = "helpers::serialize_url" + )] + Url + ) ]; new_type![ + #[derive(Deserialize, Serialize)] /// /// URL of the authorization server's token endpoint. /// - TokenUrl(Url) + TokenUrl( + #[serde( + deserialize_with = "helpers::deserialize_url", + serialize_with = "helpers::serialize_url" + )] + Url + ) ]; new_type![ + #[derive(Deserialize, Serialize)] /// /// URL of the client's redirection endpoint. /// - RedirectUrl(Url) + RedirectUrl( + #[serde( + deserialize_with = "helpers::deserialize_url", + serialize_with = "helpers::serialize_url" + )] + Url + ) ]; new_type![ /// @@ -1357,6 +1378,7 @@ pub mod insecure { /// pub mod helpers { use serde::{Deserialize, Deserializer, Serializer}; + use url::Url; /// /// Serde case-insensitive deserializer for an untagged `enum`. @@ -1481,4 +1503,27 @@ pub mod helpers { serializer.serialize_none() } } + + /// + /// Serde string deserializer for a `Url`. + /// + pub fn deserialize_url<'de, D>( + deserializer: D + ) -> Result + where D: Deserializer<'de> { + use serde::de::Error; + let url_str = String::deserialize(deserializer)?; + Url::parse(url_str.as_ref()).map_err(Error::custom) + } + + /// + /// Serde string serializer for a `Url`. + /// + pub fn serialize_url( + url: &Url, + serializer: S + ) -> Result + where S: Serializer { + serializer.serialize_str(url.as_str()) + } } From d407cb0465631f5f017e60b05aaa3a5c73d385ae Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Tue, 10 Apr 2018 23:00:48 -0700 Subject: [PATCH 09/11] Add variant_name() helper function. --- src/lib.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 160 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8021b64d..fda3381d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1288,18 +1288,6 @@ pub mod basic { /// InvalidScope, } - impl BasicErrorResponseType { - fn to_str(&self) -> &str { - match *self { - BasicErrorResponseType::InvalidRequest => "invalid_request", - BasicErrorResponseType::InvalidClient => "invalid_client", - BasicErrorResponseType::InvalidGrant => "invalid_grant", - BasicErrorResponseType::UnauthorizedClient => "unauthorized_client", - BasicErrorResponseType::UnsupportedGrantType => "unsupported_grant_type", - BasicErrorResponseType::InvalidScope => "invalid_scope", - } - } - } impl ErrorResponseType for BasicErrorResponseType {} @@ -1311,9 +1299,7 @@ pub mod basic { impl Display for BasicErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - let message: &str = self.to_str(); - - write!(f, "{}", message) + write!(f, "{}", helpers::variant_name(&self)) } } @@ -1377,7 +1363,11 @@ pub mod insecure { /// Helper methods used by OAuth2 implementations/extensions. /// pub mod helpers { - use serde::{Deserialize, Deserializer, Serializer}; + use std; + + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde::ser; + use serde::ser::{Impossible, SerializeStructVariant, SerializeTupleVariant}; use url::Url; /// @@ -1526,4 +1516,158 @@ pub mod helpers { where S: Serializer { serializer.serialize_str(url.as_str()) } + + /// + /// Serde string serializer for an enum. + /// + /// Source: + /// [https://github.com/serde-rs/serde/issues/553](https://github.com/serde-rs/serde/issues/553) + /// + pub fn variant_name(t: &T) -> &'static str { + #[derive(Debug)] + struct NotEnum; + type Result = std::result::Result; + impl std::error::Error for NotEnum { + fn description(&self) -> &str { "not struct" } + } + impl std::fmt::Display for NotEnum { + fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result { unimplemented!() } + } + impl ser::Error for NotEnum { + fn custom(_msg: T) -> Self { NotEnum } + } + + struct VariantName; + impl Serializer for VariantName { + type Ok = &'static str; + type Error = NotEnum; + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Enum; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Enum; + fn serialize_bool(self, _v: bool) -> Result { Err(NotEnum) } + fn serialize_i8(self, _v: i8) -> Result { Err(NotEnum) } + fn serialize_i16(self, _v: i16) -> Result { Err(NotEnum) } + fn serialize_i32(self, _v: i32) -> Result { Err(NotEnum) } + fn serialize_i64(self, _v: i64) -> Result { Err(NotEnum) } + fn serialize_u8(self, _v: u8) -> Result { Err(NotEnum) } + fn serialize_u16(self, _v: u16) -> Result { Err(NotEnum) } + fn serialize_u32(self, _v: u32) -> Result { Err(NotEnum) } + fn serialize_u64(self, _v: u64) -> Result { Err(NotEnum) } + fn serialize_f32(self, _v: f32) -> Result { Err(NotEnum) } + fn serialize_f64(self, _v: f64) -> Result { Err(NotEnum) } + fn serialize_char(self, _v: char) -> Result { Err(NotEnum) } + fn serialize_str(self, _v: &str) -> Result { Err(NotEnum) } + fn serialize_bytes(self, _v: &[u8]) -> Result { Err(NotEnum) } + fn serialize_none(self) -> Result { Err(NotEnum) } + fn serialize_some(self, _value: &T) -> Result { + Err(NotEnum) + } + fn serialize_unit(self) -> Result { Err(NotEnum) } + fn serialize_unit_struct(self, _name: &'static str) -> Result { Err(NotEnum) } + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str + ) -> Result { + Ok(variant) + } + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T + ) -> Result { + Err(NotEnum) + } + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _value: &T + ) -> Result { + Ok(variant) + } + fn serialize_seq( + self, + _len: Option + ) -> Result { + Err(NotEnum) + } + fn serialize_tuple(self, _len: usize) -> Result { Err(NotEnum) } + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize + ) -> Result { + Err(NotEnum) + } + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize + ) -> Result { + Ok(Enum(variant)) + } + fn serialize_map( + self, + _len: Option + ) -> Result { + Err(NotEnum) + } + fn serialize_struct( + self, + _name: &'static str, + _len: usize + ) -> Result { + Err(NotEnum) + } + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize + ) -> Result { + Ok(Enum(variant)) + } + } + + struct Enum(&'static str); + impl SerializeStructVariant for Enum { + type Ok = &'static str; + type Error = NotEnum; + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T + ) -> Result<()> { + Ok(()) + } + fn end(self) -> Result { + Ok(self.0) + } + } + impl SerializeTupleVariant for Enum { + type Ok = &'static str; + type Error = NotEnum; + fn serialize_field( + &mut self, + _value: &T + ) -> Result<()> { + Ok(()) + } + fn end(self) -> Result { + Ok(self.0) + } + } + + t.serialize(VariantName).unwrap() + } } From 2bee99209ffaf2ddc1bfe3bc1577f470282aedf7 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Sat, 14 Apr 2018 18:26:24 -0700 Subject: [PATCH 10/11] Don't export macros --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fda3381d..a3666b25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -290,7 +290,7 @@ pub mod prelude { } } -#[macro_export] macro_rules! new_type { +macro_rules! new_type { // Convenience pattern without an impl. ( $(#[$attr:meta])* @@ -380,7 +380,7 @@ pub mod prelude { } } -#[macro_export] macro_rules! new_secret_type { +macro_rules! new_secret_type { ( $(#[$attr:meta])* $name:ident($type:ty) From d658a0dbbd676ebce39c8f2bf6d9f3c233caa87c Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Sat, 14 Apr 2018 22:57:46 -0700 Subject: [PATCH 11/11] Derive Serialize, Deserialize for ClientSecret --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index a3666b25..6da0b826 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -533,6 +533,7 @@ new_secret_type![ /// Client password issued to the client during the registration process described by /// [Section 2.2](https://tools.ietf.org/html/rfc6749#section-2.2). /// + #[derive(Deserialize, Serialize)] ClientSecret(String) ]; new_secret_type![