Begin rewrite

This commit is contained in:
Curtis McEnroe 2015-12-21 21:31:14 -05:00
parent fcd1945dba
commit ac156bc7b4
8 changed files with 0 additions and 710 deletions

View File

@ -1,24 +0,0 @@
extern crate inth_oauth2;
use std::io;
use inth_oauth2::{Client, GitHub};
fn main() {
let client = Client::<GitHub>::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);
}

View File

@ -1,31 +0,0 @@
extern crate inth_oauth2;
use std::io;
use inth_oauth2::{Client, Google};
fn main() {
let client = Client::<Google>::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);
}

View File

@ -1,24 +0,0 @@
extern crate inth_oauth2;
use std::io;
use inth_oauth2::{Client, Imgur};
fn main() {
let client = Client::<Imgur>::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);
}

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

View File

@ -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<String>,
/// A URI identifying a human-readable web page with information about the error.
pub uri: Option<String>,
}
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<T> = result::Result<T, Error>;
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);

View File

@ -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::<Google>::new(
//! Default::default(),
//! "CLIENT_ID",
//! "CLIENT_SECRET",
//! Some("REDIRECT_URI")
//! );
//! ```
//!
//! ### Constructing an authorization URI
//!
//! ```
//! # use inth_oauth2::{Client, Google};
//! # let client = Client::<Google>::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::<Google>::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::<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();
//! }
//! }
//! ```
//!
//! ### 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::<Google>::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;

View File

@ -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" }
}

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)
}
}