| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use failure::{format_err, Error}; |
| use std::ops::Deref; |
| use std::time::{Duration, SystemTime}; |
| use token_cache::{CacheKey, CacheToken, KeyFor}; |
| |
| /// Representation of a single OAuth token including its expiry time. |
| #[derive(Clone, Debug, PartialEq, Eq)] |
| pub struct OAuthToken { |
| expiry_time: SystemTime, |
| token: String, |
| } |
| |
| impl CacheToken for OAuthToken { |
| fn expiry_time(&self) -> &SystemTime { |
| &self.expiry_time |
| } |
| } |
| |
| impl Deref for OAuthToken { |
| type Target = str; |
| |
| fn deref(&self) -> &str { |
| &*self.token |
| } |
| } |
| |
| impl From<fidl_fuchsia_auth::AuthToken> for OAuthToken { |
| fn from(auth_token: fidl_fuchsia_auth::AuthToken) -> OAuthToken { |
| OAuthToken { |
| expiry_time: SystemTime::now() + Duration::from_secs(auth_token.expires_in), |
| token: auth_token.token, |
| } |
| } |
| } |
| |
| /// Representation of a single Firebase token including its expiry time. |
| #[derive(Clone, Debug, PartialEq, Eq)] |
| pub struct FirebaseAuthToken { |
| id_token: String, |
| local_id: Option<String>, |
| email: Option<String>, |
| expiry_time: SystemTime, |
| } |
| |
| impl CacheToken for FirebaseAuthToken { |
| fn expiry_time(&self) -> &SystemTime { |
| &self.expiry_time |
| } |
| } |
| |
| impl From<fidl_fuchsia_auth::FirebaseToken> for FirebaseAuthToken { |
| fn from(firebase_token: fidl_fuchsia_auth::FirebaseToken) -> FirebaseAuthToken { |
| FirebaseAuthToken { |
| id_token: firebase_token.id_token, |
| local_id: firebase_token.local_id, |
| email: firebase_token.email, |
| expiry_time: SystemTime::now() + Duration::from_secs(firebase_token.expires_in), |
| } |
| } |
| } |
| |
| impl FirebaseAuthToken { |
| /// Returns a new FIDL `FirebaseToken` using data cloned from our |
| /// internal representation. |
| pub fn to_fidl(&self) -> fidl_fuchsia_auth::FirebaseToken { |
| fidl_fuchsia_auth::FirebaseToken { |
| id_token: self.id_token.clone(), |
| local_id: self.local_id.clone(), |
| email: self.email.clone(), |
| expires_in: match self.expiry_time.duration_since(SystemTime::now()) { |
| Ok(duration) => duration.as_secs(), |
| Err(_) => 0, |
| }, |
| } |
| } |
| } |
| |
| /// Key for storing OAuth access tokens in the token cache. |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct AccessTokenKey { |
| auth_provider_type: String, |
| user_profile_id: String, |
| scopes: String, |
| } |
| |
| impl CacheKey for AccessTokenKey { |
| fn auth_provider_type(&self) -> &str { |
| &self.auth_provider_type |
| } |
| |
| fn user_profile_id(&self) -> &str { |
| &self.user_profile_id |
| } |
| |
| fn subkey(&self) -> &str { |
| &self.scopes |
| } |
| } |
| |
| impl KeyFor for AccessTokenKey { |
| type TokenType = OAuthToken; |
| } |
| |
| impl AccessTokenKey { |
| /// Create a new access token key. |
| pub fn new<T: Deref<Target = str>>( |
| auth_provider_type: String, |
| user_profile_id: String, |
| scopes: &[T], |
| ) -> Result<AccessTokenKey, Error> { |
| validate_provider_and_id(&auth_provider_type, &user_profile_id)?; |
| Ok(AccessTokenKey { |
| auth_provider_type: auth_provider_type, |
| user_profile_id: user_profile_id, |
| scopes: Self::combine_scopes(scopes), |
| }) |
| } |
| |
| fn combine_scopes<T: Deref<Target = str>>(scopes: &[T]) -> String { |
| // Use the scope strings concatenated with a newline as the key. Note that this |
| // is order dependent; a client that requested the same scopes with two |
| // different orders would create two cache entries. We argue that the |
| // harm of this is limited compared to the cost of sorting scopes to |
| // create a canonical ordering on every access. Most clients are likely |
| // to use a consistent order anyway and we request this behaviour in the |
| // interface. TODO(satsukiu): Consider a zero-copy solution for the |
| // simple case of a single scope. |
| match scopes.len() { |
| 0 => String::from(""), |
| 1 => scopes.first().unwrap().to_string(), |
| _ => String::from(scopes.iter().fold(String::new(), |acc, el| { |
| let sep = if acc.is_empty() { "" } else { "\n" }; |
| acc + sep + el |
| })), |
| } |
| } |
| } |
| |
| /// Key for storing OpenID tokens in the token cache. |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct IdTokenKey { |
| auth_provider_type: String, |
| user_profile_id: String, |
| audience: String, |
| } |
| |
| impl CacheKey for IdTokenKey { |
| fn auth_provider_type(&self) -> &str { |
| &self.auth_provider_type |
| } |
| |
| fn user_profile_id(&self) -> &str { |
| &self.user_profile_id |
| } |
| |
| fn subkey(&self) -> &str { |
| &self.audience |
| } |
| } |
| |
| impl KeyFor for IdTokenKey { |
| type TokenType = OAuthToken; |
| } |
| |
| impl IdTokenKey { |
| /// Create a new ID token key. |
| pub fn new( |
| auth_provider_type: String, |
| user_profile_id: String, |
| audience: String, |
| ) -> Result<IdTokenKey, Error> { |
| validate_provider_and_id(&auth_provider_type, &user_profile_id)?; |
| Ok(IdTokenKey { |
| auth_provider_type: auth_provider_type, |
| user_profile_id: user_profile_id, |
| audience: audience, |
| }) |
| } |
| } |
| |
| /// Key for storing Firebase tokens in the token cache. |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct FirebaseTokenKey { |
| auth_provider_type: String, |
| user_profile_id: String, |
| api_key: String, |
| } |
| |
| impl CacheKey for FirebaseTokenKey { |
| fn auth_provider_type(&self) -> &str { |
| &self.auth_provider_type |
| } |
| |
| fn user_profile_id(&self) -> &str { |
| &self.user_profile_id |
| } |
| |
| fn subkey(&self) -> &str { |
| &self.api_key |
| } |
| } |
| |
| impl KeyFor for FirebaseTokenKey { |
| type TokenType = FirebaseAuthToken; |
| } |
| |
| impl FirebaseTokenKey { |
| /// Creates a new Firebase token key. |
| pub fn new( |
| auth_provider_type: String, |
| user_profile_id: String, |
| api_key: String, |
| ) -> Result<FirebaseTokenKey, Error> { |
| validate_provider_and_id(&auth_provider_type, &user_profile_id)?; |
| Ok(FirebaseTokenKey { |
| auth_provider_type: auth_provider_type, |
| user_profile_id: user_profile_id, |
| api_key: api_key, |
| }) |
| } |
| } |
| |
| /// Validates that the given auth_provider_type and user_profile_id are |
| /// nonempty. |
| fn validate_provider_and_id(auth_provider_type: &str, user_profile_id: &str) -> Result<(), Error> { |
| if auth_provider_type.is_empty() { |
| Err(format_err!("auth_provider_type cannot be empty")) |
| } else if user_profile_id.is_empty() { |
| Err(format_err!("user_profile_id cannot be empty")) |
| } else { |
| Ok(()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use fidl_fuchsia_auth::TokenType; |
| |
| const LONG_EXPIRY: Duration = Duration::from_secs(3000); |
| const TEST_ACCESS_TOKEN: &str = "access token"; |
| const TEST_FIREBASE_ID_TOKEN: &str = "firebase token"; |
| const TEST_FIREBASE_LOCAL_ID: &str = "firebase local id"; |
| const TEST_EMAIL: &str = "user@test.com"; |
| const TEST_AUTH_PROVIDER_TYPE: &str = "test-provider"; |
| const TEST_USER_PROFILE_ID: &str = "test-user-123"; |
| const TEST_SCOPE_1: &str = "scope-1"; |
| const TEST_SCOPE_2: &str = "scope-2"; |
| const TEST_AUDIENCE: &str = "audience"; |
| const TEST_FIREBASE_API: &str = "firebase-api"; |
| |
| #[test] |
| fn test_oauth_from_fidl() { |
| let fidl_type = fidl_fuchsia_auth::AuthToken { |
| token_type: TokenType::AccessToken, |
| expires_in: LONG_EXPIRY.as_secs(), |
| token: TEST_ACCESS_TOKEN.to_string(), |
| }; |
| |
| let time_before_conversion = SystemTime::now(); |
| let native_type = OAuthToken::from(fidl_type); |
| let time_after_conversion = SystemTime::now(); |
| |
| assert_eq!(&native_type.token, TEST_ACCESS_TOKEN); |
| assert!(native_type.expiry_time >= time_before_conversion + LONG_EXPIRY); |
| assert!(native_type.expiry_time <= time_after_conversion + LONG_EXPIRY); |
| |
| // Also verify our implementation of the Deref trait |
| assert_eq!(&*native_type, TEST_ACCESS_TOKEN); |
| } |
| |
| #[test] |
| fn test_firebase_from_fidl() { |
| let fidl_type = fidl_fuchsia_auth::FirebaseToken { |
| id_token: TEST_FIREBASE_ID_TOKEN.to_string(), |
| local_id: Some(TEST_FIREBASE_LOCAL_ID.to_string()), |
| email: Some(TEST_EMAIL.to_string()), |
| expires_in: LONG_EXPIRY.as_secs(), |
| }; |
| |
| let time_before_conversion = SystemTime::now(); |
| let native_type = FirebaseAuthToken::from(fidl_type); |
| let time_after_conversion = SystemTime::now(); |
| |
| assert_eq!(&native_type.id_token, TEST_FIREBASE_ID_TOKEN); |
| assert_eq!(native_type.local_id, Some(TEST_FIREBASE_LOCAL_ID.to_string())); |
| assert_eq!(native_type.email, Some(TEST_EMAIL.to_string())); |
| assert!(native_type.expiry_time >= time_before_conversion + LONG_EXPIRY); |
| assert!(native_type.expiry_time <= time_after_conversion + LONG_EXPIRY); |
| } |
| |
| #[test] |
| fn test_firebase_to_fidl() { |
| let time_before_conversion = SystemTime::now(); |
| let native_type = FirebaseAuthToken { |
| id_token: TEST_FIREBASE_ID_TOKEN.to_string(), |
| local_id: Some(TEST_FIREBASE_LOCAL_ID.to_string()), |
| email: Some(TEST_EMAIL.to_string()), |
| expiry_time: time_before_conversion + LONG_EXPIRY, |
| }; |
| |
| let fidl_type = native_type.to_fidl(); |
| let elapsed_time_during_conversion = |
| SystemTime::now().duration_since(time_before_conversion).unwrap(); |
| |
| assert_eq!(&fidl_type.id_token, TEST_FIREBASE_ID_TOKEN); |
| assert_eq!(fidl_type.local_id, Some(TEST_FIREBASE_LOCAL_ID.to_string())); |
| assert_eq!(fidl_type.email, Some(TEST_EMAIL.to_string())); |
| assert!(fidl_type.expires_in <= LONG_EXPIRY.as_secs()); |
| assert!( |
| fidl_type.expires_in |
| >= (LONG_EXPIRY.as_secs() - elapsed_time_during_conversion.as_secs()) - 1 |
| ); |
| } |
| |
| #[test] |
| fn test_create_access_token_key() { |
| let scopes = vec![TEST_SCOPE_1, TEST_SCOPE_2]; |
| let auth_token_key = AccessTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| TEST_USER_PROFILE_ID.to_string(), |
| &scopes, |
| ) |
| .unwrap(); |
| assert_eq!( |
| AccessTokenKey { |
| auth_provider_type: TEST_AUTH_PROVIDER_TYPE.to_string(), |
| user_profile_id: TEST_USER_PROFILE_ID.to_string(), |
| scopes: TEST_SCOPE_1.to_string() + "\n" + TEST_SCOPE_2, |
| }, |
| auth_token_key |
| ); |
| |
| // Verify single scope creation |
| let single_scope = vec![TEST_SCOPE_1]; |
| let auth_token_key = AccessTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| TEST_USER_PROFILE_ID.to_string(), |
| &single_scope, |
| ) |
| .unwrap(); |
| assert_eq!( |
| AccessTokenKey { |
| auth_provider_type: TEST_AUTH_PROVIDER_TYPE.to_string(), |
| user_profile_id: TEST_USER_PROFILE_ID.to_string(), |
| scopes: TEST_SCOPE_1.to_string(), |
| }, |
| auth_token_key |
| ); |
| |
| // Verify no scopes creation |
| let no_scopes: Vec<&str> = vec![]; |
| let auth_token_key = AccessTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| TEST_USER_PROFILE_ID.to_string(), |
| &no_scopes, |
| ) |
| .unwrap(); |
| assert_eq!( |
| AccessTokenKey { |
| auth_provider_type: TEST_AUTH_PROVIDER_TYPE.to_string(), |
| user_profile_id: TEST_USER_PROFILE_ID.to_string(), |
| scopes: "".to_string(), |
| }, |
| auth_token_key |
| ); |
| |
| // Verify empty auth provider and user profile id cases fail. |
| assert!(AccessTokenKey::new("".to_string(), TEST_USER_PROFILE_ID.to_string(), &no_scopes) |
| .is_err()); |
| assert!(AccessTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| "".to_string(), |
| &no_scopes |
| ) |
| .is_err()); |
| } |
| |
| #[test] |
| fn test_create_id_token_key() { |
| assert_eq!( |
| IdTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| TEST_USER_PROFILE_ID.to_string(), |
| TEST_AUDIENCE.to_string() |
| ) |
| .unwrap(), |
| IdTokenKey { |
| auth_provider_type: TEST_AUTH_PROVIDER_TYPE.to_string(), |
| user_profile_id: TEST_USER_PROFILE_ID.to_string(), |
| audience: TEST_AUDIENCE.to_string() |
| } |
| ); |
| |
| // Verify empty auth provider and user profile id cases fail. |
| assert!(IdTokenKey::new( |
| "".to_string(), |
| TEST_USER_PROFILE_ID.to_string(), |
| TEST_AUDIENCE.to_string() |
| ) |
| .is_err()); |
| assert!(IdTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| "".to_string(), |
| TEST_AUDIENCE.to_string() |
| ) |
| .is_err()); |
| } |
| |
| #[test] |
| fn test_create_firebase_token_key() { |
| assert_eq!( |
| FirebaseTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| TEST_USER_PROFILE_ID.to_string(), |
| TEST_FIREBASE_API.to_string() |
| ) |
| .unwrap(), |
| FirebaseTokenKey { |
| auth_provider_type: TEST_AUTH_PROVIDER_TYPE.to_string(), |
| user_profile_id: TEST_USER_PROFILE_ID.to_string(), |
| api_key: TEST_FIREBASE_API.to_string() |
| } |
| ); |
| |
| // Verify empty auth provider and user profile id cases fail. |
| assert!(FirebaseTokenKey::new( |
| "".to_string(), |
| TEST_USER_PROFILE_ID.to_string(), |
| TEST_FIREBASE_API.to_string() |
| ) |
| .is_err()); |
| assert!(FirebaseTokenKey::new( |
| TEST_AUTH_PROVIDER_TYPE.to_string(), |
| "".to_string(), |
| TEST_FIREBASE_API.to_string() |
| ) |
| .is_err()); |
| } |
| } |