Begin rewrite
This commit is contained in:
parent
fcd1945dba
commit
ac156bc7b4
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
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
|
|
||||||
}
|
|
||||||
}
|
|
135
src/error.rs
135
src/error.rs
|
@ -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);
|
|
132
src/lib.rs
132
src/lib.rs
|
@ -1,136 +1,4 @@
|
||||||
//! # "It's not that hard" OAuth2 Client
|
|
||||||
//!
|
|
||||||
//! OAuth 2.0 really isn't that hard, you know?
|
|
||||||
//!
|
|
||||||
//! Implementation of [RFC6749](http://tools.ietf.org/html/rfc6749).
|
|
||||||
//!
|
|
||||||
//! `inth_oauth2` is on [Crates.io][crate] and [GitHub][github].
|
|
||||||
//!
|
|
||||||
//! [crate]: https://crates.io/crates/inth-oauth2
|
|
||||||
//! [github]: https://github.com/programble/inth-oauth2
|
|
||||||
//!
|
|
||||||
//! ## Providers
|
|
||||||
//!
|
|
||||||
//! `inth_oauth2` supports the following OAuth 2.0 providers:
|
|
||||||
//!
|
|
||||||
//! - `Google`
|
|
||||||
//! - `GitHub`
|
|
||||||
//! - `Imgur`
|
|
||||||
//!
|
|
||||||
//! Support for others can be added by implementing the `Provider` trait.
|
|
||||||
//!
|
|
||||||
//! ## Examples
|
|
||||||
//!
|
|
||||||
//! ### Creating a client
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! use inth_oauth2::{Client, Google};
|
|
||||||
//!
|
|
||||||
//! let client = Client::<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 chrono;
|
||||||
extern crate hyper;
|
extern crate hyper;
|
||||||
extern crate rustc_serialize;
|
extern crate rustc_serialize;
|
||||||
extern crate url;
|
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;
|
|
||||||
|
|
|
@ -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" }
|
|
||||||
}
|
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue