From 95ca880f03d8c147dbffdf1b30ab253911e2adec Mon Sep 17 00:00:00 2001 From: Nate Heagy Date: Wed, 18 Oct 2017 14:13:55 -0600 Subject: [PATCH 1/7] Add social 2fa to LoginFacade --- WordPress/Classes/Services/LoginFacade.h | 20 +++++++++++++++++++ WordPress/Classes/Services/LoginFacade.m | 25 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/WordPress/Classes/Services/LoginFacade.h b/WordPress/Classes/Services/LoginFacade.h index 6e52a79190ec..1526fcc1fed8 100644 --- a/WordPress/Classes/Services/LoginFacade.h +++ b/WordPress/Classes/Services/LoginFacade.h @@ -51,6 +51,17 @@ */ - (void)loginToWordPressDotComWithGoogleIDToken:(NSString *)googleIDToken; +/** + * Social login via a social account with 2FA using a nonce. + * + * @param googleIDToken A Google id_token. + */ +- (void)loginToWordPressDotComWithUser:(NSInteger)userID + authType:(NSString *)authType + twoStepCode:(NSString *)twoStepCode + twoStepNonce:(NSString *)twoStepNonce; + + /** * A delegate with a few methods that indicate various aspects of the login process */ @@ -139,6 +150,15 @@ - (void)finishedLoginWithGoogleIDToken:(NSString *)googleIDToken authToken:(NSString *)authToken; +/** + * Called when finished logging in to a WordPress.com site via a 2FA Nonce. + * + * @param googleIDToken the token used + * @param authToken authToken to be used to access the site + */ +- (void)finishedLoginWithNonceAuthToken:(NSString *)authToken; + + /** * Lets the delegate know that a social login attempt found a matching user, but * their account has not been connected to the social service previously. diff --git a/WordPress/Classes/Services/LoginFacade.m b/WordPress/Classes/Services/LoginFacade.m index 7cbb9ef88e88..42ac8f5221de 100644 --- a/WordPress/Classes/Services/LoginFacade.m +++ b/WordPress/Classes/Services/LoginFacade.m @@ -87,6 +87,31 @@ - (void)loginToWordPressDotComWithGoogleIDToken:(NSString *)googleIDToken }]; } +- (void)loginToWordPressDotComWithUser:(NSInteger)userID + authType:(NSString *)authType + twoStepCode:(NSString *)twoStepCode + twoStepNonce:(NSString *)twoStepNonce +{ + if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { + [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; + } + + [self.wordpressComOAuthClientFacade authenticateSocialLoginUser:userID + authType:authType + twoStepCode:twoStepCode + twoStepNonce:twoStepNonce + success:^(NSString *authToken) { + if ([self.delegate respondsToSelector:@selector(finishedLoginWithNonceAuthToken:)]) { + [self.delegate finishedLoginWithNonceAuthToken:authToken]; + } + } 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:)]) { From 5adc3e5e0e0e1635c0bd9fdc1c78086a54c4e481 Mon Sep 17 00:00:00 2001 From: Nate Heagy Date: Wed, 18 Oct 2017 14:14:27 -0600 Subject: [PATCH 2/7] Let SocialLogin2FANonceInfo track nonces --- .../SocialLogin2FANonceInfo.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift b/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift index d87bd5d13d7f..3bee42609a4f 100644 --- a/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift +++ b/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift @@ -8,4 +8,45 @@ public class SocialLogin2FANonceInfo: NSObject { 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. + + private enum Constants { + static let lastUsedPlaceholder = "last_used_placeholder" + } + + private enum AuthTypeLengths { + static let authenticator = 6 + static let sms = 7 + static let backup = 8 + } + + public func authTypeAndNonce(for code: String) -> (String, String) { + let typeNoncePair: (String, String) + switch code.count { + case AuthTypeLengths.sms: + typeNoncePair = ("sms", nonceSMS) + nonceSMS = Constants.lastUsedPlaceholder + case AuthTypeLengths.backup: + typeNoncePair = ("backup", nonceBackup) + nonceBackup = Constants.lastUsedPlaceholder + case AuthTypeLengths.authenticator: + fallthrough + default: + typeNoncePair = ("authenticator", nonceAuthenticator) + nonceAuthenticator = Constants.lastUsedPlaceholder + } + return typeNoncePair + } + + public func updateNonce(with newNonce: String) { + switch Constants.lastUsedPlaceholder { + case nonceSMS: + nonceSMS = newNonce + case nonceBackup: + nonceBackup = newNonce + case nonceAuthenticator: + fallthrough + default: + nonceAuthenticator = newNonce + } + } } From 52845a059a244636f782b847266df97520229b20 Mon Sep 17 00:00:00 2001 From: Nate Heagy Date: Wed, 18 Oct 2017 14:14:44 -0600 Subject: [PATCH 3/7] Add nonce to LoginFields --- WordPress/Classes/ViewRelated/NUX/LoginFields.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WordPress/Classes/ViewRelated/NUX/LoginFields.swift b/WordPress/Classes/ViewRelated/NUX/LoginFields.swift index 84fa9eb98f14..fcb81777ee4c 100644 --- a/WordPress/Classes/ViewRelated/NUX/LoginFields.swift +++ b/WordPress/Classes/ViewRelated/NUX/LoginFields.swift @@ -21,6 +21,12 @@ class LoginFields: NSObject { /// The two factor code entered by a user. var multifactorCode = "" // 2fa code + /// Nonce info in the event of a social login with 2fa + var nonceInfo: SocialLogin2FANonceInfo? + + /// User ID for use with the nonce for social login + var nonceUserID: Int = 0 + /// Used by the SignupViewController. Signup currently asks for both a /// username and an email address. This can be factored away when we revamp /// the signup flow. From 5140c87568795e17c03ba3034b58f5715c54cbf1 Mon Sep 17 00:00:00 2001 From: Nate Heagy Date: Wed, 18 Oct 2017 14:15:38 -0600 Subject: [PATCH 4/7] Add 2fa/nonce support to login VCs --- .../NUX/Login2FAViewController.swift | 32 ++++++++++++++++++- .../NUX/LoginEmailViewController.swift | 5 ++- .../WordPressComOAuthClient.swift | 21 +++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift b/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift index 510776505294..37b76acac703 100644 --- a/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift @@ -1,6 +1,7 @@ import UIKit import SVProgressHUD import WordPressShared +import GoogleSignIn /// Provides a form and functionality for entering a two factor auth code and /// signing into WordPress.com @@ -86,6 +87,11 @@ class Login2FAViewController: LoginViewController, SigninKeyboardResponder { func configureTextFields() { verificationCodeField.textInsets = WPStyleGuide.edgeInsetForLoginTextFields() + + guard let _ = loginFields.nonceInfo else { + return + } + sendCodeButton.isHidden = true } @@ -137,9 +143,26 @@ class Login2FAViewController: LoginViewController, SigninKeyboardResponder { /// proceeds with the submit action. /// func validateForm() { + if let nonce = loginFields.nonceInfo { + loginWithNonce(info: nonce) + return + } validateFormAndLogin() } + private func loginWithNonce(info nonceInfo: SocialLogin2FANonceInfo) { + let code = loginFields.multifactorCode + let (authType, nonce) = nonceInfo.authTypeAndNonce(for: code) + loginFacade.loginToWordPressDotCom(withUser: loginFields.nonceUserID, authType: authType, twoStepCode: code, twoStepNonce: nonce) + } + + func finishedLogin(withNonceAuthToken authToken: String!) { + let username = loginFields.username + syncWPCom(username, authToken: authToken, requiredMultifactor: true) + // Disconnect now that we're done with Google. + GIDSignIn.sharedInstance().disconnect() + } + // MARK: - Actions @@ -186,7 +209,7 @@ class Login2FAViewController: LoginViewController, SigninKeyboardResponder { return } let isNumeric = pasteString.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil - guard isNumeric && pasteString.characters.count == 6 else { + guard isNumeric && pasteString.count == 6 else { return } verificationCodeField.text = pasteString @@ -219,6 +242,13 @@ extension Login2FAViewController { // Invalid verification code. displayError(message: NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", comment: "Error message shown when an incorrect two factor code is provided.")) + } else if err.domain == "WordPressComOAuthError" && err.code == WordPressComOAuthError.invalidTwoStepCode.rawValue { + // Invalid 2FA during social login + if let newNonce = (error as NSError).userInfo[WordPressComOAuthClient.WordPressComOAuthErrorNewNonceKey] as? String { + loginFields.nonceInfo?.updateNonce(with: newNonce) + } + displayError(message: NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", + comment: "Error message shown when an incorrect two factor code is provided.")) } else { displayError(error as NSError, sourceTag: sourceTag) } diff --git a/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift b/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift index 9795f73145de..235958803ebe 100644 --- a/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/LoginEmailViewController.swift @@ -443,7 +443,10 @@ extension LoginEmailViewController { func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo!) { - // TODO: to be implemented. + loginFields.nonceInfo = nonceInfo + loginFields.nonceUserID = userID + + performSegue(withIdentifier: NUXAbstractViewController.SegueIdentifier.show2FA, sender: self) } } diff --git a/WordPressKit/WordPressKit/WordPressComOAuthClient.swift b/WordPressKit/WordPressKit/WordPressComOAuthClient.swift index fa4c5ea546f9..131146806023 100644 --- a/WordPressKit/WordPressKit/WordPressComOAuthClient.swift +++ b/WordPressKit/WordPressKit/WordPressComOAuthClient.swift @@ -9,6 +9,7 @@ import CocoaLumberjack case needsMultifactorCode case invalidOneTimePassword case socialLoginExistingUserUnconnected + case invalidTwoStepCode } /// `WordPressComOAuthClient` encapsulates the pattern of authenticating against WordPress.com OAuth2 service. @@ -19,6 +20,7 @@ import CocoaLumberjack public final class WordPressComOAuthClient: NSObject { public static let WordPressComOAuthErrorResponseObjectKey = "WordPressComOAuthErrorResponseObjectKey" + public static let WordPressComOAuthErrorNewNonceKey = "WordPressComOAuthErrorNewNonceKey" 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" @@ -342,6 +344,7 @@ final class WordPressComOAuthResponseSerializer: AFJSONResponseSerializer { "needs_2fa": WordPressComOAuthError.needsMultifactorCode, "invalid_otp": WordPressComOAuthError.invalidOneTimePassword, "user_exists": WordPressComOAuthError.socialLoginExistingUserUnconnected, + "invalid_two_step_code": WordPressComOAuthError.invalidTwoStepCode ] @@ -383,6 +386,18 @@ final class WordPressComOAuthResponseSerializer: AFJSONResponseSerializer { error?.pointee = errorFor(errorCode: errorCode, errorDescription: errorDescription, responseObject: responseObject) } + } else if httpResponse.statusCode == 403 { + // Social 2FA token wasn't good + if let responseDict = responseObject as? [String: AnyObject], + let data = responseDict["data"] as? [String: AnyObject], + let newNonce = data["two_step_nonce"] as? String, + let errors = data["errors"] as? NSArray, + let err = errors[0] as? [String: AnyObject], + let errorCode = err["code"] as? String, + let errorDescription = err["message"] as? String { + + error?.pointee = errorFor(errorCode: errorCode, errorDescription: errorDescription, responseObject: responseObject, newNonce: newNonce) + } } return responseObject as AnyObject? @@ -396,12 +411,16 @@ final class WordPressComOAuthResponseSerializer: AFJSONResponseSerializer { /// - errorCode: A string representing the error code. This is not the same as an HTTP status code. /// - errorDescription: A description of the error. /// - responseObject: The responseObject (if any) that was passed with the error. + /// - newNonce: *optional* The new nonce provided when a 2FA fails /// - Returns: An NSError. - func errorFor(errorCode: String, errorDescription: String, responseObject: Any?) -> NSError { + func errorFor(errorCode: String, errorDescription: String, responseObject: Any?, newNonce: String? = nil) -> NSError { var userInfo:[String: AnyObject] = [NSLocalizedDescriptionKey: errorDescription as AnyObject] if let responseObject = responseObject { userInfo[WordPressComOAuthClient.WordPressComOAuthErrorResponseObjectKey] = responseObject as AnyObject } + if let newNonce = newNonce { + userInfo[WordPressComOAuthClient.WordPressComOAuthErrorNewNonceKey] = newNonce as AnyObject + } let mappedCode = errorsMap[errorCode]?.rawValue ?? WordPressComOAuthError.unknown.rawValue return NSError(domain: WordPressComOAuthClient.WordPressComOAuthErrorDomain, code: mappedCode, From 29ea111db4cffa24ad671fbd0e60d60780cd6e02 Mon Sep 17 00:00:00 2001 From: Nate Heagy Date: Thu, 19 Oct 2017 10:41:10 -0600 Subject: [PATCH 5/7] Fix code hiding the SMS request button for social login --- .../ViewRelated/NUX/Login2FAViewController.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift b/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift index 37b76acac703..bd41974f936c 100644 --- a/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift @@ -29,6 +29,7 @@ class Login2FAViewController: LoginViewController, SigninKeyboardResponder { localizeControls() configureTextFields() configureSubmitButton(animating: false) + configureSendCodeButton() } @@ -84,14 +85,20 @@ class Login2FAViewController: LoginViewController, SigninKeyboardResponder { sendCodeButton.titleLabel?.numberOfLines = 0 } - + /// configures the text fields + /// func configureTextFields() { verificationCodeField.textInsets = WPStyleGuide.edgeInsetForLoginTextFields() + } + /// Hides the send code button when appropriate + /// + func configureSendCodeButton() { guard let _ = loginFields.nonceInfo else { return } - sendCodeButton.isHidden = true + sendCodeButton.isEnabled = false + sendCodeButton.setTitle("", for: .normal) } From a207ab240a8937c0752cd69208d3b0b6c203c2d3 Mon Sep 17 00:00:00 2001 From: Nate Heagy Date: Thu, 19 Oct 2017 11:49:54 -0600 Subject: [PATCH 6/7] Remove label duplication in Login2FAViewController --- .../Classes/ViewRelated/NUX/Login2FAViewController.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift b/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift index bd41974f936c..92f0b119f819 100644 --- a/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift +++ b/WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift @@ -245,17 +245,16 @@ extension Login2FAViewController { configureViewLoading(false) let err = error as NSError + let bad2FAMessage = NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", comment: "Error message shown when an incorrect two factor code is provided.") if err.domain == "WordPressComOAuthError" && err.code == WordPressComOAuthError.invalidOneTimePassword.rawValue { // Invalid verification code. - displayError(message: NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", - comment: "Error message shown when an incorrect two factor code is provided.")) + displayError(message: bad2FAMessage) } else if err.domain == "WordPressComOAuthError" && err.code == WordPressComOAuthError.invalidTwoStepCode.rawValue { // Invalid 2FA during social login if let newNonce = (error as NSError).userInfo[WordPressComOAuthClient.WordPressComOAuthErrorNewNonceKey] as? String { loginFields.nonceInfo?.updateNonce(with: newNonce) } - displayError(message: NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", - comment: "Error message shown when an incorrect two factor code is provided.")) + displayError(message: bad2FAMessage) } else { displayError(error as NSError, sourceTag: sourceTag) } From 506d15ff29a0d61262c123ba1b82fe7fa2a81040 Mon Sep 17 00:00:00 2001 From: Nate Heagy Date: Thu, 19 Oct 2017 15:37:36 -0600 Subject: [PATCH 7/7] Add clarifying comment about auth type length constants --- WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift b/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift index 3bee42609a4f..ec8c65c87fa3 100644 --- a/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift +++ b/WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift @@ -13,6 +13,7 @@ public class SocialLogin2FANonceInfo: NSObject { static let lastUsedPlaceholder = "last_used_placeholder" } + /// These constants match the server-side authentication code private enum AuthTypeLengths { static let authenticator = 6 static let sms = 7