Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions WordPress/Classes/Services/LoginFacade.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
Expand Down
25 changes: 25 additions & 0 deletions WordPress/Classes/Services/LoginFacade.m
Original file line number Diff line number Diff line change
Expand Up @@ -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:)]) {
Expand Down
44 changes: 40 additions & 4 deletions WordPress/Classes/ViewRelated/NUX/Login2FAViewController.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -28,6 +29,7 @@ class Login2FAViewController: LoginViewController, SigninKeyboardResponder {
localizeControls()
configureTextFields()
configureSubmitButton(animating: false)
configureSendCodeButton()
}


Expand Down Expand Up @@ -83,11 +85,22 @@ 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.isEnabled = false
sendCodeButton.setTitle("", for: .normal)
}


/// Configures the appearance and state of the submit button.
///
Expand Down Expand Up @@ -137,9 +150,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

Expand Down Expand Up @@ -186,7 +216,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
Expand Down Expand Up @@ -215,10 +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: bad2FAMessage)
} else {
displayError(error as NSError, sourceTag: sourceTag)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
6 changes: 6 additions & 0 deletions WordPress/Classes/ViewRelated/NUX/LoginFields.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions WordPressKit/WordPressKit/SocialLogin2FANonceInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,46 @@ 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"
}

/// These constants match the server-side authentication code
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
}
}
}
21 changes: 20 additions & 1 deletion WordPressKit/WordPressKit/WordPressComOAuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand Down Expand Up @@ -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
]


Expand Down Expand Up @@ -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?
Expand All @@ -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,
Expand Down