From ac156bc7b4d10fe4a16b808497bf63fb64ea8743 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 21:31:14 -0500 Subject: [PATCH 01/54] Begin rewrite --- examples/github.rs | 24 ------ examples/google.rs | 31 ------- examples/imgur.rs | 24 ------ src/client.rs | 206 --------------------------------------------- src/error.rs | 135 ----------------------------- src/lib.rs | 132 ----------------------------- src/provider.rs | 36 -------- src/token.rs | 122 --------------------------- 8 files changed, 710 deletions(-) delete mode 100644 examples/github.rs delete mode 100644 examples/google.rs delete mode 100644 examples/imgur.rs delete mode 100644 src/client.rs delete mode 100644 src/error.rs delete mode 100644 src/provider.rs delete mode 100644 src/token.rs diff --git a/examples/github.rs b/examples/github.rs deleted file mode 100644 index 4461482..0000000 --- a/examples/github.rs +++ /dev/null @@ -1,24 +0,0 @@ -extern crate inth_oauth2; - -use std::io; -use inth_oauth2::{Client, GitHub}; - -fn main() { - let client = Client::::new( - Default::default(), - "01774654cd9a6051e478", - "9f14d16d95d605e715ec1a9aecec220d2565fd5c", - Some("https://cmcenroe.me/oauth2-paste/") - ); - - let auth_uri = client.auth_uri(Some("user"), None).unwrap(); - - println!("{}", auth_uri); - - let mut code = String::new(); - io::stdin().read_line(&mut code).unwrap(); - - let token_pair = client.request_token(code.trim()).unwrap(); - - println!("{:?}", token_pair); -} diff --git a/examples/google.rs b/examples/google.rs deleted file mode 100644 index a64ebaf..0000000 --- a/examples/google.rs +++ /dev/null @@ -1,31 +0,0 @@ -extern crate inth_oauth2; - -use std::io; -use inth_oauth2::{Client, Google}; - -fn main() { - let client = Client::::new( - Default::default(), - "143225766783-ip2d9qv6sdr37276t77luk6f7bhd6bj5.apps.googleusercontent.com", - "3kZ5WomzHFlN2f_XbhkyPd3o", - Some("urn:ietf:wg:oauth:2.0:oob") - ); - - let auth_uri = client.auth_uri( - Some("https://www.googleapis.com/auth/userinfo.email"), - None - ).unwrap(); - - println!("{}", auth_uri); - - let mut code = String::new(); - io::stdin().read_line(&mut code).unwrap(); - - let token_pair = client.request_token(code.trim()).unwrap(); - - println!("{:?}", token_pair); - - let refreshed = client.refresh_token(token_pair.refresh.unwrap(), None).unwrap(); - - println!("{:?}", refreshed); -} diff --git a/examples/imgur.rs b/examples/imgur.rs deleted file mode 100644 index 08b0dea..0000000 --- a/examples/imgur.rs +++ /dev/null @@ -1,24 +0,0 @@ -extern crate inth_oauth2; - -use std::io; -use inth_oauth2::{Client, Imgur}; - -fn main() { - let client = Client::::new( - Default::default(), - "505c8ca804230e0", - "c898d8cf28404102752b2119a3a1c6aab49899c8", - Some("https://cmcenroe.me/oauth2-paste/") - ); - - let auth_uri = client.auth_uri(None, None).unwrap(); - - println!("{}", auth_uri); - - let mut code = String::new(); - io::stdin().read_line(&mut code).unwrap(); - - let token_pair = client.request_token(code.trim()).unwrap(); - - println!("{:?}", token_pair); -} diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 13264a7..0000000 --- a/src/client.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::io::Read; -use std::marker::PhantomData; - -use chrono::{UTC, Duration}; -use hyper::{self, header, mime}; -use rustc_serialize::json; -use url::{Url, form_urlencoded}; - -use super::Provider; -use super::{TokenPair, AccessTokenType, AccessToken, RefreshToken}; -use super::error::{Error, Result, OAuth2Error, OAuth2ErrorCode}; - -/// OAuth 2.0 client. -/// -/// Performs HTTP requests using the provided `hyper::Client`. -/// -/// See [RFC6749 section 4.1](http://tools.ietf.org/html/rfc6749#section-4.1). -pub struct Client { - http_client: hyper::Client, - - client_id: String, - client_secret: String, - redirect_uri: Option, - - provider: PhantomData

, -} - -impl Client

{ - /// Creates an OAuth 2.0 client. - pub fn new( - http_client: hyper::Client, - client_id: S, - client_secret: S, - redirect_uri: Option - ) -> Self where S: Into { - Client { - http_client: http_client, - client_id: client_id.into(), - client_secret: client_secret.into(), - redirect_uri: redirect_uri.map(Into::into), - provider: PhantomData, - } - } -} - -#[derive(RustcDecodable)] -struct TokenResponse { - access_token: String, - token_type: String, - expires_in: Option, - refresh_token: Option, - scope: Option, -} - -impl Into for TokenResponse { - fn into(self) -> TokenPair { - TokenPair { - access: AccessToken { - token: self.access_token, - token_type: match &self.token_type[..] { - "Bearer" | "bearer" => AccessTokenType::Bearer, - _ => AccessTokenType::Unrecognized(self.token_type), - }, - expires: self.expires_in.map(|s| UTC::now() + Duration::seconds(s)), - scope: self.scope, - }, - refresh: self.refresh_token.map(|t| RefreshToken { token: t }), - } - } -} - -#[derive(RustcDecodable)] -struct ErrorResponse { - error: String, - error_description: Option, - error_uri: Option, -} - -impl Into for ErrorResponse { - fn into(self) -> OAuth2Error { - let code = match &self.error[..] { - "invalid_request" => OAuth2ErrorCode::InvalidRequest, - "invalid_client" => OAuth2ErrorCode::InvalidClient, - "invalid_grant" => OAuth2ErrorCode::InvalidGrant, - "unauthorized_client" => OAuth2ErrorCode::UnauthorizedClient, - "unsupported_grant_type" => OAuth2ErrorCode::UnsupportedGrantType, - "invalid_scope" => OAuth2ErrorCode::InvalidScope, - _ => OAuth2ErrorCode::Unrecognized(self.error), - }; - OAuth2Error { - code: code, - description: self.error_description, - uri: self.error_uri, - } - } -} - -impl Client

{ - /// Constructs an authorization request URI. - /// - /// See [RFC6749 section 4.1.1](http://tools.ietf.org/html/rfc6749#section-4.1.1). - pub fn auth_uri(&self, scope: Option<&str>, state: Option<&str>) -> Result { - let mut uri = try!(Url::parse(P::auth_uri())); - - let mut query_pairs = vec![ - ("response_type", "code"), - ("client_id", &self.client_id), - ]; - if let Some(ref redirect_uri) = self.redirect_uri { - query_pairs.push(("redirect_uri", redirect_uri)); - } - if let Some(scope) = scope { - query_pairs.push(("scope", scope)); - } - if let Some(state) = state { - query_pairs.push(("state", state)); - } - - uri.set_query_from_pairs(query_pairs); - - Ok(uri.serialize()) - } - - fn auth_header(&self) -> header::Authorization { - header::Authorization( - header::Basic { - username: self.client_id.clone(), - password: Some(self.client_secret.clone()), - } - ) - } - - fn accept_header(&self) -> header::Accept { - header::Accept(vec![ - header::qitem( - mime::Mime( - mime::TopLevel::Application, - mime::SubLevel::Json, - vec![] - ) - ), - ]) - } - - fn token_post(&self, body_pairs: Vec<(&str, &str)>) -> Result { - let post_body = form_urlencoded::serialize(body_pairs); - let request = self.http_client.post(P::token_uri()) - .header(self.auth_header()) - .header(self.accept_header()) - .header(header::ContentType::form_url_encoded()) - .body(&post_body); - - let mut response = try!(request.send()); - let mut body = String::new(); - try!(response.read_to_string(&mut body)); - - let token = json::decode::(&body); - if let Ok(token) = token { - return Ok(token.into()); - } - - let error: ErrorResponse = try!(json::decode(&body)); - Err(Error::OAuth2(error.into())) - } - - /// Requests an access token using an authorization code. - /// - /// See [RFC6749 section 4.1.3](http://tools.ietf.org/html/rfc6749#section-4.1.3). - pub fn request_token(&self, code: &str) -> Result { - let mut body_pairs = vec![ - ("grant_type", "authorization_code"), - ("code", code), - ]; - if let Some(ref redirect_uri) = self.redirect_uri { - body_pairs.push(("redirect_uri", redirect_uri)); - } - self.token_post(body_pairs) - } - - /// Refreshes an access token. - /// - /// The returned `TokenPair` will always have a `refresh`. - /// - /// See [RFC6749 section 6](http://tools.ietf.org/html/rfc6749#section-6). - pub fn refresh_token(&self, refresh: RefreshToken, scope: Option<&str>) -> Result { - let mut result = { - let mut body_pairs = vec![ - ("grant_type", "refresh_token"), - ("refresh_token", &refresh.token), - ]; - if let Some(scope) = scope { - body_pairs.push(("scope", scope)); - } - - self.token_post(body_pairs) - }; - - if let Ok(ref mut pair) = result { - if pair.refresh.is_none() { - pair.refresh = Some(refresh); - } - } - - result - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index b05e7c9..0000000 --- a/src/error.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::{error, fmt, io, result}; - -use hyper; -use rustc_serialize::json; -use url; - -/// OAuth 2.0 error codes. -/// -/// See [RFC6749 section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). -#[derive(Debug, Clone)] -pub enum OAuth2ErrorCode { - /// The request is missing a required parameter, includes an unsupported parameter value (other - /// than grant type), repeats a parameter, includes multiple credentials, utilizes more than - /// one mechanism for authenticating the client, or is otherwise malformed. - InvalidRequest, - - /// Client authentication failed (e.g., unknown client, no client authentication included, or - /// unsupported authentication method). - InvalidClient, - - /// The provided authorization grant (e.g., authorization code, resource owner credentials) or - /// refresh token is invalid, expired, revoked, does not match the redirection URI used in the - /// authorization request, or was issued to another client. - InvalidGrant, - - /// The authenticated client is not authorized to use this authorization grant type. - UnauthorizedClient, - - /// The authorization grant type is not supported by the authorization server. - UnsupportedGrantType, - - /// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the - /// resource owner. - InvalidScope, - - /// An unrecognized error code, not defined in RFC6749. - Unrecognized(String), -} - -/// OAuth 2.0 error. -/// -/// See [RFC6749 section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). -#[derive(Debug, Clone)] -pub struct OAuth2Error { - /// Error code. - pub code: OAuth2ErrorCode, - - /// Human-readable text providing additional information about the error. - pub description: Option, - - /// A URI identifying a human-readable web page with information about the error. - pub uri: Option, -} - -impl fmt::Display for OAuth2Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - try!(write!(f, "{:?}", self.code)); - if let Some(ref description) = self.description { - try!(write!(f, ": {}", description)); - } - if let Some(ref uri) = self.uri { - try!(write!(f, " ({})", uri)); - } - Ok(()) - } -} - -impl error::Error for OAuth2Error { - fn description(&self) -> &str { - "OAuth2 API error" - } -} - -/// Errors that can occur during authentication flow. -#[derive(Debug)] -pub enum Error { - Io(io::Error), - Url(url::ParseError), - Hyper(hyper::Error), - Json(json::DecoderError), - OAuth2(OAuth2Error), -} - -/// Result type returned from authentication flow methods. -pub type Result = result::Result; - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Error::Io(ref err) => write!(f, "{}", err), - Error::Url(ref err) => write!(f, "{}", err), - Error::Hyper(ref err) => write!(f, "{}", err), - Error::Json(ref err) => write!(f, "{}", err), - Error::OAuth2(ref err) => write!(f, "{}", err), - } - } -} - -impl error::Error for Error { - fn description(&self) -> &str { - match *self { - Error::Io(_) => "OAuth2 IO error", - Error::Url(_) => "OAuth2 URL error", - Error::Hyper(_) => "OAuth2 Hyper error", - Error::Json(_) => "OAuth2 JSON error", - Error::OAuth2(_) => "OAuth2 API error", - } - } - - fn cause(&self) -> Option<&error::Error> { - match *self { - Error::Io(ref err) => Some(err), - Error::Url(ref err) => Some(err), - Error::Hyper(ref err) => Some(err), - Error::Json(ref err) => Some(err), - Error::OAuth2(ref err) => Some(err), - } - } -} - -macro_rules! impl_from { - ($v:path, $t:ty) => { - impl From<$t> for Error { - fn from(err: $t) -> Error { - $v(err) - } - } - } -} - -impl_from!(Error::Io, io::Error); -impl_from!(Error::Url, url::ParseError); -impl_from!(Error::Hyper, hyper::Error); -impl_from!(Error::Json, json::DecoderError); -impl_from!(Error::OAuth2, OAuth2Error); diff --git a/src/lib.rs b/src/lib.rs index b91533b..2132d39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,136 +1,4 @@ -//! # "It's not that hard" OAuth2 Client -//! -//! OAuth 2.0 really isn't that hard, you know? -//! -//! Implementation of [RFC6749](http://tools.ietf.org/html/rfc6749). -//! -//! `inth_oauth2` is on [Crates.io][crate] and [GitHub][github]. -//! -//! [crate]: https://crates.io/crates/inth-oauth2 -//! [github]: https://github.com/programble/inth-oauth2 -//! -//! ## Providers -//! -//! `inth_oauth2` supports the following OAuth 2.0 providers: -//! -//! - `Google` -//! - `GitHub` -//! - `Imgur` -//! -//! Support for others can be added by implementing the `Provider` trait. -//! -//! ## Examples -//! -//! ### Creating a client -//! -//! ``` -//! use inth_oauth2::{Client, Google}; -//! -//! let client = Client::::new( -//! Default::default(), -//! "CLIENT_ID", -//! "CLIENT_SECRET", -//! Some("REDIRECT_URI") -//! ); -//! ``` -//! -//! ### Constructing an authorization URI -//! -//! ``` -//! # use inth_oauth2::{Client, Google}; -//! # let client = Client::::new(Default::default(), "", "", None); -//! let auth_uri = client.auth_uri(Some("scope"), Some("state")).unwrap(); -//! ``` -//! -//! Direct the user to an authorization URI to have them authorize your application. -//! -//! ### Requesting an access token -//! -//! Request an access token using a code obtained from the redirect of the authorization URI. -//! -//! ```no_run -//! # use inth_oauth2::{Client, Google}; -//! # let client = Client::::new(Default::default(), "", "", None); -//! # let code = String::new(); -//! let token_pair = client.request_token(&code).unwrap(); -//! println!("{}", token_pair.access.token); -//! ``` -//! -//! ### Refreshing an access token -//! -//! ```no_run -//! # use inth_oauth2::{Client, Google}; -//! # let client = Client::::new(Default::default(), "", "", None); -//! # let mut token_pair = client.request_token("").unwrap(); -//! if token_pair.expired() { -//! if let Some(refresh) = token_pair.refresh { -//! token_pair = client.refresh_token(refresh, None).unwrap(); -//! } -//! } -//! ``` -//! -//! ### Using bearer access tokens -//! -//! If the obtained token is of the `Bearer` type, a Hyper `Authorization` header can be created -//! from it. -//! -//! ```no_run -//! # extern crate hyper; -//! # extern crate inth_oauth2; -//! # fn main() { -//! # use inth_oauth2::{Client, Google}; -//! # let client = Client::::new(Default::default(), "", "", None); -//! # let mut token_pair = client.request_token("").unwrap(); -//! let client = hyper::Client::new(); -//! let res = client.get("https://example.com/resource") -//! .header(token_pair.to_bearer_header().unwrap()) -//! .send() -//! .unwrap(); -//! # } -//! ``` -//! -//! ### Persisting tokens -//! -//! `TokenPair` implements `Encodable` and `Decodable` from `rustc_serialize`, so can be persisted -//! as JSON. -//! -//! ``` -//! # extern crate inth_oauth2; -//! # extern crate rustc_serialize; -//! # extern crate chrono; -//! use inth_oauth2::{TokenPair, AccessTokenType, AccessToken, RefreshToken}; -//! use rustc_serialize::json; -//! # use chrono::{UTC, Timelike}; -//! # fn main() { -//! # let token_pair = TokenPair { -//! # access: AccessToken { -//! # token: String::from("AAAAAAAA"), -//! # token_type: AccessTokenType::Bearer, -//! # expires: Some(UTC::now().with_nanosecond(0).unwrap()), -//! # scope: None, -//! # }, -//! # refresh: Some(RefreshToken { token: String::from("BBBBBBBB") }), -//! # }; -//! -//! let json = json::encode(&token_pair).unwrap(); -//! let decoded: TokenPair = json::decode(&json).unwrap(); -//! assert_eq!(token_pair, decoded); -//! # } -//! ``` - extern crate chrono; extern crate hyper; extern crate rustc_serialize; extern crate url; - -pub use client::Client; -pub mod client; - -pub use provider::{Provider, Google, GitHub, Imgur}; -pub mod provider; - -pub use token::{TokenPair, AccessTokenType, AccessToken, RefreshToken}; -pub mod token; - -pub use error::{Error, Result}; -pub mod error; diff --git a/src/provider.rs b/src/provider.rs deleted file mode 100644 index f4e6141..0000000 --- a/src/provider.rs +++ /dev/null @@ -1,36 +0,0 @@ -/// An OAuth 2.0 provider. -pub trait Provider { - /// The authorization endpoint URI. - fn auth_uri() -> &'static str; - - /// The token endpoint URI. - fn token_uri() -> &'static str; -} - -/// Google OAuth 2.0 provider. -/// -/// See [Using OAuth 2.0 to Access Google -/// APIs](https://developers.google.com/identity/protocols/OAuth2). -pub struct Google; -impl Provider for Google { - fn auth_uri() -> &'static str { "https://accounts.google.com/o/oauth2/auth" } - fn token_uri() -> &'static str { "https://accounts.google.com/o/oauth2/token" } -} - -/// GitHub OAuth 2.0 provider. -/// -/// See [OAuth, GitHub API](https://developer.github.com/v3/oauth/). -pub struct GitHub; -impl Provider for GitHub { - fn auth_uri() -> &'static str { "https://github.com/login/oauth/authorize" } - fn token_uri() -> &'static str { "https://github.com/login/oauth/access_token" } -} - -/// Imgur OAuth 2.0 provider. -/// -/// See [OAuth 2.0, Imgur](https://api.imgur.com/oauth2). -pub struct Imgur; -impl Provider for Imgur { - fn auth_uri() -> &'static str { "https://api.imgur.com/oauth2/authorize" } - fn token_uri() -> &'static str { "https://api.imgur.com/oauth2/token" } -} diff --git a/src/token.rs b/src/token.rs deleted file mode 100644 index 9cca8f3..0000000 --- a/src/token.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::ops::Deref; - -use chrono::{DateTime, UTC, TimeZone}; -use hyper::header; -use rustc_serialize::{Encodable, Encoder, Decodable, Decoder}; - -/// OAuth 2.0 access token and refresh token pair. -#[derive(Debug, Clone, PartialEq, Eq, RustcEncodable, RustcDecodable)] -pub struct TokenPair { - /// The access token. - pub access: AccessToken, - /// The refresh token. - pub refresh: Option, -} - -/// OAuth 2.0 access token type. -/// -/// See [RFC6749 section 7.1](http://tools.ietf.org/html/rfc6749#section-7.1). -#[derive(Debug, Clone, PartialEq, Eq, RustcEncodable, RustcDecodable)] -pub enum AccessTokenType { - /// The bearer token type. - /// - /// See [RFC6750](http://tools.ietf.org/html/rfc6750). - Bearer, - - /// An unrecognized token type. - Unrecognized(String), -} - -/// OAuth 2.0 access token. -/// -/// See [RFC6749 section 5](http://tools.ietf.org/html/rfc6749#section-5). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccessToken { - /// The access token issued by the authorization server. - pub token: String, - - /// The type of the token issued. - pub token_type: AccessTokenType, - - /// The expiry time of the access token. - pub expires: Option>, - - /// The scope of the access token. - pub scope: Option, -} - -/// OAuth 2.0 refresh token. -/// -/// See [RFC6749 section 1.5](http://tools.ietf.org/html/rfc6749#section-1.5). -#[derive(Debug, Clone, PartialEq, Eq, RustcEncodable, RustcDecodable)] -pub struct RefreshToken { - /// The refresh token issued by the authorization server. - pub token: String, -} - -impl AccessToken { - /// Returns true if token is expired. - pub fn expired(&self) -> bool { - self.expires.map_or(false, |dt| dt < UTC::now()) - } - - /// Creates an Authorization header. - /// - /// Returns `None` if `token_type` is not `Bearer`. - pub fn to_bearer_header(&self) -> Option> { - if self.token_type == AccessTokenType::Bearer { - Some(header::Authorization(header::Bearer { token: self.token.clone() })) - } else { - None - } - } -} - -impl Deref for TokenPair { - type Target = AccessToken; - - fn deref(&self) -> &AccessToken { - &self.access - } -} - -#[derive(RustcEncodable, RustcDecodable)] -struct SerializableAccessToken { - token: String, - token_type: AccessTokenType, - expires: Option, - scope: Option, -} - -impl SerializableAccessToken { - fn from_access_token(access: &AccessToken) -> Self { - SerializableAccessToken { - token: access.token.clone(), - token_type: access.token_type.clone(), - expires: access.expires.as_ref().map(DateTime::timestamp), - scope: access.scope.clone(), - } - } - - fn into_access_token(self) -> AccessToken { - AccessToken { - token: self.token, - token_type: self.token_type, - expires: self.expires.map(|t| UTC.timestamp(t, 0)), - scope: self.scope, - } - } -} - -impl Encodable for AccessToken { - fn encode(&self, s: &mut S) -> Result<(), S::Error> { - SerializableAccessToken::from_access_token(self).encode(s) - } -} - -impl Decodable for AccessToken { - fn decode(d: &mut D) -> Result { - SerializableAccessToken::decode(d) - .map(SerializableAccessToken::into_access_token) - } -} From bfc17a30203abdd3c868be6ea7813e8f5cdaf61f Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 21:40:38 -0500 Subject: [PATCH 02/54] Add token traits --- src/lib.rs | 2 ++ src/token/mod.rs | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/token/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 2132d39..dd63bef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,5 @@ extern crate chrono; extern crate hyper; extern crate rustc_serialize; extern crate url; + +pub mod token; diff --git a/src/token/mod.rs b/src/token/mod.rs new file mode 100644 index 0000000..881c9bc --- /dev/null +++ b/src/token/mod.rs @@ -0,0 +1,23 @@ +//! Tokens. + +/// OAuth 2.0 tokens. +/// +/// See [RFC6749, section 5](http://tools.ietf.org/html/rfc6749#section-5). +pub trait Token { + /// Returns the access token. + /// + /// See [RFC6749, section 1.4](http://tools.ietf.org/html/rfc6749#section-1.4). + fn access_token(&self) -> &str; + + /// Returns the scope, if available. + fn scope(&self) -> Option<&str>; + + /// Returns the token lifetime. + fn lifetime(&self) -> &L; +} + +/// OAuth 2.0 token lifetimes. +pub trait Lifetime { + /// Returns true if the token is no longer valid. + fn expired(&self) -> bool; +} From 40e428f5e9260d66f0a101a3e3904eab82e8dc43 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 21:44:05 -0500 Subject: [PATCH 03/54] Add token trait abstraction explanation --- src/token/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/token/mod.rs b/src/token/mod.rs index 881c9bc..39032ea 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -1,4 +1,9 @@ //! Tokens. +//! +//! Access token types are abstracted through the `Token` trait. See +//! [RFC6749, section 7.1](http://tools.ietf.org/html/rfc6749#section-7.1). +//! +//! Expiring and non-expiring tokens are abstracted through the `Lifetime` trait. /// OAuth 2.0 tokens. /// From 527ab2deed63d4ca7e6b152c10e9fc59dc6aa68b Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:07:52 -0500 Subject: [PATCH 04/54] Add bearer token type --- src/token/bearer.rs | 24 ++++++++++++++++++++++++ src/token/mod.rs | 3 +++ 2 files changed, 27 insertions(+) create mode 100644 src/token/bearer.rs diff --git a/src/token/bearer.rs b/src/token/bearer.rs new file mode 100644 index 0000000..3394d99 --- /dev/null +++ b/src/token/bearer.rs @@ -0,0 +1,24 @@ +use hyper::header; + +use super::{Token, Lifetime}; + +/// The bearer token type. +/// +/// See [RFC6750](http://tools.ietf.org/html/rfc6750). +pub struct Bearer { + access_token: String, + scope: Option, + lifetime: L, +} + +impl Token for Bearer { + fn access_token(&self) -> &str { &self.access_token } + fn scope(&self) -> Option<&str> { self.scope.as_ref().map(|s| &s[..]) } + fn lifetime(&self) -> &L { &self.lifetime } +} + +impl<'a, L: Lifetime> Into> for &'a Bearer { + fn into(self) -> header::Authorization { + header::Authorization(header::Bearer { token: self.access_token.clone() }) + } +} diff --git a/src/token/mod.rs b/src/token/mod.rs index 39032ea..468ca70 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -26,3 +26,6 @@ pub trait Lifetime { /// Returns true if the token is no longer valid. fn expired(&self) -> bool; } + +pub use self::bearer::Bearer; +mod bearer; From 38d81ed931524006c1c31c76c610cb441370b304 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:13:15 -0500 Subject: [PATCH 05/54] RFC links tweak --- src/token/bearer.rs | 2 +- src/token/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/token/bearer.rs b/src/token/bearer.rs index 3394d99..ddd2b8a 100644 --- a/src/token/bearer.rs +++ b/src/token/bearer.rs @@ -4,7 +4,7 @@ use super::{Token, Lifetime}; /// The bearer token type. /// -/// See [RFC6750](http://tools.ietf.org/html/rfc6750). +/// See [RFC 6750](http://tools.ietf.org/html/rfc6750). pub struct Bearer { access_token: String, scope: Option, diff --git a/src/token/mod.rs b/src/token/mod.rs index 468ca70..a32dc77 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -1,17 +1,17 @@ //! Tokens. //! //! Access token types are abstracted through the `Token` trait. See -//! [RFC6749, section 7.1](http://tools.ietf.org/html/rfc6749#section-7.1). +//! [RFC 6749, section 7.1](http://tools.ietf.org/html/rfc6749#section-7.1). //! //! Expiring and non-expiring tokens are abstracted through the `Lifetime` trait. /// OAuth 2.0 tokens. /// -/// See [RFC6749, section 5](http://tools.ietf.org/html/rfc6749#section-5). +/// See [RFC 6749, section 5](http://tools.ietf.org/html/rfc6749#section-5). pub trait Token { /// Returns the access token. /// - /// See [RFC6749, section 1.4](http://tools.ietf.org/html/rfc6749#section-1.4). + /// See [RF C6749, section 1.4](http://tools.ietf.org/html/rfc6749#section-1.4). fn access_token(&self) -> &str; /// Returns the scope, if available. From 397578840d3d8a878774bd572340613557e3bbd5 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:13:44 -0500 Subject: [PATCH 06/54] Add static token lifetime --- src/token/mod.rs | 3 +++ src/token/statik.rs | 8 ++++++++ 2 files changed, 11 insertions(+) create mode 100644 src/token/statik.rs diff --git a/src/token/mod.rs b/src/token/mod.rs index a32dc77..fe89d50 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -29,3 +29,6 @@ pub trait Lifetime { pub use self::bearer::Bearer; mod bearer; + +pub use self::statik::Static; +mod statik; diff --git a/src/token/statik.rs b/src/token/statik.rs new file mode 100644 index 0000000..1dbec00 --- /dev/null +++ b/src/token/statik.rs @@ -0,0 +1,8 @@ +use super::Lifetime; + +/// A static, non-expiring token. +pub struct Static; + +impl Lifetime for Static { + fn expired(&self) -> bool { false } +} From 76dcc305f39d0a1ce884ac4e388e53d855917a5e Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:16:29 -0500 Subject: [PATCH 07/54] Add expiring token lifetime --- src/token/expiring.rs | 23 +++++++++++++++++++++++ src/token/mod.rs | 5 ++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/token/expiring.rs diff --git a/src/token/expiring.rs b/src/token/expiring.rs new file mode 100644 index 0000000..4c2ae45 --- /dev/null +++ b/src/token/expiring.rs @@ -0,0 +1,23 @@ +use chrono::{DateTime, UTC}; + +use super::Lifetime; + +/// An expiring token. +pub struct Expiring { + refresh_token: String, + expires: DateTime, +} + +impl Expiring { + /// Returns the refresh token. + /// + /// See [RFC 6749, section 1.5](http://tools.ietf.org/html/rfc6749#section-1.5). + pub fn refresh_token(&self) -> &str { &self.refresh_token } + + /// Returns the expiry time of the access token. + pub fn expires(&self) -> &DateTime { &self.expires } +} + +impl Lifetime for Expiring { + fn expired(&self) -> bool { self.expires < UTC::now() } +} diff --git a/src/token/mod.rs b/src/token/mod.rs index fe89d50..5952333 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -23,7 +23,7 @@ pub trait Token { /// OAuth 2.0 token lifetimes. pub trait Lifetime { - /// Returns true if the token is no longer valid. + /// Returns true if the access token is no longer valid. fn expired(&self) -> bool; } @@ -32,3 +32,6 @@ mod bearer; pub use self::statik::Static; mod statik; + +pub use self::expiring::Expiring; +mod expiring; From af0b8bfe43f200fa81795d556254ccb52b15561a Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:21:53 -0500 Subject: [PATCH 08/54] Add Provider trait --- src/lib.rs | 1 + src/provider.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/provider.rs diff --git a/src/lib.rs b/src/lib.rs index dd63bef..2d7e474 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,3 +4,4 @@ extern crate rustc_serialize; extern crate url; pub mod token; +pub mod provider; diff --git a/src/provider.rs b/src/provider.rs new file mode 100644 index 0000000..d54bd81 --- /dev/null +++ b/src/provider.rs @@ -0,0 +1,22 @@ +//! Providers. + +use token::{Token, Lifetime}; + +/// OAuth 2.0 providers. +pub trait Provider { + /// The lifetime of tokens issued by the provider. + type Lifetime: Lifetime; + + /// The type of token issued by the provider. + type Token: Token; + + /// The authorization endpoint URI. + /// + /// See [RFC 6749, section 3.1](http://tools.ietf.org/html/rfc6749#section-3.1). + fn auth_uri() -> &'static str; + + /// The token endpoint URI. + /// + /// See [RFC 6749, section 3.2](http://tools.ietf.org/html/rfc6749#section-3.2). + fn token_uri() -> &'static str; +} From c3e4d78ad36dd9f9a3647e461986f0e0f4733013 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:25:50 -0500 Subject: [PATCH 09/54] Add Google provider --- src/provider.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/provider.rs b/src/provider.rs index d54bd81..b115e65 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,6 +1,6 @@ //! Providers. -use token::{Token, Lifetime}; +use token::{Token, Lifetime, Bearer, Expiring}; /// OAuth 2.0 providers. pub trait Provider { @@ -20,3 +20,15 @@ pub trait Provider { /// See [RFC 6749, section 3.2](http://tools.ietf.org/html/rfc6749#section-3.2). fn token_uri() -> &'static str; } + +/// Google OAuth 2.0 provider. +/// +/// See [Using OAuth 2.0 to Access Google +/// APIs](https://developers.google.com/identity/protocols/OAuth2). +pub struct Google; +impl Provider for Google { + type Lifetime = Expiring; + type Token = Bearer; + fn auth_uri() -> &'static str { "https://accounts.google.com/o/oauth2/v2/auth" } + fn token_uri() -> &'static str { "https://www.googleapis.com/oauth2/v4/token" } +} From cd66c77a941a7d5d3fc401eb87b79220c4515659 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:28:48 -0500 Subject: [PATCH 10/54] Add GitHub provider --- src/provider.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/provider.rs b/src/provider.rs index b115e65..2e6a129 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,6 +1,6 @@ //! Providers. -use token::{Token, Lifetime, Bearer, Expiring}; +use token::{Token, Lifetime, Bearer, Static, Expiring}; /// OAuth 2.0 providers. pub trait Provider { @@ -32,3 +32,14 @@ impl Provider for Google { fn auth_uri() -> &'static str { "https://accounts.google.com/o/oauth2/v2/auth" } fn token_uri() -> &'static str { "https://www.googleapis.com/oauth2/v4/token" } } + +/// GitHub OAuth 2.0 provider. +/// +/// See [OAuth, GitHub Developer Guide](https://developer.github.com/v3/oauth/). +pub struct GitHub; +impl Provider for GitHub { + type Lifetime = Static; + type Token = Bearer; + fn auth_uri() -> &'static str { "https://github.com/login/oauth/authorize" } + fn token_uri() -> &'static str { "https://github.com/login/oauth/access_token" } +} From 3d8bd4eb1aaef9993b1d6f3bd54d11c58d0c90f8 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:31:47 -0500 Subject: [PATCH 11/54] Add Imgur provider --- src/provider.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/provider.rs b/src/provider.rs index 2e6a129..66ecb54 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -43,3 +43,14 @@ impl Provider for GitHub { fn auth_uri() -> &'static str { "https://github.com/login/oauth/authorize" } fn token_uri() -> &'static str { "https://github.com/login/oauth/access_token" } } + +/// Imgur OAuth 2.0 provider. +/// +/// See [OAuth 2.0, Imgur](https://api.imgur.com/oauth2). +pub struct Imgur; +impl Provider for Imgur { + type Lifetime = Expiring; + type Token = Bearer; + fn auth_uri() -> &'static str { "https://api.imgur.com/oauth2/authorize" } + fn token_uri() -> &'static str { "https://api.imgur.com/oauth2/token" } +} From 0496dbc8fdd829e7b37c62a91c98076c740308d0 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:40:18 -0500 Subject: [PATCH 12/54] Add Client struct --- src/client.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 49 insertions(+) create mode 100644 src/client.rs diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..a532913 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,48 @@ +//! Client. + +use std::marker::PhantomData; + +use hyper; + +use provider::Provider; + +/// OAuth 2.0 client. +pub struct Client { + http_client: hyper::Client, + client_id: String, + client_secret: String, + redirect_uri: Option, + provider: PhantomData

, +} + +impl Client

{ + /// Creates a client. + /// + /// # Examples + /// + /// ``` + /// use inth_oauth2::client::Client; + /// use inth_oauth2::provider::Google; + /// + /// let client = Client::::new( + /// Default::default(), + /// "CLIENT_ID", + /// "CLIENT_SECRET", + /// Some("urn:ietf:wg:oauth:2.0:oob") + /// ); + /// ``` + pub fn new( + http_client: hyper::Client, + client_id: S, + client_secret: S, + redirect_uri: Option + ) -> Self where S: Into { + Client { + http_client: http_client, + client_id: client_id.into(), + client_secret: client_secret.into(), + redirect_uri: redirect_uri.map(Into::into), + provider: PhantomData, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 2d7e474..d487658 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ extern crate url; pub mod token; pub mod provider; +pub mod client; From a7710660c1328020fd9b783dc302d02c3795d14f Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:50:41 -0500 Subject: [PATCH 13/54] Implement Client::auth_uri --- src/client.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/client.rs b/src/client.rs index a532913..b8ebf7c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use hyper; +use url::{self, form_urlencoded, Url}; use provider::Provider; @@ -45,4 +46,30 @@ impl Client

{ provider: PhantomData, } } + + /// Returns an authorization endpoint URI to direct the user to. + /// + /// See [RFC 6749, section 3.1](http://tools.ietf.org/html/rfc6749#section-3.1). + pub fn auth_uri(&self, scope: Option<&str>, state: Option<&str>) -> Result + { + let mut uri = try!(Url::parse(P::auth_uri())); + + let mut query_pairs = vec![ + ("response_type", "code"), + ("client_id", &self.client_id), + ]; + if let Some(ref redirect_uri) = self.redirect_uri { + query_pairs.push(("redirect_uri", redirect_uri)); + } + if let Some(scope) = scope { + query_pairs.push(("scope", scope)); + } + if let Some(state) = state { + query_pairs.push(("state", state)); + } + + uri.set_query_from_pairs(query_pairs.iter()); + + Ok(uri.serialize()) + } } From af0d3283d04dedcb48f0d8c5709cac63ea2d1ba3 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 22:53:45 -0500 Subject: [PATCH 14/54] Add Google example --- examples/google.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/google.rs diff --git a/examples/google.rs b/examples/google.rs new file mode 100644 index 0000000..1637d8f --- /dev/null +++ b/examples/google.rs @@ -0,0 +1,17 @@ +extern crate inth_oauth2; + +use inth_oauth2::client::Client; +use inth_oauth2::provider::Google; + +fn main() { + let client = Client::::new( + Default::default(), + "143225766783-ip2d9qv6sdr37276t77luk6f7bhd6bj5.apps.googleusercontent.com", + "3kZ5WomzHFlN2f_XbhkyPd3o", + Some("urn:ietf:wg:oauth:2.0:oob") + ); + + let auth_uri = client.auth_uri(Some("https://www.googleapis.com/auth/userinfo.email"), None) + .unwrap(); + println!("{}", auth_uri); +} From e7d099716dbeb083d5eed268d19aee9a84242a01 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 23:05:15 -0500 Subject: [PATCH 15/54] Add Client::auth_uri unit tests --- src/client.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/client.rs b/src/client.rs index b8ebf7c..0642510 100644 --- a/src/client.rs +++ b/src/client.rs @@ -73,3 +73,59 @@ impl Client

{ Ok(uri.serialize()) } } + +#[cfg(test)] +mod tests { + use token::{Bearer, Static}; + use provider::Provider; + use super::Client; + + struct Test; + impl Provider for Test { + type Lifetime = Static; + type Token = Bearer; + fn auth_uri() -> &'static str { "http://example.com/oauth2/auth" } + fn token_uri() -> &'static str { "http://example.com/oauth2/token" } + } + + #[test] + fn auth_uri() { + let client = Client::::new(Default::default(), "foo", "bar", None); + assert_eq!( + "http://example.com/oauth2/auth?response_type=code&client_id=foo", + client.auth_uri(None, None).unwrap() + ); + } + + #[test] + fn auth_uri_with_redirect_uri() { + let client = Client::::new( + Default::default(), + "foo", + "bar", + Some("http://example.com/oauth2/callback") + ); + assert_eq!( + "http://example.com/oauth2/auth?response_type=code&client_id=foo&redirect_uri=http%3A%2F%2Fexample.com%2Foauth2%2Fcallback", + client.auth_uri(None, None).unwrap() + ); + } + + #[test] + fn auth_uri_with_scope() { + let client = Client::::new(Default::default(), "foo", "bar", None); + assert_eq!( + "http://example.com/oauth2/auth?response_type=code&client_id=foo&scope=baz", + client.auth_uri(Some("baz"), None).unwrap() + ); + } + + #[test] + fn auth_uri_with_state() { + let client = Client::::new(Default::default(), "foo", "bar", None); + assert_eq!( + "http://example.com/oauth2/auth?response_type=code&client_id=foo&state=baz", + client.auth_uri(None, Some("baz")).unwrap() + ); + } +} From eb8d3ab7b0c6d97026cb8e5e1b67dc8c67a490f6 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 23:08:45 -0500 Subject: [PATCH 16/54] Add Client::auth_uri example --- src/client.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/client.rs b/src/client.rs index 0642510..d9c142a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -50,6 +50,25 @@ impl Client

{ /// Returns an authorization endpoint URI to direct the user to. /// /// See [RFC 6749, section 3.1](http://tools.ietf.org/html/rfc6749#section-3.1). + /// + /// # Examples + /// + /// ``` + /// use inth_oauth2::client::Client; + /// use inth_oauth2::provider::Google; + /// + /// let client = Client::::new( + /// Default::default(), + /// "CLIENT_ID", + /// "CLIENT_SECRET", + /// Some("urn:ietf:wg:oauth:2.0:oob") + /// ); + /// + /// let auth_uri = client.auth_uri( + /// Some("https://www.googleapis.com/auth/userinfo.email"), + /// None + /// ); + /// ``` pub fn auth_uri(&self, scope: Option<&str>, state: Option<&str>) -> Result { let mut uri = try!(Url::parse(P::auth_uri())); From f3cdd6142df3ace232b550adf29ffb5f51826d9c Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 23:34:38 -0500 Subject: [PATCH 17/54] Add GitHub example --- examples/github.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/github.rs diff --git a/examples/github.rs b/examples/github.rs new file mode 100644 index 0000000..60de494 --- /dev/null +++ b/examples/github.rs @@ -0,0 +1,16 @@ +extern crate inth_oauth2; + +use inth_oauth2::client::Client; +use inth_oauth2::provider::GitHub; + +fn main() { + let client = Client::::new( + Default::default(), + "01774654cd9a6051e478", + "9f14d16d95d605e715ec1a9aecec220d2565fd5c", + Some("https://cmcenroe.me/oauth2-paste/") + ); + + let auth_uri = client.auth_uri(Some("user"), None).unwrap(); + println!("{}", auth_uri); +} From 557257e71dfb59050167e81686f0a55be42fbf75 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 21 Dec 2015 23:36:04 -0500 Subject: [PATCH 18/54] Add Imgur example --- examples/imgur.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/imgur.rs diff --git a/examples/imgur.rs b/examples/imgur.rs new file mode 100644 index 0000000..e3af30b --- /dev/null +++ b/examples/imgur.rs @@ -0,0 +1,16 @@ +extern crate inth_oauth2; + +use inth_oauth2::client::Client; +use inth_oauth2::provider::Imgur; + +fn main() { + let client = Client::::new( + Default::default(), + "505c8ca804230e0", + "c898d8cf28404102752b2119a3a1c6aab49899c8", + Some("https://cmcenroe.me/oauth2-paste/") + ); + + let auth_uri = client.auth_uri(None, None).unwrap(); + println!("{}", auth_uri); +} From bc9555c9b97b7f2ba60639affe69eace8898e53d Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:02:29 -0500 Subject: [PATCH 19/54] Add FromResponse trait and ParseError --- src/client/from_response.rs | 45 ++++++++++++++++++++++++++++++++ src/{client.rs => client/mod.rs} | 3 +++ 2 files changed, 48 insertions(+) create mode 100644 src/client/from_response.rs rename src/{client.rs => client/mod.rs} (98%) diff --git a/src/client/from_response.rs b/src/client/from_response.rs new file mode 100644 index 0000000..1416a13 --- /dev/null +++ b/src/client/from_response.rs @@ -0,0 +1,45 @@ +use std::error::Error; +use std::fmt; + +use rustc_serialize::json::Json; + +/// Response parsing. +pub trait FromResponse: Sized { + /// Parse a JSON response. + fn from_response(json: &Json) -> Result; +} + +/// Response parse errors. +#[derive(Debug)] +pub enum ParseError { + /// Expected response to be of type. + ExpectedType(&'static str), + + /// Expected field to be of type. + ExpectedFieldType(&'static str, &'static str), + + /// Expected field to equal value. + ExpectedFieldValue(&'static str, &'static str), + + /// Expected field to not be present. + UnexpectedField(&'static str), +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + ParseError::ExpectedType(t) => + write!(f, "Expected response of type {}", t), + ParseError::ExpectedFieldType(k, t) => + write!(f, "Expected field {} of type {}", k, t), + ParseError::ExpectedFieldValue(k, v) => + write!(f, "Expected field {} to equal {}", k, v), + ParseError::UnexpectedField(k) => + write!(f, "Unexpected field {}", k), + } + } +} + +impl Error for ParseError { + fn description(&self) -> &str { "response parse error" } +} diff --git a/src/client.rs b/src/client/mod.rs similarity index 98% rename from src/client.rs rename to src/client/mod.rs index d9c142a..c69df71 100644 --- a/src/client.rs +++ b/src/client/mod.rs @@ -7,6 +7,9 @@ use url::{self, form_urlencoded, Url}; use provider::Provider; +pub use self::from_response::{FromResponse, ParseError}; +mod from_response; + /// OAuth 2.0 client. pub struct Client { http_client: hyper::Client, From 7e3f77aa8393bc2edf7109e2c4a1914fe232b03c Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:08:34 -0500 Subject: [PATCH 20/54] Rename client::response and make public --- src/client/mod.rs | 3 +-- src/client/{from_response.rs => response.rs} | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) rename src/client/{from_response.rs => response.rs} (98%) diff --git a/src/client/mod.rs b/src/client/mod.rs index c69df71..603d677 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -7,8 +7,7 @@ use url::{self, form_urlencoded, Url}; use provider::Provider; -pub use self::from_response::{FromResponse, ParseError}; -mod from_response; +pub mod response; /// OAuth 2.0 client. pub struct Client { diff --git a/src/client/from_response.rs b/src/client/response.rs similarity index 98% rename from src/client/from_response.rs rename to src/client/response.rs index 1416a13..134a9b3 100644 --- a/src/client/from_response.rs +++ b/src/client/response.rs @@ -1,3 +1,5 @@ +//! Response parsing. + use std::error::Error; use std::fmt; From 59874c36da1a81540d155224a9bc5f74e0b6e769 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:16:30 -0500 Subject: [PATCH 21/54] Add FromResponse::from_response_inherit --- src/client/response.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client/response.rs b/src/client/response.rs index 134a9b3..f3067d6 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -9,6 +9,15 @@ use rustc_serialize::json::Json; pub trait FromResponse: Sized { /// Parse a JSON response. fn from_response(json: &Json) -> Result; + + /// Parse a JSON response, inheriting missing values from the previous instance. + /// + /// Necessary for parsing refresh token responses where the absence of a new refresh token + /// implies that the previous refresh token is still valid. + #[allow(unused_variables)] + fn from_response_inherit(json: &Json, prev: &Self) -> Result { + FromResponse::from_response(json) + } } /// Response parse errors. From 7dcc49da8dff06ce54b876ad1d7221deca6c0f2f Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:30:09 -0500 Subject: [PATCH 22/54] Enable extra lints --- src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index d487658..5a66f01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,15 @@ +#![warn( + missing_docs, + missing_debug_implementations, + trivial_casts, + trivial_numeric_casts, + unused_extern_crates, + unused_import_braces, + unused_qualifications, + unused_results, + variant_size_differences +)] + extern crate chrono; extern crate hyper; extern crate rustc_serialize; From de9dea402d69a892aed1713433bef3b7b2c96845 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:34:06 -0500 Subject: [PATCH 23/54] Add derivable Debug implementations --- src/provider.rs | 3 +++ src/token/bearer.rs | 1 + src/token/expiring.rs | 1 + src/token/statik.rs | 1 + 4 files changed, 6 insertions(+) diff --git a/src/provider.rs b/src/provider.rs index 66ecb54..ca86a57 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -25,6 +25,7 @@ pub trait Provider { /// /// See [Using OAuth 2.0 to Access Google /// APIs](https://developers.google.com/identity/protocols/OAuth2). +#[derive(Debug)] pub struct Google; impl Provider for Google { type Lifetime = Expiring; @@ -36,6 +37,7 @@ impl Provider for Google { /// GitHub OAuth 2.0 provider. /// /// See [OAuth, GitHub Developer Guide](https://developer.github.com/v3/oauth/). +#[derive(Debug)] pub struct GitHub; impl Provider for GitHub { type Lifetime = Static; @@ -47,6 +49,7 @@ impl Provider for GitHub { /// Imgur OAuth 2.0 provider. /// /// See [OAuth 2.0, Imgur](https://api.imgur.com/oauth2). +#[derive(Debug)] pub struct Imgur; impl Provider for Imgur { type Lifetime = Expiring; diff --git a/src/token/bearer.rs b/src/token/bearer.rs index ddd2b8a..3775b0f 100644 --- a/src/token/bearer.rs +++ b/src/token/bearer.rs @@ -5,6 +5,7 @@ use super::{Token, Lifetime}; /// The bearer token type. /// /// See [RFC 6750](http://tools.ietf.org/html/rfc6750). +#[derive(Debug)] pub struct Bearer { access_token: String, scope: Option, diff --git a/src/token/expiring.rs b/src/token/expiring.rs index 4c2ae45..fe88947 100644 --- a/src/token/expiring.rs +++ b/src/token/expiring.rs @@ -3,6 +3,7 @@ use chrono::{DateTime, UTC}; use super::Lifetime; /// An expiring token. +#[derive(Debug)] pub struct Expiring { refresh_token: String, expires: DateTime, diff --git a/src/token/statik.rs b/src/token/statik.rs index 1dbec00..63655fc 100644 --- a/src/token/statik.rs +++ b/src/token/statik.rs @@ -1,6 +1,7 @@ use super::Lifetime; /// A static, non-expiring token. +#[derive(Debug)] pub struct Static; impl Lifetime for Static { From dc513cab349b3f792182d403097674536c316fdb Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:41:36 -0500 Subject: [PATCH 24/54] Implement Debug for Client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ಠ╭╮ಠ --- src/client/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/client/mod.rs b/src/client/mod.rs index 603d677..05b1e6a 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,5 +1,6 @@ //! Client. +use std::fmt; use std::marker::PhantomData; use hyper; @@ -18,6 +19,16 @@ pub struct Client { provider: PhantomData

, } +impl fmt::Debug for Client

{ + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + f.debug_struct("Client") + .field("client_id", &self.client_id) + .field("client_secret", &self.client_secret) + .field("redirect_uri", &self.redirect_uri) + .finish() + } +} + impl Client

{ /// Creates a client. /// From 225f156bb6faa5b16f2a5bc0049825ce3a4c6384 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:54:39 -0500 Subject: [PATCH 25/54] Add OAuth2Error --- src/error.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 70 insertions(+) create mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ef69f54 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,69 @@ +//! Errors. + +use std::error::Error; +use std::fmt; + +/// OAuth 2.0 error codes. +/// +/// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). +#[derive(Debug)] +pub enum OAuth2ErrorCode { + /// The request is missing a required parameter, includes an unsupported parameter value (other + /// than grant type), repeats a parameter, includes multiple credentials, utilizes more than + /// one mechanism for authenticating the client, or is otherwise malformed. + InvalidRequest, + + /// Client authentication failed (e.g., unknown client, no client authentication included, or + /// unsupported authentication method). + InvalidClient, + + /// The provided authorization grant (e.g., authorization code, resource owner credentials) or + /// refresh token is invalid, expired, revoked, does not match the redirection URI used in the + /// authorization request, or was issued to another client. + InvalidGrant, + + /// The authenticated client is not authorized to use this authorization grant type. + UnauthorizedClient, + + /// The authorization grant type is not supported by the authorization server. + UnsupportedGrantType, + + /// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the + /// resource owner. + InvalidScope, + + /// An unrecognized error code, not defined in RFC 6749. + Unrecognized(String), +} + +/// OAuth 2.0 error. +/// +/// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). +#[derive(Debug)] +pub struct OAuth2Error { + /// Error code. + pub code: OAuth2ErrorCode, + + /// Human-readable text providing additional information about the error. + pub description: Option, + + /// A URI identifying a human-readable web page with information about the error. + pub uri: Option, +} + +impl fmt::Display for OAuth2Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + try!(write!(f, "{:?}", self.code)); + if let Some(ref description) = self.description { + try!(write!(f, ": {}", description)); + } + if let Some(ref uri) = self.uri { + try!(write!(f, " ({})", uri)); + } + Ok(()) + } +} + +impl Error for OAuth2Error { + fn description(&self) -> &str { "OAuth 2.0 API error" } +} diff --git a/src/lib.rs b/src/lib.rs index 5a66f01..b15d66f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,4 +17,5 @@ extern crate url; pub mod token; pub mod provider; +pub mod error; pub mod client; From 565ee931137c3484710e0cc2613b1964d703016d Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 00:59:58 -0500 Subject: [PATCH 26/54] Implement From<&str> for OAuth2ErrorCode --- src/error.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/error.rs b/src/error.rs index ef69f54..b7093fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,6 +36,20 @@ pub enum OAuth2ErrorCode { Unrecognized(String), } +impl<'a> From<&'a str> for OAuth2ErrorCode { + fn from(s: &str) -> OAuth2ErrorCode { + match s { + "invalid_request" => OAuth2ErrorCode::InvalidRequest, + "invalid_client" => OAuth2ErrorCode::InvalidClient, + "invalid_grant" => OAuth2ErrorCode::InvalidGrant, + "unauthorized_client" => OAuth2ErrorCode::UnauthorizedClient, + "unsupported_grant_type" => OAuth2ErrorCode::UnsupportedGrantType, + "invalid_scope" => OAuth2ErrorCode::InvalidScope, + s => OAuth2ErrorCode::Unrecognized(s.to_string()), + } + } +} + /// OAuth 2.0 error. /// /// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). From 71d147eb8394e9d9f6228e01e72a5d07ddc099ce Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 01:45:12 -0500 Subject: [PATCH 27/54] Add Json helpers --- src/client/response.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/client/response.rs b/src/client/response.rs index f3067d6..bca915c 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -3,7 +3,7 @@ use std::error::Error; use std::fmt; -use rustc_serialize::json::Json; +use rustc_serialize::json::{self, Json}; /// Response parsing. pub trait FromResponse: Sized { @@ -54,3 +54,29 @@ impl fmt::Display for ParseError { impl Error for ParseError { fn description(&self) -> &str { "response parse error" } } + +/// JSON helper for response parsing. +#[derive(Debug)] +pub struct JsonHelper<'a>(pub &'a Json); + +impl<'a> JsonHelper<'a> { + /// Returns self as a `JsonObjectHelper` or fails with `ParseError::ExpectedType`. + pub fn as_object(&self) -> Result, ParseError>{ + self.0.as_object() + .ok_or(ParseError::ExpectedType("object")) + .map(|o| JsonObjectHelper(o)) + } +} + +/// JSON object helper for response parsing. +#[derive(Debug)] +pub struct JsonObjectHelper<'a>(pub &'a json::Object); + +impl<'a> JsonObjectHelper<'a> { + /// Gets a field as a string or fails with `ParseError::ExpectedFieldType`. + pub fn get_string(&self, key: &'static str) -> Result<&'a str, ParseError> { + self.0.get(key) + .and_then(Json::as_string) + .ok_or(ParseError::ExpectedFieldType(key, "string")) + } +} From cb404df6d8fe0913f3e3d63ccdb283d9828e6e2e Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 22 Dec 2015 01:45:21 -0500 Subject: [PATCH 28/54] Implement FromResponse for OAuth2Error --- src/error.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/error.rs b/src/error.rs index b7093fa..40e0416 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,10 @@ use std::error::Error; use std::fmt; +use rustc_serialize::json::Json; + +use client::response::{FromResponse, ParseError, JsonHelper}; + /// OAuth 2.0 error codes. /// /// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). @@ -81,3 +85,20 @@ impl fmt::Display for OAuth2Error { impl Error for OAuth2Error { fn description(&self) -> &str { "OAuth 2.0 API error" } } + +impl FromResponse for OAuth2Error { + fn from_response(json: &Json) -> Result { + let json = JsonHelper(json); + let obj = try!(json.as_object()); + + let code = try!(obj.get_string("error")); + let description = obj.0.get("error_description").and_then(Json::as_string); + let uri = obj.0.get("error_uri").and_then(Json::as_string); + + Ok(OAuth2Error { + code: code.into(), + description: description.map(Into::into), + uri: uri.map(Into::into), + }) + } +} From 228c732e2e77d49bb06dccc5359bfd30ad7b4795 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 22:38:14 -0500 Subject: [PATCH 29/54] Add JsonObjectHelper::get_string_option --- src/client/response.rs | 9 ++++++--- src/error.rs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/client/response.rs b/src/client/response.rs index bca915c..09aafe2 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -73,10 +73,13 @@ impl<'a> JsonHelper<'a> { pub struct JsonObjectHelper<'a>(pub &'a json::Object); impl<'a> JsonObjectHelper<'a> { + /// Gets a field as a string or returns `None`. + pub fn get_string_option(&self, key: &'static str) -> Option<&'a str> { + self.0.get(key).and_then(Json::as_string) + } + /// Gets a field as a string or fails with `ParseError::ExpectedFieldType`. pub fn get_string(&self, key: &'static str) -> Result<&'a str, ParseError> { - self.0.get(key) - .and_then(Json::as_string) - .ok_or(ParseError::ExpectedFieldType(key, "string")) + self.get_string_option(key).ok_or(ParseError::ExpectedFieldType(key, "string")) } } diff --git a/src/error.rs b/src/error.rs index 40e0416..7f92cfe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -92,8 +92,8 @@ impl FromResponse for OAuth2Error { let obj = try!(json.as_object()); let code = try!(obj.get_string("error")); - let description = obj.0.get("error_description").and_then(Json::as_string); - let uri = obj.0.get("error_uri").and_then(Json::as_string); + let description = obj.get_string_option("error_description"); + let uri = obj.get_string_option("error_uri"); Ok(OAuth2Error { code: code.into(), From db058fffdf48d310d174da67609fac0c21d0abb9 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:07:26 -0500 Subject: [PATCH 30/54] Derive Eq for ParseError --- src/client/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/response.rs b/src/client/response.rs index 09aafe2..43c5e23 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -21,7 +21,7 @@ pub trait FromResponse: Sized { } /// Response parse errors. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum ParseError { /// Expected response to be of type. ExpectedType(&'static str), From fbfb2fa2848bfdd9b5762ebc5e9b71a6529640fd Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:07:46 -0500 Subject: [PATCH 31/54] Derive Eq for OAuth2Error --- src/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index 7f92cfe..5a8dd16 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,7 +10,7 @@ use client::response::{FromResponse, ParseError, JsonHelper}; /// OAuth 2.0 error codes. /// /// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum OAuth2ErrorCode { /// The request is missing a required parameter, includes an unsupported parameter value (other /// than grant type), repeats a parameter, includes multiple credentials, utilizes more than @@ -57,7 +57,7 @@ impl<'a> From<&'a str> for OAuth2ErrorCode { /// OAuth 2.0 error. /// /// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2). -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct OAuth2Error { /// Error code. pub code: OAuth2ErrorCode, From 3c32646ea4e784140e2f3338310594d0a31b2393 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:07:56 -0500 Subject: [PATCH 32/54] Test OAuth2Error::from_response --- src/error.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/error.rs b/src/error.rs index 5a8dd16..ac867eb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -102,3 +102,62 @@ impl FromResponse for OAuth2Error { }) } } + +#[cfg(test)] +mod tests { + use rustc_serialize::json::Json; + + use client::response::{FromResponse, ParseError}; + use super::{OAuth2Error, OAuth2ErrorCode}; + + #[test] + fn from_response_empty() { + let json = Json::from_str("{}").unwrap(); + assert_eq!( + ParseError::ExpectedFieldType("error", "string"), + OAuth2Error::from_response(&json).unwrap_err() + ); + } + + #[test] + fn from_response() { + let json = Json::from_str(r#"{"error":"invalid_request"}"#).unwrap(); + assert_eq!( + OAuth2Error { + code: OAuth2ErrorCode::InvalidRequest, + description: None, + uri: None, + }, + OAuth2Error::from_response(&json).unwrap() + ); + } + + #[test] + fn from_response_with_description() { + let json = Json::from_str(r#"{"error":"invalid_request","error_description":"foo"}"#) + .unwrap(); + assert_eq!( + OAuth2Error { + code: OAuth2ErrorCode::InvalidRequest, + description: Some(String::from("foo")), + uri: None, + }, + OAuth2Error::from_response(&json).unwrap() + ); + } + + #[test] + fn from_response_with_uri() { + let json = Json::from_str( + r#"{"error":"invalid_request","error_uri":"http://example.com"}"# + ).unwrap(); + assert_eq!( + OAuth2Error { + code: OAuth2ErrorCode::InvalidRequest, + description: None, + uri: Some(String::from("http://example.com")), + }, + OAuth2Error::from_response(&json).unwrap() + ); + } +} From 3aa66c2d30e7467bac8a0e3742d0c96ee2b383c3 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:12:00 -0500 Subject: [PATCH 33/54] OAuth2Error FromResponse tweak --- src/error.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index ac867eb..7d65502 100644 --- a/src/error.rs +++ b/src/error.rs @@ -88,8 +88,7 @@ impl Error for OAuth2Error { impl FromResponse for OAuth2Error { fn from_response(json: &Json) -> Result { - let json = JsonHelper(json); - let obj = try!(json.as_object()); + let obj = try!(JsonHelper(json).as_object()); let code = try!(obj.get_string("error")); let description = obj.get_string_option("error_description"); From 2628759a2e4334af3b2a6af9a1f68778bc41f25b Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:46:21 -0500 Subject: [PATCH 34/54] Implement FromResponse for tokens --- src/client/response.rs | 10 ++++++++++ src/token/bearer.rs | 34 ++++++++++++++++++++++++++++++++++ src/token/expiring.rs | 33 ++++++++++++++++++++++++++++++++- src/token/mod.rs | 6 ++++-- src/token/statik.rs | 13 +++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/client/response.rs b/src/client/response.rs index 43c5e23..47a54cc 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -82,4 +82,14 @@ impl<'a> JsonObjectHelper<'a> { pub fn get_string(&self, key: &'static str) -> Result<&'a str, ParseError> { self.get_string_option(key).ok_or(ParseError::ExpectedFieldType(key, "string")) } + + /// Gets a field as an i64 or returns `None`. + pub fn get_i64_option(&self, key: &'static str) -> Option { + self.0.get(key).and_then(Json::as_i64) + } + + /// Gets a field as an i64 or fails with `ParseError::ExpectedFieldType`. + pub fn get_i64(&self, key: &'static str) -> Result { + self.get_i64_option(key).ok_or(ParseError::ExpectedFieldType(key, "i64")) + } } diff --git a/src/token/bearer.rs b/src/token/bearer.rs index 3775b0f..2d8485c 100644 --- a/src/token/bearer.rs +++ b/src/token/bearer.rs @@ -1,6 +1,8 @@ use hyper::header; +use rustc_serialize::json::Json; use super::{Token, Lifetime}; +use client::response::{FromResponse, ParseError, JsonHelper}; /// The bearer token type. /// @@ -23,3 +25,35 @@ impl<'a, L: Lifetime> Into> for &'a Bearer header::Authorization(header::Bearer { token: self.access_token.clone() }) } } + +impl Bearer { + fn from_response_and_lifetime(json: &Json, lifetime: L) -> Result { + let obj = try!(JsonHelper(json).as_object()); + + let token_type = try!(obj.get_string("token_type")); + if token_type != "Bearer" && token_type != "bearer" { + return Err(ParseError::ExpectedFieldValue("token_type", "Bearer")); + } + + let access_token = try!(obj.get_string("access_token")); + let scope = obj.get_string_option("scope"); + + Ok(Bearer { + access_token: access_token.into(), + scope: scope.map(Into::into), + lifetime: lifetime, + }) + } +} + +impl FromResponse for Bearer { + fn from_response(json: &Json) -> Result { + let lifetime = try!(FromResponse::from_response(json)); + Bearer::from_response_and_lifetime(json, lifetime) + } + + fn from_response_inherit(json: &Json, prev: &Self) -> Result { + let lifetime = try!(FromResponse::from_response_inherit(json, &prev.lifetime)); + Bearer::from_response_and_lifetime(json, lifetime) + } +} diff --git a/src/token/expiring.rs b/src/token/expiring.rs index fe88947..74422ed 100644 --- a/src/token/expiring.rs +++ b/src/token/expiring.rs @@ -1,6 +1,8 @@ -use chrono::{DateTime, UTC}; +use chrono::{DateTime, UTC, Duration}; +use rustc_serialize::json::Json; use super::Lifetime; +use client::response::{FromResponse, ParseError, JsonHelper}; /// An expiring token. #[derive(Debug)] @@ -22,3 +24,32 @@ impl Expiring { impl Lifetime for Expiring { fn expired(&self) -> bool { self.expires < UTC::now() } } + +impl FromResponse for Expiring { + fn from_response(json: &Json) -> Result { + let obj = try!(JsonHelper(json).as_object()); + + let refresh_token = try!(obj.get_string("refresh_token")); + let expires_in = try!(obj.get_i64("expires_in")); + + Ok(Expiring { + refresh_token: refresh_token.into(), + expires: UTC::now() + Duration::seconds(expires_in), + }) + } + + fn from_response_inherit(json: &Json, prev: &Self) -> Result { + let obj = try!(JsonHelper(json).as_object()); + + let refresh_token = try! { + obj.get_string("refresh_token") + .or(Ok(&prev.refresh_token)) + }; + let expires_in = try!(obj.get_i64("expires_in")); + + Ok(Expiring { + refresh_token: refresh_token.into(), + expires: UTC::now() + Duration::seconds(expires_in), + }) + } +} diff --git a/src/token/mod.rs b/src/token/mod.rs index 5952333..1771a95 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -5,10 +5,12 @@ //! //! Expiring and non-expiring tokens are abstracted through the `Lifetime` trait. +use client::response::FromResponse; + /// OAuth 2.0 tokens. /// /// See [RFC 6749, section 5](http://tools.ietf.org/html/rfc6749#section-5). -pub trait Token { +pub trait Token: FromResponse { /// Returns the access token. /// /// See [RF C6749, section 1.4](http://tools.ietf.org/html/rfc6749#section-1.4). @@ -22,7 +24,7 @@ pub trait Token { } /// OAuth 2.0 token lifetimes. -pub trait Lifetime { +pub trait Lifetime: FromResponse { /// Returns true if the access token is no longer valid. fn expired(&self) -> bool; } diff --git a/src/token/statik.rs b/src/token/statik.rs index 63655fc..4e64fad 100644 --- a/src/token/statik.rs +++ b/src/token/statik.rs @@ -1,4 +1,7 @@ +use rustc_serialize::json::Json; + use super::Lifetime; +use client::response::{FromResponse, ParseError, JsonHelper}; /// A static, non-expiring token. #[derive(Debug)] @@ -7,3 +10,13 @@ pub struct Static; impl Lifetime for Static { fn expired(&self) -> bool { false } } + +impl FromResponse for Static { + fn from_response(json: &Json) -> Result { + let obj = try!(JsonHelper(json).as_object()); + if obj.0.contains_key("expires_in") { + return Err(ParseError::UnexpectedField("expires_in")); + } + Ok(Static) + } +} From 2bde28f1dea17865ecaf7679fa33248f0c5c542f Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:54:49 -0500 Subject: [PATCH 35/54] Derive Eq for Static --- src/token/statik.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/statik.rs b/src/token/statik.rs index 4e64fad..df3c78b 100644 --- a/src/token/statik.rs +++ b/src/token/statik.rs @@ -4,7 +4,7 @@ use super::Lifetime; use client::response::{FromResponse, ParseError, JsonHelper}; /// A static, non-expiring token. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Static; impl Lifetime for Static { From e09b0726fe307f41010028e21f661ab516058f10 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:54:56 -0500 Subject: [PATCH 36/54] Test Static::from_response --- src/token/statik.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/token/statik.rs b/src/token/statik.rs index df3c78b..ece2bac 100644 --- a/src/token/statik.rs +++ b/src/token/statik.rs @@ -20,3 +20,26 @@ impl FromResponse for Static { Ok(Static) } } + +#[cfg(test)] +mod tests { + use rustc_serialize::json::Json; + + use client::response::{FromResponse, ParseError}; + use super::Static; + + #[test] + fn from_response() { + let json = Json::from_str("{}").unwrap(); + assert_eq!(Static, Static::from_response(&json).unwrap()); + } + + #[test] + fn from_response_with_expires_in() { + let json = Json::from_str(r#"{"expires_in":3600}"#).unwrap(); + assert_eq!( + ParseError::UnexpectedField("expires_in"), + Static::from_response(&json).unwrap_err() + ); + } +} From 02a764720b18efd3e698769ac1da19e8dcbb2f18 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 23 Dec 2015 23:57:12 -0500 Subject: [PATCH 37/54] Derive Eq for Expiring --- src/token/expiring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/expiring.rs b/src/token/expiring.rs index 74422ed..1efc3e7 100644 --- a/src/token/expiring.rs +++ b/src/token/expiring.rs @@ -5,7 +5,7 @@ use super::Lifetime; use client::response::{FromResponse, ParseError, JsonHelper}; /// An expiring token. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Expiring { refresh_token: String, expires: DateTime, From 552507478fdf3bb70684919696ad784b844ef1a6 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 00:22:12 -0500 Subject: [PATCH 38/54] Test Expiring::from_response --- src/token/expiring.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/token/expiring.rs b/src/token/expiring.rs index 1efc3e7..d274ec6 100644 --- a/src/token/expiring.rs +++ b/src/token/expiring.rs @@ -53,3 +53,34 @@ impl FromResponse for Expiring { }) } } + +#[cfg(test)] +mod tests { + use chrono::{UTC, Duration}; + use rustc_serialize::json::Json; + + use client::response::FromResponse; + use super::Expiring; + + #[test] + fn from_response() { + let json = Json::from_str(r#"{"refresh_token":"aaaaaaaa","expires_in":3600}"#).unwrap(); + let expiring = Expiring::from_response(&json).unwrap(); + assert_eq!("aaaaaaaa", expiring.refresh_token); + assert!(expiring.expires > UTC::now()); + assert!(expiring.expires <= UTC::now() + Duration::seconds(3600)); + } + + #[test] + fn from_response_inherit() { + let json = Json::from_str(r#"{"expires_in":3600}"#).unwrap(); + let prev = Expiring { + refresh_token: String::from("aaaaaaaa"), + expires: UTC::now(), + }; + let expiring = Expiring::from_response_inherit(&json, &prev).unwrap(); + assert_eq!("aaaaaaaa", expiring.refresh_token); + assert!(expiring.expires > UTC::now()); + assert!(expiring.expires <= UTC::now() + Duration::seconds(3600)); + } +} From a8c1f7501ebf57e578b7f397ce22c90762f47c2e Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 00:44:09 -0500 Subject: [PATCH 39/54] Test Bearer::from_response --- src/token/bearer.rs | 109 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/src/token/bearer.rs b/src/token/bearer.rs index 2d8485c..a001589 100644 --- a/src/token/bearer.rs +++ b/src/token/bearer.rs @@ -7,7 +7,7 @@ use client::response::{FromResponse, ParseError, JsonHelper}; /// The bearer token type. /// /// See [RFC 6750](http://tools.ietf.org/html/rfc6750). -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Bearer { access_token: String, scope: Option, @@ -57,3 +57,110 @@ impl FromResponse for Bearer { Bearer::from_response_and_lifetime(json, lifetime) } } + +#[cfg(test)] +mod tests { + use chrono::{UTC, Duration}; + use rustc_serialize::json::Json; + + use client::response::{FromResponse, ParseError}; + use token::{Static, Expiring}; + use super::Bearer; + + #[test] + fn from_response_with_invalid_token_type() { + let json = Json::from_str(r#"{"token_type":"MAC","access_token":"aaaaaaaa"}"#).unwrap(); + assert_eq!( + ParseError::ExpectedFieldValue("token_type", "Bearer"), + Bearer::::from_response(&json).unwrap_err() + ); + } + + #[test] + fn from_response_capital_b() { + let json = Json::from_str(r#"{"token_type":"Bearer","access_token":"aaaaaaaa"}"#).unwrap(); + assert_eq!( + Bearer { + access_token: String::from("aaaaaaaa"), + scope: None, + lifetime: Static, + }, + Bearer::::from_response(&json).unwrap() + ); + } + + #[test] + fn from_response_little_b() { + let json = Json::from_str(r#"{"token_type":"bearer","access_token":"aaaaaaaa"}"#).unwrap(); + assert_eq!( + Bearer { + access_token: String::from("aaaaaaaa"), + scope: None, + lifetime: Static, + }, + Bearer::::from_response(&json).unwrap() + ); + } + + #[test] + fn from_response_with_scope() { + let json = Json::from_str( + r#"{"token_type":"Bearer","access_token":"aaaaaaaa","scope":"foo"}"# + ).unwrap(); + assert_eq!( + Bearer { + access_token: String::from("aaaaaaaa"), + scope: Some(String::from("foo")), + lifetime: Static, + }, + Bearer::::from_response(&json).unwrap() + ); + } + + #[test] + fn from_response_expiring() { + let json = Json::from_str(r#" + { + "token_type":"Bearer", + "access_token":"aaaaaaaa", + "expires_in":3600, + "refresh_token":"bbbbbbbb" + } + "#).unwrap(); + let bearer = Bearer::::from_response(&json).unwrap(); + assert_eq!("aaaaaaaa", bearer.access_token); + assert_eq!(None, bearer.scope); + let expiring = bearer.lifetime; + assert_eq!("bbbbbbbb", expiring.refresh_token()); + assert!(expiring.expires() > &UTC::now()); + assert!(expiring.expires() <= &(UTC::now() + Duration::seconds(3600))); + } + + #[test] + fn from_response_inherit_expiring() { + let json = Json::from_str(r#" + { + "token_type":"Bearer", + "access_token":"aaaaaaaa", + "expires_in":3600, + "refresh_token":"bbbbbbbb" + } + "#).unwrap(); + let prev = Bearer::::from_response(&json).unwrap(); + + let json = Json::from_str(r#" + { + "token_type":"Bearer", + "access_token":"cccccccc", + "expires_in":3600 + } + "#).unwrap(); + let bearer = Bearer::::from_response_inherit(&json, &prev).unwrap(); + assert_eq!("cccccccc", bearer.access_token); + assert_eq!(None, bearer.scope); + let expiring = bearer.lifetime; + assert_eq!("bbbbbbbb", expiring.refresh_token()); + assert!(expiring.expires() > &UTC::now()); + assert!(expiring.expires() <= &(UTC::now() + Duration::seconds(3600))); + } +} From 8192d806f7a0e56be5c7766297e63f7c043ceebf Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 00:58:20 -0500 Subject: [PATCH 40/54] Add ClientError enum --- src/client/error.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++ src/client/mod.rs | 3 ++ 2 files changed, 83 insertions(+) create mode 100644 src/client/error.rs diff --git a/src/client/error.rs b/src/client/error.rs new file mode 100644 index 0000000..2b39579 --- /dev/null +++ b/src/client/error.rs @@ -0,0 +1,80 @@ +use std::error::Error; +use std::{fmt, io}; + +use hyper; +use rustc_serialize::json; +use url; + +use client::response::ParseError; +use error::OAuth2Error; + +/// Errors that can occur during authorization. +#[derive(Debug)] +pub enum ClientError { + /// IO error. + Io(io::Error), + /// URL error. + Url(url::ParseError), + /// Hyper error. + Hyper(hyper::Error), + /// JSON error. + Json(json::ParserError), + /// Response parse error. + Parse(ParseError), + /// OAuth 2.0 error. + OAuth2(OAuth2Error), +} + +impl fmt::Display for ClientError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + ClientError::Io(ref err) => write!(f, "{}", err), + ClientError::Url(ref err) => write!(f, "{}", err), + ClientError::Hyper(ref err) => write!(f, "{}", err), + ClientError::Json(ref err) => write!(f, "{}", err), + ClientError::Parse(ref err) => write!(f, "{}", err), + ClientError::OAuth2(ref err) => write!(f, "{}", err), + } + } +} + +impl Error for ClientError { + fn description(&self) -> &str { + match *self { + ClientError::Io(ref err) => err.description(), + ClientError::Url(ref err) => err.description(), + ClientError::Hyper(ref err) => err.description(), + ClientError::Json(ref err) => err.description(), + ClientError::Parse(ref err) => err.description(), + ClientError::OAuth2(ref err) => err.description(), + } + } + + fn cause(&self) -> Option<&Error> { + match *self { + ClientError::Io(ref err) => Some(err), + ClientError::Url(ref err) => Some(err), + ClientError::Hyper(ref err) => Some(err), + ClientError::Json(ref err) => Some(err), + ClientError::Parse(ref err) => Some(err), + ClientError::OAuth2(ref err) => Some(err), + } + } +} + +macro_rules! impl_from { + ($v:path, $t:ty) => { + impl From<$t> for ClientError { + fn from(err: $t) -> Self { + $v(err) + } + } + } +} + +impl_from!(ClientError::Io, io::Error); +impl_from!(ClientError::Url, url::ParseError); +impl_from!(ClientError::Hyper, hyper::Error); +impl_from!(ClientError::Json, json::ParserError); +impl_from!(ClientError::Parse, ParseError); +impl_from!(ClientError::OAuth2, OAuth2Error); diff --git a/src/client/mod.rs b/src/client/mod.rs index 05b1e6a..a283d71 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -10,6 +10,9 @@ use provider::Provider; pub mod response; +pub use self::error::ClientError; +mod error; + /// OAuth 2.0 client. pub struct Client { http_client: hyper::Client, From 03b178c89cee92d43a97124ee17ba34ac2681b7c Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 01:11:01 -0500 Subject: [PATCH 41/54] Implement Client::request_token --- src/client/mod.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index a283d71..7387e92 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,11 +3,14 @@ use std::fmt; use std::marker::PhantomData; -use hyper; +use hyper::{self, header, mime}; +use rustc_serialize::json::Json; use url::{self, form_urlencoded, Url}; +use error::OAuth2Error; use provider::Provider; +use self::response::FromResponse; pub mod response; pub use self::error::ClientError; @@ -107,6 +110,52 @@ impl Client

{ Ok(uri.serialize()) } + + /// Requests an access token using an authorization code. + /// + /// See [RFC 6749, section 4.1.3](http://tools.ietf.org/html/rfc6749#section-4.1.3). + pub fn request_token(&self, code: &str) -> Result { + let mut body_pairs = vec![ + ("grant_type", "authorization_code"), + ("code", code), + ]; + if let Some(ref redirect_uri) = self.redirect_uri { + body_pairs.push(("redirect_uri", redirect_uri)); + } + + let post_body = form_urlencoded::serialize(body_pairs); + let request = self.http_client.post(P::token_uri()) + .header( + header::Authorization(header::Basic { + username: self.client_id.clone(), + password: Some(self.client_secret.clone()), + }) + ) + .header( + header::Accept(vec![ + header::qitem( + mime::Mime( + mime::TopLevel::Application, + mime::SubLevel::Json, + vec![] + ) + ) + ]) + ) + .header(header::ContentType::form_url_encoded()) + .body(&post_body); + + let mut response = try!(request.send()); + let json = try!(Json::from_reader(&mut response)); + + let error = OAuth2Error::from_response(&json); + if let Ok(error) = error { + return Err(ClientError::from(error)); + } + + let token = try!(P::Token::from_response(&json)); + Ok(token) + } } #[cfg(test)] From cfa53d8b3519f4097b02bb671bc10f31bd0d4552 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 01:11:14 -0500 Subject: [PATCH 42/54] Request token in Google example --- examples/google.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/google.rs b/examples/google.rs index 1637d8f..18a62a4 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -1,5 +1,7 @@ extern crate inth_oauth2; +use std::io; + use inth_oauth2::client::Client; use inth_oauth2::provider::Google; @@ -14,4 +16,10 @@ fn main() { let auth_uri = client.auth_uri(Some("https://www.googleapis.com/auth/userinfo.email"), None) .unwrap(); println!("{}", auth_uri); + + let mut code = String::new(); + io::stdin().read_line(&mut code).unwrap(); + + let token = client.request_token(code.trim()).unwrap(); + println!("{:?}", token); } From d31ccaf473aaed58a1c8b7f8a2b80bb353d69cb7 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 14:22:04 -0500 Subject: [PATCH 43/54] Return ClientError from Client::auth_uri --- src/client/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 7387e92..5b72289 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use hyper::{self, header, mime}; use rustc_serialize::json::Json; -use url::{self, form_urlencoded, Url}; +use url::{form_urlencoded, Url}; use error::OAuth2Error; use provider::Provider; @@ -88,7 +88,7 @@ impl Client

{ /// None /// ); /// ``` - pub fn auth_uri(&self, scope: Option<&str>, state: Option<&str>) -> Result + pub fn auth_uri(&self, scope: Option<&str>, state: Option<&str>) -> Result { let mut uri = try!(Url::parse(P::auth_uri())); From 4fbe31693fee32b01ad4a51ba15040c90b9da90a Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 14:30:07 -0500 Subject: [PATCH 44/54] Factor out Client::post_token --- src/client/mod.rs | 61 ++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 5b72289..869a4fa 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -111,6 +111,36 @@ impl Client

{ Ok(uri.serialize()) } + fn post_token(&self, body_pairs: Vec<(&str, &str)>) -> Result { + let body = form_urlencoded::serialize(body_pairs); + let auth_header = header::Authorization( + header::Basic { + username: self.client_id.clone(), + password: Some(self.client_secret.clone()), + } + ); + let accept_header = header::Accept(vec![ + header::qitem(mime::Mime(mime::TopLevel::Application, mime::SubLevel::Json, vec![])), + ]); + + let request = self.http_client.post(P::token_uri()) + .header(auth_header) + .header(accept_header) + .header(header::ContentType::form_url_encoded()) + .body(&body); + + let mut response = try!(request.send()); + let json = try!(Json::from_reader(&mut response)); + + let error = OAuth2Error::from_response(&json); + + if let Ok(error) = error { + Err(ClientError::from(error)) + } else { + Ok(json) + } + } + /// Requests an access token using an authorization code. /// /// See [RFC 6749, section 4.1.3](http://tools.ietf.org/html/rfc6749#section-4.1.3). @@ -123,36 +153,7 @@ impl Client

{ body_pairs.push(("redirect_uri", redirect_uri)); } - let post_body = form_urlencoded::serialize(body_pairs); - let request = self.http_client.post(P::token_uri()) - .header( - header::Authorization(header::Basic { - username: self.client_id.clone(), - password: Some(self.client_secret.clone()), - }) - ) - .header( - header::Accept(vec![ - header::qitem( - mime::Mime( - mime::TopLevel::Application, - mime::SubLevel::Json, - vec![] - ) - ) - ]) - ) - .header(header::ContentType::form_url_encoded()) - .body(&post_body); - - let mut response = try!(request.send()); - let json = try!(Json::from_reader(&mut response)); - - let error = OAuth2Error::from_response(&json); - if let Ok(error) = error { - return Err(ClientError::from(error)); - } - + let json = try!(self.post_token(body_pairs)); let token = try!(P::Token::from_response(&json)); Ok(token) } From 43bc9f4e488764dc12fc1a1c4108e1aadd7570b7 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 14:37:42 -0500 Subject: [PATCH 45/54] Implement Client::refresh_token --- src/client/mod.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/client/mod.rs b/src/client/mod.rs index 869a4fa..c3487b4 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -9,6 +9,7 @@ use url::{form_urlencoded, Url}; use error::OAuth2Error; use provider::Provider; +use token::{Token, Expiring}; use self::response::FromResponse; pub mod response; @@ -159,6 +160,29 @@ impl Client

{ } } +impl Client

where P::Token: Token { + /// Refreshes an access token. + /// + /// See [RFC 6749, section 6](http://tools.ietf.org/html/rfc6749#section-6). + pub fn refresh_token( + &self, + token: P::Token, + scope: Option<&str> + ) -> Result { + let mut body_pairs = vec![ + ("grant_type", "refresh_token"), + ("refresh_token", token.lifetime().refresh_token()), + ]; + if let Some(scope) = scope { + body_pairs.push(("scope", scope)); + } + + let json = try!(self.post_token(body_pairs)); + let token = try!(P::Token::from_response_inherit(&json, &token)); + Ok(token) + } +} + #[cfg(test)] mod tests { use token::{Bearer, Static}; From 33bcbd97903b30c2e000351b664eacef9a11b5cb Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 14:38:00 -0500 Subject: [PATCH 46/54] Refresh token in Google example --- examples/google.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/google.rs b/examples/google.rs index 18a62a4..0d4859e 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -22,4 +22,7 @@ fn main() { let token = client.request_token(code.trim()).unwrap(); println!("{:?}", token); + + let token = client.refresh_token(token, None).unwrap(); + println!("{:?}", token); } From 0e9bbb80d97929a5d6456a09ae67a57f722ee11f Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 14:44:06 -0500 Subject: [PATCH 47/54] Request token in GitHub example --- examples/github.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/github.rs b/examples/github.rs index 60de494..4ab2000 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -1,5 +1,7 @@ extern crate inth_oauth2; +use std::io; + use inth_oauth2::client::Client; use inth_oauth2::provider::GitHub; @@ -13,4 +15,10 @@ fn main() { let auth_uri = client.auth_uri(Some("user"), None).unwrap(); println!("{}", auth_uri); + + let mut code = String::new(); + io::stdin().read_line(&mut code).unwrap(); + + let token = client.request_token(code.trim()).unwrap(); + println!("{:?}", token); } From 34efa2fad05e1e25f87b3da6c0ac30ea19257ffa Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 14:50:44 -0500 Subject: [PATCH 48/54] Request and refresh token in Imgur example --- examples/imgur.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/imgur.rs b/examples/imgur.rs index e3af30b..0eaa394 100644 --- a/examples/imgur.rs +++ b/examples/imgur.rs @@ -1,5 +1,7 @@ extern crate inth_oauth2; +use std::io; + use inth_oauth2::client::Client; use inth_oauth2::provider::Imgur; @@ -13,4 +15,13 @@ fn main() { let auth_uri = client.auth_uri(None, None).unwrap(); println!("{}", auth_uri); + + let mut code = String::new(); + io::stdin().read_line(&mut code).unwrap(); + + let token = client.request_token(code.trim()).unwrap(); + println!("{:?}", token); + + let token = client.refresh_token(token, None).unwrap(); + println!("{:?}", token); } From f28d128c20fbe693555ff0758baca7e8df902255 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 15:00:54 -0500 Subject: [PATCH 49/54] Reexport Token, Lifetime, Client, ClientError --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b15d66f..cd3942f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,9 @@ extern crate hyper; extern crate rustc_serialize; extern crate url; +pub use token::{Token, Lifetime}; +pub use client::{Client, ClientError}; + pub mod token; pub mod provider; pub mod error; From cce6471a938c95160f89f149a8326b544147353a Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 15:49:48 -0500 Subject: [PATCH 50/54] Implement Client::ensure_token --- src/client/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index c3487b4..84c2b06 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -9,7 +9,7 @@ use url::{form_urlencoded, Url}; use error::OAuth2Error; use provider::Provider; -use token::{Token, Expiring}; +use token::{Token, Lifetime, Expiring}; use self::response::FromResponse; pub mod response; @@ -181,6 +181,15 @@ impl Client

where P::Token: Token { let token = try!(P::Token::from_response_inherit(&json, &token)); Ok(token) } + + /// Ensures an access token is valid by refreshing it if necessary. + pub fn ensure_token(&self, token: P::Token) -> Result { + if token.lifetime().expired() { + self.refresh_token(token, None) + } else { + Ok(token) + } + } } #[cfg(test)] From f2fed3726df0fcf52610f76202f2816b633db427 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Thu, 24 Dec 2015 16:15:18 -0500 Subject: [PATCH 51/54] Derive or impl Encodable and Decodable for tokens --- src/token/bearer.rs | 2 +- src/token/expiring.rs | 39 ++++++++++++++++++++++++++++++++++++++- src/token/statik.rs | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/token/bearer.rs b/src/token/bearer.rs index a001589..0ff675d 100644 --- a/src/token/bearer.rs +++ b/src/token/bearer.rs @@ -7,7 +7,7 @@ use client::response::{FromResponse, ParseError, JsonHelper}; /// The bearer token type. /// /// See [RFC 6750](http://tools.ietf.org/html/rfc6750). -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, RustcEncodable, RustcDecodable)] pub struct Bearer { access_token: String, scope: Option, diff --git a/src/token/expiring.rs b/src/token/expiring.rs index d274ec6..f47d225 100644 --- a/src/token/expiring.rs +++ b/src/token/expiring.rs @@ -1,5 +1,6 @@ -use chrono::{DateTime, UTC, Duration}; +use chrono::{DateTime, UTC, Duration, TimeZone}; use rustc_serialize::json::Json; +use rustc_serialize::{Encodable, Encoder, Decodable, Decoder}; use super::Lifetime; use client::response::{FromResponse, ParseError, JsonHelper}; @@ -54,6 +55,42 @@ impl FromResponse for Expiring { } } +#[derive(RustcEncodable, RustcDecodable)] +struct Serializable { + refresh_token: String, + expires: i64, +} + +impl<'a> From<&'a Expiring> for Serializable { + fn from(expiring: &Expiring) -> Self { + Serializable { + refresh_token: expiring.refresh_token.clone(), + expires: expiring.expires.timestamp(), + } + } +} + +impl Into for Serializable { + fn into(self) -> Expiring { + Expiring { + refresh_token: self.refresh_token, + expires: UTC.timestamp(self.expires, 0), + } + } +} + +impl Encodable for Expiring { + fn encode(&self, s: &mut S) -> Result<(), S::Error> { + Serializable::from(self).encode(s) + } +} + +impl Decodable for Expiring { + fn decode(d: &mut D) -> Result { + Serializable::decode(d).map(Into::into) + } +} + #[cfg(test)] mod tests { use chrono::{UTC, Duration}; diff --git a/src/token/statik.rs b/src/token/statik.rs index ece2bac..d9ee569 100644 --- a/src/token/statik.rs +++ b/src/token/statik.rs @@ -4,7 +4,7 @@ use super::Lifetime; use client::response::{FromResponse, ParseError, JsonHelper}; /// A static, non-expiring token. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, RustcEncodable, RustcDecodable)] pub struct Static; impl Lifetime for Static { From 0dace6bb6abdf1e391ff445f0d0aecabbf836ee1 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Fri, 25 Dec 2015 17:46:38 -0500 Subject: [PATCH 52/54] Test Expiring encode and decode --- src/token/expiring.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/token/expiring.rs b/src/token/expiring.rs index f47d225..647eaf1 100644 --- a/src/token/expiring.rs +++ b/src/token/expiring.rs @@ -93,8 +93,8 @@ impl Decodable for Expiring { #[cfg(test)] mod tests { - use chrono::{UTC, Duration}; - use rustc_serialize::json::Json; + use chrono::{UTC, Duration, Timelike}; + use rustc_serialize::json::{self, Json}; use client::response::FromResponse; use super::Expiring; @@ -120,4 +120,15 @@ mod tests { assert!(expiring.expires > UTC::now()); assert!(expiring.expires <= UTC::now() + Duration::seconds(3600)); } + + #[test] + fn encode_decode() { + let expiring = Expiring { + refresh_token: String::from("foo"), + expires: UTC::now().with_nanosecond(0).unwrap(), + }; + let json = json::encode(&expiring).unwrap(); + let decoded = json::decode(&json).unwrap(); + assert_eq!(expiring, decoded); + } } From 6a07e0f9d59df5b4e7195847f21d39e7c0039afc Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Fri, 25 Dec 2015 17:52:47 -0500 Subject: [PATCH 53/54] Add crate documentation and examples --- src/lib.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index cd3942f..67a80ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,128 @@ +//! # "It's not that hard" OAuth 2.0 +//! +//! OAuth 2.0 really isn't that hard, you know? +//! +//! Implementation of [RFC 6749](http://tools.ietf.org/html/rfc6749). +//! +//! `inth_oauth2` is on [Crates.io][crate] and [GitHub][github]. +//! +//! [crate]: https://crates.io/crates/inth-oauth2 +//! [github]: https://github.com/programble/inth-oauth2 +//! +//! ## Providers +//! +//! Support for the following OAuth 2.0 providers is included: +//! +//! - Google +//! - GitHub +//! - Imgur +//! +//! Support for other providers can be added by implementing the `Provider` trait. +//! +//! ## Token types +//! +//! The only supported token type is Bearer. Support for others can be added by implementing the +//! `Token` trait. +//! +//! ## Examples +//! +//! ### Creating a client +//! +//! ``` +//! use inth_oauth2::Client; +//! use inth_oauth2::provider::Google; +//! +//! let client = Client::::new( +//! Default::default(), +//! "client_id", +//! "client_secret", +//! Some("redirect_uri") +//! ); +//! ``` +//! +//! ### Constructing an authorization URI +//! +//! ``` +//! # use inth_oauth2::Client; +//! # use inth_oauth2::provider::Google; +//! # let client = Client::::new(Default::default(), "", "", None); +//! let auth_uri = client.auth_uri(Some("scope"), Some("state")).unwrap(); +//! println!("Authorize the application by clicking on the link: {}", auth_uri); +//! ``` +//! +//! ### Requesting an access token +//! +//! ```no_run +//! use std::io; +//! use inth_oauth2::{Client, Token}; +//! # use inth_oauth2::provider::Google; +//! # let client = Client::::new(Default::default(), "", "", None); +//! +//! let mut code = String::new(); +//! io::stdin().read_line(&mut code).unwrap(); +//! +//! let token = client.request_token(code.trim()).unwrap(); +//! println!("{}", token.access_token()); +//! ``` +//! +//! ### Refreshing an access token +//! +//! ```no_run +//! # use inth_oauth2::Client; +//! # use inth_oauth2::provider::Google; +//! # let client = Client::::new(Default::default(), "", "", None); +//! # let token = client.request_token("").unwrap(); +//! let token = client.refresh_token(token, None).unwrap(); +//! ``` +//! +//! ### Ensuring an access token is still valid +//! +//! ```no_run +//! # use inth_oauth2::Client; +//! # use inth_oauth2::provider::Google; +//! # let client = Client::::new(Default::default(), "", "", None); +//! # let mut token = client.request_token("").unwrap(); +//! // Refresh token only if it has expired. +//! token = client.ensure_token(token).unwrap(); +//! ``` +//! +//! ### Using bearer access tokens +//! +//! Bearer tokens can be converted to Hyper headers. +//! +//! ```no_run +//! # extern crate hyper; +//! # extern crate inth_oauth2; +//! # use inth_oauth2::Client; +//! # use inth_oauth2::provider::Google; +//! use hyper::header::Authorization; +//! +//! # fn main() { +//! # let client = Client::::new(Default::default(), "", "", None); +//! # let token = client.request_token("").unwrap(); +//! let client = hyper::Client::new(); +//! let request = client.get("https://example.com/resource") +//! .header(Into::>::into(&token)); +//! # } +//! ``` +//! +//! ### Persisting tokens +//! +//! All token types implement `Encodable` and `Decodable` from `rustc_serialize`. +//! +//! ```no_run +//! # extern crate inth_oauth2; +//! # extern crate rustc_serialize; +//! # use inth_oauth2::Client; +//! # use inth_oauth2::provider::Google; +//! use rustc_serialize::json; +//! # fn main() { +//! # let client = Client::::new(Default::default(), "", "", None); +//! # let token = client.request_token("").unwrap(); +//! let json = json::encode(&token).unwrap(); +//! # } +//! ``` + #![warn( missing_docs, missing_debug_implementations, From c0da5aa016528347ec20b0f7cb9e83a5755c072a Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Fri, 25 Dec 2015 20:23:48 -0500 Subject: [PATCH 54/54] Update examples for Client reexport --- examples/github.rs | 2 +- examples/google.rs | 2 +- examples/imgur.rs | 2 +- src/client/mod.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/github.rs b/examples/github.rs index 4ab2000..3eaa5e5 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -2,7 +2,7 @@ extern crate inth_oauth2; use std::io; -use inth_oauth2::client::Client; +use inth_oauth2::Client; use inth_oauth2::provider::GitHub; fn main() { diff --git a/examples/google.rs b/examples/google.rs index 0d4859e..0e431db 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -2,7 +2,7 @@ extern crate inth_oauth2; use std::io; -use inth_oauth2::client::Client; +use inth_oauth2::Client; use inth_oauth2::provider::Google; fn main() { diff --git a/examples/imgur.rs b/examples/imgur.rs index 0eaa394..c48a39e 100644 --- a/examples/imgur.rs +++ b/examples/imgur.rs @@ -2,7 +2,7 @@ extern crate inth_oauth2; use std::io; -use inth_oauth2::client::Client; +use inth_oauth2::Client; use inth_oauth2::provider::Imgur; fn main() { diff --git a/src/client/mod.rs b/src/client/mod.rs index 84c2b06..613411d 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -42,7 +42,7 @@ impl Client

{ /// # Examples /// /// ``` - /// use inth_oauth2::client::Client; + /// use inth_oauth2::Client; /// use inth_oauth2::provider::Google; /// /// let client = Client::::new( @@ -74,7 +74,7 @@ impl Client

{ /// # Examples /// /// ``` - /// use inth_oauth2::client::Client; + /// use inth_oauth2::Client; /// use inth_oauth2::provider::Google; /// /// let client = Client::::new(