commit
6cb32dd368
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
206
src/client.rs
206
src/client.rs
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
165
src/error.rs
165
src/error.rs
|
@ -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);
|
||||
|
|
137
src/lib.rs
137
src/lib.rs
|
@ -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;
|
||||
|
|
|
@ -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" }
|
||||
}
|
||||
|
|
122
src/token.rs
122
src/token.rs
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue