Files
yup-oauth2/src/refresh.rs
Glenn Griffin d0880d07db Refactor error handling and as a consequence delegates.
This Removes RefreshError and PollError. Both those types can be fully
represented within Error and there seems little value in distinguishing
that they were resulting from device polling or refreshes. In either
case the user will need to handle the response from token() calls
similarly. This also removes the AuthenticatorDelegate since it only
served to notify users when refreshes failed, which can already be done
by looking at the return code from token. DeviceFlow no longer has the
ability to set a wait_timeout. This is trivial to do by wrapping the
token() call in a tokio::Timeout future so there's little benefit for
users specifying this value. The DeviceFlowDelegate also no longer has
the ability to specify when to abort, or alter the interval polling
happens on, but it does gain understanding of the 'slow_down' response
as documented in the oauth rfc. It seemed very unlikely the delegate was
going to do anything other that timeout after a given time and that's
already possible using tokio::Timeout so it needlessly complicated the
implementation.
2019-12-18 09:07:45 -08:00

136 lines
5.6 KiB
Rust

use crate::error::{AuthErrorOr, Error};
use crate::types::{ApplicationSecret, Token};
use chrono::Utc;
use futures_util::try_stream::TryStreamExt;
use hyper::header;
use serde::Deserialize;
use url::form_urlencoded;
/// Implements the [OAuth2 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 async fn refresh_token<C: hyper::client::connect::Connect + 'static>(
client: &hyper::Client<C>,
client_secret: &ApplicationSecret,
refresh_token: &str,
) -> Result<Token, Error> {
let req = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&[
("client_id", client_secret.client_id.as_str()),
("client_secret", client_secret.client_secret.as_str()),
("refresh_token", refresh_token),
("grant_type", "refresh_token"),
])
.finish();
let request = hyper::Request::post(&client_secret.token_uri)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(hyper::Body::from(req))
.unwrap();
let resp = client.request(request).await?;
let body = resp.into_body().try_concat().await?;
#[derive(Deserialize)]
struct JsonToken {
access_token: String,
token_type: String,
expires_in: i64,
}
let JsonToken {
access_token,
token_type,
expires_in,
} = serde_json::from_slice::<AuthErrorOr<_>>(&body)?.into_result()?;
Ok(Token {
access_token,
token_type,
refresh_token: Some(refresh_token.to_string()),
expires_in: None,
expires_in_timestamp: Some(Utc::now().timestamp() + expires_in),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helper;
use hyper_rustls::HttpsConnector;
#[tokio::test]
async 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";
let https = HttpsConnector::new();
let client = hyper::Client::builder()
.keep_alive(false)
.build::<_, hyper::Body>(https);
// 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 token = RefreshFlow::refresh_token(&client, &app_secret, refresh_token)
.await
.expect("token failed");
assert_eq!("new-access-token", token.access_token);
assert_eq!("Bearer", token.token_type);
_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_request"}"#)
.create();
let rr = RefreshFlow::refresh_token(&client, &app_secret, refresh_token).await;
match rr {
Err(Error::AuthError(auth_error)) => {
assert_eq!(
auth_error.error,
crate::error::AuthErrorCode::InvalidRequest
);
}
_ => panic!(format!("unexpected RefreshResult {:?}", rr)),
}
_m.assert();
}
}
}