diff --git a/Cargo.toml b/Cargo.toml index 90967a2..d75d935 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "yup-oauth2" -version = "8.1.0" +version = "8.3.0" authors = ["Sebastian Thiel ", "Lewin Bormann "] repository = "https://github.com/dermesser/yup-oauth2" description = "An oauth2 implementation, providing the 'device', 'service account' and 'installed' authorization flows" @@ -37,12 +37,12 @@ base64 = "0.13.0" futures = "0.3" http = "0.2" hyper = { version = "0.14", features = ["client", "server", "tcp", "http2"] } -hyper-rustls = { version = "0.23", optional = true, features = ["http2"] } +hyper-rustls = { version = "0.24", optional = true, features = ["http2"] } hyper-tls = { version = "0.5.0", optional = true } itertools = "0.10.0" log = "0.4" percent-encoding = "2" -rustls = { version = "0.20.4", optional = true } +rustls = { version = "0.21.0", optional = true } rustls-pemfile = { version = "1.0.1", optional = true } seahash = "4" serde = {version = "1.0", features = ["derive"]} @@ -57,7 +57,7 @@ httptest = "0.15" env_logger = "0.10" tempfile = "3.1" webbrowser = "0.8" -hyper-rustls = "0.23" +hyper-rustls = "0.24" [workspace] members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/", "examples/test-adc"] diff --git a/README.md b/README.md index aae16f6..e3dcf6e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Build -Status](https://travis-ci.org/dermesser/yup-oauth2.svg)](https://travis-ci.org/dermesser/yup-oauth2) -[![codecov](https://codecov.io/gh/dermesser/yup-oauth2/branch/master/graph/badge.svg)](https://codecov.io/gh/dermesser/yup-oauth2) +Status](https://github.com/dermesser/yup-oauth2/actions/workflows/test.yml/badge.svg)](https://github.com/dermesser/yup-oauth2/actions) [![crates.io](https://img.shields.io/crates/v/yup-oauth2.svg)](https://crates.io/crates/yup-oauth2) **yup-oauth2** is a utility library which implements several OAuth 2.0 flows. It's mainly used by @@ -27,18 +26,6 @@ doesn't, please let us know and/or contribute a fix! * Service account flow: Non-interactive authorization of server-to-server communication based on public key cryptography. Used for services like Cloud Pubsub, Cloud Storage, ... -### Usage - -Please have a look at the [API landing page][API-docs] for all the examples you will ever need. - -A simple commandline program which authenticates any scope and prints token information can be found -in [the examples directory][examples]. - -The video below shows the *auth* example in action. It's meant to be used as utility to record all -server communication and improve protocol compliance. - -![usage][auth-usage] - ## Versions * Version 1.x for Hyper versions below 12 diff --git a/examples/auth.rs-usage.gif b/examples/auth.rs-usage.gif deleted file mode 100644 index 475c10c..0000000 Binary files a/examples/auth.rs-usage.gif and /dev/null differ diff --git a/examples/auth.rs-usage.ttyrec b/examples/auth.rs-usage.ttyrec deleted file mode 100644 index ef97b7a..0000000 Binary files a/examples/auth.rs-usage.ttyrec and /dev/null differ diff --git a/examples/old/auth.rs b/examples/old/auth.rs deleted file mode 100644 index 15fd3af..0000000 --- a/examples/old/auth.rs +++ /dev/null @@ -1,112 +0,0 @@ -use chrono::Local; -use getopts::{Fail, HasArg, Occur, Options}; -use hyper_tls::HttpsConnector; -use std::default::Default; -use std::env; -use std::thread::sleep; -use std::time::Duration; -use yup_oauth2::{self as oauth2, GetToken}; - -fn usage(program: &str, opts: &Options, err: Option) -> ! { - if err.is_some() { - println!("{}", err.unwrap()); - std::process::exit(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" - ) - ); - - std::process::exit(0); -} - -fn main() { - let args: Vec = env::args().collect(); - 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[1..]) { - Ok(m) => m, - Err(e) => { - usage(&prog, &opts, Some(e)); - } - }; - - 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)); - } - - let secret = oauth2::ApplicationSecret { - client_id: m.opt_str("c").unwrap(), - client_secret: m.opt_str("s").unwrap(), - token_uri: Default::default(), - auth_uri: Default::default(), - redirect_uris: Default::default(), - ..Default::default() - }; - - println!("THIS PROGRAM PRINTS ALL COMMUNICATION TO STDERR !!!"); - - struct StdoutHandler; - impl oauth2::AuthenticatorDelegate 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.with_timezone(&Local) - ); - let delay = Duration::from_secs(5); - println!("Browser opens automatically in {:?} seconds", delay); - sleep(delay); - open::that(&pi.verification_url).ok(); - println!("DONE - waiting for authorization ..."); - } - } - - let https = HttpsConnector::new(4).unwrap(); - let client = hyper::Client::builder().build(https); - - match oauth2::Authenticator::new(&secret, StdoutHandler, client, oauth2::NullStorage, None) - .token(&m.free) - { - Ok(t) => { - println!("Authentication granted !"); - println!("You should store the following information for use, or revoke it."); - println!("All dates are given in UTC."); - println!("{:?}", t); - } - Err(err) => { - println!("Access token wasn't obtained: {}", err); - std::process::exit(10); - } - }; -} diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index 58436ba..7b513d0 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -156,3 +156,11 @@ impl DeviceFlowDelegate for DefaultDeviceFlowDelegate {} #[derive(Copy, Clone)] pub struct DefaultInstalledFlowDelegate; impl InstalledFlowDelegate for DefaultInstalledFlowDelegate {} + +/// The default installed-flow delegate (i.e.: show URL on stdout). Use this to specify +/// a custom redirect URL. +#[derive(Clone)] +pub struct DefaultInstalledFlowDelegateWithRedirectURI(pub String); +impl InstalledFlowDelegate for DefaultInstalledFlowDelegateWithRedirectURI { + fn redirect_uri(&self) -> Option<&str> { Some(self.0.as_str()) } +} diff --git a/src/installed.rs b/src/installed.rs index cab6d71..8fee434 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -97,6 +97,15 @@ pub struct InstalledFlow { impl InstalledFlow { /// Create a new InstalledFlow with the provided secret and method. + /// + /// In order to specify the redirect URL to use (in the case of `HTTPRedirect` or + /// `HTTPPortRedirect` as method), either implement the `InstalledFlowDelegate` trait, or + /// use the `DefaultInstalledFlowDelegateWithRedirectURI`, which presents the URL on stdout. + /// The redirect URL to use is configured with the OAuth provider, and possible options are + /// given in the `ApplicationSecret.redirect_uris` field. + /// + /// The `InstalledFlowDelegate` implementation should be assigned to the `flow_delegate` field + /// of the `InstalledFlow` struct. pub(crate) fn new( app_secret: ApplicationSecret, method: InstalledFlowReturnMethod, diff --git a/src/lib.rs b/src/lib.rs index 56d4acd..0fb0b21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,6 +93,11 @@ pub mod storage; mod types; +pub use hyper; + +#[cfg(feature = "hyper-rustls")] +pub use hyper_rustls; + #[cfg(feature = "service_account")] #[doc(inline)] pub use crate::authenticator::ServiceAccountAuthenticator; diff --git a/src/types.rs b/src/types.rs index a2bfa5c..ba84b41 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,7 @@ use crate::error::{AuthErrorOr, Error}; -use time::OffsetDateTime; use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; /// Represents a token returned by oauth2 servers. All tokens are Bearer tokens. Other types of /// tokens are not supported. @@ -12,7 +12,6 @@ pub struct AccessToken { } impl AccessToken { - /// A string representation of the access token. pub fn token(&self) -> Option<&str> { self.access_token.as_deref() @@ -30,7 +29,9 @@ impl AccessToken { pub fn is_expired(&self) -> bool { // Consider the token expired if it's within 1 minute of it's expiration time. self.expires_at - .map(|expiration_time| expiration_time - time::Duration::minutes(1) <= OffsetDateTime::now_utc()) + .map(|expiration_time| { + expiration_time - time::Duration::minutes(1) <= OffsetDateTime::now_utc() + }) .unwrap_or(false) } } @@ -106,8 +107,19 @@ impl TokenInfo { _ => (), } - let expires_at = expires_in - .map(|seconds_from_now| OffsetDateTime::now_utc() + time::Duration::seconds(seconds_from_now)); + let expires_at = match expires_in { + Some(seconds_from_now) => { + Some(OffsetDateTime::now_utc() + time::Duration::seconds(seconds_from_now)) + } + None if id_token.is_some() && access_token.is_none() => { + // If the response contains only an ID token, an expiration date may not be + // returned. According to the docs, the tokens are always valid for 1 hour. + // + // https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-oidc + Some(OffsetDateTime::now_utc() + time::Duration::HOUR) + } + None => None, + }; Ok(TokenInfo { id_token, @@ -120,7 +132,9 @@ impl TokenInfo { /// Returns true if we are expired. pub fn is_expired(&self) -> bool { self.expires_at - .map(|expiration_time| expiration_time - time::Duration::minutes(1) <= OffsetDateTime::now_utc()) + .map(|expiration_time| { + expiration_time - time::Duration::minutes(1) <= OffsetDateTime::now_utc() + }) .unwrap_or(false) } } @@ -183,4 +197,27 @@ pub mod tests { ), } } + + #[test] + fn default_expiry_for_id_token_only() { + // If only an ID token is present, set a default expiration date + let json = r#"{"id_token": "id"}"#; + + let token = TokenInfo::from_json(json.as_bytes()).unwrap(); + assert_eq!(token.id_token, Some("id".to_owned())); + + let expiry = token.expires_at.unwrap(); + assert!(expiry <= time::OffsetDateTime::now_utc() + time::Duration::HOUR); + } + + #[test] + fn no_default_expiry_for_access_token() { + // Don't set a default expiration date if an access token is returned + let json = r#"{"access_token": "access", "id_token": "id"}"#; + + let token = TokenInfo::from_json(json.as_bytes()).unwrap(); + assert_eq!(token.access_token, Some("access".to_owned())); + assert_eq!(token.id_token, Some("id".to_owned())); + assert_eq!(token.expires_at, None); + } }