Merge pull request #1 from programble/rewrite

The Big Types Rewrite
This commit is contained in:
Curtis McEnroe 2015-12-25 21:09:32 -05:00
commit 6cb32dd368
15 changed files with 1027 additions and 484 deletions

View File

@ -1,7 +1,9 @@
extern crate inth_oauth2;
use std::io;
use inth_oauth2::{Client, GitHub};
use inth_oauth2::Client;
use inth_oauth2::provider::GitHub;
fn main() {
let client = Client::<GitHub>::new(
@ -12,13 +14,11 @@ 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_pair = client.request_token(code.trim()).unwrap();
println!("{:?}", token_pair);
let token = client.request_token(code.trim()).unwrap();
println!("{:?}", token);
}

View File

@ -1,7 +1,9 @@
extern crate inth_oauth2;
use std::io;
use inth_oauth2::{Client, Google};
use inth_oauth2::Client;
use inth_oauth2::provider::Google;
fn main() {
let client = Client::<Google>::new(
@ -11,21 +13,16 @@ fn main() {
Some("urn:ietf:wg:oauth:2.0:oob")
);
let auth_uri = client.auth_uri(
Some("https://www.googleapis.com/auth/userinfo.email"),
None
).unwrap();
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();
let token = client.request_token(code.trim()).unwrap();
println!("{:?}", token);
println!("{:?}", token_pair);
let refreshed = client.refresh_token(token_pair.refresh.unwrap(), None).unwrap();
println!("{:?}", refreshed);
let token = client.refresh_token(token, None).unwrap();
println!("{:?}", token);
}

View File

@ -1,7 +1,9 @@
extern crate inth_oauth2;
use std::io;
use inth_oauth2::{Client, Imgur};
use inth_oauth2::Client;
use inth_oauth2::provider::Imgur;
fn main() {
let client = Client::<Imgur>::new(
@ -12,13 +14,14 @@ 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_pair = client.request_token(code.trim()).unwrap();
let token = client.request_token(code.trim()).unwrap();
println!("{:?}", token);
println!("{:?}", token_pair);
let token = client.refresh_token(token, None).unwrap();
println!("{:?}", token);
}

View File

@ -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<P: Provider> {
http_client: hyper::Client,
client_id: String,
client_secret: String,
redirect_uri: Option<String>,
provider: PhantomData<P>,
}
impl<P: Provider> Client<P> {
/// Creates an OAuth 2.0 client.
pub fn new<S>(
http_client: hyper::Client,
client_id: S,
client_secret: S,
redirect_uri: Option<S>
) -> Self where S: Into<String> {
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<i64>,
refresh_token: Option<String>,
scope: Option<String>,
}
impl Into<TokenPair> 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<String>,
error_uri: Option<String>,
}
impl Into<OAuth2Error> 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<P: Provider> Client<P> {
/// 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<String> {
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::Basic> {
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<TokenPair> {
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::<TokenResponse>(&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<TokenPair> {
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<TokenPair> {
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
}
}

80
src/client/error.rs Normal file
View File

@ -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);

249
src/client/mod.rs Normal file
View File

@ -0,0 +1,249 @@
//! Client.
use std::fmt;
use std::marker::PhantomData;
use hyper::{self, header, mime};
use rustc_serialize::json::Json;
use url::{form_urlencoded, Url};
use error::OAuth2Error;
use provider::Provider;
use token::{Token, Lifetime, Expiring};
use self::response::FromResponse;
pub mod response;
pub use self::error::ClientError;
mod error;
/// OAuth 2.0 client.
pub struct Client<P: Provider> {
http_client: hyper::Client,
client_id: String,
client_secret: String,
redirect_uri: Option<String>,
provider: PhantomData<P>,
}
impl<P: Provider> fmt::Debug for Client<P> {
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<P: Provider> Client<P> {
/// Creates a client.
///
/// # Examples
///
/// ```
/// use inth_oauth2::Client;
/// use inth_oauth2::provider::Google;
///
/// let client = Client::<Google>::new(
/// Default::default(),
/// "CLIENT_ID",
/// "CLIENT_SECRET",
/// Some("urn:ietf:wg:oauth:2.0:oob")
/// );
/// ```
pub fn new<S>(
http_client: hyper::Client,
client_id: S,
client_secret: S,
redirect_uri: Option<S>
) -> Self where S: Into<String> {
Client {
http_client: http_client,
client_id: client_id.into(),
client_secret: client_secret.into(),
redirect_uri: redirect_uri.map(Into::into),
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).
///
/// # Examples
///
/// ```
/// use inth_oauth2::Client;
/// use inth_oauth2::provider::Google;
///
/// let client = Client::<Google>::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<String, ClientError>
{
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())
}
fn post_token(&self, body_pairs: Vec<(&str, &str)>) -> Result<Json, ClientError> {
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).
pub fn request_token(&self, code: &str) -> Result<P::Token, ClientError> {
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 json = try!(self.post_token(body_pairs));
let token = try!(P::Token::from_response(&json));
Ok(token)
}
}
impl<P: Provider> Client<P> where P::Token: Token<Expiring> {
/// 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<P::Token, ClientError> {
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)
}
/// Ensures an access token is valid by refreshing it if necessary.
pub fn ensure_token(&self, token: P::Token) -> Result<P::Token, ClientError> {
if token.lifetime().expired() {
self.refresh_token(token, None)
} else {
Ok(token)
}
}
}
#[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<Static>;
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::<Test>::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::<Test>::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::<Test>::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::<Test>::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()
);
}
}

95
src/client/response.rs Normal file
View File

@ -0,0 +1,95 @@
//! Response parsing.
use std::error::Error;
use std::fmt;
use rustc_serialize::json::{self, Json};
/// Response parsing.
pub trait FromResponse: Sized {
/// Parse a JSON response.
fn from_response(json: &Json) -> Result<Self, ParseError>;
/// 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<Self, ParseError> {
FromResponse::from_response(json)
}
}
/// Response parse errors.
#[derive(Debug, PartialEq, Eq)]
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" }
}
/// 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<JsonObjectHelper<'a>, 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 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.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<i64> {
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<i64, ParseError> {
self.get_i64_option(key).ok_or(ParseError::ExpectedFieldType(key, "i64"))
}
}

View File

@ -1,13 +1,16 @@
use std::{error, fmt, io, result};
//! Errors.
use hyper;
use rustc_serialize::json;
use url;
use std::error::Error;
use std::fmt;
use rustc_serialize::json::Json;
use client::response::{FromResponse, ParseError, JsonHelper};
/// OAuth 2.0 error codes.
///
/// See [RFC6749 section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2).
#[derive(Debug, Clone)]
/// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2).
#[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
@ -33,14 +36,28 @@ pub enum OAuth2ErrorCode {
/// resource owner.
InvalidScope,
/// An unrecognized error code, not defined in RFC6749.
/// An unrecognized error code, not defined in RFC 6749.
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 [RFC6749 section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2).
#[derive(Debug, Clone)]
/// See [RFC 6749, section 5.2](http://tools.ietf.org/html/rfc6749#section-5.2).
#[derive(Debug, PartialEq, Eq)]
pub struct OAuth2Error {
/// Error code.
pub code: OAuth2ErrorCode,
@ -53,7 +70,7 @@ pub struct OAuth2Error {
}
impl fmt::Display for OAuth2Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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));
@ -65,71 +82,81 @@ impl fmt::Display for OAuth2Error {
}
}
impl error::Error for OAuth2Error {
fn description(&self) -> &str {
"OAuth2 API error"
impl Error for OAuth2Error {
fn description(&self) -> &str { "OAuth 2.0 API error" }
}
impl FromResponse for OAuth2Error {
fn from_response(json: &Json) -> Result<Self, ParseError> {
let obj = try!(JsonHelper(json).as_object());
let code = try!(obj.get_string("error"));
let description = obj.get_string_option("error_description");
let uri = obj.get_string_option("error_uri");
Ok(OAuth2Error {
code: code.into(),
description: description.map(Into::into),
uri: uri.map(Into::into),
})
}
}
/// 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),
}
#[cfg(test)]
mod tests {
use rustc_serialize::json::Json;
/// Result type returned from authentication flow methods.
pub type Result<T> = result::Result<T, Error>;
use client::response::{FromResponse, ParseError};
use super::{OAuth2Error, OAuth2ErrorCode};
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",
}
#[test]
fn from_response_empty() {
let json = Json::from_str("{}").unwrap();
assert_eq!(
ParseError::ExpectedFieldType("error", "string"),
OAuth2Error::from_response(&json).unwrap_err()
);
}
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),
}
#[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()
);
}
}
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);

View File

@ -1,8 +1,8 @@
//! # "It's not that hard" OAuth2 Client
//! # "It's not that hard" OAuth 2.0
//!
//! OAuth 2.0 really isn't that hard, you know?
//!
//! Implementation of [RFC6749](http://tools.ietf.org/html/rfc6749).
//! Implementation of [RFC 6749](http://tools.ietf.org/html/rfc6749).
//!
//! `inth_oauth2` is on [Crates.io][crate] and [GitHub][github].
//!
@ -11,126 +11,139 @@
//!
//! ## Providers
//!
//! `inth_oauth2` supports the following OAuth 2.0 providers:
//! Support for the following OAuth 2.0 providers is included:
//!
//! - `Google`
//! - `GitHub`
//! - `Imgur`
//! - Google
//! - GitHub
//! - Imgur
//!
//! Support for others can be added by implementing the `Provider` trait.
//! 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, Google};
//! use inth_oauth2::Client;
//! use inth_oauth2::provider::Google;
//!
//! let client = Client::<Google>::new(
//! Default::default(),
//! "CLIENT_ID",
//! "CLIENT_SECRET",
//! Some("REDIRECT_URI")
//! "client_id",
//! "client_secret",
//! Some("redirect_uri")
//! );
//! ```
//!
//! ### Constructing an authorization URI
//!
//! ```
//! # use inth_oauth2::{Client, Google};
//! # use inth_oauth2::Client;
//! # use inth_oauth2::provider::Google;
//! # let client = Client::<Google>::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);
//! ```
//!
//! 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};
//! use std::io;
//! use inth_oauth2::{Client, Token};
//! # use inth_oauth2::provider::Google;
//! # let client = Client::<Google>::new(Default::default(), "", "", None);
//! # let code = String::new();
//! let token_pair = client.request_token(&code).unwrap();
//! println!("{}", token_pair.access.token);
//!
//! 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, Google};
//! # use inth_oauth2::Client;
//! # use inth_oauth2::provider::Google;
//! # let client = Client::<Google>::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();
//! }
//! }
//! # 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::<Google>::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
//!
//! If the obtained token is of the `Bearer` type, a Hyper `Authorization` header can be created
//! from it.
//! 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() {
//! # use inth_oauth2::{Client, Google};
//! # let client = Client::<Google>::new(Default::default(), "", "", None);
//! # let mut token_pair = client.request_token("").unwrap();
//! # let token = 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();
//! let request = client.get("https://example.com/resource")
//! .header(Into::<Authorization<_>>::into(&token));
//! # }
//! ```
//!
//! ### Persisting tokens
//!
//! `TokenPair` implements `Encodable` and `Decodable` from `rustc_serialize`, so can be persisted
//! as JSON.
//! All token types implement `Encodable` and `Decodable` from `rustc_serialize`.
//!
//! ```
//! ```no_run
//! # extern crate inth_oauth2;
//! # extern crate rustc_serialize;
//! # extern crate chrono;
//! use inth_oauth2::{TokenPair, AccessTokenType, AccessToken, RefreshToken};
//! # use inth_oauth2::Client;
//! # use inth_oauth2::provider::Google;
//! 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);
//! # let client = Client::<Google>::new(Default::default(), "", "", None);
//! # let token = client.request_token("").unwrap();
//! let json = json::encode(&token).unwrap();
//! # }
//! ```
#![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;
extern crate url;
pub use client::Client;
pub mod client;
pub use token::{Token, Lifetime};
pub use client::{Client, ClientError};
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 provider;
pub mod error;
pub mod client;

View File

@ -1,9 +1,23 @@
/// An OAuth 2.0 provider.
//! Providers.
use token::{Token, Lifetime, Bearer, Static, Expiring};
/// 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<Self::Lifetime>;
/// 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;
}
@ -11,17 +25,23 @@ 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 {
fn auth_uri() -> &'static str { "https://accounts.google.com/o/oauth2/auth" }
fn token_uri() -> &'static str { "https://accounts.google.com/o/oauth2/token" }
type Lifetime = Expiring;
type Token = Bearer<Expiring>;
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 API](https://developer.github.com/v3/oauth/).
/// See [OAuth, GitHub Developer Guide](https://developer.github.com/v3/oauth/).
#[derive(Debug)]
pub struct GitHub;
impl Provider for GitHub {
type Lifetime = Static;
type Token = Bearer<Static>;
fn auth_uri() -> &'static str { "https://github.com/login/oauth/authorize" }
fn token_uri() -> &'static str { "https://github.com/login/oauth/access_token" }
}
@ -29,8 +49,11 @@ 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;
type Token = Bearer<Expiring>;
fn auth_uri() -> &'static str { "https://api.imgur.com/oauth2/authorize" }
fn token_uri() -> &'static str { "https://api.imgur.com/oauth2/token" }
}

View File

@ -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<RefreshToken>,
}
/// 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<DateTime<UTC>>,
/// The scope of the access token.
pub scope: Option<String>,
}
/// 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<header::Authorization<header::Bearer>> {
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<i64>,
scope: Option<String>,
}
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<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
SerializableAccessToken::from_access_token(self).encode(s)
}
}
impl Decodable for AccessToken {
fn decode<D: Decoder>(d: &mut D) -> Result<Self, D::Error> {
SerializableAccessToken::decode(d)
.map(SerializableAccessToken::into_access_token)
}
}

166
src/token/bearer.rs Normal file
View File

@ -0,0 +1,166 @@
use hyper::header;
use rustc_serialize::json::Json;
use super::{Token, Lifetime};
use client::response::{FromResponse, ParseError, JsonHelper};
/// The bearer token type.
///
/// See [RFC 6750](http://tools.ietf.org/html/rfc6750).
#[derive(Debug, PartialEq, Eq, RustcEncodable, RustcDecodable)]
pub struct Bearer<L: Lifetime> {
access_token: String,
scope: Option<String>,
lifetime: L,
}
impl<L: Lifetime> Token<L> for Bearer<L> {
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<header::Authorization<header::Bearer>> for &'a Bearer<L> {
fn into(self) -> header::Authorization<header::Bearer> {
header::Authorization(header::Bearer { token: self.access_token.clone() })
}
}
impl<L: Lifetime> Bearer<L> {
fn from_response_and_lifetime(json: &Json, lifetime: L) -> Result<Self, ParseError> {
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<L: Lifetime> FromResponse for Bearer<L> {
fn from_response(json: &Json) -> Result<Self, ParseError> {
let lifetime = try!(FromResponse::from_response(json));
Bearer::from_response_and_lifetime(json, lifetime)
}
fn from_response_inherit(json: &Json, prev: &Self) -> Result<Self, ParseError> {
let lifetime = try!(FromResponse::from_response_inherit(json, &prev.lifetime));
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::<Static>::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::<Static>::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::<Static>::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::<Static>::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::<Expiring>::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::<Expiring>::from_response(&json).unwrap();
let json = Json::from_str(r#"
{
"token_type":"Bearer",
"access_token":"cccccccc",
"expires_in":3600
}
"#).unwrap();
let bearer = Bearer::<Expiring>::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)));
}
}

134
src/token/expiring.rs Normal file
View File

@ -0,0 +1,134 @@
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};
/// An expiring token.
#[derive(Debug, PartialEq, Eq)]
pub struct Expiring {
refresh_token: String,
expires: DateTime<UTC>,
}
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<UTC> { &self.expires }
}
impl Lifetime for Expiring {
fn expired(&self) -> bool { self.expires < UTC::now() }
}
impl FromResponse for Expiring {
fn from_response(json: &Json) -> Result<Self, ParseError> {
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<Self, ParseError> {
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),
})
}
}
#[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<Expiring> for Serializable {
fn into(self) -> Expiring {
Expiring {
refresh_token: self.refresh_token,
expires: UTC.timestamp(self.expires, 0),
}
}
}
impl Encodable for Expiring {
fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
Serializable::from(self).encode(s)
}
}
impl Decodable for Expiring {
fn decode<D: Decoder>(d: &mut D) -> Result<Self, D::Error> {
Serializable::decode(d).map(Into::into)
}
}
#[cfg(test)]
mod tests {
use chrono::{UTC, Duration, Timelike};
use rustc_serialize::json::{self, 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));
}
#[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);
}
}

39
src/token/mod.rs Normal file
View File

@ -0,0 +1,39 @@
//! Tokens.
//!
//! Access token types are abstracted through the `Token` trait. See
//! [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.
use client::response::FromResponse;
/// OAuth 2.0 tokens.
///
/// See [RFC 6749, section 5](http://tools.ietf.org/html/rfc6749#section-5).
pub trait Token<L: Lifetime>: FromResponse {
/// Returns the access token.
///
/// 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.
fn scope(&self) -> Option<&str>;
/// Returns the token lifetime.
fn lifetime(&self) -> &L;
}
/// OAuth 2.0 token lifetimes.
pub trait Lifetime: FromResponse {
/// Returns true if the access token is no longer valid.
fn expired(&self) -> bool;
}
pub use self::bearer::Bearer;
mod bearer;
pub use self::statik::Static;
mod statik;
pub use self::expiring::Expiring;
mod expiring;

45
src/token/statik.rs Normal file
View File

@ -0,0 +1,45 @@
use rustc_serialize::json::Json;
use super::Lifetime;
use client::response::{FromResponse, ParseError, JsonHelper};
/// A static, non-expiring token.
#[derive(Debug, PartialEq, Eq, RustcEncodable, RustcDecodable)]
pub struct Static;
impl Lifetime for Static {
fn expired(&self) -> bool { false }
}
impl FromResponse for Static {
fn from_response(json: &Json) -> Result<Self, ParseError> {
let obj = try!(JsonHelper(json).as_object());
if obj.0.contains_key("expires_in") {
return Err(ParseError::UnexpectedField("expires_in"));
}
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()
);
}
}