| // Copyright 2018 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 crate::auth_provider_cache::AuthProviderCache; |
| use crate::tokens::{AccessTokenKey, IdTokenKey, OAuthToken}; |
| use crate::{AuthProviderSupplier, ResultExt, TokenManagerContext, TokenManagerError}; |
| use anyhow::format_err; |
| use fidl; |
| use fidl::endpoints::{create_endpoints, ClientEnd}; |
| use fidl_fuchsia_auth::{ |
| AppConfig, AuthenticationUiContextMarker, Status, TokenManagerAuthorizeResponder, |
| TokenManagerDeleteAllTokensResponder, TokenManagerGetAccessTokenResponder, |
| TokenManagerGetIdTokenResponder, TokenManagerListProfileIdsResponder, TokenManagerRequest, |
| TokenManagerRequestStream, UserProfileInfo, |
| }; |
| use fidl_fuchsia_identity_external::{ |
| OauthAccessTokenFromOauthRefreshTokenRequest, OauthRefreshTokenRequest, |
| OpenIdTokenFromOauthRefreshTokenRequest, OpenIdUserInfoFromOauthAccessTokenRequest, |
| }; |
| use fidl_fuchsia_identity_tokens::OauthRefreshToken; |
| use futures::prelude::*; |
| use identity_common::{cancel_or, TaskGroup, TaskGroupCancel}; |
| use log::{error, info, warn}; |
| use parking_lot::Mutex; |
| use std::convert::TryInto; |
| use std::path::Path; |
| use std::sync::Arc; |
| use token_cache::{AuthCacheError, TokenCache}; |
| use token_store::file::AuthDbFile; |
| use token_store::mem::AuthDbInMemory; |
| use token_store::{AuthDb, AuthDbError, CredentialKey, CredentialValue}; |
| |
| /// The maximum number of entries to be stored in the `TokenCache`. |
| const CACHE_SIZE: usize = 128; |
| |
| type TokenManagerResult<T> = Result<T, TokenManagerError>; |
| |
| /// The supplier references and mutable state used to create, store, and cache authentication |
| /// tokens for a particular user across a range of third party services. |
| pub struct TokenManager<APS: AuthProviderSupplier> { |
| /// An in-memory cache that retrieves and caches token provider proxies. |
| auth_provider_cache: AuthProviderCache<APS>, |
| /// A persistent store of long term credentials. |
| token_store: Mutex<Box<dyn AuthDb + Send + Sync>>, |
| /// An in-memory cache of recently used tokens. |
| token_cache: Mutex<TokenCache>, |
| /// Collection of tasks that are using this instance. |
| task_group: TaskGroup, |
| } |
| |
| impl<APS: AuthProviderSupplier> TokenManager<APS> { |
| /// Creates a new TokenManager, backed by a file db. |
| pub fn new( |
| db_path: &Path, |
| auth_provider_supplier: APS, |
| task_group: TaskGroup, |
| ) -> Result<Self, anyhow::Error> { |
| let token_store = AuthDbFile::new(db_path) |
| .map_err(|err| format_err!("Error creating AuthDb at {:?}, {:?}", db_path, err))?; |
| let token_cache = TokenCache::new(CACHE_SIZE); |
| |
| Ok(TokenManager { |
| auth_provider_cache: AuthProviderCache::new(auth_provider_supplier), |
| token_store: Mutex::new(Box::new(token_store)), |
| token_cache: Mutex::new(token_cache), |
| task_group, |
| }) |
| } |
| |
| /// Create a new in-memory TokenManager. |
| pub fn new_in_memory(auth_provider_supplier: APS, task_group: TaskGroup) -> Self { |
| let token_store = AuthDbInMemory::new(); |
| let token_cache = TokenCache::new(CACHE_SIZE); |
| TokenManager { |
| auth_provider_cache: AuthProviderCache::new(auth_provider_supplier), |
| token_store: Mutex::new(Box::new(token_store)), |
| token_cache: Mutex::new(token_cache), |
| task_group, |
| } |
| } |
| |
| /// Returns a task group which can be used to spawn and cancel tasks that use this instance. |
| pub fn task_group(&self) -> &TaskGroup { |
| &self.task_group |
| } |
| |
| /// Asynchronously handles the supplied stream of `TokenManagerRequest` messages. |
| pub async fn handle_requests_from_stream<'a>( |
| &'a self, |
| context: &'a TokenManagerContext, |
| mut stream: TokenManagerRequestStream, |
| cancel: TaskGroupCancel, |
| ) -> Result<(), anyhow::Error> { |
| // TODO(dnordstrom): Allow cancellation within long running requests |
| while let Some(result) = cancel_or(&cancel, stream.try_next()).await { |
| match result? { |
| Some(request) => self.handle_request(context, request).await?, |
| None => break, |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Handles a single request to the TokenManager by dispatching to more specific functions for |
| /// each method. |
| pub async fn handle_request<'a>( |
| &'a self, |
| context: &'a TokenManagerContext, |
| req: TokenManagerRequest, |
| ) -> Result<(), anyhow::Error> { |
| // TODO(jsankey): Determine how best to enforce the application_url in context. |
| match req { |
| TokenManagerRequest::Authorize { |
| app_config, |
| auth_ui_context, |
| user_profile_id, |
| app_scopes, |
| auth_code, |
| responder, |
| } => responder.send_result( |
| self.authorize( |
| context, |
| app_config, |
| auth_ui_context, |
| user_profile_id, |
| app_scopes, |
| auth_code, |
| ) |
| .await, |
| ), |
| TokenManagerRequest::GetAccessToken { |
| app_config, |
| user_profile_id, |
| app_scopes, |
| responder, |
| } => responder |
| .send_result(self.get_access_token(app_config, user_profile_id, app_scopes).await), |
| TokenManagerRequest::GetIdToken { |
| app_config, |
| user_profile_id, |
| audience, |
| responder, |
| } => responder |
| .send_result(self.get_id_token(app_config, user_profile_id, audience).await), |
| TokenManagerRequest::DeleteAllTokens { |
| app_config, |
| user_profile_id, |
| force, |
| responder, |
| } => responder |
| .send_result(self.delete_all_tokens(app_config, user_profile_id, force).await), |
| TokenManagerRequest::ListProfileIds { app_config, responder } => { |
| responder.send_result(self.list_profile_ids(app_config)) |
| } |
| } |
| } |
| |
| /// Implements the FIDL TokenManager.Authorize method. |
| async fn authorize<'a>( |
| &'a self, |
| context: &'a TokenManagerContext, |
| app_config: AppConfig, |
| _auth_ui_context: Option<ClientEnd<AuthenticationUiContextMarker>>, |
| user_profile_id: Option<String>, |
| _app_scopes: Vec<String>, |
| _auth_code: Option<String>, |
| ) -> TokenManagerResult<UserProfileInfo> { |
| // TODO(jsankey): Currently auth_ui_context is neither supplied by Topaz nor allowed to |
| // override the auth UI context supplied at token manager construction (fxbug.dev/459). |
| // Depending on the outcome of design discussions either pass it through or remove it |
| // entirely. |
| let auth_provider_type = &app_config.auth_provider_type; |
| let oauth_proxy = self.auth_provider_cache.get_oauth_proxy(auth_provider_type).await?; |
| |
| let (ui_context_client_end, ui_context_server_end) = |
| create_endpoints().token_manager_status(Status::UnknownError)?; |
| context |
| .auth_ui_context_provider |
| .get_authentication_ui_context(ui_context_server_end) |
| .token_manager_status(Status::InvalidAuthContext)?; |
| |
| let (refresh_token, access_token) = oauth_proxy |
| .create_refresh_token(OauthRefreshTokenRequest { |
| account_id: user_profile_id, |
| ui_context: Some(ui_context_client_end), |
| ..OauthRefreshTokenRequest::EMPTY |
| }) |
| .await |
| .map_err(|err| { |
| TokenManagerError::new(Status::AuthProviderServerError).with_cause(err) |
| })??; |
| |
| // Get user profile info |
| let oauth_open_id_proxy = |
| self.auth_provider_cache.get_oauth_open_id_connect_proxy(auth_provider_type).await?; |
| |
| let user_profile_info = oauth_open_id_proxy |
| .get_user_info_from_access_token(OpenIdUserInfoFromOauthAccessTokenRequest { |
| access_token: Some(access_token), |
| ..OpenIdUserInfoFromOauthAccessTokenRequest::EMPTY |
| }) |
| .await |
| .map_err(|err| { |
| TokenManagerError::new(Status::AuthProviderServerError).with_cause(err) |
| })??; |
| let db_value = CredentialValue::new( |
| app_config.auth_provider_type, |
| user_profile_info.subject.clone().ok_or(Status::AuthProviderServerError)?, |
| refresh_token.content.ok_or(Status::AuthProviderServerError)?, |
| None, |
| ) |
| .map_err(|_| Status::AuthProviderServerError)?; |
| self.token_store.lock().add_credential(db_value)?; |
| |
| Ok(UserProfileInfo { |
| id: user_profile_info.subject.ok_or(Status::AuthProviderServerError)?, |
| display_name: user_profile_info.name, |
| url: None, |
| image_url: user_profile_info.picture, |
| }) |
| } |
| |
| /// Implements the FIDL TokenManager.GetAccessToken method. |
| async fn get_access_token( |
| &self, |
| app_config: AppConfig, |
| user_profile_id: String, |
| app_scopes: Vec<String>, |
| ) -> TokenManagerResult<Arc<OAuthToken>> { |
| let cache_key = AccessTokenKey::new( |
| app_config.auth_provider_type.clone(), |
| user_profile_id.clone(), |
| &app_scopes, |
| ) |
| .token_manager_status(Status::InvalidRequest)?; |
| |
| // Attempt to read the token from cache. |
| if let Some(cached_token) = self.token_cache.lock().get(&cache_key) { |
| return Ok(cached_token); |
| } |
| |
| // If no cached entry was found use an auth provider to mint a new one from the refresh |
| // token, then place it in the cache. |
| let db_key = Self::create_db_key(&app_config, &user_profile_id)?; |
| let refresh_token = self.get_refresh_token(&db_key)?; |
| |
| self.handle_get_access_token(app_config, refresh_token, app_scopes, cache_key).await |
| } |
| |
| /// Implements the FIDL TokenManager.GetAccessToken method using an auth provider that does not |
| /// support IoT ID. |
| async fn handle_get_access_token( |
| &self, |
| app_config: AppConfig, |
| refresh_token: OauthRefreshToken, |
| app_scopes: Vec<String>, |
| cache_key: AccessTokenKey, |
| ) -> TokenManagerResult<Arc<OAuthToken>> { |
| let auth_provider_type = &app_config.auth_provider_type; |
| let oauth_proxy = self.auth_provider_cache.get_oauth_proxy(auth_provider_type).await?; |
| |
| let provider_token = oauth_proxy |
| .get_access_token_from_refresh_token(OauthAccessTokenFromOauthRefreshTokenRequest { |
| refresh_token: Some(refresh_token), |
| client_id: app_config.client_id.clone(), |
| scopes: Some(app_scopes), |
| ..OauthAccessTokenFromOauthRefreshTokenRequest::EMPTY |
| }) |
| .await |
| .map_err(|err| { |
| TokenManagerError::new(Status::AuthProviderServerError).with_cause(err) |
| })??; |
| |
| let native_token = Arc::new(provider_token.try_into()?); |
| self.token_cache.lock().put(cache_key, Arc::clone(&native_token)); |
| Ok(native_token) |
| } |
| |
| /// Implements the FIDL TokenManager.GetIdToken method. |
| async fn get_id_token( |
| &self, |
| app_config: AppConfig, |
| user_profile_id: String, |
| audience: Option<String>, |
| ) -> TokenManagerResult<Arc<OAuthToken>> { |
| let audience_str = audience.clone().unwrap_or("".to_string()); |
| let cache_key = IdTokenKey::new( |
| app_config.auth_provider_type.clone(), |
| user_profile_id.clone(), |
| audience_str, |
| ) |
| .token_manager_status(Status::InvalidRequest)?; |
| |
| // Attempt to read the token from cache. |
| if let Some(cached_token) = self.token_cache.lock().get(&cache_key) { |
| return Ok(cached_token); |
| } |
| |
| // If no cached entry was found use an auth provider to mint a new one from the refresh |
| // token, then place it in the cache. |
| let db_key = Self::create_db_key(&app_config, &user_profile_id)?; |
| let refresh_token = self.get_refresh_token(&db_key)?; |
| let auth_provider_type = &app_config.auth_provider_type; |
| let oauth_open_id_proxy = |
| self.auth_provider_cache.get_oauth_open_id_connect_proxy(auth_provider_type).await?; |
| let get_id_token_request = OpenIdTokenFromOauthRefreshTokenRequest { |
| refresh_token: Some(refresh_token), |
| audiences: audience.map(|audience| vec![audience]), |
| ..OpenIdTokenFromOauthRefreshTokenRequest::EMPTY |
| }; |
| |
| let provider_token = |
| match oauth_open_id_proxy.get_id_token_from_refresh_token(get_id_token_request).await { |
| Ok(api_result) => api_result?, |
| Err(fidl_err) => { |
| return Err(TokenManagerError::new(Status::AuthProviderServerError) |
| .with_cause(fidl_err)); |
| } |
| }; |
| |
| // TODO(satsukiu): At the moment, the ID token is stored as an AuthToken type because |
| // the fuchsia.auth.TokenManager protocol returns AuthTokens. As part of the migration |
| // to fuchsia.identity.tokens ID tokens should be stored as a separate type from Oauth |
| // tokens. |
| let native_token = Arc::new(provider_token.try_into()?); |
| self.token_cache.lock().put(cache_key, Arc::clone(&native_token)); |
| Ok(native_token) |
| } |
| |
| /// Implements the FIDL TokenManager.DeleteAllTokens method. |
| /// |
| /// This deletes any existing tokens for a user in both the database and cache and requests |
| /// that the service provider revoke them. |
| async fn delete_all_tokens( |
| &self, |
| app_config: AppConfig, |
| user_profile_id: String, |
| force: bool, |
| ) -> TokenManagerResult<()> { |
| let db_key = Self::create_db_key(&app_config, &user_profile_id)?; |
| |
| // Try to find an associated refresh token, returning immediately with a success if we |
| // can't. |
| let refresh_token = match (**self.token_store.lock()).get_refresh_token(&db_key) { |
| Ok(rt) => rt.to_string(), |
| Err(AuthDbError::CredentialNotFound) => return Ok(()), |
| Err(err) => return Err(TokenManagerError::from(err)), |
| }; |
| |
| // Request that the auth provider revoke the credential server-side. |
| let auth_provider_type = &app_config.auth_provider_type; |
| let oauth_proxy = self.auth_provider_cache.get_oauth_proxy(auth_provider_type).await?; |
| let result = oauth_proxy |
| .revoke_refresh_token(OauthRefreshToken { |
| content: Some(refresh_token), |
| account_id: None, |
| ..OauthRefreshToken::EMPTY |
| }) |
| .await |
| .map_err(|err| { |
| TokenManagerError::new(Status::AuthProviderServerError).with_cause(err) |
| })?; |
| |
| if let Err(api_error) = result { |
| if force { |
| warn!("Removing stored tokens even though revocation failed with {:?}", api_error) |
| } else { |
| return Err(TokenManagerError::from(api_error)); |
| } |
| } |
| |
| // TODO(fxbug.dev/43340): define when revocation for id tokens are called, and behaviors when the |
| // corresponding protocols return errors or are not implemented. |
| |
| match self.token_cache.lock().delete_matching(&auth_provider_type, &user_profile_id) { |
| Ok(()) | Err(AuthCacheError::KeyNotFound) => {} |
| Err(err) => return Err(TokenManagerError::from(err)), |
| } |
| |
| match self.token_store.lock().delete_credential(&db_key) { |
| Ok(()) | Err(AuthDbError::CredentialNotFound) => {} |
| Err(err) => return Err(TokenManagerError::from(err)), |
| } |
| |
| Ok(()) |
| } |
| |
| /// Implements the FIDL TokenManager.ListProfileIds method. |
| fn list_profile_ids(&self, app_config: AppConfig) -> TokenManagerResult<Vec<String>> { |
| let token_store = self.token_store.lock(); |
| Ok(token_store |
| .get_all_credential_keys()? |
| .into_iter() |
| .filter(|k| k.auth_provider_type() == &app_config.auth_provider_type) |
| .map(|k| k.user_profile_id().to_string()) |
| .collect()) |
| } |
| |
| /// Returns index keys for referencing a token in the database. |
| fn create_db_key( |
| app_config: &AppConfig, |
| user_profile_id: &String, |
| ) -> Result<CredentialKey, TokenManagerError> { |
| let db_key = |
| CredentialKey::new(app_config.auth_provider_type.clone(), user_profile_id.clone()) |
| .map_err(|_| TokenManagerError::new(Status::InvalidRequest))?; |
| Ok(db_key) |
| } |
| |
| /// Returns the current refresh token for a user from the data store. Failure to find the user |
| /// leads to an Error. |
| fn get_refresh_token( |
| &self, |
| db_key: &CredentialKey, |
| ) -> Result<OauthRefreshToken, TokenManagerError> { |
| match (**self.token_store.lock()).get_refresh_token(db_key) { |
| Ok(rt) => Ok(OauthRefreshToken { |
| content: Some(rt.to_string()), |
| account_id: Some(db_key.user_profile_id().to_string()), |
| ..OauthRefreshToken::EMPTY |
| }), |
| Err(err) => Err(TokenManagerError::from(err)), |
| } |
| } |
| } |
| |
| /// A trait that we implement for the autogenerated FIDL responder types for each method to |
| /// simplify the process of responding. |
| trait Responder: Sized { |
| type Data; |
| |
| /// Sends the supplied result logging any errors in the result or sending. The return value is |
| /// an error if the input was a fatal error, or Ok(()) otherwise. |
| fn send_result( |
| self, |
| result: Result<Self::Data, TokenManagerError>, |
| ) -> Result<(), anyhow::Error> { |
| match result { |
| Ok(val) => { |
| if let Err(err) = self.send_raw(Status::Ok, Some(val)) { |
| warn!("Error sending response to {}: {:?}", Self::METHOD_NAME, &err); |
| } |
| Ok(()) |
| } |
| Err(err) => { |
| if let Err(err) = self.send_raw(err.status, None) { |
| warn!("Error sending error response to {}: {:?}", Self::METHOD_NAME, &err); |
| } |
| if err.fatal { |
| error!("Fatal error during {}: {:?}", Self::METHOD_NAME, &err); |
| Err(anyhow::Error::from(err)) |
| } else { |
| warn!("Error during {}: {:?}", Self::METHOD_NAME, &err); |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| /// Sends a status and optional data without logging or failure handling. |
| fn send_raw(self, status: Status, data: Option<Self::Data>) -> Result<(), fidl::Error>; |
| |
| /// Defines the name of the TokenManger method for use in logging. |
| const METHOD_NAME: &'static str; |
| } |
| |
| impl Responder for TokenManagerAuthorizeResponder { |
| type Data = UserProfileInfo; |
| const METHOD_NAME: &'static str = "Authorize"; |
| |
| fn send_raw( |
| self, |
| status: Status, |
| mut data: Option<UserProfileInfo>, |
| ) -> Result<(), fidl::Error> { |
| // Explicitly log successes for the infrequent Authorize request to help debug. |
| if status == Status::Ok { |
| info!("Success authorizing new account"); |
| } |
| self.send(status, data.as_mut()) |
| } |
| } |
| |
| impl Responder for TokenManagerGetAccessTokenResponder { |
| type Data = Arc<OAuthToken>; |
| const METHOD_NAME: &'static str = "GetAccessToken"; |
| |
| fn send_raw(self, status: Status, data: Option<Arc<OAuthToken>>) -> Result<(), fidl::Error> { |
| self.send(status, data.as_ref().map(|v| &***v)) |
| } |
| } |
| |
| impl Responder for TokenManagerGetIdTokenResponder { |
| type Data = Arc<OAuthToken>; |
| const METHOD_NAME: &'static str = "GetIdToken"; |
| |
| fn send_raw(self, status: Status, data: Option<Arc<OAuthToken>>) -> Result<(), fidl::Error> { |
| self.send(status, data.as_ref().map(|v| &***v)) |
| } |
| } |
| |
| impl Responder for TokenManagerDeleteAllTokensResponder { |
| type Data = (); |
| const METHOD_NAME: &'static str = "DeleteAllTokens"; |
| |
| fn send_raw(self, status: Status, _data: Option<()>) -> Result<(), fidl::Error> { |
| self.send(status) |
| } |
| } |
| |
| impl Responder for TokenManagerListProfileIdsResponder { |
| type Data = Vec<String>; |
| const METHOD_NAME: &'static str = "ListProfileIds"; |
| |
| fn send_raw(self, status: Status, data: Option<Vec<String>>) -> Result<(), fidl::Error> { |
| match data { |
| None => self.send(status, &mut std::iter::empty()), |
| Some(profile_ids) => self.send(status, &mut profile_ids.iter().map(|x| &**x)), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::fake_auth_provider_supplier::FakeAuthProviderSupplier; |
| use fidl::endpoints::create_proxy_and_stream; |
| use fidl_fuchsia_auth::{ |
| AuthenticationContextProviderMarker, TokenManagerMarker, TokenManagerProxy, |
| }; |
| use fidl_fuchsia_identity_external::{OauthOpenIdConnectRequest, OauthRequest}; |
| use fidl_fuchsia_identity_tokens::{OauthAccessToken, OpenIdToken, OpenIdUserInfo}; |
| use fuchsia_async as fasync; |
| use fuchsia_zircon::Duration; |
| use futures::channel::oneshot; |
| use futures::future::join3; |
| use tempfile::TempDir; |
| |
| /// Generate a pre-populated AppConfig fidl struct |
| fn create_app_config(auth_provider_type: &str) -> AppConfig { |
| AppConfig { |
| auth_provider_type: auth_provider_type.to_string(), |
| client_id: None, |
| client_secret: None, |
| redirect_uri: None, |
| } |
| } |
| |
| /// Generate a TokenManagerContext which answers standard calls |
| fn create_token_manager_context() -> TokenManagerContext { |
| let (ui_context_provider_proxy, mut stream) = |
| create_proxy_and_stream::<AuthenticationContextProviderMarker>().unwrap(); |
| fasync::Task::spawn(async move { while let Some(_) = stream.try_next().await.unwrap() {} }) |
| .detach(); |
| TokenManagerContext { |
| application_url: "APPLICATION_URL".to_string(), |
| auth_ui_context_provider: ui_context_provider_proxy, |
| } |
| } |
| |
| /// Creates a TokenManager and serves requests for it. |
| async fn create_and_serve_token_manager( |
| stream: TokenManagerRequestStream, |
| auth_provider_supplier: FakeAuthProviderSupplier, |
| ) -> Result<(), anyhow::Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let db_path = tmp_dir.path().join("tokens.json"); |
| create_and_serve_token_manager_with_db_path(&db_path, stream, auth_provider_supplier).await |
| } |
| |
| /// Creates an in-memory TokenManager and serves requests for it. |
| async fn create_and_serve_in_memory_token_manager( |
| stream: TokenManagerRequestStream, |
| auth_provider_supplier: FakeAuthProviderSupplier, |
| ) -> Result<(), anyhow::Error> { |
| let task_group = TaskGroup::new(); |
| let token_manager = TokenManager::new_in_memory(auth_provider_supplier, task_group.clone()); |
| let context = create_token_manager_context(); |
| let (_sender, receiver) = oneshot::channel(); |
| token_manager.handle_requests_from_stream(&context, stream, receiver.shared()).await |
| } |
| |
| /// Creates a TokenManager given a location to the file db, and serves requests for it. |
| async fn create_and_serve_token_manager_with_db_path( |
| db_path: &Path, |
| stream: TokenManagerRequestStream, |
| auth_provider_supplier: FakeAuthProviderSupplier, |
| ) -> Result<(), anyhow::Error> { |
| let task_group = TaskGroup::new(); |
| |
| let token_manager = TokenManager::new(&db_path, auth_provider_supplier, task_group.clone()) |
| .expect("failed creating TokenManager"); |
| let context = create_token_manager_context(); |
| let (_sender, receiver) = oneshot::channel(); |
| token_manager.handle_requests_from_stream(&context, stream, receiver.shared()).await |
| } |
| |
| /// Expect a CreateRefreshToken request, configurable with user_id. Generates a response. |
| fn expect_create_refresh_token( |
| request: OauthRequest, |
| user_id: &str, |
| ) -> Result<(), fidl::Error> { |
| match request { |
| OauthRequest::CreateRefreshToken { request, responder } => { |
| assert!(request.ui_context.is_some()); |
| assert_eq!(request.account_id, Some(user_id.to_string())); |
| responder.send(&mut Ok(( |
| OauthRefreshToken { |
| content: Some(format!("{}_CREDENTIAL", user_id)), |
| account_id: Some(user_id.to_string()), |
| ..OauthRefreshToken::EMPTY |
| }, |
| OauthAccessToken { |
| content: Some("ACCESS_TOKEN".to_string()), |
| expiry_time: None, |
| ..OauthAccessToken::EMPTY |
| }, |
| )))?; |
| } |
| _ => panic!("Unexpected message received"), |
| } |
| Ok(()) |
| } |
| |
| /// Expect a GetAppAccessToken request, configurable with a token. Generates a response. |
| fn expect_get_access_token( |
| oauth_request: OauthRequest, |
| token: &str, |
| ) -> Result<(), fidl::Error> { |
| match oauth_request { |
| OauthRequest::GetAccessTokenFromRefreshToken { request, responder } => { |
| assert_eq!(request.refresh_token.unwrap().content.unwrap(), token); |
| assert_eq!(request.client_id, None); |
| assert_eq!(request.scopes.unwrap(), Vec::<String>::default()); |
| let access_token = OauthAccessToken { |
| content: Some(format!("{}_ACCESS", token)), |
| expiry_time: None, // todo(before-submit) |
| ..OauthAccessToken::EMPTY |
| }; |
| responder.send(&mut Ok(access_token))?; |
| } |
| _ => panic!("Unexpected message received"), |
| } |
| Ok(()) |
| } |
| |
| fn expect_get_user_info( |
| request: OauthOpenIdConnectRequest, |
| user_id: &str, |
| ) -> Result<(), fidl::Error> { |
| match request { |
| OauthOpenIdConnectRequest::GetUserInfoFromAccessToken { request, responder } => { |
| assert_eq!(request.access_token.unwrap().content.unwrap(), "ACCESS_TOKEN"); |
| responder.send(&mut Ok(OpenIdUserInfo { |
| subject: Some(user_id.to_string()), |
| name: Some(format!("{}_DISPLAY", user_id)), |
| email: Some(format!("{}@example.org", user_id)), |
| picture: Some(format!("http://example.org/{}/img", user_id)), |
| ..OpenIdUserInfo::EMPTY |
| })) |
| } |
| _ => panic!("Unexpected message received"), |
| } |
| } |
| |
| /// Expect a GetIdTokenFromRefreshToken request, configurable with a token. Generates a response. |
| fn expect_get_id_token( |
| request: OauthOpenIdConnectRequest, |
| expected_refresh_token: &str, |
| ) -> Result<(), fidl::Error> { |
| match request { |
| OauthOpenIdConnectRequest::GetIdTokenFromRefreshToken { request, responder } => { |
| let OpenIdTokenFromOauthRefreshTokenRequest { refresh_token, audiences, .. } = |
| request; |
| assert_eq!(&refresh_token.unwrap().content.unwrap(), expected_refresh_token); |
| assert_eq!(audiences, Some(vec!["AUDIENCE".to_string()])); |
| let expiry_time = fuchsia_runtime::utc_time() + Duration::from_seconds(3600); |
| let mut id_token = Ok(OpenIdToken { |
| content: Some(format!("{}_ID", expected_refresh_token)), |
| expiry_time: Some(expiry_time.into_nanos()), |
| ..OpenIdToken::EMPTY |
| }); |
| responder.send(&mut id_token)?; |
| } |
| _ => panic!("Unexpected message received"), |
| } |
| Ok(()) |
| } |
| |
| /// Expect a RevokeAppOrPersistentCredential request, configurable with a token. Responds Ok. |
| fn expect_revoke_refresh_token(request: OauthRequest, token: &str) -> Result<(), fidl::Error> { |
| match request { |
| OauthRequest::RevokeRefreshToken { refresh_token, responder } => { |
| assert_eq!(refresh_token.content.unwrap().as_str(), token); |
| responder.send(&mut Ok(()))?; |
| } |
| _ => panic!("Unexpected message received"), |
| } |
| Ok(()) |
| } |
| |
| /// Wrapper for TokenManager::Authorize |
| async fn authorize_simple<'a>( |
| tm_proxy: &'a TokenManagerProxy, |
| auth_provider_type: &'a str, |
| user_profile_id: &'a str, |
| ) -> Result<(Status, Option<Box<UserProfileInfo>>), fidl::Error> { |
| let mut app_config = create_app_config(auth_provider_type); |
| let auth_ui_context = None; |
| let app_scopes = Vec::<String>::default(); |
| let auth_code = None; |
| tm_proxy |
| .authorize( |
| &mut app_config, |
| auth_ui_context, |
| &mut app_scopes.iter().map(|x| &**x), |
| Some(user_profile_id), |
| auth_code, |
| ) |
| .await |
| } |
| |
| /// Wrapper for TokenManager::GetAccessToken |
| async fn get_access_token_simple<'a>( |
| tm_proxy: &'a TokenManagerProxy, |
| auth_provider_type: &'a str, |
| user_profile_id: &'a str, |
| ) -> Result<(Status, Option<String>), fidl::Error> { |
| let mut app_config = create_app_config(auth_provider_type); |
| let app_scopes = Vec::<String>::default(); |
| tm_proxy |
| .get_access_token( |
| &mut app_config, |
| user_profile_id, |
| &mut app_scopes.iter().map(|x| &**x), |
| ) |
| .await |
| } |
| |
| /// Wrapper for TokenManager::GetIdToken |
| async fn get_id_token_simple<'a>( |
| tm_proxy: &'a TokenManagerProxy, |
| auth_provider_type: &'a str, |
| user_profile_id: &'a str, |
| ) -> Result<(Status, Option<String>), fidl::Error> { |
| let mut app_config = create_app_config(auth_provider_type); |
| tm_proxy.get_id_token(&mut app_config, user_profile_id, Some("AUDIENCE")).await |
| } |
| |
| /// Wrapper for TokenManager::DeleteAllTokens |
| async fn delete_all_tokens_simple<'a>( |
| tm_proxy: &'a TokenManagerProxy, |
| auth_provider_type: &'a str, |
| user_profile_id: &'a str, |
| ) -> Result<Status, fidl::Error> { |
| let mut app_config = create_app_config(auth_provider_type); |
| let force = true; |
| tm_proxy.delete_all_tokens(&mut app_config, user_profile_id, force).await |
| } |
| |
| /// Create a TokenManager and terminate it using its task group. |
| #[fasync::run_until_stalled(test)] |
| async fn create_and_terminate() { |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| let tmp_dir = TempDir::new().expect("failed creating temp dir"); |
| let db_path = tmp_dir.path().join("tokens.json"); |
| let task_group = TaskGroup::new(); |
| |
| let token_manager = Arc::new( |
| TokenManager::new(&db_path, auth_provider_supplier, task_group.clone()) |
| .expect("failed creating TokenManager"), |
| ); |
| let (proxy, stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let token_manager_clone = Arc::clone(&token_manager); |
| token_manager |
| .task_group() |
| .spawn(|cancel| async move { |
| let context = create_token_manager_context(); |
| assert!(token_manager_clone |
| .handle_requests_from_stream(&context, stream, cancel) |
| .await |
| .is_ok()); |
| }) |
| .await |
| .expect("spawning failed"); |
| assert!(token_manager.task_group().cancel().await.is_ok()); |
| // Check that the accessor's task group cancelled the task group that was passed to new() |
| assert!(task_group.cancel().await.is_err()); |
| |
| // Now the channel should be closed |
| let mut app_config = create_app_config("hooli"); |
| assert!(proxy.list_profile_ids(&mut app_config).await.is_err()); |
| } |
| |
| /// Authorize with multiple auth providers and multiple users per auth provider |
| #[fasync::run_until_stalled(test)] |
| async fn authorize() { |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| auth_provider_supplier.add_oauth("hooli", |mut stream| async move { |
| expect_create_refresh_token(stream.try_next().await?.expect("End of stream"), "GAVIN")?; |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "DENPAK", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("hooli", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "GAVIN")?; |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "DENPAK")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move { |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("pied-piper", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "RICHARD")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| |
| let (tm_proxy, tm_stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let client_fut = async move { |
| // Authorize Gavin to Hooli |
| let (status, user_profile_info) = authorize_simple(&tm_proxy, "hooli", "GAVIN").await?; |
| assert_eq!(status, Status::Ok); |
| let user_profile_info = user_profile_info.expect("user_profile_info is empty"); |
| assert_eq!(user_profile_info.id, "GAVIN"); |
| assert_eq!(user_profile_info.display_name, Some("GAVIN_DISPLAY".to_string())); |
| assert_eq!(user_profile_info.url, None); |
| assert_eq!( |
| user_profile_info.image_url, |
| Some("http://example.org/GAVIN/img".to_string()) |
| ); |
| |
| // Authorize Richard to Pied Piper |
| let (status, user_profile_info) = |
| authorize_simple(&tm_proxy, "pied-piper", "RICHARD").await?; |
| assert_eq!(status, Status::Ok); |
| let user_profile_info = user_profile_info.expect("user_profile_info is empty"); |
| assert_eq!(user_profile_info.id, "RICHARD"); |
| assert_eq!(user_profile_info.display_name, Some("RICHARD_DISPLAY".to_string())); |
| assert_eq!(user_profile_info.url, None); |
| assert_eq!( |
| user_profile_info.image_url, |
| Some("http://example.org/RICHARD/img".to_string()) |
| ); |
| |
| // Again, authorize Gavin to Hooli (with just basic assertions) |
| let (status, user_profile_info) = |
| authorize_simple(&tm_proxy, "hooli", "DENPAK").await?; |
| assert_eq!(status, Status::Ok); |
| assert_eq!(user_profile_info.expect("user_profile_info is empty").id, "DENPAK"); |
| |
| // Authorize against a non-existing auth provider |
| assert_eq!( |
| (Status::AuthProviderServiceUnavailable, None), |
| authorize_simple(&tm_proxy, "myspace", "MR_ROBOT").await? |
| ); |
| |
| Result::<(), fidl::Error>::Ok(()) |
| }; |
| let (ap_result, client_result, tm_result) = join3( |
| auth_provider_supplier.run(), |
| client_fut, |
| create_and_serve_token_manager(tm_stream, auth_provider_supplier), |
| ) |
| .await; |
| assert!(ap_result.is_ok()); |
| assert!(client_result.is_ok()); |
| assert!(tm_result.is_ok()); |
| } |
| |
| /// Check that user profiles can be listed |
| #[fasync::run_until_stalled(test)] |
| async fn list_profile_ids() { |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| auth_provider_supplier.add_oauth("hooli", |mut stream| async move { |
| expect_create_refresh_token(stream.try_next().await?.expect("End of stream"), "GAVIN")?; |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "DENPAK", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("hooli", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "GAVIN")?; |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "DENPAK")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move { |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("pied-piper", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "RICHARD")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| |
| let (tm_proxy, tm_stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let client_fut = async move { |
| assert_eq!(authorize_simple(&tm_proxy, "hooli", "GAVIN").await?.0, Status::Ok); |
| assert_eq!(authorize_simple(&tm_proxy, "hooli", "DENPAK").await?.0, Status::Ok); |
| assert_eq!(authorize_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, Status::Ok); |
| |
| let profile_ids = tm_proxy.list_profile_ids(&mut create_app_config("hooli")).await?; |
| assert_eq!(profile_ids, (Status::Ok, vec!["DENPAK".to_string(), "GAVIN".to_string()])); |
| let profile_ids = |
| tm_proxy.list_profile_ids(&mut create_app_config("pied-piper")).await?; |
| assert_eq!(profile_ids, (Status::Ok, vec!["RICHARD".to_string()])); |
| let profile_ids = tm_proxy.list_profile_ids(&mut create_app_config("myspace")).await?; |
| assert_eq!(profile_ids, (Status::Ok, vec![])); |
| |
| Result::<(), fidl::Error>::Ok(()) |
| }; |
| let (ap_result, client_result, tm_result) = join3( |
| auth_provider_supplier.run(), |
| client_fut, |
| create_and_serve_token_manager(tm_stream, auth_provider_supplier), |
| ) |
| .await; |
| assert!(ap_result.is_ok()); |
| assert!(client_result.is_ok()); |
| assert!(tm_result.is_ok()); |
| } |
| |
| /// Check that we can get access and id tokens, both by exchanging the refresh token and by |
| /// using the cache to retrieve the same value (without calling the auth provider). |
| #[fasync::run_until_stalled(test)] |
| async fn get_tokens() { |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move { |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD", |
| )?; |
| expect_get_access_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("pied-piper", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "RICHARD")?; |
| expect_get_id_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| |
| let (tm_proxy, tm_stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let client_fut = async move { |
| assert_eq!(authorize_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, Status::Ok); |
| |
| // Check that "_CREDENTIAL" was added by the authorize call and that "_ACCESS" or "_ID" |
| // respectively, was added by the token exchange calls. In the second |
| // iteration, make the same calls, but this time they should be delivered from cache. |
| for _ in 0..2 { |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| (Status::Ok, Some("RICHARD_CREDENTIAL_ACCESS".to_string())) |
| ); |
| assert_eq!( |
| get_id_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| (Status::Ok, Some("RICHARD_CREDENTIAL_ID".to_string())) |
| ); |
| } |
| |
| // Errors |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "pied-piper", "DINESH").await?, |
| (Status::UserNotFound, None) |
| ); |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "myspace", "MR_ROBOT").await?, |
| (Status::UserNotFound, None) |
| ); |
| |
| Result::<(), fidl::Error>::Ok(()) |
| }; |
| let (ap_result, client_result, tm_result) = join3( |
| auth_provider_supplier.run(), |
| client_fut, |
| create_and_serve_token_manager(tm_stream, auth_provider_supplier), |
| ) |
| .await; |
| assert!(ap_result.is_ok()); |
| assert!(client_result.is_ok()); |
| assert!(tm_result.is_ok()); |
| } |
| |
| /// Check that tokens are correctly deleted, including the cached ones such as access tokens |
| #[fasync::run_until_stalled(test)] |
| async fn delete_all_tokens() { |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move { |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD", |
| )?; |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "DINESH", |
| )?; |
| expect_get_access_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| expect_revoke_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("pied-piper", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "RICHARD")?; |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "DINESH")?; |
| expect_get_id_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| |
| let (tm_proxy, tm_stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let client_fut = async move { |
| assert_eq!(authorize_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, Status::Ok); |
| assert_eq!(authorize_simple(&tm_proxy, "pied-piper", "DINESH").await?.0, Status::Ok); |
| |
| // Put an access token and an id token in the cache |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, |
| Status::Ok |
| ); |
| assert_eq!( |
| get_id_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, |
| Status::Ok |
| ); |
| |
| // Deleting an existing token succeeds |
| assert_eq!( |
| delete_all_tokens_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| Status::Ok |
| ); |
| // Deleting a non-existing token succeeds |
| assert_eq!( |
| delete_all_tokens_simple(&tm_proxy, "myspace", "MR_ROBOT").await?, |
| Status::Ok |
| ); |
| |
| // No longer present in list |
| let profile_ids = |
| tm_proxy.list_profile_ids(&mut create_app_config("pied-piper")).await?; |
| assert_eq!(profile_ids, (Status::Ok, vec!["DINESH".to_string()])); |
| |
| // Check that they was also removed from cache |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| (Status::UserNotFound, None) |
| ); |
| assert_eq!( |
| get_id_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| (Status::UserNotFound, None) |
| ); |
| |
| Result::<(), fidl::Error>::Ok(()) |
| }; |
| let (ap_result, client_result, tm_result) = join3( |
| auth_provider_supplier.run(), |
| client_fut, |
| create_and_serve_token_manager(tm_stream, auth_provider_supplier), |
| ) |
| .await; |
| assert!(ap_result.is_ok()); |
| assert!(client_result.is_ok()); |
| assert!(tm_result.is_ok()); |
| } |
| |
| /// Check that state is preserved when a TokenManager is recreated with the same file db. |
| #[fasync::run_until_stalled(test)] |
| async fn file_db() { |
| let tmp_dir = TempDir::new().expect("Could not create temp dir"); |
| let db_path = tmp_dir.path().join("tokens.json"); |
| |
| // Part 1: Create some state in a token manager |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move { |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("pied-piper", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "RICHARD")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| |
| let (tm_proxy, tm_stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let client_fut = async move { |
| assert_eq!(authorize_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, Status::Ok); |
| |
| Result::<(), fidl::Error>::Ok(()) |
| }; |
| let (ap_result, client_result, tm_result) = join3( |
| auth_provider_supplier.run(), |
| client_fut, |
| create_and_serve_token_manager_with_db_path( |
| &db_path, |
| tm_stream, |
| auth_provider_supplier, |
| ), |
| ) |
| .await; |
| assert!(ap_result.is_ok()); |
| assert!(client_result.is_ok()); |
| assert!(tm_result.is_ok()); |
| |
| // Part 2: Create a new token manager and check that the state survived |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move { |
| expect_get_access_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| |
| let (tm_proxy, tm_stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let client_fut = async move { |
| // Check that the profile list survived |
| let profile_ids = |
| tm_proxy.list_profile_ids(&mut create_app_config("pied-piper")).await?; |
| assert_eq!(profile_ids, (Status::Ok, vec!["RICHARD".to_string()])); |
| |
| // Check that the credential survived, and that it triggers a call to the auth provider |
| // since the cache did not survive |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| (Status::Ok, Some("RICHARD_CREDENTIAL_ACCESS".to_string())) |
| ); |
| |
| Result::<(), fidl::Error>::Ok(()) |
| }; |
| let (ap_result, client_result, tm_result) = join3( |
| auth_provider_supplier.run(), |
| client_fut, |
| create_and_serve_token_manager_with_db_path( |
| &db_path, |
| tm_stream, |
| auth_provider_supplier, |
| ), |
| ) |
| .await; |
| assert!(ap_result.is_ok()); |
| assert!(client_result.is_ok()); |
| assert!(tm_result.is_ok()); |
| } |
| |
| /// Check a number of use-cases for the in-memory TokenManager |
| #[fasync::run_until_stalled(test)] |
| async fn in_memory() { |
| let auth_provider_supplier = FakeAuthProviderSupplier::new(); |
| auth_provider_supplier.add_oauth("hooli", |mut stream| async move { |
| expect_create_refresh_token(stream.try_next().await?.expect("End of stream"), "GAVIN")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("hooli", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "GAVIN")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move { |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD", |
| )?; |
| expect_create_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "DINESH", |
| )?; |
| expect_get_access_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| expect_revoke_refresh_token( |
| stream.try_next().await?.expect("End of stream"), |
| "RICHARD_CREDENTIAL", |
| )?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| auth_provider_supplier.add_oauth_open_id_connect("pied-piper", |mut stream| async move { |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "RICHARD")?; |
| expect_get_user_info(stream.try_next().await?.expect("End of stream"), "DINESH")?; |
| Ok(assert!(stream.try_next().await?.is_none())) |
| }); |
| |
| let (tm_proxy, tm_stream) = create_proxy_and_stream::<TokenManagerMarker>().unwrap(); |
| let client_fut = async move { |
| assert_eq!(authorize_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, Status::Ok); |
| assert_eq!(authorize_simple(&tm_proxy, "hooli", "GAVIN").await?.0, Status::Ok); |
| assert_eq!(authorize_simple(&tm_proxy, "pied-piper", "DINESH").await?.0, Status::Ok); |
| |
| // Check listing |
| assert_eq!( |
| tm_proxy.list_profile_ids(&mut create_app_config("hooli")).await?, |
| (Status::Ok, vec!["GAVIN".to_string()]) |
| ); |
| |
| assert_eq!( |
| tm_proxy.list_profile_ids(&mut create_app_config("pied-piper")).await?, |
| (Status::Ok, vec!["DINESH".to_string(), "RICHARD".to_string()]) |
| ); |
| |
| assert_eq!( |
| tm_proxy.list_profile_ids(&mut create_app_config("myspace")).await?, |
| (Status::Ok, vec![]) |
| ); |
| |
| // This puts an access token in the cache |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?.0, |
| Status::Ok |
| ); |
| |
| // Ask for the same token again, expecting it delivered from the cache |
| assert_eq!( |
| delete_all_tokens_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| Status::Ok |
| ); |
| // Deleting a non-existing token succeeds |
| assert_eq!( |
| delete_all_tokens_simple(&tm_proxy, "myspace", "MR_ROBOT").await?, |
| Status::Ok |
| ); |
| |
| // Richard no longer present in list |
| let profile_ids = |
| tm_proxy.list_profile_ids(&mut create_app_config("pied-piper")).await?; |
| assert_eq!(profile_ids, (Status::Ok, vec!["DINESH".to_string()])); |
| |
| // Check that it was also removed from cache |
| assert_eq!( |
| get_access_token_simple(&tm_proxy, "pied-piper", "RICHARD").await?, |
| (Status::UserNotFound, None) |
| ); |
| |
| Result::<(), fidl::Error>::Ok(()) |
| }; |
| let (ap_result, client_result, tm_result) = join3( |
| auth_provider_supplier.run(), |
| client_fut, |
| create_and_serve_in_memory_token_manager(tm_stream, auth_provider_supplier), |
| ) |
| .await; |
| assert!(ap_result.is_ok()); |
| assert!(client_result.is_ok()); |
| assert!(tm_result.is_ok()); |
| } |
| } |