mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-09 12:19:20 +01:00
Merge branch 'installed-flow' of https://github.com/dermesser/yup-oauth2 into all-upgrades
This commit is contained in:
@@ -15,7 +15,7 @@ chrono = ">= 0.2"
|
||||
log = ">= 0.3"
|
||||
mime = ">= 0.1"
|
||||
url = "= 0.5"
|
||||
hyper = ">= 0.8.0"
|
||||
hyper = "= 0.8.0"
|
||||
itertools = ">= 0.4"
|
||||
serde = "0.6.0"
|
||||
serde_json = "0.6.0"
|
||||
|
||||
@@ -136,8 +136,17 @@ impl Token {
|
||||
/// All known authentication types, for suitable constants
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum FlowType {
|
||||
/// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices)
|
||||
/// [device authentication](https://developers.google.com/youtube/v3/guides/authentication#devices). Only works
|
||||
/// for certain scopes.
|
||||
Device,
|
||||
/// [installed app flow](https://developers.google.com/identity/protocols/OAuth2InstalledApp). Required
|
||||
/// for Drive, Calendar, Gmail...; Requires user to paste a code from the browser.
|
||||
InstalledInteractive,
|
||||
/// Same as InstalledInteractive, but uses a redirect: The OAuth provider redirects the user's
|
||||
/// browser to a web server that is running on localhost. This may not work as well with the
|
||||
/// Windows Firewall, but is more comfortable otherwise. The integer describes which port to
|
||||
/// bind to (default: 8080)
|
||||
InstalledRedirect(u32),
|
||||
}
|
||||
|
||||
impl AsRef<str> for FlowType {
|
||||
@@ -145,6 +154,8 @@ impl AsRef<str> for FlowType {
|
||||
fn as_ref(&self) -> &'static str {
|
||||
match *self {
|
||||
FlowType::Device => "https://accounts.google.com/o/oauth2/device/code",
|
||||
FlowType::InstalledInteractive => "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
FlowType::InstalledRedirect(_) => "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ use std::cmp::min;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::convert::From;
|
||||
use std::io;
|
||||
|
||||
use common::{Token, FlowType, ApplicationSecret};
|
||||
use device::{PollInformation, RequestError, DeviceFlow, PollError};
|
||||
use installed::{InstalledFlow, InstalledFlowReturnMethod};
|
||||
use refresh::{RefreshResult, RefreshFlow};
|
||||
use chrono::{DateTime, UTC, Local};
|
||||
use std::time::Duration;
|
||||
@@ -189,6 +191,26 @@ impl<D, S, C> Authenticator<D, S, C>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn do_installed_flow(&mut self, scopes: &Vec<&str>) -> Result<Token, Box<Error>> {
|
||||
let installed_type;
|
||||
|
||||
match self.flow_type {
|
||||
FlowType::InstalledInteractive => {
|
||||
installed_type = Some(InstalledFlowReturnMethod::Interactive)
|
||||
}
|
||||
FlowType::InstalledRedirect(port) => {
|
||||
installed_type = Some(InstalledFlowReturnMethod::HTTPRedirect(port))
|
||||
}
|
||||
_ => installed_type = None,
|
||||
}
|
||||
|
||||
let mut flow = InstalledFlow::new(self.client.borrow_mut(), installed_type);
|
||||
flow.obtain_token(&mut self.delegate,
|
||||
&self.secret,
|
||||
scopes.iter())
|
||||
}
|
||||
|
||||
fn retrieve_device_token(&mut self, scopes: &Vec<&str>) -> Result<Token, Box<Error>> {
|
||||
let mut flow = DeviceFlow::new(self.client.borrow_mut());
|
||||
|
||||
@@ -343,6 +365,8 @@ impl<D, S, C> GetToken for Authenticator<D, S, C>
|
||||
match
|
||||
match self.flow_type {
|
||||
FlowType::Device => self.retrieve_device_token(&scopes),
|
||||
FlowType::InstalledInteractive => self.do_installed_flow(&scopes),
|
||||
FlowType::InstalledRedirect(_) => self.do_installed_flow(&scopes),
|
||||
}
|
||||
{
|
||||
Ok(token) => {
|
||||
@@ -448,9 +472,31 @@ pub trait AuthenticatorDelegate {
|
||||
/// * Will only be called if the Authenticator's flow_type is `FlowType::Device`.
|
||||
fn present_user_code(&mut self, pi: &PollInformation) {
|
||||
println!("Please enter {} at {} and grant access to this application",
|
||||
pi.user_code, pi.verification_url);
|
||||
pi.user_code,
|
||||
pi.verification_url);
|
||||
println!("Do not close this application until you either denied or granted access.");
|
||||
println!("You have time until {}.", pi.expires_at.with_timezone(&Local));
|
||||
println!("You have time until {}.",
|
||||
pi.expires_at.with_timezone(&Local));
|
||||
}
|
||||
|
||||
/// Only method currently used by the InstalledFlow.
|
||||
/// We need the user to navigate to a URL using their browser and potentially paste back a code
|
||||
/// (or maybe not). Whether they have to enter a code depends on the InstalledFlowReturnMethod
|
||||
/// used.
|
||||
fn present_user_url(&mut self, url: &String, need_code: bool) -> Option<String> {
|
||||
if need_code {
|
||||
println!("Please direct your browser to {}, follow the instructions and enter the \
|
||||
code displayed here: ",
|
||||
url);
|
||||
|
||||
let mut code = String::new();
|
||||
io::stdin().read_line(&mut code).ok().map(|_| code)
|
||||
} else {
|
||||
println!("Please direct your browser to {} and follow the instructions displayed \
|
||||
there.",
|
||||
url);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
356
src/installed.rs
Normal file
356
src/installed.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
// Copyright (c) 2016 Google Inc (lewinb@google.com).
|
||||
//
|
||||
// Refer to the project root for licensing information.
|
||||
//
|
||||
extern crate serde_json;
|
||||
extern crate url;
|
||||
|
||||
use std::borrow::BorrowMut;
|
||||
use std::convert::AsRef;
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
|
||||
use hyper;
|
||||
use hyper::{client, header, server, status, uri};
|
||||
use serde_json::error;
|
||||
use url::form_urlencoded;
|
||||
use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET};
|
||||
|
||||
use common::{ApplicationSecret, Token};
|
||||
use helper::AuthenticatorDelegate;
|
||||
|
||||
const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob";
|
||||
|
||||
/// Assembles a URL to request an authorization token (with user interaction).
|
||||
/// Note that the redirect_uri here has to be either None or some variation of
|
||||
/// http://localhost:{port}, or the authorization won't work (error "redirect_uri_mismatch")
|
||||
fn build_authentication_request_url<'a, T, I>(auth_uri: &str,
|
||||
client_id: &str,
|
||||
scopes: I,
|
||||
redirect_uri: Option<String>)
|
||||
-> String
|
||||
where T: AsRef<str> + 'a,
|
||||
I: IntoIterator<Item = &'a T>
|
||||
{
|
||||
let mut url = String::new();
|
||||
let mut scopes_string = scopes.into_iter().fold(String::new(), |mut acc, sc| {
|
||||
acc.push_str(sc.as_ref());
|
||||
acc.push_str(" ");
|
||||
acc
|
||||
});
|
||||
// Remove last space
|
||||
scopes_string.pop();
|
||||
|
||||
url.push_str(auth_uri);
|
||||
vec![format!("?scope={}", scopes_string),
|
||||
format!("&redirect_uri={}",
|
||||
redirect_uri.unwrap_or(OOB_REDIRECT_URI.to_string())),
|
||||
format!("&response_type=code"),
|
||||
format!("&client_id={}", client_id)]
|
||||
.into_iter()
|
||||
.fold(url, |mut u, param| {
|
||||
u.push_str(&percent_encode(param.as_ref(), QUERY_ENCODE_SET));
|
||||
u
|
||||
})
|
||||
}
|
||||
|
||||
pub struct InstalledFlow<C> {
|
||||
client: C,
|
||||
server: Option<server::Listening>,
|
||||
port: Option<u32>,
|
||||
|
||||
auth_code_rcv: Option<Receiver<String>>,
|
||||
}
|
||||
|
||||
/// cf. https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi
|
||||
pub enum InstalledFlowReturnMethod {
|
||||
/// Involves showing a URL to the user and asking to copy a code from their browser
|
||||
/// (default)
|
||||
Interactive,
|
||||
/// Involves spinning up a local HTTP server and Google redirecting the browser to
|
||||
/// the server with a URL containing the code (preferred, but not as reliable). The
|
||||
/// parameter is the port to listen on.
|
||||
HTTPRedirect(u32),
|
||||
}
|
||||
|
||||
impl<C> InstalledFlow<C> where C: BorrowMut<hyper::Client>
|
||||
{
|
||||
/// Starts a new Installed App auth flow.
|
||||
/// If HTTPRedirect is chosen as method and the server can't be started, the flow falls
|
||||
/// back to Interactive.
|
||||
pub fn new(client: C, method: Option<InstalledFlowReturnMethod>) -> InstalledFlow<C> {
|
||||
let default = InstalledFlow {
|
||||
client: client,
|
||||
server: None,
|
||||
port: None,
|
||||
auth_code_rcv: None,
|
||||
};
|
||||
match method {
|
||||
None => default,
|
||||
Some(InstalledFlowReturnMethod::Interactive) => default,
|
||||
// Start server on localhost to accept auth code.
|
||||
Some(InstalledFlowReturnMethod::HTTPRedirect(port)) => {
|
||||
let server = server::Server::http(format!("127.0.0.1:{}", port).as_str());
|
||||
|
||||
match server {
|
||||
Result::Err(_) => default,
|
||||
Result::Ok(server) => {
|
||||
let (tx, rx) = channel();
|
||||
let listening = server.handle(InstalledFlowHandler {
|
||||
auth_code_snd: Mutex::new(tx),
|
||||
});
|
||||
|
||||
match listening {
|
||||
Result::Err(_) => default,
|
||||
Result::Ok(listening) => {
|
||||
InstalledFlow {
|
||||
client: default.client,
|
||||
server: Some(listening),
|
||||
port: Some(port),
|
||||
auth_code_rcv: Some(rx),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the token request flow; it consists of the following steps:
|
||||
/// . Obtain a auhorization code with user cooperation or internal redirect.
|
||||
/// . Obtain a token and refresh token using that code.
|
||||
/// . Return that token
|
||||
///
|
||||
/// It's recommended not to use the DefaultAuthenticatorDelegate, but a specialized one.
|
||||
pub fn obtain_token<'a, AD: AuthenticatorDelegate, S, T>(&mut self,
|
||||
auth_delegate: &mut AD,
|
||||
appsecret: &ApplicationSecret,
|
||||
scopes: S)
|
||||
-> Result<Token, Box<Error>>
|
||||
where T: AsRef<str> + 'a,
|
||||
S: Iterator<Item = &'a T>
|
||||
{
|
||||
use std::error::Error;
|
||||
let authcode = try!(self.get_authorization_code(auth_delegate, &appsecret, scopes));
|
||||
let tokens = try!(self.request_token(&appsecret, &authcode));
|
||||
|
||||
// Successful response
|
||||
if tokens.access_token.is_some() {
|
||||
let token = Token {
|
||||
access_token: tokens.access_token.unwrap(),
|
||||
refresh_token: tokens.refresh_token.unwrap(),
|
||||
token_type: tokens.token_type.unwrap(),
|
||||
expires_in: tokens.expires_in,
|
||||
expires_in_timestamp: None,
|
||||
};
|
||||
|
||||
Result::Ok(token)
|
||||
} else {
|
||||
let err = io::Error::new(io::ErrorKind::Other,
|
||||
format!("Token API error: {} {}",
|
||||
tokens.error.unwrap_or("<unknown err>".to_string()),
|
||||
tokens.error_description
|
||||
.unwrap_or("".to_string()))
|
||||
.as_str());
|
||||
Result::Err(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtains an authorization code either interactively or via HTTP redirect (see
|
||||
/// InstalledFlowReturnMethod).
|
||||
fn get_authorization_code<'a, AD: AuthenticatorDelegate, S, T>(&mut self,
|
||||
auth_delegate: &mut AD,
|
||||
appsecret: &ApplicationSecret,
|
||||
scopes: S)
|
||||
-> Result<String, Box<Error>>
|
||||
where T: AsRef<str> + 'a,
|
||||
S: Iterator<Item = &'a T>
|
||||
{
|
||||
let result: Result<String, Box<Error>> = match self.server {
|
||||
None => {
|
||||
let url = build_authentication_request_url(&appsecret.auth_uri,
|
||||
&appsecret.client_id,
|
||||
scopes,
|
||||
None);
|
||||
match auth_delegate.present_user_url(&url, true /* need_code */) {
|
||||
None => {
|
||||
Result::Err(Box::new(io::Error::new(io::ErrorKind::UnexpectedEof,
|
||||
"couldn't read code")))
|
||||
}
|
||||
// Remove newline
|
||||
Some(mut code) => {
|
||||
code.pop();
|
||||
Result::Ok(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// The redirect URI must be this very localhost URL, otherwise Google refuses
|
||||
// authorization.
|
||||
let url = build_authentication_request_url(&appsecret.auth_uri,
|
||||
&appsecret.client_id,
|
||||
scopes,
|
||||
Some(format!("http://localhost:{}",
|
||||
self.port
|
||||
.unwrap_or(8080))));
|
||||
auth_delegate.present_user_url(&url, false /* need_code */);
|
||||
|
||||
match self.auth_code_rcv.as_ref().unwrap().recv() {
|
||||
Result::Err(e) => Result::Err(Box::new(e)),
|
||||
Result::Ok(s) => Result::Ok(s),
|
||||
}
|
||||
}
|
||||
};
|
||||
self.server.as_mut().map(|l| l.close()).is_some();
|
||||
result
|
||||
}
|
||||
|
||||
/// Sends the authorization code to the provider in order to obtain access and refresh tokens.
|
||||
fn request_token(&mut self,
|
||||
appsecret: &ApplicationSecret,
|
||||
authcode: &str)
|
||||
-> Result<JSONTokenResponse, Box<Error>> {
|
||||
let redirect_uri;
|
||||
|
||||
match self.port {
|
||||
None => redirect_uri = OOB_REDIRECT_URI.to_string(),
|
||||
Some(p) => redirect_uri = format!("http://localhost:{}", p),
|
||||
}
|
||||
|
||||
let body = form_urlencoded::serialize(vec![("code".to_string(), authcode.to_string()),
|
||||
("client_id".to_string(),
|
||||
appsecret.client_id.clone()),
|
||||
("client_secret".to_string(),
|
||||
appsecret.client_secret.clone()),
|
||||
("redirect_uri".to_string(), redirect_uri),
|
||||
("grant_type".to_string(),
|
||||
"authorization_code".to_string())]);
|
||||
|
||||
let result: Result<client::Response, hyper::Error> =
|
||||
self.client
|
||||
.borrow_mut()
|
||||
.post(&appsecret.token_uri)
|
||||
.body(&body)
|
||||
.header(header::ContentType("application/x-www-form-urlencoded".parse().unwrap()))
|
||||
.send();
|
||||
|
||||
let mut resp = String::new();
|
||||
|
||||
match result {
|
||||
Result::Err(e) => return Result::Err(Box::new(e)),
|
||||
Result::Ok(mut response) => {
|
||||
let result = response.read_to_string(&mut resp);
|
||||
|
||||
match result {
|
||||
Result::Err(e) => return Result::Err(Box::new(e)),
|
||||
Result::Ok(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let token_resp: Result<JSONTokenResponse, error::Error> = serde_json::from_str(&resp);
|
||||
|
||||
match token_resp {
|
||||
Result::Err(e) => return Result::Err(Box::new(e)),
|
||||
Result::Ok(tok) => Result::Ok(tok) as Result<JSONTokenResponse, Box<Error>>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JSONTokenResponse {
|
||||
access_token: Option<String>,
|
||||
refresh_token: Option<String>,
|
||||
token_type: Option<String>,
|
||||
expires_in: Option<i64>,
|
||||
|
||||
error: Option<String>,
|
||||
error_description: Option<String>,
|
||||
}
|
||||
|
||||
/// HTTP handler handling the redirect from the provider.
|
||||
struct InstalledFlowHandler {
|
||||
auth_code_snd: Mutex<Sender<String>>,
|
||||
}
|
||||
|
||||
impl server::Handler for InstalledFlowHandler {
|
||||
fn handle(&self, rq: server::Request, mut rp: server::Response) {
|
||||
match rq.uri {
|
||||
uri::RequestUri::AbsolutePath(path) => {
|
||||
// We use a fake URL because the redirect goes to a URL, meaning we
|
||||
// can't use the url form decode (because there's slashes and hashes and stuff in
|
||||
// it).
|
||||
let url = hyper::Url::parse(&format!("http://example.com{}", path));
|
||||
|
||||
if url.is_err() {
|
||||
*rp.status_mut() = status::StatusCode::BadRequest;
|
||||
let _ = rp.send("Unparseable URL".as_ref());
|
||||
} else {
|
||||
self.handle_url(url.unwrap());
|
||||
*rp.status_mut() = status::StatusCode::Ok;
|
||||
let _ = rp.send("<html><head><title>Success</title></head><body>You may now \
|
||||
close this window.</body></html>"
|
||||
.as_ref());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
*rp.status_mut() = status::StatusCode::BadRequest;
|
||||
let _ = rp.send("Invalid Request!".as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstalledFlowHandler {
|
||||
fn handle_url(&self, url: hyper::Url) {
|
||||
// Google redirects to the specified localhost URL, appending the authorization
|
||||
// code, like this: http://localhost:8080/xyz/?code=4/731fJ3BheyCouCniPufAd280GHNV5Ju35yYcGs
|
||||
// We take that code and send it to the get_authorization_code() function that
|
||||
// waits for it.
|
||||
for (param, val) in url.query_pairs().unwrap_or(Vec::new()) {
|
||||
if param == "code".to_string() {
|
||||
let _ = self.auth_code_snd.lock().unwrap().send(val);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_authentication_request_url;
|
||||
use super::InstalledFlowHandler;
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
use hyper::Url;
|
||||
|
||||
#[test]
|
||||
fn test_request_url_builder() {
|
||||
assert_eq!("https://accounts.google.\
|
||||
com/o/oauth2/auth?scope=email%20profile&redirect_uri=urn:ietf:wg:oauth:2.0:\
|
||||
oob&response_type=code&client_id=812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5amr\
|
||||
f.apps.googleusercontent.com",
|
||||
build_authentication_request_url("https://accounts.google.com/o/oauth2/auth",
|
||||
"812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5am\
|
||||
rf.apps.googleusercontent.com",
|
||||
vec![&"email".to_string(),
|
||||
&"profile".to_string()],
|
||||
None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_handle_url() {
|
||||
let (tx, rx) = channel();
|
||||
let handler = InstalledFlowHandler { auth_code_snd: Mutex::new(tx) };
|
||||
// URLs are usually a bit botched
|
||||
let url = Url::parse("http://example.com:1234/?code=ab/c%2Fd#").unwrap();
|
||||
handler.handle_url(url);
|
||||
assert_eq!(rx.recv().unwrap(), "ab/c/d".to_string());
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,14 @@ extern crate url;
|
||||
extern crate itertools;
|
||||
|
||||
mod device;
|
||||
mod installed;
|
||||
mod helper;
|
||||
mod refresh;
|
||||
mod common;
|
||||
mod helper;
|
||||
|
||||
pub use device::{DeviceFlow, PollInformation, PollError};
|
||||
pub use refresh::{RefreshFlow, RefreshResult};
|
||||
pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret, Scheme, TokenType};
|
||||
pub use helper::{TokenStorage, NullStorage, MemoryStorage, Authenticator,
|
||||
AuthenticatorDelegate, Retry, DefaultAuthenticatorDelegate, GetToken};
|
||||
pub use installed::{InstalledFlow, InstalledFlowReturnMethod};
|
||||
pub use helper::{TokenStorage, NullStorage, MemoryStorage, Authenticator, AuthenticatorDelegate,
|
||||
Retry, DefaultAuthenticatorDelegate, GetToken};
|
||||
|
||||
Reference in New Issue
Block a user