Files
yup-oauth2/src/refresh.rs
Lewin Bormann 602ea1565d refactor(errors): Move almost everything to RequestError.
This is nicer than stupid Box<dyn Error+Send> everywhere.
2019-06-22 21:53:55 +02:00

186 lines
7.5 KiB
Rust

use crate::types::{ApplicationSecret, JsonError, RefreshResult, RequestError};
use super::Token;
use chrono::Utc;
use futures::stream::Stream;
use futures::Future;
use hyper;
use hyper::header;
use serde_json as json;
use url::form_urlencoded;
/// 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;
impl RefreshFlow {
/// 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:RefreshError`, 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<'a, C: 'static + hyper::client::connect::Connect>(
client: hyper::Client<C>,
client_secret: ApplicationSecret,
refresh_token: String,
) -> impl 'a + Future<Item = RefreshResult, Error = RequestError> {
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[
("client_id", client_secret.client_id.clone()),
("client_secret", client_secret.client_secret.clone()),
("refresh_token", refresh_token.to_string()),
("grant_type", "refresh_token".to_string()),
])
.finish();
let request = hyper::Request::post(client_secret.token_uri.clone())
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(hyper::Body::from(req))
.unwrap(); // TODO: error handling
client
.request(request)
.then(|r| {
match r {
Err(err) => return Err(RefreshResult::Error(err)),
Ok(res) => {
Ok(res
.into_body()
.concat2()
.wait()
.map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap())
.unwrap()) // TODO: error handling
}
}
})
.then(move |maybe_json_str: Result<String, RefreshResult>| {
if let Err(e) = maybe_json_str {
return Ok(e);
}
let json_str = maybe_json_str.unwrap();
#[derive(Deserialize)]
struct JsonToken {
access_token: String,
token_type: String,
expires_in: i64,
}
match json::from_str::<JsonError>(&json_str) {
Err(_) => {}
Ok(res) => {
return Ok(RefreshResult::RefreshError(
res.error,
res.error_description,
))
}
}
let t: JsonToken = json::from_str(&json_str).unwrap();
Ok(RefreshResult::Success(Token {
access_token: t.access_token,
token_type: t.token_type,
refresh_token: refresh_token.to_string(),
expires_in: None,
expires_in_timestamp: Some(Utc::now().timestamp() + t.expires_in),
}))
})
.map_err(RequestError::Refresh)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helper;
use hyper;
use hyper_tls::HttpsConnector;
use mockito;
use tokio;
#[test]
fn test_refresh_end2end() {
let server_url = mockito::server_url();
let app_secret = r#"{"installed":{"client_id":"902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com","project_id":"yup-test-243420","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"iuMPN6Ne1PD7cos29Tk9rlqH","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}"#;
let mut app_secret = helper::parse_application_secret(app_secret).unwrap();
app_secret.token_uri = format!("{}/token", server_url);
let refresh_token = "my-refresh-token".to_string();
let https = HttpsConnector::new(1).unwrap();
let client = hyper::Client::builder()
.keep_alive(false)
.build::<_, hyper::Body>(https);
let mut rt = tokio::runtime::Builder::new()
.core_threads(1)
.panic_handler(|e| std::panic::resume_unwind(e))
.build()
.unwrap();
// Success
{
let _m = mockito::mock("POST", "/token")
.match_body(
mockito::Matcher::Regex(".*client_id=902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com.*refresh_token=my-refresh-token.*".to_string()))
.with_status(200)
.with_body(r#"{"access_token": "new-access-token", "token_type": "Bearer", "expires_in": 1234567}"#)
.create();
let fut = RefreshFlow::refresh_token(
client.clone(),
app_secret.clone(),
refresh_token.clone(),
)
.then(|rr| {
let rr = rr.unwrap();
match rr {
RefreshResult::Success(tok) => {
assert_eq!("new-access-token", tok.access_token);
assert_eq!("Bearer", tok.token_type);
}
_ => panic!(format!("unexpected RefreshResult {:?}", rr)),
}
Ok(()) as Result<(), ()>
});
rt.block_on(fut).expect("block_on");
_m.assert();
}
// Refresh error.
{
let _m = mockito::mock("POST", "/token")
.match_body(
mockito::Matcher::Regex(".*client_id=902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com.*refresh_token=my-refresh-token.*".to_string()))
.with_status(400)
.with_body(r#"{"error": "invalid_token"}"#)
.create();
let fut = RefreshFlow::refresh_token(client, app_secret, refresh_token).then(|rr| {
let rr = rr.unwrap();
match rr {
RefreshResult::RefreshError(e, None) => {
assert_eq!(e, "invalid_token");
}
_ => panic!(format!("unexpected RefreshResult {:?}", rr)),
}
Ok(())
});
tokio::run(fut);
_m.assert();
}
}
}