| // 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 crate::constants::{ |
| FUCHSIA_CLIENT_ID, OAUTH_AUTHORIZE_URI, OAUTH_DEFAULT_SCOPES, REDIRECT_URI, |
| }; |
| use crate::error::{AuthProviderError, ResultExt}; |
| use crate::firebase::{build_firebase_token_request, parse_firebase_token_response}; |
| use crate::http::HttpClient; |
| use crate::oauth::{ |
| build_request_with_auth_code, build_request_with_refresh_token, build_revocation_request, |
| parse_auth_code_from_redirect, parse_response_with_refresh_token, |
| parse_response_without_refresh_token, parse_revocation_response, AccessToken, AuthCode, |
| RefreshToken, |
| }; |
| use crate::openid::{ |
| build_id_token_request, build_user_info_request, parse_id_token_response, |
| parse_user_info_response, IdToken, |
| }; |
| use crate::web::StandaloneWebFrame; |
| use failure::Error; |
| use fidl; |
| use fidl::encoding::OutOfLine; |
| use fidl::endpoints::ClientEnd; |
| use fidl_fuchsia_auth::{ |
| AssertionJwtParams, AttestationJwtParams, AttestationSignerMarker, AuthChallenge, |
| AuthProviderGetAppAccessTokenFromAssertionJwtResponder, AuthProviderGetAppAccessTokenResponder, |
| AuthProviderGetAppFirebaseTokenResponder, AuthProviderGetAppIdTokenResponder, |
| AuthProviderGetPersistentCredentialFromAttestationJwtResponder, |
| AuthProviderGetPersistentCredentialResponder, AuthProviderRequest, AuthProviderRequestStream, |
| AuthProviderRevokeAppOrPersistentCredentialResponder, AuthProviderStatus, AuthToken, |
| AuthenticationUiContextMarker, FirebaseToken, TokenType, UserProfileInfo, |
| }; |
| use fuchsia_scenic::ViewTokenPair; |
| use futures::prelude::*; |
| use log::{info, warn}; |
| use url::Url; |
| |
| type AuthProviderResult<T> = Result<T, AuthProviderError>; |
| |
| /// Trait for structs capable of creating new Web frames. |
| pub trait WebFrameSupplier { |
| /// The concrete `StandaloneWebFrame` type the supplier produces. |
| type Frame: StandaloneWebFrame; |
| /// Creates a new `StandaloneWebFrame`. This method guarantees that the |
| /// new frame is in its own web context. |
| /// Although implementation of this method does not require state, `self` |
| /// is added here to allow injection of mocks with canned responses. |
| fn new_standalone_frame(&self) -> Result<Self::Frame, Error>; |
| } |
| |
| /// An implementation of the `AuthProvider` FIDL protocol that communicates |
| /// with the Google identity system to perform authentication for and issue |
| /// tokens for Google accounts. |
| pub struct GoogleAuthProvider<W, H> |
| where |
| W: WebFrameSupplier, |
| H: HttpClient, |
| { |
| /// A supplier used to generate web frames on demand. |
| web_frame_supplier: W, |
| /// A client used for making HTTP requests. |
| http_client: H, |
| } |
| |
| impl<W, H> GoogleAuthProvider<W, H> |
| where |
| W: WebFrameSupplier, |
| H: HttpClient, |
| { |
| /// Create a new GoogleAuthProvider. |
| pub fn new(web_frame_supplier: W, http_client: H) -> Self { |
| GoogleAuthProvider { web_frame_supplier, http_client } |
| } |
| |
| /// Handle requests passed to the supplied stream. |
| pub async fn handle_requests_from_stream( |
| &self, |
| mut stream: AuthProviderRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(request) = stream.try_next().await? { |
| self.handle_request(request).await; |
| } |
| Ok(()) |
| } |
| |
| /// Handle a single `AuthProviderRequest`. |
| async fn handle_request(&self, req: AuthProviderRequest) { |
| match req { |
| AuthProviderRequest::GetPersistentCredential { |
| auth_ui_context, |
| user_profile_id, |
| responder, |
| } => responder.send_result( |
| self.get_persistent_credential(auth_ui_context, user_profile_id).await, |
| ), |
| AuthProviderRequest::GetAppAccessToken { credential, client_id, scopes, responder } => { |
| responder |
| .send_result(self.get_app_access_token(credential, client_id, scopes).await) |
| } |
| AuthProviderRequest::GetAppIdToken { credential, audience, responder } => { |
| responder.send_result(self.get_app_id_token(credential, audience).await) |
| } |
| AuthProviderRequest::GetAppFirebaseToken { id_token, firebase_api_key, responder } => { |
| responder.send_result(self.get_app_firebase_token(id_token, firebase_api_key).await) |
| } |
| AuthProviderRequest::RevokeAppOrPersistentCredential { credential, responder } => { |
| responder.send_result(self.revoke_app_or_persistent_credential(credential).await) |
| } |
| AuthProviderRequest::GetPersistentCredentialFromAttestationJwt { |
| attestation_signer, |
| jwt_params, |
| auth_ui_context, |
| user_profile_id, |
| responder, |
| } => responder.send_result( |
| self.get_persistent_credential_from_attestation_jwt( |
| attestation_signer, |
| jwt_params, |
| auth_ui_context, |
| user_profile_id, |
| ) |
| .await, |
| ), |
| AuthProviderRequest::GetAppAccessTokenFromAssertionJwt { |
| attestation_signer, |
| jwt_params, |
| credential, |
| scopes, |
| responder, |
| } => responder.send_result( |
| self.get_app_access_token_from_assertion_jwt( |
| attestation_signer, |
| jwt_params, |
| credential, |
| scopes, |
| ) |
| .await, |
| ), |
| } |
| } |
| |
| /// Implementation of `GetPersistentCredential` method for the |
| /// `AuthProvider` interface. |
| async fn get_persistent_credential( |
| &self, |
| auth_ui_context: Option<ClientEnd<AuthenticationUiContextMarker>>, |
| user_profile_id: Option<String>, |
| ) -> AuthProviderResult<(String, UserProfileInfo)> { |
| match auth_ui_context { |
| Some(ui_context) => { |
| let auth_code = self.get_auth_code(ui_context, user_profile_id).await?; |
| info!("Received auth code of length: {:?}", &auth_code.0.len()); |
| let (refresh_token, access_token) = self.exchange_auth_code(auth_code).await?; |
| info!("Received refresh token of length {:?}", &refresh_token.0.len()); |
| let user_profile_info = self.get_user_profile_info(access_token).await?; |
| Ok((refresh_token.0, user_profile_info)) |
| } |
| None => Err(AuthProviderError::new(AuthProviderStatus::BadRequest)), |
| } |
| } |
| |
| /// Implementation of `GetAppAccessToken` method for the `AuthProvider` |
| /// interface. |
| async fn get_app_access_token( |
| &self, |
| credential: String, |
| mut client_id: Option<String>, |
| scopes: Vec<String>, |
| ) -> AuthProviderResult<AuthToken> { |
| // TODO(fxb/37188): Disallow use of Some(empty string) |
| if client_id.as_ref().map(String::is_empty) == Some(true) { |
| client_id.take(); |
| } |
| if credential.is_empty() { |
| return Err(AuthProviderError::new(AuthProviderStatus::BadRequest)); |
| } |
| |
| let request = |
| build_request_with_refresh_token(RefreshToken(credential), scopes, client_id)?; |
| let (response_body, status) = self.http_client.request(request).await?; |
| let (access_token, expires_in) = |
| parse_response_without_refresh_token(response_body, status)?; |
| Ok(AuthToken { token_type: TokenType::AccessToken, token: access_token.0, expires_in }) |
| } |
| |
| /// Implementation of `GetAppIdToken` method for the `AuthProvider` |
| /// interface. |
| async fn get_app_id_token( |
| &self, |
| credential: String, |
| mut audience: Option<String>, |
| ) -> AuthProviderResult<AuthToken> { |
| // TODO(fxb/37188): This allows clients to pass Some(empty string) instead |
| // of None to use the default client. Once existing clients that do this |
| // are changed, this should be explicitly disallowed by a sanity check. |
| if audience.as_ref().map(String::is_empty) == Some(true) { |
| audience.take(); |
| } |
| |
| if credential.is_empty() { |
| return Err(AuthProviderError::new(AuthProviderStatus::BadRequest)); |
| } |
| |
| let request = build_id_token_request(RefreshToken(credential), audience)?; |
| let (response_body, status) = self.http_client.request(request).await?; |
| let (id_token, expires_in) = parse_id_token_response(response_body, status)?; |
| Ok(AuthToken { token_type: TokenType::IdToken, token: id_token.0, expires_in }) |
| } |
| |
| /// Implementation of `GetAppFirebaseToken` method for the `AuthProvider` |
| /// interface. |
| async fn get_app_firebase_token( |
| &self, |
| id_token: String, |
| firebase_api_key: String, |
| ) -> AuthProviderResult<FirebaseToken> { |
| if id_token.is_empty() || firebase_api_key.is_empty() { |
| return Err(AuthProviderError::new(AuthProviderStatus::BadRequest)); |
| } |
| |
| let request = build_firebase_token_request(IdToken(id_token), firebase_api_key)?; |
| let (response_body, status) = self.http_client.request(request).await?; |
| parse_firebase_token_response(response_body, status) |
| } |
| |
| /// Implementation of `RevokeAppOrPersistentCredential` method for the |
| /// `AuthProvider` interface. |
| async fn revoke_app_or_persistent_credential( |
| &self, |
| credential: String, |
| ) -> AuthProviderResult<()> { |
| if credential.is_empty() { |
| return Err(AuthProviderError::new(AuthProviderStatus::BadRequest)); |
| } |
| |
| let request = build_revocation_request(credential)?; |
| let (response_body, status) = self.http_client.request(request).await?; |
| parse_revocation_response(response_body, status) |
| } |
| |
| /// Implementation of `GetPersistentCredentialFromAttestationJWT` method |
| /// for the `AuthProvider` interface. |
| async fn get_persistent_credential_from_attestation_jwt( |
| &self, |
| _attestation_signer: ClientEnd<AttestationSignerMarker>, |
| _jwt_params: AttestationJwtParams, |
| _auth_ui_context: Option<ClientEnd<AuthenticationUiContextMarker>>, |
| _user_profile_id: Option<String>, |
| ) -> AuthProviderResult<(String, AuthToken, AuthChallenge, UserProfileInfo)> { |
| // Remote attestation flow is not supported for traditional OAuth. |
| Err(AuthProviderError::new(AuthProviderStatus::InternalError)) |
| } |
| |
| /// Implementation of `GetAppAccessTokenFromAssertionJWT` method for the |
| /// `AuthProvider` interface. |
| async fn get_app_access_token_from_assertion_jwt( |
| &self, |
| _attestation_signer: ClientEnd<AttestationSignerMarker>, |
| _jwt_params: AssertionJwtParams, |
| _credential: String, |
| _scopes: Vec<String>, |
| ) -> AuthProviderResult<(String, AuthToken, AuthChallenge)> { |
| // Remote attestation flow is not supported for traditional OAuth. |
| Err(AuthProviderError::new(AuthProviderStatus::InternalError)) |
| } |
| |
| /// Direct user through Google OAuth authorization flow and obtain an auth |
| /// code. |
| async fn get_auth_code( |
| &self, |
| auth_ui_context: ClientEnd<AuthenticationUiContextMarker>, |
| user_profile_id: Option<String>, |
| ) -> AuthProviderResult<AuthCode> { |
| let auth_ui_context = |
| auth_ui_context.into_proxy().auth_provider_status(AuthProviderStatus::UnknownError)?; |
| let mut web_frame = self |
| .web_frame_supplier |
| .new_standalone_frame() |
| .auth_provider_status(AuthProviderStatus::UnknownError)?; |
| let ViewTokenPair { view_token, mut view_holder_token } = |
| ViewTokenPair::new().auth_provider_status(AuthProviderStatus::UnknownError)?; |
| let authorize_url = Self::authorize_url(user_profile_id)?; |
| |
| web_frame.display_url(view_token, authorize_url).await?; |
| auth_ui_context |
| .start_overlay(&mut view_holder_token) |
| .auth_provider_status(AuthProviderStatus::UnknownError)?; |
| |
| let redirect_url = web_frame.wait_for_redirect(REDIRECT_URI.clone()).await?; |
| |
| if let Err(err) = auth_ui_context.stop_overlay() { |
| warn!("Error while attempting to stop UI overlay: {:?}", err); |
| } |
| |
| parse_auth_code_from_redirect(redirect_url) |
| } |
| |
| fn authorize_url(user_profile_id: Option<String>) -> AuthProviderResult<Url> { |
| let mut params = vec![ |
| ("scope", OAUTH_DEFAULT_SCOPES.as_str()), |
| ("glif", "false"), // TODO(satsukiu): add a command line parameter to set this |
| ("response_type", "code"), |
| ("redirect_uri", REDIRECT_URI.as_str()), |
| ("client_id", FUCHSIA_CLIENT_ID), |
| ]; // TODO(satsukiu): add 'state' parameter here and verify it in the redirect. |
| if let Some(user) = user_profile_id.as_ref() { |
| params.push(("login_hint", user)); |
| } |
| |
| Url::parse_with_params(OAUTH_AUTHORIZE_URI.as_str(), ¶ms) |
| .auth_provider_status(AuthProviderStatus::InternalError) |
| } |
| |
| /// Trades an OAuth auth code for a refresh token and access token. |
| async fn exchange_auth_code( |
| &self, |
| auth_code: AuthCode, |
| ) -> AuthProviderResult<(RefreshToken, AccessToken)> { |
| let request = build_request_with_auth_code(auth_code)?; |
| let (response_body, status_code) = self.http_client.request(request).await?; |
| parse_response_with_refresh_token(response_body, status_code) |
| } |
| |
| /// Use an access token to retrieve profile information. |
| async fn get_user_profile_info( |
| &self, |
| access_token: AccessToken, |
| ) -> AuthProviderResult<UserProfileInfo> { |
| let request = build_user_info_request(access_token)?; |
| let (response_body, status_code) = self.http_client.request(request).await?; |
| parse_user_info_response(response_body, status_code) |
| } |
| } |
| |
| /// Trait containing logic for sending responses. Enables `Result` returntype |
| /// for API implementations. |
| trait Responder: Sized { |
| type Data; |
| /// Consume the responder to send a result. Logs any errors encountered |
| /// while sending response. |
| fn send_result(self, result: AuthProviderResult<Self::Data>) { |
| let send_result = match result { |
| Ok(val) => self.send_raw(AuthProviderStatus::Ok, Some(val)), |
| Err(err) => self.send_raw(err.status, None), |
| }; |
| if let Err(err) = send_result { |
| warn!("Error sending response to {}: {:?}", Self::METHOD_NAME, err); |
| } |
| } |
| |
| /// Send response without handling failures. |
| fn send_raw( |
| self, |
| status: AuthProviderStatus, |
| data: Option<Self::Data>, |
| ) -> Result<(), fidl::Error>; |
| |
| const METHOD_NAME: &'static str; |
| } |
| |
| impl Responder for AuthProviderGetPersistentCredentialResponder { |
| type Data = (String, UserProfileInfo); |
| const METHOD_NAME: &'static str = "GetPersistentCredential"; |
| |
| fn send_raw( |
| self, |
| status: AuthProviderStatus, |
| data: Option<(String, UserProfileInfo)>, |
| ) -> Result<(), fidl::Error> { |
| match data { |
| None => self.send(status, None, None), |
| Some((refresh_token, mut user_profile_info)) => self.send( |
| status, |
| Some(refresh_token.as_str()), |
| Some(OutOfLine(&mut user_profile_info)), |
| ), |
| } |
| } |
| } |
| |
| impl Responder for AuthProviderGetAppAccessTokenResponder { |
| type Data = AuthToken; |
| const METHOD_NAME: &'static str = "GetAppAccessToken"; |
| |
| fn send_raw( |
| self, |
| status: AuthProviderStatus, |
| mut data: Option<AuthToken>, |
| ) -> Result<(), fidl::Error> { |
| self.send(status, data.as_mut().map(OutOfLine)) |
| } |
| } |
| |
| impl Responder for AuthProviderGetAppIdTokenResponder { |
| type Data = AuthToken; |
| const METHOD_NAME: &'static str = "GetAppIdToken"; |
| |
| fn send_raw( |
| self, |
| status: AuthProviderStatus, |
| mut data: Option<AuthToken>, |
| ) -> Result<(), fidl::Error> { |
| self.send(status, data.as_mut().map(OutOfLine)) |
| } |
| } |
| |
| impl Responder for AuthProviderGetAppFirebaseTokenResponder { |
| type Data = FirebaseToken; |
| const METHOD_NAME: &'static str = "GetAppFirebaseToken"; |
| |
| fn send_raw( |
| self, |
| status: AuthProviderStatus, |
| mut data: Option<FirebaseToken>, |
| ) -> Result<(), fidl::Error> { |
| self.send(status, data.as_mut().map(OutOfLine)) |
| } |
| } |
| |
| impl Responder for AuthProviderRevokeAppOrPersistentCredentialResponder { |
| type Data = (); |
| const METHOD_NAME: &'static str = "RevokeAppOrPersistentCredential"; |
| |
| fn send_raw(self, status: AuthProviderStatus, _data: Option<()>) -> Result<(), fidl::Error> { |
| self.send(status) |
| } |
| } |
| |
| impl Responder for AuthProviderGetPersistentCredentialFromAttestationJwtResponder { |
| type Data = (String, AuthToken, AuthChallenge, UserProfileInfo); |
| const METHOD_NAME: &'static str = "GetPersistentCredentialFromAttestationJwt"; |
| |
| fn send_raw( |
| self, |
| status: AuthProviderStatus, |
| data: Option<(String, AuthToken, AuthChallenge, UserProfileInfo)>, |
| ) -> Result<(), fidl::Error> { |
| match data { |
| None => self.send(status, None, None, None, None), |
| Some((credential, mut auth_token, mut auth_challenge, mut user_profile_info)) => self |
| .send( |
| status, |
| Some(credential.as_str()), |
| Some(OutOfLine(&mut auth_token)), |
| Some(OutOfLine(&mut auth_challenge)), |
| Some(OutOfLine(&mut user_profile_info)), |
| ), |
| } |
| } |
| } |
| |
| impl Responder for AuthProviderGetAppAccessTokenFromAssertionJwtResponder { |
| type Data = (String, AuthToken, AuthChallenge); |
| const METHOD_NAME: &'static str = "GetAppAccessTokenFromAssertionJwt"; |
| |
| fn send_raw( |
| self, |
| status: AuthProviderStatus, |
| data: Option<(String, AuthToken, AuthChallenge)>, |
| ) -> Result<(), fidl::Error> { |
| match data { |
| None => self.send(status, None, None, None), |
| Some((credential, mut auth_token, mut auth_challenge)) => self.send( |
| status, |
| Some(credential.as_str()), |
| Some(OutOfLine(&mut auth_token)), |
| Some(OutOfLine(&mut auth_challenge)), |
| ), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| |
| use super::*; |
| use crate::http::HttpRequest; |
| use fidl::endpoints::{create_proxy_and_stream, create_request_stream}; |
| use fidl_fuchsia_auth::{AuthProviderMarker, AuthProviderProxy}; |
| use fidl_fuchsia_ui_views::ViewToken; |
| use fuchsia_async as fasync; |
| use futures::future::{ready, FutureObj}; |
| use hyper::StatusCode; |
| use std::collections::VecDeque; |
| use std::sync::Mutex; |
| |
| /// Clones an AuthProviderResult. This is provided instead of a Clone |
| /// implementation due to orphan rules. |
| fn clone_result<T: Clone>(result: &AuthProviderResult<T>) -> AuthProviderResult<T> { |
| match result { |
| Ok(res) => Ok(res.clone()), |
| // error cause cannot be cloned so don't replicate it. |
| Err(err) => Err(AuthProviderError::new(err.status)), |
| } |
| } |
| |
| /// A mock implementation of StandaloneWebFrame that always returns the responses |
| /// specified during its creation. |
| struct TestWebFrame { |
| display_url_response: AuthProviderResult<()>, |
| wait_for_redirect_response: AuthProviderResult<Url>, |
| } |
| |
| impl TestWebFrame { |
| fn new( |
| display_url_response: AuthProviderResult<()>, |
| wait_for_redirect_response: AuthProviderResult<Url>, |
| ) -> Self { |
| TestWebFrame { display_url_response, wait_for_redirect_response } |
| } |
| } |
| |
| impl StandaloneWebFrame for TestWebFrame { |
| fn display_url<'a>( |
| &'a mut self, |
| _view_token: ViewToken, |
| _url: Url, |
| ) -> FutureObj<'a, AuthProviderResult<()>> { |
| FutureObj::new(Box::new(ready(clone_result(&self.display_url_response)))) |
| } |
| |
| fn wait_for_redirect<'a>( |
| &'a mut self, |
| _redirect_target: Url, |
| ) -> FutureObj<'a, AuthProviderResult<Url>> { |
| FutureObj::new(Box::new(ready(clone_result(&self.wait_for_redirect_response)))) |
| } |
| } |
| |
| /// A mock implementation of `WebFrameSupplier` that supplies `TestWebFrames`. |
| /// The supplied `TestWebFrames` will return the responses provided during |
| /// creation of the `TestWebFrameSupplier`. |
| struct TestWebFrameSupplier { |
| display_url_response: AuthProviderResult<()>, |
| wait_for_redirect_response: AuthProviderResult<Url>, |
| } |
| |
| impl TestWebFrameSupplier { |
| fn new( |
| display_url_response: AuthProviderResult<()>, |
| wait_for_redirect_response: AuthProviderResult<Url>, |
| ) -> Self { |
| TestWebFrameSupplier { display_url_response, wait_for_redirect_response } |
| } |
| } |
| |
| impl WebFrameSupplier for TestWebFrameSupplier { |
| type Frame = TestWebFrame; |
| fn new_standalone_frame(&self) -> Result<TestWebFrame, Error> { |
| Ok(TestWebFrame::new( |
| clone_result(&self.display_url_response), |
| clone_result(&self.wait_for_redirect_response), |
| )) |
| } |
| } |
| |
| /// A mock implementation of `HttpClient` |
| struct TestHttpClient { |
| /// Response returned on `request`. |
| responses: Mutex<VecDeque<AuthProviderResult<(Option<String>, StatusCode)>>>, |
| } |
| |
| impl TestHttpClient { |
| fn with_responses( |
| responses: Vec<AuthProviderResult<(Option<String>, StatusCode)>>, |
| ) -> Self { |
| TestHttpClient { responses: Mutex::new(VecDeque::from(responses)) } |
| } |
| |
| /// Create a new test client that returns the given response on `request`. |
| fn with_response(body: Option<&str>, status: StatusCode) -> Self { |
| Self::with_responses(vec![Ok((body.map(str::to_string), status))]) |
| } |
| |
| /// Create a new test client that returns the given response on `request`. |
| fn with_error(status: AuthProviderStatus) -> Self { |
| Self::with_responses(vec![Err(AuthProviderError::new(status))]) |
| } |
| } |
| |
| impl HttpClient for TestHttpClient { |
| fn request<'a>( |
| &'a self, |
| _http_request: HttpRequest, |
| ) -> FutureObj<'a, AuthProviderResult<(Option<String>, StatusCode)>> { |
| let response = self |
| .responses |
| .lock() |
| .unwrap() |
| .pop_front() |
| .expect("Mock received more requests than the supplied requests!"); |
| FutureObj::new(Box::new(ready(response))) |
| } |
| } |
| |
| /// Creates an auth provider. If frame_supplier is not given, the auth |
| /// provider is instantiated with a `TestWebFrameProvider` that supplies |
| /// `StandaloneWebFrame`s that return an `UnsupportedProvider` error. |
| /// Similarly, if http_client is not given, the auth provider is |
| /// instantiated with a `TestHttpClient` that returns an |
| /// `UnsupportedProvider` error. |
| fn get_auth_provider_proxy( |
| frame_supplier: Option<TestWebFrameSupplier>, |
| http_client: Option<TestHttpClient>, |
| ) -> AuthProviderProxy { |
| let (provider_proxy, provider_request_stream) = |
| create_proxy_and_stream::<AuthProviderMarker>() |
| .expect("Failed to create proxy and stream"); |
| |
| let frame_supplier = frame_supplier.unwrap_or(TestWebFrameSupplier::new( |
| Err(AuthProviderError::new(AuthProviderStatus::UnsupportedProvider)), |
| Err(AuthProviderError::new(AuthProviderStatus::UnsupportedProvider)), |
| )); |
| let http = http_client |
| .unwrap_or(TestHttpClient::with_error(AuthProviderStatus::UnsupportedProvider)); |
| |
| let auth_provider = GoogleAuthProvider::new(frame_supplier, http); |
| fasync::spawn(async move { |
| auth_provider |
| .handle_requests_from_stream(provider_request_stream) |
| .await |
| .expect("Error handling AuthProvider channel"); |
| }); |
| |
| provider_proxy |
| } |
| |
| fn get_authentication_ui_context() -> ClientEnd<AuthenticationUiContextMarker> { |
| let (client, mut stream) = create_request_stream::<AuthenticationUiContextMarker>() |
| .expect("Failed to create authentication UI context stream"); |
| fasync::spawn(async move { while let Some(_) = stream.try_next().await.unwrap() {} }); |
| client |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_persistent_credential_success() -> Result<(), Error> { |
| let mock_frame_supplier = TestWebFrameSupplier::new( |
| Ok(()), |
| Ok(Url::parse_with_params(REDIRECT_URI.as_str(), vec![("code", "test-auth-code")]) |
| .unwrap()), |
| ); |
| let refresh_token_response = |
| "{\"refresh_token\": \"test-refresh-token\", \"access_token\": \"test-access-token\"}"; |
| let user_info_response = |
| "{\"sub\": \"test-id\", \"name\": \"Bill\", \"profile\": \"profile-url\", \ |
| \"picture\": \"picture-url\"}"; |
| |
| let mock_http = TestHttpClient::with_responses(vec![ |
| Ok((Some(refresh_token_response.to_string()), StatusCode::OK)), |
| Ok((Some(user_info_response.to_string()), StatusCode::OK)), |
| ]); |
| let auth_provider = get_auth_provider_proxy(Some(mock_frame_supplier), Some(mock_http)); |
| |
| let ui_context = get_authentication_ui_context(); |
| let (status, refresh_token, user_info) = |
| auth_provider.get_persistent_credential(Some(ui_context), None).await?; |
| |
| assert_eq!(status, AuthProviderStatus::Ok); |
| assert_eq!(refresh_token.unwrap(), "test-refresh-token".to_string()); |
| assert_eq!( |
| user_info.unwrap(), |
| Box::new(UserProfileInfo { |
| id: "test-id".to_string(), |
| display_name: Some("Bill".to_string()), |
| url: Some("profile-url".to_string()), |
| image_url: Some("picture-url".to_string()), |
| }) |
| ); |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_persistent_credential_exchange_web_errors() -> Result<(), Error> { |
| // User denies access |
| let frame_supplier = TestWebFrameSupplier::new( |
| Ok(()), |
| Ok(Url::parse_with_params(REDIRECT_URI.as_str(), vec![("error", "access_denied")]) |
| .unwrap()), |
| ); |
| assert_persistent_credential_web_err(frame_supplier, AuthProviderStatus::UserCancelled) |
| .await?; |
| |
| // Web frame error - UI maybe canceled |
| let frame_supplier = TestWebFrameSupplier::new( |
| Ok(()), |
| Err(AuthProviderError::new(AuthProviderStatus::UnknownError)), |
| ); |
| assert_persistent_credential_web_err(frame_supplier, AuthProviderStatus::UnknownError) |
| .await?; |
| |
| // Network error |
| let frame_supplier = TestWebFrameSupplier::new( |
| Err(AuthProviderError::new(AuthProviderStatus::NetworkError)), |
| Err(AuthProviderError::new(AuthProviderStatus::NetworkError)), |
| ); |
| assert_persistent_credential_web_err(frame_supplier, AuthProviderStatus::NetworkError) |
| .await?; |
| Ok(()) |
| } |
| |
| /// Assert GetPersistentCredential returns the given error when using the given |
| /// WebFrameSupplier mock |
| async fn assert_persistent_credential_web_err( |
| mock_frame_supplier: TestWebFrameSupplier, |
| expected_status: AuthProviderStatus, |
| ) -> Result<(), Error> { |
| let auth_provider = get_auth_provider_proxy(Some(mock_frame_supplier), None); |
| |
| let ui_context = get_authentication_ui_context(); |
| let (status, refresh_token, user_info) = |
| auth_provider.get_persistent_credential(Some(ui_context), None).await?; |
| assert_eq!(status, expected_status); |
| assert!(refresh_token.is_none()); |
| assert!(user_info.is_none()); |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_persistent_credential_exchange_auth_code_failures() -> Result<(), Error> { |
| // Error response |
| let http = TestHttpClient::with_response( |
| Some("{\"error\": \"invalid_grant\", \"error_description\": \"ouch\"}"), |
| StatusCode::BAD_REQUEST, |
| ); |
| assert_persistent_credential_http_err(http, AuthProviderStatus::ReauthRequired).await?; |
| |
| // Network error |
| let http = TestHttpClient::with_error(AuthProviderStatus::NetworkError); |
| assert_persistent_credential_http_err(http, AuthProviderStatus::NetworkError).await?; |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_persistent_credential_get_user_info_failures() -> Result<(), Error> { |
| let auth_code_exchange_body = |
| "{\"refresh_token\": \"test-refresh-token\", \"access_token\": \"test-access-token\"}" |
| .to_string(); |
| let auth_code_exchange_response = Ok((Some(auth_code_exchange_body), StatusCode::OK)); |
| |
| // Error response |
| let http = TestHttpClient::with_responses(vec![ |
| clone_result(&auth_code_exchange_response), |
| Ok((None, StatusCode::INTERNAL_SERVER_ERROR)), |
| ]); |
| assert_persistent_credential_http_err(http, AuthProviderStatus::OauthServerError).await?; |
| |
| // Network error |
| let http = TestHttpClient::with_responses(vec![ |
| auth_code_exchange_response, |
| Err(AuthProviderError::new(AuthProviderStatus::NetworkError)), |
| ]); |
| assert_persistent_credential_http_err(http, AuthProviderStatus::NetworkError).await?; |
| Ok(()) |
| } |
| |
| /// Assert GetPersistentCredential returns the given error when the authentication |
| /// flow succeeds and using the given HttpClient mock |
| async fn assert_persistent_credential_http_err( |
| mock_http: TestHttpClient, |
| expected_status: AuthProviderStatus, |
| ) -> Result<(), Error> { |
| let frame_supplier = TestWebFrameSupplier::new( |
| Ok(()), |
| Ok(Url::parse_with_params(REDIRECT_URI.as_str(), vec![("code", "test-auth-code")]) |
| .unwrap()), |
| ); |
| let auth_provider = get_auth_provider_proxy(Some(frame_supplier), Some(mock_http)); |
| |
| let ui_context = get_authentication_ui_context(); |
| let (status, refresh_token, user_info) = |
| auth_provider.get_persistent_credential(Some(ui_context), None).await?; |
| assert_eq!(status, expected_status); |
| assert!(refresh_token.is_none()); |
| assert!(user_info.is_none()); |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_persistent_credential_requires_ui_context() -> Result<(), Error> { |
| let auth_provider = get_auth_provider_proxy(None, None); |
| let result = auth_provider.get_persistent_credential(None, None).await?; |
| assert_eq!(result.0, AuthProviderStatus::BadRequest); |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_app_access_token_success() -> Result<(), Error> { |
| let http_result = "{\"access_token\": \"test-access-token\", \"expires_in\": 3600}"; |
| let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::OK); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let (result_status, result_token) = |
| auth_provider.get_app_access_token("credential", None, &mut vec![].into_iter()).await?; |
| assert_eq!(result_status, AuthProviderStatus::Ok); |
| assert_eq!( |
| result_token.unwrap(), |
| Box::new(AuthToken { |
| token_type: TokenType::AccessToken, |
| token: "test-access-token".to_string(), |
| expires_in: 3600, |
| }) |
| ); |
| |
| // Accept empty string client_id as if it means default - see fxb/37188 |
| let http_result = "{\"access_token\": \"test-access-token\", \"expires_in\": 3600}"; |
| let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::OK); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let (result_status, result_token) = auth_provider |
| .get_app_access_token("credential", Some(""), &mut vec![].into_iter()) |
| .await?; |
| assert_eq!(result_status, AuthProviderStatus::Ok); |
| assert_eq!( |
| result_token.unwrap(), |
| Box::new(AuthToken { |
| token_type: TokenType::AccessToken, |
| token: "test-access-token".to_string(), |
| expires_in: 3600, |
| }) |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_app_access_token_failures() -> Result<(), Error> { |
| // Invalid request |
| let mock_http = TestHttpClient::with_error(AuthProviderStatus::InternalError); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.get_app_access_token("", None, &mut vec![].into_iter()).await?; |
| assert_eq!(result.0, AuthProviderStatus::BadRequest); |
| |
| // Error response |
| let http_result = "{\"error\": \"invalid_scope\", \"error_description\": \"bad scope\"}"; |
| let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::BAD_REQUEST); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider |
| .get_app_access_token("credential", None, &mut vec!["bad-scope"].into_iter()) |
| .await?; |
| assert_eq!(result.0, AuthProviderStatus::OauthServerError); |
| |
| // Network error |
| let mock_http = TestHttpClient::with_error(AuthProviderStatus::NetworkError); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = |
| auth_provider.get_app_access_token("credential", None, &mut vec![].into_iter()).await?; |
| assert_eq!(result.0, AuthProviderStatus::NetworkError); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_app_id_token_success() -> Result<(), Error> { |
| let http_result = "{\"id_token\": \"test-id-token\", \"expires_in\": 3600}"; |
| let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::OK); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| |
| let (status, result_token) = auth_provider.get_app_id_token("credential", None).await?; |
| assert_eq!(status, AuthProviderStatus::Ok); |
| assert_eq!( |
| result_token.unwrap(), |
| Box::new(AuthToken { |
| token_type: TokenType::IdToken, |
| token: "test-id-token".to_string(), |
| expires_in: 3600, |
| }) |
| ); |
| |
| // Accept empty string audience as if it is default audience. See fxb/37188 |
| let http_result = "{\"id_token\": \"test-id-token\", \"expires_in\": 3600}"; |
| let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::OK); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let (status, result_token) = auth_provider.get_app_id_token("cred", Some("")).await?; |
| |
| assert_eq!(status, AuthProviderStatus::Ok); |
| assert_eq!( |
| result_token.unwrap(), |
| Box::new(AuthToken { |
| token_type: TokenType::IdToken, |
| token: "test-id-token".to_string(), |
| expires_in: 3600, |
| }) |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_app_id_token_failures() -> Result<(), Error> { |
| // Invalid request |
| let mock_http = TestHttpClient::with_error(AuthProviderStatus::InternalError); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.get_app_id_token("", None).await?; |
| assert_eq!(result.0, AuthProviderStatus::BadRequest); |
| |
| // Error response |
| let http_result = "{\"error\": \"invalid_client\"}"; |
| let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::BAD_REQUEST); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.get_app_id_token("credential", None).await?; |
| assert_eq!(result.0, AuthProviderStatus::OauthServerError); |
| |
| // Network error |
| let mock_http = TestHttpClient::with_error(AuthProviderStatus::NetworkError); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.get_app_id_token("credential", None).await?; |
| assert_eq!(result.0, AuthProviderStatus::NetworkError); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_app_firebase_token_success() -> Result<(), Error> { |
| let response_body = "{\"idToken\": \"test-firebase-token\", \"localId\": \"test-id\",\ |
| \"email\": \"test@example.com\", \"expiresIn\": \"3600\"}"; |
| let mock_http = TestHttpClient::with_response(Some(response_body), StatusCode::OK); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| |
| let (status, result_token) = |
| auth_provider.get_app_firebase_token("id_token", "api_key").await?; |
| assert_eq!(status, AuthProviderStatus::Ok); |
| assert_eq!( |
| result_token.unwrap(), |
| Box::new(FirebaseToken { |
| id_token: "test-firebase-token".to_string(), |
| email: Some("test@example.com".to_string()), |
| local_id: Some("test-id".to_string()), |
| expires_in: 3600, |
| }) |
| ); |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_get_app_firebase_token_failures() -> Result<(), Error> { |
| // Invalid request |
| let mock_http = TestHttpClient::with_error(AuthProviderStatus::InternalError); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.get_app_firebase_token("", "").await?; |
| assert_eq!(result.0, AuthProviderStatus::BadRequest); |
| |
| // Error response |
| let http_result = "{\"message\": \"invalid api key\"}"; |
| let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::BAD_REQUEST); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.get_app_firebase_token("id_token", "api_key").await?; |
| assert_eq!(result.0, AuthProviderStatus::OauthServerError); |
| |
| // Network error |
| let mock_http = TestHttpClient::with_error(AuthProviderStatus::NetworkError); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.get_app_firebase_token("id_token", "api_key").await?; |
| assert_eq!(result.0, AuthProviderStatus::NetworkError); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_revoke_app_or_persistent_credential_success() -> Result<(), Error> { |
| let mock_http = TestHttpClient::with_response(None, StatusCode::OK); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| let result = auth_provider.revoke_app_or_persistent_credential("credential").await?; |
| assert_eq!(result, AuthProviderStatus::Ok); |
| Ok(()) |
| } |
| |
| #[fasync::run_until_stalled(test)] |
| async fn test_revoke_app_or_persistent_credential_failures() -> Result<(), Error> { |
| // Empty credential |
| let auth_provider = get_auth_provider_proxy(None, None); |
| let result = auth_provider.revoke_app_or_persistent_credential("").await?; |
| assert_eq!(result, AuthProviderStatus::BadRequest); |
| |
| // Error response |
| let http_response = "bad response"; |
| let mock_http = TestHttpClient::with_response(Some(http_response), StatusCode::BAD_REQUEST); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| assert_eq!( |
| auth_provider.revoke_app_or_persistent_credential("credential").await?, |
| AuthProviderStatus::OauthServerError |
| ); |
| |
| // Network error |
| let mock_http = TestHttpClient::with_error(AuthProviderStatus::NetworkError); |
| let auth_provider = get_auth_provider_proxy(None, Some(mock_http)); |
| assert_eq!( |
| auth_provider.revoke_app_or_persistent_credential("credential").await?, |
| AuthProviderStatus::NetworkError |
| ); |
| |
| Ok(()) |
| } |
| } |