blob: 6220d05911027e73cdcb71cabad286f6e9889e14 [file] [log] [blame]
// 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());
}
}