mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-04 18:30:26 +01:00
initial commit after moving it out of yup/lib
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.sublime-workspace
|
||||
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
|
||||
name = "yup-oauth2"
|
||||
version = "0.0.1"
|
||||
authors = ["Sebastian Thiel <byronimo@gmail.com>"]
|
||||
repository = "https://github.com/Byron/yup/lib/oauth2"
|
||||
description = """A partial oauth2 implementation, providing the 'device' authorization flow.
|
||||
More information about how it's done can be found here: https://developers.google.com/youtube/v3/guides/authentication#devices
|
||||
"""
|
||||
|
||||
[dependencies]
|
||||
chrono = "*"
|
||||
hyper = "*"
|
||||
log = "*"
|
||||
mime = "*"
|
||||
url = "*"
|
||||
itertools = "*"
|
||||
rustc-serialize = "*"
|
||||
|
||||
[dev-dependencies]
|
||||
getopts = "*"
|
||||
|
||||
[dev-dependencies.yup-hyper-mock]
|
||||
path = "../hyper-mock"
|
||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
**yup-oauth2** is a utility library which will implement [oauthv2 device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices) suitable for [**yup**](https://github.com/Byron/yup) to work.
|
||||
|
||||
It is implemented such that it makes no assumptions about the front-end, allowing more uses than just in yup.
|
||||
|
||||
Architecturally, it may never be implementing more than device authentication, yet is set up not to constrain itself.
|
||||
72
examples/auth.rs
Normal file
72
examples/auth.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
extern crate "yup-oauth2" as oauth2;
|
||||
extern crate "yup-hyper-mock" as mock;
|
||||
extern crate hyper;
|
||||
extern crate getopts;
|
||||
|
||||
use getopts::{HasArg,Options,Occur,Fail};
|
||||
use std::os;
|
||||
use std::old_io::{File, FileMode, FileAccess};
|
||||
use std::old_path::Path;
|
||||
|
||||
fn usage(program: &str, opts: &Options, err: Option<Fail>) {
|
||||
if err.is_some() {
|
||||
println!("{}", err.unwrap());
|
||||
os::set_exit_status(1);
|
||||
}
|
||||
println!("{}", opts.short_usage(program) + " SCOPE [SCOPE ...]");
|
||||
println!("{}", opts.usage("A program to authenticate against oauthv2 services.\n\
|
||||
See https://developers.google.com/youtube/registering_an_application\n\
|
||||
and https://developers.google.com/youtube/v3/guides/authentication#devices"));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = os::args();
|
||||
let prog = args[0].clone();
|
||||
|
||||
let mut opts = Options::new();
|
||||
opts.opt("c", "id", "oauthv2 ID of your application", "CLIENT_ID", HasArg::Yes, Occur::Req)
|
||||
.opt("s", "secret", "oauthv2 secret of your application", "CLIENT_SECRET", HasArg::Yes, Occur::Req);
|
||||
|
||||
let m = match opts.parse(args.tail()) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
usage(&prog, &opts, Some(e));
|
||||
return
|
||||
}
|
||||
};
|
||||
|
||||
if m.free.len() == 0 {
|
||||
let msg = Fail::ArgumentMissing("you must provide one or more authorization scopes as free options".to_string());
|
||||
usage(&prog, &opts, Some(msg));
|
||||
return
|
||||
}
|
||||
|
||||
let client_id = m.opt_str("c").unwrap();
|
||||
let client_secret = m.opt_str("s").unwrap();
|
||||
|
||||
println!("THIS PROGRAM PRINTS ALL COMMUNICATION TO STDERR !!!");
|
||||
|
||||
struct StdoutHandler;
|
||||
impl oauth2::DeviceFlowHelperDelegate for StdoutHandler {
|
||||
fn present_user_code(&mut self, pi: oauth2::PollInformation) {
|
||||
println!("Please enter '{}' at {} and authenticate the application for the\n\
|
||||
given scopes. This is not a test !\n\
|
||||
You have time until {} to do that.
|
||||
Do not terminate the program until you deny or grant access !",
|
||||
pi.user_code, pi.verification_url, pi.expires_at);
|
||||
}
|
||||
}
|
||||
|
||||
let client = hyper::Client::with_connector(mock::TeeConnector {
|
||||
connector: hyper::net::HttpConnector(None)
|
||||
});
|
||||
if let Some(t) = oauth2::DeviceFlowHelper::new(&mut StdoutHandler)
|
||||
.retrieve_token(client, &client_id, &client_secret, &m.free) {
|
||||
println!("Authentication granted !");
|
||||
println!("You should store the following information for use, or revoke it.");
|
||||
println!("{:?}", t);
|
||||
} else {
|
||||
println!("Invalid client id, invalid scope, user denied access or request expired");
|
||||
os::set_exit_status(10);
|
||||
}
|
||||
}
|
||||
33
src/common.rs
Normal file
33
src/common.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use chrono::{DateTime, UTC};
|
||||
|
||||
/// Represents a token as returned by OAuth2 servers.
|
||||
///
|
||||
/// It is produced by all authentication flows.
|
||||
/// It authenticates certain operations, and must be refreshed once
|
||||
/// it reached it's expirey date.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct Token {
|
||||
/// used when authenticating calls to oauth2 enabled services
|
||||
pub access_token: String,
|
||||
/// used to refresh an expired access_token
|
||||
pub refresh_token: String,
|
||||
/// The token type as string - usually 'Bearer'
|
||||
pub token_type: String,
|
||||
/// access_token is not valid for use after this date
|
||||
pub expires_at: DateTime<UTC>
|
||||
}
|
||||
|
||||
/// All known authentication types, for suitable constants
|
||||
pub enum AuthenticationType {
|
||||
/// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices)
|
||||
Device,
|
||||
}
|
||||
|
||||
impl AuthenticationType {
|
||||
/// Converts itself into a URL string
|
||||
pub fn url(&self) -> &'static str {
|
||||
match *self {
|
||||
AuthenticationType::Device => "https://accounts.google.com/o/oauth2/device/code",
|
||||
}
|
||||
}
|
||||
}
|
||||
527
src/device.rs
Normal file
527
src/device.rs
Normal file
@@ -0,0 +1,527 @@
|
||||
use std::iter::IntoIterator;
|
||||
use std::time::Duration;
|
||||
use std::default::Default;
|
||||
use std::cmp::min;
|
||||
use std::old_io::timer;
|
||||
|
||||
use hyper;
|
||||
use hyper::header::ContentType;
|
||||
use url::form_urlencoded;
|
||||
use itertools::Itertools;
|
||||
use rustc_serialize::{self, json};
|
||||
use chrono::{DateTime,UTC};
|
||||
|
||||
use common::{Token, AuthenticationType};
|
||||
|
||||
pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token";
|
||||
|
||||
/// Implements the [Oauth2 Device Flow](https://developers.google.com/youtube/v3/guides/authentication#devices)
|
||||
/// It operates in two steps:
|
||||
/// * obtain a code to show to the user
|
||||
/// * (repeatedly) poll for the user to authenticate your application
|
||||
pub struct DeviceFlow<NC> {
|
||||
client: hyper::Client<NC>,
|
||||
device_code: String,
|
||||
state: PollResult,
|
||||
secret: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
|
||||
/// Contains state of pending authentication requests
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PollInformation {
|
||||
/// Code the user must enter ...
|
||||
pub user_code: String,
|
||||
/// ... at the verification URL
|
||||
pub verification_url: String,
|
||||
|
||||
/// The `user_code` expires at the given time
|
||||
/// It's the time the user has left to authenticate your application
|
||||
pub expires_at: DateTime<UTC>,
|
||||
/// The interval in which we may poll for a status change
|
||||
/// The server responds with errors of we poll too fast.
|
||||
pub interval: Duration,
|
||||
|
||||
/// The message given by the server while polling it,
|
||||
/// usually not relevant to the user or the application
|
||||
pub server_message: String,
|
||||
}
|
||||
|
||||
/// Encapsulates all possible results of the `request_token(...)` operation
|
||||
#[derive(Clone)]
|
||||
pub enum RequestResult {
|
||||
/// Indicates connection failure
|
||||
Error(hyper::HttpError),
|
||||
/// The OAuth client was not found
|
||||
InvalidClient,
|
||||
/// Some requested scopes were invalid. String contains the scopes as part of
|
||||
/// the server error message
|
||||
InvalidScope(String),
|
||||
/// Indicates we may enter the next phase
|
||||
ProceedWithPolling(PollInformation),
|
||||
}
|
||||
|
||||
impl RequestResult {
|
||||
fn from_server_message(msg: &str, desc: &str) -> RequestResult {
|
||||
match msg {
|
||||
"invalid_client" => RequestResult::InvalidClient,
|
||||
"invalid_scope" => RequestResult::InvalidScope(desc.to_string()),
|
||||
_ => panic!("'{}' not understood", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates all possible results of a `poll_token(...)` operation
|
||||
#[derive(Clone)]
|
||||
pub enum PollResult {
|
||||
/// Connection failure - retry if you think it's worth it
|
||||
Error(hyper::HttpError),
|
||||
/// See `PollInformation`
|
||||
AuthorizationPending(PollInformation),
|
||||
/// indicates we are expired, including the expiration date
|
||||
Expired(DateTime<UTC>),
|
||||
/// Indicates that the user declined access. String is server response
|
||||
AccessDenied,
|
||||
/// Indicates that access is granted, and you are done
|
||||
AccessGranted(Token),
|
||||
}
|
||||
|
||||
impl Default for PollResult {
|
||||
fn default() -> PollResult {
|
||||
PollResult::Error(hyper::HttpError::HttpStatusError)
|
||||
}
|
||||
}
|
||||
|
||||
impl<NC> DeviceFlow<NC>
|
||||
where NC: hyper::net::NetworkConnector {
|
||||
|
||||
/// # Examples
|
||||
/// ```test_harness
|
||||
/// extern crate hyper;
|
||||
/// extern crate "yup-oauth2" as oauth2;
|
||||
/// use oauth2::DeviceFlow;
|
||||
///
|
||||
/// # #[test] fn new() {
|
||||
/// let mut f = DeviceFlow::new(hyper::Client::new());
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(client: hyper::Client<NC>) -> DeviceFlow<NC> {
|
||||
DeviceFlow {
|
||||
client: client,
|
||||
device_code: Default::default(),
|
||||
secret: Default::default(),
|
||||
id: Default::default(),
|
||||
state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The first step involves asking the server for a code that the user
|
||||
/// can type into a field at a specified URL. It is called only once, assuming
|
||||
/// there was no connection error. Otherwise, it may be called again until
|
||||
/// the state changes to `PollResult::AuthorizationPending`.
|
||||
/// # Arguments
|
||||
/// * `client_id` & `client_secret` - as obtained when [registering your application](https://developers.google.com/youtube/registering_an_application)
|
||||
/// * `scopes` - an iterator yielding String-like objects which are URLs defining what your
|
||||
/// application is able to do. It is considered good behaviour to authenticate
|
||||
/// only once, with all scopes you will ever require.
|
||||
/// However, you can also manage multiple tokens for different scopes, if your
|
||||
/// application is providing distinct read-only and write modes.
|
||||
/// # Handling the `PollResult`
|
||||
/// * will panic if called while our state is not `PollResult::Error`
|
||||
/// or `PollResult::NeedToken`
|
||||
/// # Examples
|
||||
/// See test-cases in source code for a more complete example.
|
||||
pub fn request_code<'b, T, I>(&mut self, client_id: &str, client_secret: &str, scopes: I)
|
||||
-> RequestResult
|
||||
where T: Str,
|
||||
I: IntoIterator<Item=&'b T> {
|
||||
if self.device_code.len() > 0 {
|
||||
panic!("Must not be called after we have obtained a token and have no error");
|
||||
}
|
||||
|
||||
// note: cloned() shouldn't be needed, see issue
|
||||
// https://github.com/servo/rust-url/issues/81
|
||||
let req = form_urlencoded::serialize(
|
||||
[("client_id", client_id),
|
||||
("scope", scopes.into_iter()
|
||||
.map(|s| s.as_slice())
|
||||
.intersperse(" ")
|
||||
.collect::<String>()
|
||||
.as_slice())].iter().cloned());
|
||||
|
||||
match self.client.post(AuthenticationType::Device.url())
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(req.as_slice())
|
||||
.send() {
|
||||
Err(err) => {
|
||||
return RequestResult::Error(err);
|
||||
}
|
||||
Ok(mut res) => {
|
||||
|
||||
|
||||
#[derive(RustcDecodable)]
|
||||
struct JsonData {
|
||||
device_code: String,
|
||||
user_code: String,
|
||||
verification_url: String,
|
||||
expires_in: i64,
|
||||
interval: i64,
|
||||
}
|
||||
|
||||
|
||||
#[derive(RustcDecodable)]
|
||||
struct JsonError {
|
||||
error: String,
|
||||
error_description: String
|
||||
}
|
||||
|
||||
// This will work once hyper uses std::io::Reader
|
||||
// let decoded: JsonData = rustc_serialize::Decodable::decode(
|
||||
// &mut json::Decoder::new(
|
||||
// json::Json::from_reader(&mut res)
|
||||
// .ok()
|
||||
// .expect("decode must work!"))).unwrap();
|
||||
let json_str = String::from_utf8(res.read_to_end().unwrap()).unwrap();
|
||||
|
||||
// check for error
|
||||
match json::decode::<JsonError>(&json_str) {
|
||||
Err(_) => {}, // ignore, move on
|
||||
Ok(res) => {
|
||||
return RequestResult::from_server_message(&res.error,
|
||||
&res.error_description)
|
||||
}
|
||||
}
|
||||
|
||||
let decoded: JsonData = json::decode(&json_str).ok().expect("valid reply thanks to valid client_id and client_secret");
|
||||
|
||||
self.device_code = decoded.device_code;
|
||||
let pi = PollInformation {
|
||||
user_code: decoded.user_code,
|
||||
verification_url: decoded.verification_url,
|
||||
expires_at: UTC::now() + Duration::seconds(decoded.expires_in),
|
||||
interval: Duration::seconds(decoded.interval),
|
||||
server_message: Default::default(),
|
||||
};
|
||||
self.state = PollResult::AuthorizationPending(pi.clone());
|
||||
|
||||
self.secret = client_secret.to_string();
|
||||
self.id = client_id.to_string();
|
||||
RequestResult::ProceedWithPolling(pi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the first call is successful, which is expected unless there is a network problem,
|
||||
/// the returned `PollResult::AuthorizationPending` variant contains enough information to
|
||||
/// poll within a given `interval` to at some point obtain a result which is
|
||||
/// not `PollResult::AuthorizationPending`.
|
||||
/// # Handling the `PollResult`
|
||||
/// * call within `PollResult::AuthorizationPending.interval` until the variant changes.
|
||||
/// Keep calling as desired, even after `PollResult::Error`.
|
||||
/// * Do not call after `PollResult::Expired`, `PollResult::AccessDenied`
|
||||
/// or `PollResult::AccessGranted` as the flow will do nothing anymore.
|
||||
/// Thus in any unsuccessful case, you will have to start over the entire flow, which
|
||||
/// requires a new instance of this type.
|
||||
///
|
||||
/// > ⚠️ **Warning**: We assume the caller doesn't call faster than `interval` and are not
|
||||
/// > protected against this kind of mis-use. The latter will be indicated in
|
||||
/// > `PollResult::AuthorizationPending.server_message`
|
||||
///
|
||||
/// # Examples
|
||||
/// See test-cases in source code for a more complete example.
|
||||
pub fn poll_token(&mut self) -> PollResult {
|
||||
// clone, as we may re-assign our state later
|
||||
let state = self.state.clone();
|
||||
match state {
|
||||
PollResult::AuthorizationPending(mut pi) => {
|
||||
if pi.expires_at <= UTC::now() {
|
||||
self.state = PollResult::Expired(pi.expires_at);
|
||||
return self.state.clone();
|
||||
}
|
||||
|
||||
// We should be ready for a new request
|
||||
let req = form_urlencoded::serialize(
|
||||
[("client_id", self.id.as_slice()),
|
||||
("client_secret", &self.secret),
|
||||
("code", &self.device_code),
|
||||
("grant_type", "http://oauth.net/grant_type/device/1.0")]
|
||||
.iter().cloned());
|
||||
|
||||
let json_str =
|
||||
match self.client.post(GOOGLE_TOKEN_URL)
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(req.as_slice())
|
||||
.send() {
|
||||
Err(err) => {
|
||||
return PollResult::Error(err);
|
||||
}
|
||||
Ok(mut res) => {
|
||||
String::from_utf8(res.read_to_end().unwrap()).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(RustcDecodable)]
|
||||
struct JsonToken {
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
token_type: String,
|
||||
expires_in: i64,
|
||||
}
|
||||
|
||||
#[derive(RustcDecodable)]
|
||||
struct JsonError {
|
||||
error: String
|
||||
}
|
||||
|
||||
match json::decode::<JsonError>(&json_str) {
|
||||
Err(_) => {}, // ignore, move on, it's not an error
|
||||
Ok(res) => {
|
||||
pi.server_message = res.error;
|
||||
self.state = match pi.server_message.as_slice() {
|
||||
"access_denied" => PollResult::AccessDenied,
|
||||
"authorization_pending" => PollResult::AuthorizationPending(pi),
|
||||
_ => panic!("server message '{}' not understood", pi.server_message),
|
||||
};
|
||||
return self.state.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// yes, we expect that !
|
||||
let t: JsonToken = json::decode(&json_str).unwrap();
|
||||
self.state = PollResult::AccessGranted(Token {
|
||||
access_token: t.access_token,
|
||||
refresh_token: t.refresh_token,
|
||||
token_type: t.token_type,
|
||||
expires_at: UTC::now() + Duration::seconds(t.expires_in)
|
||||
});
|
||||
},
|
||||
// In any other state, we will bail out and do nothing
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.state.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A utility type to help executing the `DeviceFlow` correctly.
|
||||
///
|
||||
/// This involves polling the authentication server in the given intervals
|
||||
/// until there is a definitive result.
|
||||
///
|
||||
/// These results will be passed the `DeviceFlowHelperDelegate` implementation to deal with
|
||||
/// * presenting the user code
|
||||
/// * inform the user about the progress or errors
|
||||
/// * abort the operation
|
||||
///
|
||||
pub struct DeviceFlowHelper<'a> {
|
||||
delegate: &'a mut (DeviceFlowHelperDelegate + 'a),
|
||||
}
|
||||
|
||||
impl<'a> DeviceFlowHelper<'a> {
|
||||
|
||||
/// Initialize a new instance with the given delegate
|
||||
pub fn new(delegate: &'a mut DeviceFlowHelperDelegate) -> DeviceFlowHelper<'a> {
|
||||
DeviceFlowHelper {
|
||||
delegate: delegate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocks until a token was retrieved from the server, or the delegate
|
||||
/// decided to abort the attempt, or the user decided not to authorize
|
||||
/// the application.
|
||||
pub fn retrieve_token<'b, NC, T, I>(&mut self,
|
||||
client: hyper::Client<NC>,
|
||||
client_id: &str, client_secret: &str, scopes: I)
|
||||
-> Option<Token>
|
||||
where T: Str,
|
||||
I: IntoIterator<Item=&'b T> + Clone,
|
||||
NC: hyper::net::NetworkConnector {
|
||||
let mut flow = DeviceFlow::new(client);
|
||||
|
||||
// PHASE 1: REQUEST CODE
|
||||
loop {
|
||||
let res = flow.request_code(client_id, client_secret, scopes.clone());
|
||||
match res {
|
||||
RequestResult::Error(err) => {
|
||||
match self.delegate.connection_error(err) {
|
||||
Retry::Abort => return None,
|
||||
Retry::After(d) => timer::sleep(d),
|
||||
}
|
||||
},
|
||||
RequestResult::InvalidClient
|
||||
|RequestResult::InvalidScope(_) => {
|
||||
self.delegate.request_failure(res);
|
||||
return None
|
||||
}
|
||||
RequestResult::ProceedWithPolling(pi) => {
|
||||
self.delegate.present_user_code(pi);
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE 1: POLL TOKEN
|
||||
loop {
|
||||
match flow.poll_token() {
|
||||
PollResult::Error(err) => {
|
||||
match self.delegate.connection_error(err) {
|
||||
Retry::Abort => return None,
|
||||
Retry::After(d) => timer::sleep(d),
|
||||
}
|
||||
},
|
||||
PollResult::Expired(t) => {
|
||||
self.delegate.expired(t);
|
||||
return None
|
||||
},
|
||||
PollResult::AccessDenied => {
|
||||
self.delegate.denied();
|
||||
return None
|
||||
},
|
||||
PollResult::AuthorizationPending(pi) => {
|
||||
match self.delegate.pending(&pi) {
|
||||
Retry::Abort => return None,
|
||||
Retry::After(d) => timer::sleep(min(d, pi.interval)),
|
||||
}
|
||||
},
|
||||
PollResult::AccessGranted(token) => {
|
||||
return Some(token)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A utility type to indicate how operations DeviceFlowHelper operations should be retried
|
||||
pub enum Retry {
|
||||
/// Signal you don't want to retry
|
||||
Abort,
|
||||
/// Signals you want to retry after the given duration
|
||||
After(Duration)
|
||||
}
|
||||
|
||||
/// A partially implemented trait to interact with the `DeviceFlowHelper`
|
||||
///
|
||||
/// The only method that needs to be implemented manually is `present_user_code(...)`,
|
||||
/// as no assumptions are made on how this presentation should happen.
|
||||
pub trait DeviceFlowHelperDelegate {
|
||||
|
||||
/// Called whenever there is an HttpError, usually if there are network problems.
|
||||
///
|
||||
/// Return retry information.
|
||||
fn connection_error(&mut self, hyper::HttpError) -> Retry {
|
||||
Retry::After(Duration::seconds(5))
|
||||
}
|
||||
|
||||
/// The server denied the attempt to obtain a request code
|
||||
fn request_failure(&mut self, RequestResult) {}
|
||||
|
||||
/// The server has returned a `user_code` which must be shown to the user,
|
||||
/// along with the `verification_url`.
|
||||
/// Will be called exactly once, provided we didn't abort during `request_code` phase.
|
||||
fn present_user_code(&mut self, PollInformation);
|
||||
|
||||
/// Called if the request code is expired. You will have to start over in this case.
|
||||
/// This will be the last call the delegate receives.
|
||||
fn expired(&mut self, DateTime<UTC>) {}
|
||||
|
||||
/// Called if the user denied access. You would have to start over.
|
||||
/// This will be the last call the delegate receives.
|
||||
fn denied(&mut self) {}
|
||||
|
||||
/// Called as long as we are waiting for the user to authorize us.
|
||||
/// Can be used to print progress information, or decide to time-out.
|
||||
///
|
||||
/// If the returned `Retry` variant is a duration, it will only be used if it
|
||||
/// is larger than the interval desired by the server.
|
||||
fn pending(&mut self, &PollInformation) -> Retry {
|
||||
Retry::After(Duration::seconds(5))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::default::Default;
|
||||
use hyper;
|
||||
|
||||
mock_connector_in_order!(MockGoogleAuth {
|
||||
"HTTP/1.1 200 OK\r\n\
|
||||
Server: BOGUS\r\n\
|
||||
\r\n\
|
||||
{\r\n\
|
||||
\"device_code\" : \"4/L9fTtLrhY96442SEuf1Rl3KLFg3y\",\r\n\
|
||||
\"user_code\" : \"a9xfwk9c\",\r\n\
|
||||
\"verification_url\" : \"http://www.google.com/device\",\r\n\
|
||||
\"expires_in\" : \"1800\",\r\n\
|
||||
\"interval\" : 0\r\n\
|
||||
}"
|
||||
"HTTP/1.1 200 OK\r\n\
|
||||
Server: BOGUS\r\n\
|
||||
\r\n\
|
||||
{\r\n\
|
||||
\"error\" : \"authorization_pending\"\r\n\
|
||||
}"
|
||||
"HTTP/1.1 200 OK\r\n\
|
||||
Server: BOGUS\r\n\
|
||||
\r\n\
|
||||
{\r\n\
|
||||
\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\r\n\
|
||||
\"expires_in\":3920,\r\n\
|
||||
\"token_type\":\"Bearer\",\r\n\
|
||||
\"refresh_token\":\"1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ\"\r\n\
|
||||
}"
|
||||
});
|
||||
|
||||
#[test]
|
||||
fn working_flow() {
|
||||
let mut flow = DeviceFlow::new(
|
||||
hyper::Client::with_connector(<MockGoogleAuth as Default>::default()));
|
||||
|
||||
match flow.request_code("bogus_client_id",
|
||||
"bogus_secret",
|
||||
&["https://www.googleapis.com/auth/youtube.upload"]) {
|
||||
RequestResult::ProceedWithPolling(_) => {},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match flow.poll_token() {
|
||||
PollResult::AuthorizationPending(ref pi) => {
|
||||
assert_eq!(pi.server_message, "authorization_pending");
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
match flow.poll_token() {
|
||||
PollResult::AccessGranted(ref t) => {
|
||||
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg");
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// from now on, all calls will yield the same result
|
||||
// As our mock has only 3 items, we would panic on this call
|
||||
flow.poll_token();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticator() {
|
||||
struct TestHandler;
|
||||
impl DeviceFlowHelperDelegate for TestHandler {
|
||||
fn present_user_code(&mut self, pi: PollInformation) {
|
||||
println!("{:?}", pi);
|
||||
}
|
||||
}
|
||||
if let Some(t) = DeviceFlowHelper::new(&mut TestHandler)
|
||||
.retrieve_token(hyper::Client::with_connector(
|
||||
<MockGoogleAuth as Default>::default()),
|
||||
"bogus_client_id",
|
||||
"bogus_secret",
|
||||
&["https://www.googleapis.com/auth/youtube.upload"]) {
|
||||
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg")
|
||||
} else {
|
||||
panic!("Expected to retrieve token in one go");
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/lib.rs
Normal file
86
src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
#![feature(old_io, std_misc, core)]
|
||||
//! This library can be used to acquire oauth2.0 authentication for services.
|
||||
//! At the time of writing, only one way of doing so is implemented, the [device flow](https://developers.google.com/youtube/v3/guides/authentication#devices), along with a flow
|
||||
//! for [refreshing tokens](https://developers.google.com/youtube/v3/guides/authentication#devices)
|
||||
//!
|
||||
//! For your application to use this library, you will have to obtain an application
|
||||
//! id and secret by [following this guide](https://developers.google.com/youtube/registering_an_application).
|
||||
//!
|
||||
//! # Device Flow Usage
|
||||
//! As the `DeviceFlow` involves polling, the `DeviceFlowHelper` should be used
|
||||
//! as means to adhere to the protocol, and remain resilient to all kinds of errors
|
||||
//! that can occour on the way.
|
||||
//!
|
||||
//! The returned `Token` should be stored permanently to authorize future API requests.
|
||||
//!
|
||||
//! ```test_harness,no_run
|
||||
//! extern crate hyper;
|
||||
//! extern crate "yup-oauth2" as oauth2;
|
||||
//! use oauth2::{DeviceFlowHelper, DeviceFlowHelperDelegate, PollInformation};
|
||||
//!
|
||||
//! # #[test] fn device() {
|
||||
//! struct PrintHandler;
|
||||
//! impl DeviceFlowHelperDelegate for PrintHandler {
|
||||
//! fn present_user_code(&mut self, pi: PollInformation) {
|
||||
//! println!{"Please enter {} at {} and grant access to this application",
|
||||
//! pi.user_code, pi.verification_url}
|
||||
//! println!("Do not close this application until you either denied or granted access");
|
||||
//! }
|
||||
//! }
|
||||
//! if let Some(t) = DeviceFlowHelper::new(&mut PrintHandler)
|
||||
//! .retrieve_token(hyper::Client::new(),
|
||||
//! "your_client_id",
|
||||
//! "your_secret",
|
||||
//! &["https://www.googleapis.com/auth/youtube.upload"]) {
|
||||
//! // now you can use t.access_token to authenticate API calls within your
|
||||
//! // given scopes. It will not be valid forever, which is when you have to
|
||||
//! // refresh it using the `RefreshFlow`
|
||||
//! } else {
|
||||
//! println!("user declined");
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # Refresh Flow Usage
|
||||
//! As the `Token` you retrieved previously will only be valid for a certain time, you will have
|
||||
//! to use the information from the `Token.refresh_token` field to get a new `access_token`.
|
||||
//!
|
||||
//! ```test_harness,no_run
|
||||
//! extern crate hyper;
|
||||
//! extern crate "yup-oauth2" as oauth2;
|
||||
//! use oauth2::{RefreshFlow, AuthenticationType, RefreshResult};
|
||||
//!
|
||||
//! # #[test] fn refresh() {
|
||||
//! let mut f = RefreshFlow::new(hyper::Client::new());
|
||||
//! let new_token = match *f.refresh_token(AuthenticationType::Device,
|
||||
//! "my_client_id", "my_secret",
|
||||
//! "my_refresh_token") {
|
||||
//! RefreshResult::Success(ref t) => t,
|
||||
//! _ => panic!("bad luck ;)")
|
||||
//! };
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
extern crate chrono;
|
||||
|
||||
#[macro_use]
|
||||
extern crate hyper;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(test)] #[macro_use]
|
||||
extern crate "yup-hyper-mock" as hyper_mock;
|
||||
extern crate mime;
|
||||
extern crate url;
|
||||
extern crate itertools;
|
||||
extern crate "rustc-serialize" as rustc_serialize;
|
||||
|
||||
|
||||
mod device;
|
||||
mod refresh;
|
||||
mod common;
|
||||
|
||||
pub use device::{DeviceFlow, PollInformation, PollResult, DeviceFlowHelper,
|
||||
DeviceFlowHelperDelegate, Retry};
|
||||
pub use refresh::{RefreshFlow, RefreshResult};
|
||||
pub use common::{Token, AuthenticationType};
|
||||
|
||||
150
src/refresh.rs
Normal file
150
src/refresh.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use common::AuthenticationType;
|
||||
|
||||
use std::time::Duration;
|
||||
use chrono::UTC;
|
||||
use hyper;
|
||||
use hyper::header::ContentType;
|
||||
use rustc_serialize::{self, json};
|
||||
use url::form_urlencoded;
|
||||
use super::Token;
|
||||
|
||||
/// Implements the [Outh2 Refresh Token Flow](https://developers.google.com/youtube/v3/guides/authentication#devices).
|
||||
///
|
||||
/// Refresh an expired access token, as obtained by any other authentication flow.
|
||||
/// This flow is useful when your `Token` is expired and allows to obtain a new
|
||||
/// and valid access token.
|
||||
pub struct RefreshFlow<NC> {
|
||||
client: hyper::Client<NC>,
|
||||
result: RefreshResult,
|
||||
}
|
||||
|
||||
|
||||
/// All possible outcomes of the refresh flow
|
||||
pub enum RefreshResult {
|
||||
/// Indicates connection failure
|
||||
Error(hyper::HttpError),
|
||||
/// The server did not answer with a new token, providing the server message
|
||||
Refused(String),
|
||||
/// The refresh operation finished successfully, providing a new `Token`
|
||||
Success(Token),
|
||||
}
|
||||
|
||||
impl<NC> RefreshFlow<NC>
|
||||
where NC: hyper::net::NetworkConnector {
|
||||
|
||||
pub fn new(client: hyper::Client<NC>) -> RefreshFlow<NC> {
|
||||
RefreshFlow {
|
||||
client: client,
|
||||
result: RefreshResult::Error(hyper::HttpError::HttpStatusError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to refresh the given token, and obtain a new, valid one.
|
||||
/// If the `RefreshResult` is `RefreshResult::Error`, you may retry within an interval
|
||||
/// of your choice. If it is `RefreshResult::Refused`, your refresh token is invalid
|
||||
/// or your authorization was revoked. Therefore no further attempt shall be made,
|
||||
/// and you will have to re-authorize using the `DeviceFlow`
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `authentication_url` - URL matching the one used in the flow that obtained
|
||||
/// your refresh_token in the first place.
|
||||
/// * `client_id` & `client_secret` - as obtained when [registering your application](https://developers.google.com/youtube/registering_an_application)
|
||||
/// * `refresh_token` - obtained during previous call to `DeviceFlow::poll_token()` or equivalent
|
||||
///
|
||||
/// # Examples
|
||||
/// Please see the crate landing page for an example.
|
||||
pub fn refresh_token(&mut self, auth_type: AuthenticationType,
|
||||
client_id: &str, client_secret: &str,
|
||||
refresh_token: &str) -> &RefreshResult {
|
||||
if let RefreshResult::Success(_) = self.result {
|
||||
return &self.result;
|
||||
}
|
||||
|
||||
let req = form_urlencoded::serialize(
|
||||
[("client_id", client_id),
|
||||
("client_secret", client_secret),
|
||||
("refresh_token", refresh_token),
|
||||
("grant_type", "refresh_token")]
|
||||
.iter().cloned());
|
||||
|
||||
let json_str =
|
||||
match self.client.post(auth_type.url())
|
||||
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.body(req.as_slice())
|
||||
.send() {
|
||||
Err(err) => {
|
||||
self.result = RefreshResult::Error(err);
|
||||
return &self.result;
|
||||
}
|
||||
Ok(mut res) => {
|
||||
String::from_utf8(res.read_to_end().unwrap()).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(RustcDecodable)]
|
||||
struct JsonError {
|
||||
error: String
|
||||
}
|
||||
|
||||
#[derive(RustcDecodable)]
|
||||
struct JsonToken {
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
expires_in: i64,
|
||||
}
|
||||
|
||||
match json::decode::<JsonError>(&json_str) {
|
||||
Err(_) => {},
|
||||
Ok(res) => {
|
||||
self.result = RefreshResult::Refused(res.error);
|
||||
return &self.result;
|
||||
}
|
||||
}
|
||||
|
||||
let t: JsonToken = json::decode(&json_str).unwrap();
|
||||
self.result = RefreshResult::Success(Token {
|
||||
access_token: t.access_token,
|
||||
token_type: t.token_type,
|
||||
refresh_token: refresh_token.to_string(),
|
||||
expires_at: UTC::now() + Duration::seconds(t.expires_in)
|
||||
});
|
||||
|
||||
&self.result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use hyper;
|
||||
use std::default::Default;
|
||||
use super::*;
|
||||
use super::super::AuthenticationType;
|
||||
|
||||
mock_connector_in_order!(MockGoogleRefresh {
|
||||
"HTTP/1.1 200 OK\r\n\
|
||||
Server: BOGUS\r\n\
|
||||
\r\n\
|
||||
{\r\n\
|
||||
\"access_token\":\"1/fFAGRNJru1FTz70BzhT3Zg\",\r\n\
|
||||
\"expires_in\":3920,\r\n\
|
||||
\"token_type\":\"Bearer\"\r\n\
|
||||
}"
|
||||
});
|
||||
|
||||
#[test]
|
||||
fn refresh_flow() {
|
||||
let mut flow = RefreshFlow::new(
|
||||
hyper::Client::with_connector(
|
||||
<MockGoogleRefresh as Default>::default()));
|
||||
|
||||
|
||||
match *flow.refresh_token(AuthenticationType::Device,
|
||||
"bogus", "secret", "bogus_refresh_token") {
|
||||
RefreshResult::Success(ref t) => assert_eq!(t.access_token,
|
||||
"1/fFAGRNJru1FTz70BzhT3Zg"),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user