Files
yup-oauth2/src/device.rs
2016-08-31 20:01:24 +02:00

334 lines
12 KiB
Rust

use std::iter::IntoIterator;
use std::time::Duration;
use std::default::Default;
use hyper;
use hyper::header::ContentType;
use url::form_urlencoded;
use itertools::Itertools;
use serde_json as json;
use chrono::{self, UTC};
use std::borrow::BorrowMut;
use std::io::Read;
use std::i64;
use types::{Token, FlowType, Flow, RequestError, JsonError};
use authenticator_delegate::{PollError, PollInformation};
pub const GOOGLE_TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token";
/// Encapsulates all possible states of the Device Flow
enum DeviceFlowState {
/// We failed to poll a result
Error,
/// We received poll information and will periodically poll for a token
Pending(PollInformation),
/// The flow finished successfully, providing token information
Success(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<C> {
client: C,
device_code: String,
state: Option<DeviceFlowState>,
error: Option<PollError>,
secret: String,
id: String,
}
impl<C> Flow for DeviceFlow<C> {
fn type_id() -> FlowType {
FlowType::Device
}
}
impl<C> DeviceFlow<C>
where C: BorrowMut<hyper::Client>
{
/// # 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: C) -> DeviceFlow<C> {
DeviceFlow {
client: client,
device_code: Default::default(),
secret: Default::default(),
id: Default::default(),
state: None,
error: None,
}
}
/// 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
/// you receive an `Ok` result.
/// # 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.
/// # Panics
/// * If called after a successful result was returned at least once.
/// # 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)
-> Result<PollInformation, RequestError>
where T: AsRef<str> + 'b,
I: IntoIterator<Item = &'b T>
{
if self.state.is_some() {
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_ref())
.intersperse(" ")
.collect::<String>()
.as_ref())]);
// note: works around bug in rustlang
// https://github.com/rust-lang/rust/issues/22252
let ret = match self.client
.borrow_mut()
.post(FlowType::Device.as_ref())
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.body(&*req)
.send() {
Err(err) => {
return Err(RequestError::HttpError(err));
}
Ok(mut res) => {
#[derive(Deserialize)]
struct JsonData {
device_code: String,
user_code: String,
verification_url: String,
expires_in: i64,
interval: i64,
}
let mut json_str = String::new();
res.read_to_string(&mut json_str).unwrap();
// check for error
match json::from_str::<JsonError>(&json_str) {
Err(_) => {} // ignore, move on
Ok(res) => return Err(RequestError::from(res)),
}
let decoded: JsonData = json::from_str(&json_str).unwrap();
self.device_code = decoded.device_code;
let pi = PollInformation {
user_code: decoded.user_code,
verification_url: decoded.verification_url,
expires_at: UTC::now() + chrono::Duration::seconds(decoded.expires_in),
interval: Duration::from_secs(i64::abs(decoded.interval) as u64),
};
self.state = Some(DeviceFlowState::Pending(pi.clone()));
self.secret = client_secret.to_string();
self.id = client_id.to_string();
Ok(pi)
}
};
ret
}
/// If the first call is successful, this method may be called.
/// As long as we are waiting for authentication, it will return `Ok(None)`.
/// You should call it within the interval given the previously returned
/// `PollInformation.interval` field.
///
/// The operation was successful once you receive an Ok(Some(Token)) for the first time.
/// Subsequent calls will return the previous result, which may also be an error state.
///
/// Do not call after `PollError::Expired|PollError::AccessDenied` was among the
/// `Err(PollError)` variants as the flow will not do anything anymore.
/// Thus in any unsuccessful case which is not `PollError::HttpError`, 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.
///
/// # Examples
/// See test-cases in source code for a more complete example.
pub fn poll_token(&mut self) -> Result<Option<Token>, &PollError> {
// clone, as we may re-assign our state later
let pi = match self.state {
Some(ref s) => {
match *s {
DeviceFlowState::Pending(ref pi) => pi.clone(),
DeviceFlowState::Error => return Err(self.error.as_ref().unwrap()),
DeviceFlowState::Success(ref t) => return Ok(Some(t.clone())),
}
}
_ => panic!("You have to call request_code() beforehand"),
};
if pi.expires_at <= UTC::now() {
self.error = Some(PollError::Expired(pi.expires_at));
self.state = Some(DeviceFlowState::Error);
return Err(&self.error.as_ref().unwrap());
}
// We should be ready for a new request
let req = form_urlencoded::serialize(&[("client_id", &self.id[..]),
("client_secret", &self.secret),
("code", &self.device_code),
("grant_type",
"http://oauth.net/grant_type/device/1.0")]);
let json_str = match self.client
.borrow_mut()
.post(GOOGLE_TOKEN_URL)
.header(ContentType("application/x-www-form-urlencoded".parse().unwrap()))
.body(&*req)
.send() {
Err(err) => {
self.error = Some(PollError::HttpError(err));
return Err(self.error.as_ref().unwrap());
}
Ok(mut res) => {
let mut json_str = String::new();
res.read_to_string(&mut json_str).unwrap();
json_str
}
};
#[derive(Deserialize)]
struct JsonError {
error: String,
}
match json::from_str::<JsonError>(&json_str) {
Err(_) => {} // ignore, move on, it's not an error
Ok(res) => {
match res.error.as_ref() {
"access_denied" => {
self.error = Some(PollError::AccessDenied);
self.state = Some(DeviceFlowState::Error);
return Err(self.error.as_ref().unwrap());
}
"authorization_pending" => return Ok(None),
_ => panic!("server message '{}' not understood", res.error),
};
}
}
// yes, we expect that !
let mut t: Token = json::from_str(&json_str).unwrap();
t.set_expiry_absolute();
let res = Ok(Some(t.clone()));
self.state = Some(DeviceFlowState::Success(t));
return res;
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use std::default::Default;
use std::time::Duration;
use hyper;
use yup_hyper_mock::{SequentialConnector, MockStream};
pub struct MockGoogleAuth(SequentialConnector);
impl Default for MockGoogleAuth {
fn default() -> MockGoogleAuth {
let mut c = MockGoogleAuth(Default::default());
c.0.content.push("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\
}"
.to_string());
c.0.content.push("HTTP/1.1 200 OK\r\n\
Server: BOGUS\r\n\
\r\n\
{\r\n\
\"error\" : \"authorization_pending\"\r\n\
}"
.to_string());
c.0.content.push("HTTP/1.1 200 OK\r\nServer: \
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}"
.to_string());
c
}
}
impl hyper::net::NetworkConnector for MockGoogleAuth {
type Stream = MockStream;
fn connect(&self, host: &str, port: u16, scheme: &str) -> ::hyper::Result<MockStream> {
self.0.connect(host, port, scheme)
}
}
#[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"]) {
Ok(pi) => assert_eq!(pi.interval, Duration::from_secs(0)),
_ => unreachable!(),
}
match flow.poll_token() {
Ok(None) => {}
_ => unreachable!(),
}
let t = match flow.poll_token() {
Ok(Some(t)) => {
assert_eq!(t.access_token, "1/fFAGRNJru1FTz70BzhT3Zg");
t
}
_ => unreachable!(),
};
// from now on, all calls will yield the same result
// As our mock has only 3 items, we would panic on this call
assert_eq!(flow.poll_token().unwrap(), Some(t));
}
}