diff --git a/WordPress/Classes/Services/AccountService+SocialService.swift b/WordPress/Classes/Services/AccountService+SocialService.swift new file mode 100644 index 000000000000..2a17723f28fa --- /dev/null +++ b/WordPress/Classes/Services/AccountService+SocialService.swift @@ -0,0 +1,34 @@ +import Foundation +import WordPressComKit + +extension AccountService { + + /// Connect an account a social service via an ID token. + /// + /// - Parameters: + /// - service The name of the social service. + /// - token The service's OpenID Connect (JWT) ID token for the user. + /// - success + /// - failure + func connectToSocialService(_ service: SocialServiceName, serviceIDToken token:String, success:@escaping (() -> Void), failure:@escaping ((NSError) -> Void)) { + guard let api = defaultWordPressComAccount()?.wordPressComRestApi, + let remote = AccountServiceRemoteREST(wordPressComRestApi: api) else { + fatalError("Failed to initialize a valid remote via the default WordPress.com account.") + } + remote.connectToSocialService(service, serviceIDToken: token, success: success, failure: failure) + } + + /// Disconnect an account a social service via an ID token. + /// - Parameters: + /// - service The name of the social service. + /// - success + /// - failure + func disconnectFromSocialService(_ service: SocialServiceName, success:@escaping (() -> Void), failure:@escaping ((NSError) -> Void)) { + guard let api = defaultWordPressComAccount()?.wordPressComRestApi, + let remote = AccountServiceRemoteREST(wordPressComRestApi: api) else { + fatalError("Failed to initialize a valid remote via the default WordPress.com account.") + } + remote.disconnectFromSocialService(service, success: success, failure: failure) + } + +} diff --git a/WordPress/Classes/Services/LoginFacade.h b/WordPress/Classes/Services/LoginFacade.h index 97de907d3305..61d0ec6bb3d3 100644 --- a/WordPress/Classes/Services/LoginFacade.h +++ b/WordPress/Classes/Services/LoginFacade.h @@ -2,6 +2,7 @@ @class LoginFields; +@class SocialLogin2FANonceInfo; @protocol WordPressComOAuthClientFacade; @protocol WordPressXMLRPCAPIFacade; @protocol LoginFacadeDelegate; @@ -43,6 +44,9 @@ */ - (void)requestOneTimeCodeWithLoginFields:(LoginFields *)loginFields; + +- (void)loginToWordPressDotComWithGoogleIDToken:(NSString *)googleIDToken; + /** * A delegate with a few methods that indicate various aspects of the login process */ @@ -86,6 +90,14 @@ */ - (void)needsMultifactorCode; +/** + * This is called when the initial login failed because we need a 2fa code for a social login. + * + * @param userID the WPCom userID of the user logging in. + * @param nonceInfo an object containing information about available 2fa nonce options. + */ +- (void)needsMultifactorCodeForUserID:(NSInteger)userID andNonceInfo:(SocialLogin2FANonceInfo *)nonceInfo; + /** * This is called when there's been an error and we want to inform the user. * @@ -113,5 +125,14 @@ */ - (void)finishedLoginWithUsername:(NSString *)username authToken:(NSString *)authToken requiredMultifactorCode:(BOOL)requiredMultifactorCode; + +/** + * Called when finished logging in to a WordPress.com site via a Google token. + * + * @param googleIDToken the token used + * @param authToken authToken to be used to access the site + */ +- (void)finishedLoginWithGoogleIDToken:(NSString *)googleIDToken authToken:(NSString *)authToken; + @end diff --git a/WordPress/Classes/Services/LoginFacade.m b/WordPress/Classes/Services/LoginFacade.m index 2f9a6acd1d77..1da74140f7eb 100644 --- a/WordPress/Classes/Services/LoginFacade.m +++ b/WordPress/Classes/Services/LoginFacade.m @@ -40,7 +40,6 @@ - (void)signInWithLoginFields:(LoginFields *)loginFields } } - - (void)loginWithLoginFields:(LoginFields *)loginFields { NSAssert(self.delegate != nil, @"Must set delegate to use service"); @@ -52,7 +51,6 @@ - (void)loginWithLoginFields:(LoginFields *)loginFields } } - - (void)requestOneTimeCodeWithLoginFields:(LoginFields *)loginFields { [self.wordpressComOAuthClientFacade requestOneTimeCodeWithUsername:loginFields.username password:loginFields.password success:^{ @@ -62,6 +60,28 @@ - (void)requestOneTimeCodeWithLoginFields:(LoginFields *)loginFields }]; } +- (void)loginToWordPressDotComWithGoogleIDToken:(NSString *)googleIDToken +{ + if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { + [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; + } + + [self.wordpressComOAuthClientFacade authenticateWithGoogleIDToken:googleIDToken success:^(NSString *authToken) { + if ([self.delegate respondsToSelector:@selector(finishedLoginWithGoogleIDToken:authToken:)]) { + [self.delegate finishedLoginWithGoogleIDToken:googleIDToken authToken:authToken]; + } + } needsMultiFactor:^(NSInteger userID, SocialLogin2FANonceInfo *nonceInfo){ + if ([self.delegate respondsToSelector:@selector(needsMultifactorCodeForUserID:andNonceInfo:)]) { + [self.delegate needsMultifactorCodeForUserID:userID andNonceInfo:nonceInfo]; + } + } failure:^(NSError *error) { + [WPAppAnalytics track:WPAnalyticsStatLoginFailed error:error]; + if ([self.delegate respondsToSelector:@selector(displayRemoteError:)]) { + [self.delegate displayRemoteError:error]; + } + }]; +} + - (void)signInToWordpressDotCom:(LoginFields *)loginFields { if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { diff --git a/WordPress/Classes/Services/WordPressComOAuthClientFacade.h b/WordPress/Classes/Services/WordPressComOAuthClientFacade.h index 01a74d7d8311..9b8dd59102be 100644 --- a/WordPress/Classes/Services/WordPressComOAuthClientFacade.h +++ b/WordPress/Classes/Services/WordPressComOAuthClientFacade.h @@ -1,5 +1,6 @@ #import +@class SocialLogin2FANonceInfo; @protocol WordPressComOAuthClientFacade @@ -15,6 +16,17 @@ success:(void (^)(void))success failure:(void (^)(NSError *error))failure; +- (void)authenticateWithGoogleIDToken:(NSString *)token + success:(void (^)(NSString *authToken))success + needsMultiFactor:(void (^)(NSInteger userID, SocialLogin2FANonceInfo *nonceInfo))needsMultifactor + failure:(void (^)(NSError *error))failure; + +- (void)authenticateSocialLoginUser:(NSInteger)userID + authType:(NSString *)authType + twoStepCode:(NSString *)twoStepCode + twoStepNonce:(NSString *)twoStepNonce + success:(void (^)(NSString *authToken))success + failure:(void (^)(NSError *error))failure; @end @interface WordPressComOAuthClientFacade : NSObject diff --git a/WordPress/Classes/Services/WordPressComOAuthClientFacade.m b/WordPress/Classes/Services/WordPressComOAuthClientFacade.m index d0abc67030f4..a704477da043 100644 --- a/WordPress/Classes/Services/WordPressComOAuthClientFacade.m +++ b/WordPress/Classes/Services/WordPressComOAuthClientFacade.m @@ -33,4 +33,24 @@ - (void)requestOneTimeCodeWithUsername:(NSString *)username [client requestOneTimeCodeWithUsername:username password:password success:success failure:failure]; } +- (void)authenticateWithGoogleIDToken:(NSString *)token + success:(void (^)(NSString *authToken))success + needsMultiFactor:(void (^)(NSInteger userID, SocialLogin2FANonceInfo *nonceInfo))needsMultifactor + failure:(void (^)(NSError *error))failure +{ + WordPressComOAuthClient *client = [WordPressComOAuthClient clientWithClientID:ApiCredentials.client secret:ApiCredentials.secret]; + [client authenticateWithIDToken:token success:success needsMultifactor:needsMultifactor failure:failure]; +} + +- (void)authenticateSocialLoginUser:(NSInteger)userID + authType:(NSString *)authType + twoStepCode:(NSString *)twoStepCode + twoStepNonce:(NSString *)twoStepNonce + success:(void (^)(NSString *authToken))success + failure:(void (^)(NSError *error))failure +{ + WordPressComOAuthClient *client = [WordPressComOAuthClient clientWithClientID:ApiCredentials.client secret:ApiCredentials.secret]; + [client authenticateSocialLoginUser:userID authType:authType twoStepCode:twoStepCode twoStepNonce:twoStepNonce success:success failure:failure]; +} + @end diff --git a/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift b/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift index b44fe11f10e9..252a9506616e 100644 --- a/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift @@ -9,6 +9,7 @@ class LoginEmailViewController: LoginViewController, SigninKeyboardResponder { @IBOutlet var bottomContentConstraint: NSLayoutConstraint? @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? var onePasswordButton: UIButton! + var googleLoginButton: UIButton? var didFindSafariSharedCredentials = false var didRequestSafariSharedCredentials = false @@ -124,7 +125,8 @@ class LoginEmailViewController: LoginViewController, SigninKeyboardResponder { /// Add the log in with Google button to the view func addGoogleButton() { - guard Feature.enabled(.googleLogin) else { + guard Feature.enabled(.googleLogin), + let instructionLabel = instructionLabel else { return } @@ -134,10 +136,11 @@ class LoginEmailViewController: LoginViewController, SigninKeyboardResponder { view.addConstraints([ button.topAnchor.constraint(equalTo: self.emailTextField.bottomAnchor, constant: Constants.googleButtonOffset), - button.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), - button.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + button.leadingAnchor.constraint(equalTo: instructionLabel.leadingAnchor), + button.trailingAnchor.constraint(equalTo:instructionLabel.trailingAnchor), button.centerXAnchor.constraint(equalTo: emailTextField.centerXAnchor) ]) + googleLoginButton = button } func googleLoginTapped() { @@ -172,6 +175,8 @@ class LoginEmailViewController: LoginViewController, SigninKeyboardResponder { /// override func configureViewLoading(_ loading: Bool) { emailTextField.isEnabled = !loading + googleLoginButton?.isEnabled = !loading + submitButton?.isEnabled = !loading submitButton?.showActivityIndicator(loading) } @@ -370,15 +375,37 @@ class LoginEmailViewController: LoginViewController, SigninKeyboardResponder { } +// LoginFacadeDelegate methods for Google Google Sign In +extension LoginEmailViewController { + func finishedLogin(withGoogleIDToken googleIDToken: String!, authToken: String!) { + let username = loginFields.username + syncWPCom(username, authToken: authToken, requiredMultifactor: false) + } + + func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo!) { + // TODO: to be implemented. + } +} + extension LoginEmailViewController: GIDSignInDelegate { func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) { - // TODO: implement wpcom login via Google code + // TODO: finish implementing wpcom login via Google code + guard let token = user.authentication.idToken, + let email = user.profile.email else { + // The Google SignIn for may have been canceled. + //TODO: Add analytis + return + } + + // Store the email address. + loginFields.emailAddress = email + loginFields.username = email + + configureViewLoading(true) + + loginFacade.loginToWordPressDotCom(withGoogleIDToken: token) - let alert = UIAlertController(title: "Login Success", message: "Google login succeeded. Actual wpcom account login yet to be implemented.", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Ok. Thanks.", style: .default, handler: { [weak self] (action) in - self?.dismiss(animated: true) {} - })) - present(alert, animated: true) + //TODO: Add analytis } } diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 3297bfe837cd..3f747413ddd7 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -893,6 +893,7 @@ E6C448571CB091AA00458157 /* SigninKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C448561CB091AA00458157 /* SigninKeyboardResponder.swift */; }; E6C892D61C601D55007AD612 /* SharingButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C892D51C601D55007AD612 /* SharingButtonsViewController.swift */; }; E6D0088E1EF0880C009E5FA3 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D0088D1EF0880C009E5FA3 /* LoginViewController.swift */; }; + E6D0EE601F7EF9830064D3FC /* AccountService+SocialService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D0EE5F1F7EF9830064D3FC /* AccountService+SocialService.swift */; }; E6D170371EF9D8D10046D433 /* SiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D170361EF9D8D10046D433 /* SiteInfo.swift */; }; E6D2E15F1B8A9C830000ED14 /* ReaderSiteStreamHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = E6D2E15E1B8A9C830000ED14 /* ReaderSiteStreamHeader.xib */; }; E6D2E1611B8AA4410000ED14 /* ReaderTagStreamHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = E6D2E1601B8AA4410000ED14 /* ReaderTagStreamHeader.xib */; }; @@ -2322,6 +2323,7 @@ E6C448561CB091AA00458157 /* SigninKeyboardResponder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SigninKeyboardResponder.swift; sourceTree = ""; }; E6C892D51C601D55007AD612 /* SharingButtonsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingButtonsViewController.swift; sourceTree = ""; }; E6D0088D1EF0880C009E5FA3 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + E6D0EE5F1F7EF9830064D3FC /* AccountService+SocialService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountService+SocialService.swift"; sourceTree = ""; }; E6D170361EF9D8D10046D433 /* SiteInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteInfo.swift; sourceTree = ""; }; E6D2E15E1B8A9C830000ED14 /* ReaderSiteStreamHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReaderSiteStreamHeader.xib; sourceTree = ""; }; E6D2E1601B8AA4410000ED14 /* ReaderTagStreamHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReaderTagStreamHeader.xib; sourceTree = ""; }; @@ -2778,7 +2780,7 @@ name = Products; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( E1B34C091CCDFFCE00889709 /* Credentials */, @@ -3694,6 +3696,7 @@ B5EFB1C31B31B99D007608A3 /* Facades */, 93C1147D18EC5DD500DAC95C /* AccountService.h */, 93C1147E18EC5DD500DAC95C /* AccountService.m */, + E6D0EE5F1F7EF9830064D3FC /* AccountService+SocialService.swift */, E1FD45DF1C030B3800750F4C /* AccountSettingsService.swift */, 822D60B81F4CCC7A0016C46D /* BlogJetpackSettingsService.swift */, 93C1148318EDF6E100DAC95C /* BlogService.h */, @@ -5485,7 +5488,7 @@ bg, sk, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -5801,7 +5804,7 @@ inputPaths = ( "${SRCROOT}/../Pods/Target Support Files/Pods-WordPress/Pods-WordPress-resources.sh", "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", - "$PODS_CONFIGURATION_BUILD_DIR/HockeySDK/HockeySDKResources.bundle", + $PODS_CONFIGURATION_BUILD_DIR/HockeySDK/HockeySDKResources.bundle, ); name = "[CP] Copy Pods Resources"; outputPaths = ( @@ -6170,6 +6173,7 @@ 85D239B61AE5A6170074768D /* ReachabilityFacade.m in Sources */, E6D3B1431D1C702600008D4B /* ReaderFollowedSitesViewController.swift in Sources */, B5899ADE1B419C560075A3D6 /* NotificationSettingDetailsViewController.swift in Sources */, + E6D0EE601F7EF9830064D3FC /* AccountService+SocialService.swift in Sources */, FAE4201A1C5AEFE100C1D036 /* StartOverViewController.swift in Sources */, B57B99D519A2C20200506504 /* NoteTableHeaderView.swift in Sources */, 1790A4531E28F0ED00AE54C2 /* UINavigationController+Helpers.swift in Sources */, @@ -7064,7 +7068,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; - WPCOM_CONFIG = "$HOME/.wpcom_app_credentials"; + WPCOM_CONFIG = $HOME/.wpcom_app_credentials; WPCOM_SCHEME = wpdebug; }; name = Debug; @@ -7130,7 +7134,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; - WPCOM_CONFIG = "$HOME/.wpcom_app_credentials"; + WPCOM_CONFIG = $HOME/.wpcom_app_credentials; WPCOM_SCHEME = wordpress; }; name = Release; @@ -7417,7 +7421,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; - WPCOM_CONFIG = "$HOME/.wpcom_alpha_app_credentials"; + WPCOM_CONFIG = $HOME/.wpcom_alpha_app_credentials; WPCOM_SCHEME = wpalpha; }; name = "Release-Alpha"; @@ -7845,7 +7849,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; - WPCOM_CONFIG = "$HOME/.wpcom_internal_app_credentials"; + WPCOM_CONFIG = $HOME/.wpcom_internal_app_credentials; WPCOM_SCHEME = wpinternal; }; name = "Release-Internal"; diff --git a/WordPressKit/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit/WordPressKit.xcodeproj/project.pbxproj index a71bade48bd4..5bf0a8d4aba8 100644 --- a/WordPressKit/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit/WordPressKit.xcodeproj/project.pbxproj @@ -336,8 +336,10 @@ E13EE1491F332B8500C15787 /* site-plugins-success.json in Resources */ = {isa = PBXBuildFile; fileRef = E13EE1481F332B8500C15787 /* site-plugins-success.json */; }; E13EE14C1F332C4400C15787 /* PluginServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13EE14B1F332C4400C15787 /* PluginServiceRemoteTests.swift */; }; E14694031F344F71004052C8 /* site-plugins-error.json in Resources */ = {isa = PBXBuildFile; fileRef = E14694021F344F71004052C8 /* site-plugins-error.json */; }; + E632D7781F6E047400297F6D /* SocialLogin2FANonceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E632D7771F6E047400297F6D /* SocialLogin2FANonceInfo.swift */; }; E6C1E8491EF21FC100D139D9 /* is-passwordless-account-no-account-found.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8471EF21FC100D139D9 /* is-passwordless-account-no-account-found.json */; }; E6C1E84A1EF21FC100D139D9 /* is-passwordless-account-success.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8481EF21FC100D139D9 /* is-passwordless-account-success.json */; }; + E6D0EE621F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */; }; F8B82B9E92C9637430D7D032 /* Pods_WordPressKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F902B70CBB63DD7EB9D275A /* Pods_WordPressKitTests.framework */; }; /* End PBXBuildFile section */ @@ -693,8 +695,10 @@ E13EE14B1F332C4400C15787 /* PluginServiceRemoteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginServiceRemoteTests.swift; sourceTree = ""; }; E14694021F344F71004052C8 /* site-plugins-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-plugins-error.json"; sourceTree = ""; }; E5273E8FA8F6117F941D6CC1 /* Pods-WordPressKit.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKit.release-internal.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressKit/Pods-WordPressKit.release-internal.xcconfig"; sourceTree = ""; }; + E632D7771F6E047400297F6D /* SocialLogin2FANonceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialLogin2FANonceInfo.swift; sourceTree = ""; }; E6C1E8471EF21FC100D139D9 /* is-passwordless-account-no-account-found.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "is-passwordless-account-no-account-found.json"; sourceTree = ""; }; E6C1E8481EF21FC100D139D9 /* is-passwordless-account-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "is-passwordless-account-success.json"; sourceTree = ""; }; + E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountServiceRemoteREST+SocialService.swift"; sourceTree = ""; }; EF1D8EBEFA46779A06F52788 /* Pods-WordPressKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKitTests.release.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressKitTests/Pods-WordPressKitTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -970,6 +974,7 @@ 93BD27381EE73282002BB00B /* AccountServiceRemote.h */, 93BD27391EE73282002BB00B /* AccountServiceRemoteREST.h */, 93BD273A1EE73282002BB00B /* AccountServiceRemoteREST.m */, + E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */, 7403A2E31EF06ED500DED7DC /* AccountSettingsRemote.swift */, 82FFBF551F460DD400F4573F /* BlogJetpackSettingsServiceRemote.swift */, 74B5F0DB1EF829B800B411E7 /* BlogServiceRemote.h */, @@ -1097,6 +1102,7 @@ 9309995A1F16616A00F006A1 /* RemoteTheme.m */, 93BD27671EE736A8002BB00B /* RemoteUser.h */, 93BD27681EE736A8002BB00B /* RemoteUser.m */, + E632D7771F6E047400297F6D /* SocialLogin2FANonceInfo.swift */, 930F52B91ECF8A44002F921B /* Stats */, ); name = Models; @@ -1767,6 +1773,7 @@ 93BD276A1EE736A8002BB00B /* RemoteUser.m in Sources */, 742362D71F10250600BD0A7F /* MenusServiceRemote.m in Sources */, 9368C7BA1EC630270092CE8E /* StatsSummary.m in Sources */, + E6D0EE621F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift in Sources */, 7430C9B61F1927C50051B8E6 /* RemoteReaderSiteInfo.m in Sources */, 74DA56351F06EAF000FE9BF4 /* MediaServiceRemoteXMLRPC.m in Sources */, 93F50A381F226B9300B5BEBA /* WordPressComServiceRemote.m in Sources */, @@ -1775,6 +1782,7 @@ 9368C7C01EC630CE0092CE8E /* StatsStringUtilities.m in Sources */, 74BA04FA1F06DC3900ED5CD8 /* RemoteComment.m in Sources */, 93C674EC1EE8348F00BFAF05 /* RemoteBlogOptionsHelper.m in Sources */, + E632D7781F6E047400297F6D /* SocialLogin2FANonceInfo.swift in Sources */, 7403A2E41EF06ED500DED7DC /* AccountSettingsRemote.swift in Sources */, 93C674E81EE8345300BFAF05 /* RemoteBlog.m in Sources */, 93BD27831EE73944002BB00B /* WordPressRSDParser.swift in Sources */, diff --git a/WordPressKit/WordPressKit/AccountServiceRemote.h b/WordPressKit/WordPressKit/AccountServiceRemote.h index caa2c0cb9834..1372b23d07c2 100644 --- a/WordPressKit/WordPressKit/AccountServiceRemote.h +++ b/WordPressKit/WordPressKit/AccountServiceRemote.h @@ -81,4 +81,5 @@ wpcomScheme:(NSString *)scheme success:(void (^)())success failure:(void (^)(NSError *error))failure; + @end diff --git a/WordPressKit/WordPressKit/AccountServiceRemoteREST+SocialService.swift b/WordPressKit/WordPressKit/AccountServiceRemoteREST+SocialService.swift new file mode 100644 index 000000000000..3a21b4a7d04d --- /dev/null +++ b/WordPressKit/WordPressKit/AccountServiceRemoteREST+SocialService.swift @@ -0,0 +1,53 @@ +import Foundation + +public enum SocialServiceName: String { + case google +} + + +extension AccountServiceRemoteREST { + + /// Connect to the specified social service via its OpenID Connect (JWT) token. + /// + /// - Parameters: + /// - service The name of the social service. + /// - token The OpenID Connect (JWT) ID token identifying the user on the social service. + /// - success The block that will be executed on success. + /// - failure The block that will be executed on failure. + public func connectToSocialService(_ service: SocialServiceName, serviceIDToken token: String, success:@escaping (() -> Void), failure:@escaping ((NSError) -> Void)) { + guard let path = self.path(forEndpoint: "me/social-login/connect", with: .version_1_1) else { + // This should never fail but if it does we don't want to ignore the problem. + fatalError("There was a problem creating a valid path for the supplied endpoint and REST API version.") + } + let params = [ + "service": service.rawValue, + "id_token": token + ] as [String: AnyObject] + wordPressComRestApi.POST(path, parameters: params, success: { (responseObject, httpResponse) in + success() + }, failure: { (error, httpResponse) in + failure(error) + }) + } + + /// Disconnect fromm the specified social service. + /// + /// - Parameters: + /// - service The name of the social service. + /// - success The block that will be executed on success. + /// - failure The block that will be executed on failure. + public func disconnectFromSocialService(_ service: SocialServiceName, success:@escaping(() -> Void), failure:@escaping((NSError) -> Void)) { + guard let path = self.path(forEndpoint: "me/social-login/disconnect", with: .version_1_1) else { + // This should never fail but if it does we don't want to ignore the problem. + fatalError("There was a problem creating a valid path for the supplied endpoint and REST API version.") + } + let params = [ + "service": service.rawValue, + ] as [String: AnyObject] + wordPressComRestApi.POST(path, parameters: params, success: { (responseObject, httpResponse) in + success() + }, failure: { (error, httpResponse) in + failure(error) + }) + } +} diff --git a/WordPressKit/WordPressKit/AccountServiceRemoteREST.m b/WordPressKit/WordPressKit/AccountServiceRemoteREST.m index e0ea1f040da8..146a473bb47c 100644 --- a/WordPressKit/WordPressKit/AccountServiceRemoteREST.m +++ b/WordPressKit/WordPressKit/AccountServiceRemoteREST.m @@ -233,7 +233,6 @@ - (void)requestWPComAuthLinkForEmail:(NSString *)email }]; } - #pragma mark - Private Methods - (RemoteUser *)remoteUserFromDictionary:(NSDictionary *)dictionary diff --git a/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift b/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift new file mode 100644 index 000000000000..d87bd5d13d7f --- /dev/null +++ b/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift @@ -0,0 +1,11 @@ +import Foundation + +@objc +public class SocialLogin2FANonceInfo: NSObject { + var nonceSMS = "" + var nonceBackup = "" + var nonceAuthenticator = "" + var supportedAuthTypes = [String]() // backup|authenticator|sms + var notificationSent = "" // none|sms + var phoneNumber = "" // The last two digits of the phone number to which an SMS was sent. +} diff --git a/WordPressKit/WordPressKit/WordPressComOAuthClient.swift b/WordPressKit/WordPressKit/WordPressComOAuthClient.swift index ba0a22006966..65a31bc7224e 100644 --- a/WordPressKit/WordPressKit/WordPressComOAuthClient.swift +++ b/WordPressKit/WordPressKit/WordPressComOAuthClient.swift @@ -19,19 +19,33 @@ public final class WordPressComOAuthClient: NSObject { public static let WordPressComOAuthErrorDomain = "WordPressComOAuthError" public static let WordPressComOAuthBaseUrl = "https://public-api.wordpress.com/oauth2" + public static let WordPressComSocialLoginUrl = "https://wordpress.com/wp-login.php?action=social-login-endpoint&version=1.0" + public static let WordPressComSocialLogin2FAUrl = "https://wordpress.com/wp-login.php?action=two-step-authentication-endpoint&version=1.0" public static let WordPressComOAuthRedirectUrl = "https://wordpress.com/" + public static let WordPressComSocialLoginEndpointVersion = 1.0 - fileprivate let sessionManager: AFHTTPSessionManager = { - let baseURL = URL(string: WordPressComOAuthClient.WordPressComOAuthBaseUrl) - let sessionConfiguration = URLSessionConfiguration.ephemeral - let sessionManager = AFHTTPSessionManager(baseURL: baseURL, sessionConfiguration: sessionConfiguration) + fileprivate let clientID: String + fileprivate let secret: String + + fileprivate let oauth2SessionManager: AFHTTPSessionManager = { + return WordPressComOAuthClient.sessionManager(url: WordPressComOAuthClient.WordPressComOAuthBaseUrl) + }() + + fileprivate let socialSessionManager: AFHTTPSessionManager = { + return WordPressComOAuthClient.sessionManager(url: WordPressComOAuthClient.WordPressComSocialLoginUrl) + }() + + fileprivate let social2FASessionManager: AFHTTPSessionManager = { + return WordPressComOAuthClient.sessionManager(url: WordPressComOAuthClient.WordPressComSocialLogin2FAUrl) + }() + + fileprivate class func sessionManager(url: String) -> AFHTTPSessionManager { + let baseURL = URL(string: url) + let sessionManager = AFHTTPSessionManager(baseURL: baseURL, sessionConfiguration: .ephemeral) sessionManager.responseSerializer = WordPressComOAuthResponseSerializer() sessionManager.requestSerializer.setValue("application/json", forHTTPHeaderField: "Accept") return sessionManager - }() - - fileprivate let clientID: String - fileprivate let secret: String + } /// Creates a WordPresComOAuthClient initialized with the clientID and secret constants defined in the /// ApiCredentials singleton @@ -79,7 +93,7 @@ public final class WordPressComOAuthClient: NSObject { parameters["wpcom_otp"] = multifactorCode as AnyObject? } - sessionManager.post("token", parameters: parameters, progress: nil, success: { (task, responseObject) in + oauth2SessionManager.post("token", parameters: parameters, progress: nil, success: { (task, responseObject) in DDLogVerbose("Received OAuth2 response: \(self.cleanedUpResponseForLogging(responseObject as AnyObject? ?? "nil" as AnyObject))") guard let responseDictionary = responseObject as? [String: AnyObject], let authToken = responseDictionary["access_token"] as? String else { @@ -115,7 +129,7 @@ public final class WordPressComOAuthClient: NSObject { "wpcom_resend_otp": true ] as [String : Any] - sessionManager.post("token", parameters: parameters, progress: nil, success: { (task, responseObject) in + oauth2SessionManager.post("token", parameters: parameters, progress: nil, success: { (task, responseObject) in success() }, failure: { (task, error) in failure(error as NSError) @@ -123,6 +137,149 @@ public final class WordPressComOAuthClient: NSObject { ) } + /// Authenticate on WordPress.com with a social service's ID token. + /// Only google is supported at this time. + /// + /// - Parameters: + /// - token: A social ID token obtained from a supported social service. + /// - success: block to be called if authentication was successful. The OAuth2 token is passed as a parameter. + /// - needsMultifactor: block to be called if a 2fa token is needed to complete the auth process. + /// - failure: block to be called if authentication failed. The error object is passed as a parameter. + /// + public func authenticateWithIDToken(_ token: String, + success: @escaping (_ authToken: String?) -> Void, + needsMultifactor: @escaping (_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo) -> Void, + failure: @escaping (_ error: NSError) -> Void ) { + let parameters = [ + "client_id": clientID, + "client_secret": secret, + "service": "google", + "get_bearer_token": true, + "id_token" : token, + ] as [String : Any] + + // Passes an empty string for the path. The session manager was composed with the full endpoint path. + socialSessionManager.post("", parameters: parameters, progress: nil, success: { (task, responseObject) in + DDLogVerbose("Received Social Login Oauth response: \(self.cleanedUpResponseForLogging(responseObject as AnyObject? ?? "nil" as AnyObject))") + + let defaultError = NSError(domain: WordPressComOAuthClient.WordPressComOAuthErrorDomain, + code: WordPressComOAuthError.unknown.rawValue, + userInfo: nil) + + // Make sure we received expected data. + guard let responseDictionary = responseObject as? [String: AnyObject], + let responseData = responseDictionary["data"] as? [String: AnyObject] else { + failure(defaultError) + return + } + + // Check for a bearer token. If one is found then we're authed. + if let authToken = responseData["bearer_token"] as? String { + success(authToken) + return + } + + // If there is no bearer token, check for 2fa enabled. + guard let userID = responseData["user_id"] as? Int, + let _ = responseData["two_step_nonce_backup"] else { + failure(defaultError) + return + } + + let nonceInfo = self.extractNonceInfo(data: responseData) + needsMultifactor(userID, nonceInfo) + + }, failure: { (task, error) in + failure(error as NSError) + } + ) + } + + /// A helper method to get an instance of SocialLogin2FANonceInfo and populate + /// it with the supplied data. + /// + /// - Parameters: + /// - data: The dictionary to use to populate the instance. + /// + /// - Return: SocialLogin2FANonceInfo + /// + private func extractNonceInfo(data:[String: AnyObject]) -> SocialLogin2FANonceInfo { + let nonceInfo = SocialLogin2FANonceInfo() + + if let nonceAuthenticator = data["two_step_nonce_authenticator"] as? String { + nonceInfo.nonceAuthenticator = nonceAuthenticator + } + + if let nonce = data["two_step_nonce_sms"] as? String { + nonceInfo.nonceSMS = nonce + } + + if let nonce = data["two_step_nonce_backup"] as? String { + nonceInfo.nonceBackup = nonce + } + + if let notification = data["two_step_notification_sent"] as? String { + nonceInfo.notificationSent = notification + } + + if let authTypes = data["two_step_supported_auth_types"] as? [String] { + nonceInfo.supportedAuthTypes = authTypes + } + + if let phone = data["phone_number"] as? String { + nonceInfo.phoneNumber = phone + } + + return nonceInfo + } + + /// Completes a social login that has 2fa enabled. + /// + /// - Parameters: + /// - userID: The wpcom user id. + /// - authType: The type of 2fa authentication being used. (sms|backup|authenticator) + /// - twoStepCode: The user's 2fa code. + /// - twoStepNonce: The nonce returned from a social login attempt. + /// - success: block to be called if authentication was successful. The OAuth2 token is passed as a parameter. + /// - failure: block to be called if authentication failed. The error object is passed as a parameter. + /// + public func authenticateSocialLoginUser(_ userID: Int, + authType: String, + twoStepCode: String, + twoStepNonce: String, + success: @escaping (_ authToken: String?) -> Void, + failure: @escaping (_ error: NSError) -> Void ) { + let parameters = [ + "user_id" : userID, + "auth_type" : authType, + "two_step_code": twoStepCode, + "two_step_nonce": twoStepNonce, + "get_bearer_token": true, + "client_id": clientID, + "client_secret": secret, + ] as [String : Any] + + // Passes an empty string for the path. The session manager was composed with the full endpoint path. + social2FASessionManager.post("", parameters: parameters, progress: nil, success: { (task, responseObject) in + DDLogVerbose("Received Social Login Oauth response: \(self.cleanedUpResponseForLogging(responseObject as AnyObject? ?? "nil" as AnyObject))") + guard let responseDictionary = responseObject as? [String: AnyObject], + let responseData = responseDictionary["data"] as? [String: AnyObject], + let authToken = responseData["bearer_token"] as? String else { + failure(NSError(domain: WordPressComOAuthClient.WordPressComOAuthErrorDomain, + code: WordPressComOAuthError.unknown.rawValue, + userInfo: nil)) + return + } + + success(authToken) + + }, failure: { (task, error) in + failure(error as NSError) + } + ) + } + + fileprivate func cleanedUpResponseForLogging(_ response: AnyObject) -> AnyObject { guard var responseDictionary = response as? [String: AnyObject], let _ = responseDictionary["access_token"]