mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-02-23 15:50:00 +01:00
Implement support for ID tokens
For google stuff these are relevant when trying to invoke e.g. Cloud Run services. I'm not at all knowledgeable enough with OAuth to be able to tell if what I'm doing here is correct. This is a breaking change. `AccessToken` got renamed to just `Token` (since it now encompasses more than just `access_token` and there are some changes to the `TokenInfo` type too. Sponsored by: standard.ai
This commit is contained in:
@@ -16,7 +16,7 @@ where
|
|||||||
let request = http::Request::get("https://example.com")
|
let request = http::Request::get("https://example.com")
|
||||||
.header(
|
.header(
|
||||||
http::header::AUTHORIZATION,
|
http::header::AUTHORIZATION,
|
||||||
format!("Bearer {}", token.as_str()),
|
format!("Bearer {}", token.access_token().ok_or("no access token")?),
|
||||||
)
|
)
|
||||||
.body(hyper::body::Body::empty())?;
|
.body(hyper::body::Body::empty())?;
|
||||||
let response = client.request(request).await?;
|
let response = client.request(request).await?;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
|
|||||||
use crate::refresh::RefreshFlow;
|
use crate::refresh::RefreshFlow;
|
||||||
use crate::service_account::{ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey};
|
use crate::service_account::{ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey};
|
||||||
use crate::storage::{self, Storage, TokenStorage};
|
use crate::storage::{self, Storage, TokenStorage};
|
||||||
use crate::types::{AccessToken, ApplicationSecret, TokenInfo};
|
use crate::types::{ApplicationSecret, Token, TokenInfo};
|
||||||
use private::AuthFlow;
|
use private::AuthFlow;
|
||||||
|
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
@@ -53,7 +53,7 @@ where
|
|||||||
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
|
C: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
/// Return the current token for the provided scopes.
|
/// Return the current token for the provided scopes.
|
||||||
pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result<AccessToken, Error>
|
pub async fn token<'a, T>(&'a self, scopes: &'a [T]) -> Result<Token, Error>
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: AsRef<str>,
|
||||||
{
|
{
|
||||||
@@ -62,10 +62,7 @@ where
|
|||||||
|
|
||||||
/// Return a token for the provided scopes, but don't reuse cached tokens. Instead,
|
/// Return a token for the provided scopes, but don't reuse cached tokens. Instead,
|
||||||
/// always fetch a new token from the OAuth server.
|
/// always fetch a new token from the OAuth server.
|
||||||
pub async fn force_refreshed_token<'a, T>(
|
pub async fn force_refreshed_token<'a, T>(&'a self, scopes: &'a [T]) -> Result<Token, Error>
|
||||||
&'a self,
|
|
||||||
scopes: &'a [T],
|
|
||||||
) -> Result<AccessToken, Error>
|
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: AsRef<str>,
|
||||||
{
|
{
|
||||||
@@ -77,7 +74,7 @@ where
|
|||||||
&'a self,
|
&'a self,
|
||||||
scopes: &'a [T],
|
scopes: &'a [T],
|
||||||
force_refresh: bool,
|
force_refresh: bool,
|
||||||
) -> Result<AccessToken, Error>
|
) -> Result<Token, Error>
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: AsRef<str>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,4 +99,4 @@ pub use crate::service_account::ServiceAccountKey;
|
|||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use crate::error::Error;
|
pub use crate::error::Error;
|
||||||
pub use crate::types::{AccessToken, ApplicationSecret, ConsoleApplicationSecret};
|
pub use crate::types::{ApplicationSecret, ConsoleApplicationSecret, Token};
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ mod tests {
|
|||||||
const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json";
|
const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json";
|
||||||
|
|
||||||
// Uncomment this test to verify that we can successfully obtain tokens.
|
// Uncomment this test to verify that we can successfully obtain tokens.
|
||||||
//#[tokio::test]
|
// #[tokio::test]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
async fn test_service_account_e2e() {
|
async fn test_service_account_e2e() {
|
||||||
let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
|
let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
|
||||||
@@ -232,6 +232,14 @@ mod tests {
|
|||||||
acc.token(&client, &["https://www.googleapis.com/auth/pubsub"])
|
acc.token(&client, &["https://www.googleapis.com/auth/pubsub"])
|
||||||
.await
|
.await
|
||||||
);
|
);
|
||||||
|
println!(
|
||||||
|
"{:?}",
|
||||||
|
acc.token(
|
||||||
|
&client,
|
||||||
|
&["https://some.scope/likely-to-hand-out-id-tokens"]
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -449,7 +449,8 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_disk_storage() {
|
async fn test_disk_storage() {
|
||||||
let new_token = |access_token: &str| TokenInfo {
|
let new_token = |access_token: &str| TokenInfo {
|
||||||
access_token: access_token.to_owned(),
|
id_token: None,
|
||||||
|
access_token: Some(access_token.to_owned()),
|
||||||
refresh_token: None,
|
refresh_token: None,
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
};
|
};
|
||||||
|
|||||||
110
src/types.rs
110
src/types.rs
@@ -3,49 +3,56 @@ use crate::error::{AuthErrorOr, Error};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Represents an access token returned by oauth2 servers. All access tokens are
|
/// Represents a token returned by oauth2 servers. All tokens are Bearer tokens. Other types of
|
||||||
/// Bearer tokens. Other types of tokens are not supported.
|
/// tokens are not supported.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
||||||
pub struct AccessToken {
|
pub struct Token {
|
||||||
value: String,
|
id_token: Option<String>,
|
||||||
|
access_token: Option<String>,
|
||||||
expires_at: Option<DateTime<Utc>>,
|
expires_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccessToken {
|
impl Token {
|
||||||
/// A string representation of the access token.
|
/// A string representation of the ID token.
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn id_token(&self) -> Option<&str> {
|
||||||
&self.value
|
self.id_token.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The time the access token will expire, if any.
|
/// A string representation of the access token.
|
||||||
|
pub fn access_token(&self) -> Option<&str> {
|
||||||
|
self.access_token.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time at which the tokens will expire, if any.
|
||||||
pub fn expiration_time(&self) -> Option<DateTime<Utc>> {
|
pub fn expiration_time(&self) -> Option<DateTime<Utc>> {
|
||||||
self.expires_at
|
self.expires_at
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine if the access token is expired.
|
/// Determine if the access token is expired.
|
||||||
/// This will report that the token is expired 1 minute prior to the
|
///
|
||||||
/// expiration time to ensure that when the token is actually sent to the
|
/// This will report that the token is expired 1 minute prior to the expiration time to ensure
|
||||||
/// server it's still valid.
|
/// that when the token is actually sent to the server it's still valid.
|
||||||
pub fn is_expired(&self) -> bool {
|
pub fn is_expired(&self) -> bool {
|
||||||
// Consider the token expired if it's within 1 minute of it's expiration
|
// Consider the token expired if it's within 1 minute of it's expiration time.
|
||||||
// time.
|
|
||||||
self.expires_at
|
self.expires_at
|
||||||
.map(|expiration_time| expiration_time - chrono::Duration::minutes(1) <= Utc::now())
|
.map(|expiration_time| expiration_time - chrono::Duration::minutes(1) <= Utc::now())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<str> for AccessToken {
|
impl From<TokenInfo> for Token {
|
||||||
fn as_ref(&self) -> &str {
|
fn from(
|
||||||
self.as_str()
|
TokenInfo {
|
||||||
}
|
access_token,
|
||||||
}
|
id_token,
|
||||||
|
expires_at,
|
||||||
impl From<TokenInfo> for AccessToken {
|
..
|
||||||
fn from(value: TokenInfo) -> Self {
|
}: TokenInfo,
|
||||||
AccessToken {
|
) -> Self {
|
||||||
value: value.access_token,
|
Token {
|
||||||
expires_at: value.expires_at,
|
access_token,
|
||||||
|
id_token,
|
||||||
|
expires_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,12 +60,13 @@ impl From<TokenInfo> for AccessToken {
|
|||||||
/// Represents a token as returned by OAuth2 servers.
|
/// Represents a token as returned by OAuth2 servers.
|
||||||
///
|
///
|
||||||
/// It is produced by all authentication flows.
|
/// It is produced by all authentication flows.
|
||||||
/// It authenticates certain operations, and must be refreshed once
|
/// It authenticates certain operations, and must be refreshed once it reached it's expiry date.
|
||||||
/// it reached it's expiry date.
|
|
||||||
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
|
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
|
||||||
pub struct TokenInfo {
|
pub struct TokenInfo {
|
||||||
|
/// used when authorizing calls to oauth2 enabled services.
|
||||||
|
pub access_token: Option<String>,
|
||||||
/// used when authenticating calls to oauth2 enabled services.
|
/// used when authenticating calls to oauth2 enabled services.
|
||||||
pub access_token: String,
|
pub id_token: Option<String>,
|
||||||
/// used to refresh an expired access_token.
|
/// used to refresh an expired access_token.
|
||||||
pub refresh_token: Option<String>,
|
pub refresh_token: Option<String>,
|
||||||
/// The time when the token expires.
|
/// The time when the token expires.
|
||||||
@@ -68,38 +76,40 @@ pub struct TokenInfo {
|
|||||||
impl TokenInfo {
|
impl TokenInfo {
|
||||||
pub(crate) fn from_json(json_data: &[u8]) -> Result<TokenInfo, Error> {
|
pub(crate) fn from_json(json_data: &[u8]) -> Result<TokenInfo, Error> {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct RawToken {
|
struct TokenSchema<'a> {
|
||||||
access_token: String,
|
id_token: Option<&'a str>,
|
||||||
refresh_token: Option<String>,
|
access_token: Option<&'a str>,
|
||||||
token_type: String,
|
refresh_token: Option<&'a str>,
|
||||||
|
token_type: Option<&'a str>,
|
||||||
expires_in: Option<i64>,
|
expires_in: Option<i64>,
|
||||||
}
|
}
|
||||||
|
let TokenSchema {
|
||||||
let RawToken {
|
id_token,
|
||||||
access_token,
|
access_token,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
token_type,
|
token_type,
|
||||||
expires_in,
|
expires_in,
|
||||||
} = serde_json::from_slice::<AuthErrorOr<RawToken>>(json_data)?.into_result()?;
|
} = serde_json::from_slice::<AuthErrorOr<_>>(json_data)?.into_result()?;
|
||||||
|
match token_type {
|
||||||
if token_type.to_lowercase().as_str() != "bearer" {
|
Some(token_ty) if !token_ty.eq_ignore_ascii_case("bearer") => {
|
||||||
use std::io;
|
use std::io;
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidData,
|
io::ErrorKind::InvalidData,
|
||||||
format!(
|
format!(
|
||||||
r#"unknown token type returned; expected "bearer" found {}"#,
|
r#"unknown token type returned; expected "bearer" found {}"#,
|
||||||
token_type
|
token_ty
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
let expires_at = expires_in
|
let expires_at = expires_in
|
||||||
.map(|seconds_from_now| Utc::now() + chrono::Duration::seconds(seconds_from_now));
|
.map(|seconds_from_now| Utc::now() + chrono::Duration::seconds(seconds_from_now));
|
||||||
|
|
||||||
Ok(TokenInfo {
|
Ok(TokenInfo {
|
||||||
access_token,
|
id_token: id_token.map(String::from),
|
||||||
refresh_token,
|
access_token: access_token.map(String::from),
|
||||||
|
refresh_token: refresh_token.map(String::from),
|
||||||
expires_at,
|
expires_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ async fn test_device_success() {
|
|||||||
.token(&["https://www.googleapis.com/scope/1"])
|
.token(&["https://www.googleapis.com/scope/1"])
|
||||||
.await
|
.await
|
||||||
.expect("token failed");
|
.expect("token failed");
|
||||||
assert_eq!("accesstoken", token.as_str());
|
assert_eq!("accesstoken", token.access_token().expect("should have access token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -252,7 +252,7 @@ async fn test_installed_interactive_success() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!("accesstoken", tok.as_str());
|
assert_eq!("accesstoken", tok.access_token().expect("should have access token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -281,7 +281,7 @@ async fn test_installed_redirect_success() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!("accesstoken", tok.as_str());
|
assert_eq!("accesstoken", tok.access_token().expect("should have access token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -350,7 +350,7 @@ async fn test_service_account_success() {
|
|||||||
.token(&["https://www.googleapis.com/auth/pubsub"])
|
.token(&["https://www.googleapis.com/auth/pubsub"])
|
||||||
.await
|
.await
|
||||||
.expect("token failed");
|
.expect("token failed");
|
||||||
assert!(tok.as_str().contains("ya29.c.ElouBywiys0Ly"));
|
assert!(tok.access_token().expect("should have access token").contains("ya29.c.ElouBywiys0Ly"));
|
||||||
assert!(Utc::now() + chrono::Duration::seconds(3600) >= tok.expiration_time().unwrap());
|
assert!(Utc::now() + chrono::Duration::seconds(3600) >= tok.expiration_time().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +401,7 @@ async fn test_refresh() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!("accesstoken", tok.as_str());
|
assert_eq!("accesstoken", tok.access_token().expect("should have access token"));
|
||||||
|
|
||||||
server.expect(
|
server.expect(
|
||||||
Expectation::matching(all_of![
|
Expectation::matching(all_of![
|
||||||
@@ -422,7 +422,7 @@ async fn test_refresh() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!("accesstoken2", tok.as_str());
|
assert_eq!("accesstoken2", tok.access_token().expect("should have access token"));
|
||||||
|
|
||||||
server.expect(
|
server.expect(
|
||||||
Expectation::matching(all_of![
|
Expectation::matching(all_of![
|
||||||
@@ -443,7 +443,7 @@ async fn test_refresh() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!("accesstoken3", tok.as_str());
|
assert_eq!("accesstoken3", tok.access_token().expect("should have access token"));
|
||||||
|
|
||||||
server.expect(
|
server.expect(
|
||||||
Expectation::matching(all_of![
|
Expectation::matching(all_of![
|
||||||
@@ -503,7 +503,7 @@ async fn test_memory_storage() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!(token1.as_str(), "accesstoken");
|
assert_eq!(token1.access_token().expect("should have access token"), "accesstoken");
|
||||||
assert_eq!(token1, token2);
|
assert_eq!(token1, token2);
|
||||||
|
|
||||||
// Create a new authenticator. This authenticator does not share a cache
|
// Create a new authenticator. This authenticator does not share a cache
|
||||||
@@ -529,7 +529,7 @@ async fn test_memory_storage() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!(token3.as_str(), "accesstoken2");
|
assert_eq!(token3.access_token().expect("should have access token"), "accesstoken2");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -571,7 +571,7 @@ async fn test_disk_storage() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!(token1.as_str(), "accesstoken");
|
assert_eq!(token1.access_token().expect("should have access token"), "accesstoken");
|
||||||
assert_eq!(token1, token2);
|
assert_eq!(token1, token2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,6 +593,6 @@ async fn test_disk_storage() {
|
|||||||
.token(&["https://googleapis.com/some/scope"])
|
.token(&["https://googleapis.com/some/scope"])
|
||||||
.await
|
.await
|
||||||
.expect("failed to get token");
|
.expect("failed to get token");
|
||||||
assert_eq!(token1.as_str(), "accesstoken");
|
assert_eq!(token1.access_token().expect("should have access token"), "accesstoken");
|
||||||
assert_eq!(token1, token2);
|
assert_eq!(token1, token2);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user