// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "remoting/ios/facade/remoting_oauth_authentication.h"
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#import <MaterialComponents/MaterialSnackbar.h>
#import "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "net/url_request/url_request_context_getter.h"
#include "remoting/base/oauth_token_getter.h"
#include "remoting/base/oauth_token_getter_impl.h"
#import "remoting/ios/facade/ios_client_runtime_delegate.h"
#import "remoting/ios/facade/remoting_service.h"
#import "remoting/ios/persistence/remoting_keychain.h"
#import "remoting/ios/persistence/remoting_preferences.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
static const char kOauthRedirectUrl[] =
"https://chromoting-oauth.talkgadget."
"google.com/talkgadget/oauth/chrome-remote-desktop/dev";
// We currently don't support multi-account sign in for OAuth authentication, so
// we store the current refresh token for an unspecified account. If we later
// decide to support multi-account sign in, we may use the user email as the
// account name when storing the refresh token and store the current user email
// in UserDefaults.
static const auto kRefreshTokenAccount =
remoting::Keychain::kUnspecifiedAccount;
std::unique_ptr<remoting::OAuthTokenGetter>
CreateOAuthTokenGetterWithAuthorizationCode(
const std::string& auth_code,
const remoting::OAuthTokenGetter::CredentialsUpdatedCallback&
on_credentials_update) {
std::unique_ptr<remoting::OAuthTokenGetter::OAuthIntermediateCredentials>
oauth_credentials(
new remoting::OAuthTokenGetter::OAuthIntermediateCredentials(
auth_code, /*is_service_account=*/false));
oauth_credentials->oauth_redirect_uri = kOauthRedirectUrl;
std::unique_ptr<remoting::OAuthTokenGetter> oauth_tokenGetter(
new remoting::OAuthTokenGetterImpl(
std::move(oauth_credentials), on_credentials_update,
RemotingService.instance.runtime->url_loader_factory(),
/*auto_refresh=*/true));
return oauth_tokenGetter;
}
std::unique_ptr<remoting::OAuthTokenGetter> CreateOAuthTokenWithRefreshToken(
const std::string& refresh_token,
const std::string& email) {
std::unique_ptr<remoting::OAuthTokenGetter::OAuthAuthorizationCredentials>
oauth_credentials(
new remoting::OAuthTokenGetter::OAuthAuthorizationCredentials(
email, refresh_token, /*is_service_account=*/false));
std::unique_ptr<remoting::OAuthTokenGetter> oauth_tokenGetter(
new remoting::OAuthTokenGetterImpl(
std::move(oauth_credentials),
RemotingService.instance.runtime->url_loader_factory(),
/*auto_refresh=*/true));
return oauth_tokenGetter;
}
RemotingAuthenticationStatus oauthStatusToRemotingAuthenticationStatus(
remoting::OAuthTokenGetter::Status status) {
switch (status) {
case remoting::OAuthTokenGetter::Status::AUTH_ERROR:
return RemotingAuthenticationStatusAuthError;
case remoting::OAuthTokenGetter::Status::NETWORK_ERROR:
return RemotingAuthenticationStatusNetworkError;
case remoting::OAuthTokenGetter::Status::SUCCESS:
return RemotingAuthenticationStatusSuccess;
}
}
@interface RemotingOAuthAuthentication () {
std::unique_ptr<remoting::OAuthTokenGetter> _tokenGetter;
BOOL _firstLoadUserAttempt;
}
@end
@implementation RemotingOAuthAuthentication
@synthesize user = _user;
@synthesize delegate = _delegate;
- (instancetype)init {
self = [super init];
if (self) {
_user = nil;
_firstLoadUserAttempt = YES;
}
return self;
}
#pragma mark - Property Overrides
- (UserInfo*)user {
if (_firstLoadUserAttempt && _user == nil) {
_firstLoadUserAttempt = NO;
[self setUser:[self loadUserInfo]];
}
return _user;
}
- (void)setUser:(UserInfo*)user {
_user = user;
[self storeUserInfo:_user];
[_delegate userDidUpdate:_user];
}
#pragma mark - Class Implementation
- (void)authenticateWithAuthorizationCode:(NSString*)authorizationCode {
__weak RemotingOAuthAuthentication* weakSelf = self;
_tokenGetter = CreateOAuthTokenGetterWithAuthorizationCode(
std::string(base::SysNSStringToUTF8(authorizationCode)),
base::BindRepeating(
^(const std::string& user_email, const std::string& refresh_token) {
VLOG(1) << "New Creds: " << user_email << " " << refresh_token;
UserInfo* user = [[UserInfo alloc] init];
user.userEmail = base::SysUTF8ToNSString(user_email);
user.refreshToken = base::SysUTF8ToNSString(refresh_token);
[weakSelf setUser:user];
}));
// Stimulate the oAuth Token Getter to fetch and access token, this forces it
// to convert the authorization code into a refresh token, and saving the
// refresh token will happen automatically in the above block.
[self callbackWithAccessToken:^(RemotingAuthenticationStatus status,
NSString* user_email,
NSString* access_token) {
if (status == RemotingAuthenticationStatusSuccess) {
VLOG(1) << "Success fetching access token from authorization code.";
} else {
LOG(ERROR) << "Failed to fetch access token from authorization code. ("
<< status << ")";
[MDCSnackbarManager.defaultManager
showMessage:
[MDCSnackbarMessage
messageWithText:@"Authentication Failed. Please try again."]];
}
}];
}
#pragma mark - Private
// Provide the |refreshToken| and |email| to authenticate a user as a returning
// user of the application.
- (void)authenticateWithRefreshToken:(NSString*)refreshToken
email:(NSString*)email {
_tokenGetter = CreateOAuthTokenWithRefreshToken(
std::string(base::SysNSStringToUTF8(refreshToken)),
base::SysNSStringToUTF8(email));
}
- (void)callbackWithAccessToken:(AccessTokenCallback)onAccessToken {
// Be careful here since a failure to reset onAccessToken will end up with
// retain cycle and memory leakage.
if (_tokenGetter) {
_tokenGetter->CallWithToken(base::BindOnce(
^(remoting::OAuthTokenGetter::Status status,
const std::string& user_email, const std::string& access_token) {
onAccessToken(oauthStatusToRemotingAuthenticationStatus(status),
base::SysUTF8ToNSString(user_email),
base::SysUTF8ToNSString(access_token));
}));
}
}
- (void)logout {
[self storeUserInfo:nil];
[self setUser:nil];
}
- (void)invalidateCache {
_tokenGetter->InvalidateCache();
}
#pragma mark - Persistence
- (void)storeUserInfo:(UserInfo*)user {
if (user) {
[RemotingPreferences instance].activeUserKey = user.userEmail;
std::string refreshToken = base::SysNSStringToUTF8(user.refreshToken);
remoting::RemotingKeychain::GetInstance()->SetData(
remoting::Keychain::Key::REFRESH_TOKEN, kRefreshTokenAccount,
refreshToken);
} else {
[RemotingPreferences instance].activeUserKey = nil;
remoting::RemotingKeychain::GetInstance()->RemoveData(
remoting::Keychain::Key::REFRESH_TOKEN, kRefreshTokenAccount);
}
}
- (UserInfo*)loadUserInfo {
UserInfo* user = [[UserInfo alloc] init];
user.userEmail = [RemotingPreferences instance].activeUserKey;
std::string refreshTokenString =
remoting::RemotingKeychain::GetInstance()->GetData(
remoting::Keychain::Key::REFRESH_TOKEN, kRefreshTokenAccount);
user.refreshToken = base::SysUTF8ToNSString(refreshTokenString);
if (!user || ![user isAuthenticated]) {
user = nil;
} else {
[self authenticateWithRefreshToken:user.refreshToken email:user.userEmail];
}
return user;
}
@end