Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MFA beta #4823

Merged
merged 21 commits into from
Mar 10, 2020
32 changes: 10 additions & 22 deletions Example/Auth/ApiTests/PhoneMultiFactorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,25 @@ class PhoneMultiFactorTests: FIRAuthApiTestsBase {
XCTFail("No valid user after attempted sign-in.")
}
user.multiFactor.getSessionWithCompletion({ (session, error) in
XCTAssertNil(String(format: "Get multi factor session failed. Error: %@", error!.localizedDescription))
XCTAssertNil("Get multi factor session failed. Error: \(error!.localizedDescription)")
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
PhoneAuthProvider.provider().verifyPhoneNumber(
kPhoneSecondFactorPhoneNumber,
uiDelegate: nil,
multiFactorSession: session) { (verificationId, error) in
if (error != nil) {
XCTFail(String(format: "Verify phone number failed. Error: %@", error!.localizedDescription))
}
XCTAssertNil(error, "Verify phone number failed. Error: \(error!.localizedDescription)")
let credential = PhoneAuthProvider.provider().credential(
withVerificationID: verificationId!,
verificationCode: kPhoneSecondFactorVerificationCode)
let assertion = PhoneMultiFactorGenerator.assertion(with: credential);
user?.multiFactor.enroll(with: assertion, displayName: kPhoneSecondFactorDisplayName) { (error) in
if (error != nil) {
XCTFail(String(format: "Phone multi factor enroll failed. Error: %@", error!.localizedDescription))
}
XCTAssertNil(error, "Phone multi factor enroll failed. Error: \(error!.localizedDescription)")
XCTAssertEqual(Auth.auth().currentUser?.multiFactor.enrolledFactors.first?.displayName, kPhoneSecondFactorDisplayName)
enrollExpectation.fulfill()

// Unenroll
user = Auth.auth().currentUser
user?.multiFactor.unenroll(with: (user?.multiFactor.enrolledFactors.first)!, completion: { (error) in
XCTAssertNil(String(format: "Phone multi factor unenroll failed. Error: %@", error!.localizedDescription))
XCTAssertNil("Phone multi factor unenroll failed. Error: \(error!.localizedDescription)")
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
XCTAssertEqual(Auth.auth().currentUser?.multiFactor.enrolledFactors.count, 0)
unenrollExpectation.fulfill()
})
Expand All @@ -71,9 +67,7 @@ class PhoneMultiFactorTests: FIRAuthApiTestsBase {
}

self.waitForExpectations(timeout: 30) { error in
if error != nil {
XCTFail(String(format: "Failed to wait for enroll and unenroll phone multi factor finished. Error: %@", error!.localizedDescription))
}
XCTAssertNil(error, "Failed to wait for enroll and unenroll phone multi factor finished. Error: \(error!.localizedDescription)")
}
}

Expand All @@ -82,34 +76,28 @@ class PhoneMultiFactorTests: FIRAuthApiTestsBase {
Auth.auth().signIn(withEmail: kOneSecondFactorUserEmail, password: kOneSecondFactorUserPassword) { (result, error) in
// SignIn
guard let error = error, error.code == AuthErrorCode.secondFactorRequired.rawValue else {
XCTFail(String(format: "User sign in returns wrong error. Error: %@", error!.localizedDescription))
XCTFail("User sign in returns wrong error. Error: \(error!.localizedDescription)")
}
let resolver = authError!.userInfo["FIRAuthErrorUserInfoMultiFactorResolverKey"] as! MultiFactorResolver
let resolver = error.userInfo["FIRAuthErrorUserInfoMultiFactorResolverKey"] as! MultiFactorResolver
let hint = resolver.hints.first as! PhoneMultiFactorInfo
PhoneAuthProvider.provider().verifyPhoneNumber(
with: hint,
uiDelegate: nil,
multiFactorSession: resolver.session) { (verificationId, error) in
if error != nil {
XCTFail(String(format: "Failed to verify phone number. Error: %@", error!.localizedDescription))
}
XCTAssertNil(error, "Failed to verify phone number. Error: \(error!.localizedDescription)")
let credential = PhoneAuthProvider.provider().credential(
withVerificationID: verificationId!,
verificationCode: kPhoneSecondFactorVerificationCode)
let assertion = PhoneMultiFactorGenerator.assertion(with: credential);
resolver.resolveSignIn(with: assertion) { (authResult, error) in
if error != nil {
XCTFail(String(format: "Failed to sign in with phone multi factor. Error: %@", error!.localizedDescription))
}
XCTAssertNil(error, "Failed to sign in with phone multi factor. Error: \(error!.localizedDescription)")
signInExpectation.fulfill()
}
}
}

self.waitForExpectations(timeout: 300) { error in
if error != nil {
XCTFail(String(format: "Failed to wait for enroll and unenroll phone multi factor finished. Error: %@", error!.localizedDescription))
}
XCTAssertNil(error, "Failed to wait for enroll and unenroll phone multi factor finished. Error: \(error!.localizedDescription)")
}
}
}
75 changes: 75 additions & 0 deletions Firebase/Auth/Source/Auth/FIRAuth.m
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,34 @@
*/
static NSMutableDictionary *gKeychainServiceNameForAppName;

/** @fn FIRAuthParseURL:NSString
@brief Parses an incoming URL into all available query items.
@param urlString The url to be parsed.
@return A dictionary of available query items in the target URL.
*/
static NSDictionary<NSString *, NSString *> *FIRAuthParseURL(NSString *urlString) {
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
NSString *linkURL = [NSURLComponents componentsWithString:urlString].query;
if (!linkURL) {
return @{};
}
NSArray<NSString *> *URLComponents = [linkURL componentsSeparatedByString:@"&"];
NSMutableDictionary<NSString *, NSString *> *queryItems =
[[NSMutableDictionary alloc] initWithCapacity:URLComponents.count];
for (NSString *component in URLComponents) {
NSRange equalRange = [component rangeOfString:@"="];
if (equalRange.location != NSNotFound) {
NSString *queryItemKey =
[[component substringToIndex:equalRange.location] stringByRemovingPercentEncoding];
NSString *queryItemValue =
[[component substringFromIndex:equalRange.location + 1] stringByRemovingPercentEncoding];
if (queryItemKey && queryItemValue) {
queryItems[queryItemKey] = queryItemValue;
}
}
}
return queryItems;
}

#pragma mark - FIRActionCodeInfo

@interface FIRActionCodeInfo ()
Expand Down Expand Up @@ -264,6 +292,53 @@ + (FIRActionCodeOperation)actionCodeOperationForRequestType:(NSString *)requestT

@end

#pragma mark - FIRActionCodeURL

@implementation FIRActionCodeURL

+ (nullable instancetype)actionCodeURLWithLink:(NSString *)link {
NSDictionary<NSString *, NSString *> *queryItems = FIRAuthParseURL(link);
if (!queryItems.count) {
NSURLComponents *urlComponents = [NSURLComponents componentsWithString:link];
queryItems = FIRAuthParseURL(urlComponents.query);
}
if (!queryItems.count) {
return nil;
}
NSString *APIKey = queryItems[@"apiKey"];
NSString *actionCode = queryItems[@"oobCode"];
NSString *continueURLString = queryItems[@"continueUrl"];
NSString *languageCode = queryItems[@"languageCode"];
NSString *mode = queryItems[@"mode"];
NSString *tenantID = queryItems[@"tenantID"];
Comment on lines +308 to +313
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these constants defined anywhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope.

return [[FIRActionCodeURL alloc] initWithAPIKey:APIKey
actionCode:actionCode
continueURLString:continueURLString
languageCode:languageCode
mode:mode
tenantID:tenantID];
}

- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
actionCode:(NSString *)actionCode
continueURLString:(NSString *)continueURLString
languageCode:(NSString *)languageCode
mode:(NSString *)mode
tenantID:(NSString *)tenantID {

self = [super init];
if (self) {
_APIKey = APIKey;
_operation = [FIRActionCodeInfo actionCodeOperationForRequestType:mode];
_code = actionCode;
_continueURL = [NSURL URLWithString:continueURLString];
_languageCode = languageCode;
}
return self;
}

@end

#pragma mark - FIRAuth

#if TARGET_OS_IOS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ - (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
FIRStartMfaSignInRequest *request =
[[FIRStartMfaSignInRequest alloc] initWithMfaProvider:multiFactorProvider
mfaPendingCredential:session.mfaPendingCredential
mfaEnrollmentId:session.multiFactorInfo.uid
mfaEnrollmentID:session.multiFactorInfo.uid
signInInfo:startMfaRequestInfo
requestConfiguration:self->_auth.requestConfiguration];
[FIRAuthBackend startMultiFactorSignIn:request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ NS_ASSUME_NONNULL_BEGIN

@property(nonatomic, copy, readonly, nullable) NSString *mfaPendingCredential;

@property(nonatomic, copy, readonly, nullable) NSString *mfaEnrollmentId;
@property(nonatomic, copy, readonly, nullable) NSString *mfaEnrollmentID;

@property(nonatomic, copy, readonly, nullable) FIRAuthProtoStartMfaPhoneRequestInfo *signInInfo;

- (nullable instancetype)initWithMfaProvider:(NSString *)mfaProvider
mfaPendingCredential:(NSString *)mfaPendingCredential
mfaEnrollmentId:(NSString *)mfaEnrollmentId
mfaEnrollmentID:(NSString *)mfaEnrollmentID
signInInfo:(FIRAuthProtoStartMfaPhoneRequestInfo *)signInInfo
requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ @implementation FIRStartMfaSignInRequest

- (nullable instancetype)initWithMfaProvider:(NSString *)mfaProvider
mfaPendingCredential:(NSString *)mfaPendingCredential
mfaEnrollmentId:(NSString *)mfaEnrollmentId
mfaEnrollmentID:(NSString *)mfaEnrollmentID
signInInfo:(FIRAuthProtoStartMfaPhoneRequestInfo *)signInInfo
requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration {
self = [super initWithEndpoint:kStartMfaSignInEndPoint
Expand All @@ -32,7 +32,7 @@ - (nullable instancetype)initWithMfaProvider:(NSString *)mfaProvider
if (self) {
_mfaProvider = mfaProvider;
_mfaPendingCredential = mfaPendingCredential;
_mfaEnrollmentId = mfaEnrollmentId;
_mfaEnrollmentID = mfaEnrollmentID;
_signInInfo = signInInfo;
}
return self;
Expand All @@ -46,8 +46,8 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nul
if (_mfaPendingCredential) {
postBody[@"mfaPendingCredential"] = _mfaPendingCredential;
}
if (_mfaEnrollmentId) {
postBody[@"mfaEnrollmentId"] = _mfaEnrollmentId;
if (_mfaEnrollmentID) {
postBody[@"mfaEnrollmentId"] = _mfaEnrollmentID;
}
if (_signInInfo) {
if ([_signInInfo isKindOfClass:[FIRAuthProtoStartMfaPhoneRequestInfo class]]) {
Expand Down
56 changes: 50 additions & 6 deletions Firebase/Auth/Source/Public/FIRAuth.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@
@class FIRAuthCredential;
@class FIRAuthDataResult;
@class FIRAuthSettings;
#if TARGET_OS_IOS
@class FIRMultiFactorInfo;
#endif
@class FIRUser;
@protocol FIRAuthStateListener;
@protocol FIRAuthUIDelegate;
Expand Down Expand Up @@ -180,7 +177,8 @@ typedef void (^FIRVerifyPasswordResetCodeCallback)(NSString *_Nullable email,
typedef void (^FIRApplyActionCodeCallback)(NSError *_Nullable error)
NS_SWIFT_NAME(ApplyActionCodeCallback);

typedef void (^FIRAuthVoidErrorCallback)(NSError *_Nullable);
typedef void (^FIRAuthVoidErrorCallback)(NSError *_Nullable)
NS_SWIFT_NAME(AuthVoidErrorCallback);

/**
@brief Deprecated. Please directly use email or previousEmail properties instead.
Expand Down Expand Up @@ -220,10 +218,10 @@ typedef NS_ENUM(NSInteger, FIRActionCodeOperation) {
/** Action code for email link operation. */
FIRActionCodeOperationEmailLink = 4,

/** Action code for verifing and changing email*/
/** Action code for verifing and changing email */
FIRActionCodeOperationVerifyAndChangeEmail = 5,

/** Action code for reverting second factor addition*/
/** Action code for reverting second factor addition */
FIRActionCodeOperationRevertSecondFactorAddition = 6,

} NS_SWIFT_NAME(ActionCodeOperation);
Expand Down Expand Up @@ -257,6 +255,52 @@ DEPRECATED_MSG_ATTRIBUTE("Please directly use email or previousEmail properties

@end

/** @class FIRActionCodeURL
@brief This class will allow developers to easily extract information about out of band links.
*/
NS_SWIFT_NAME(FIRActionCodeURL)
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
@interface FIRActionCodeURL : NSObject

/** @property APIKey
@brief Returns the API key from the link. nil, if not provided.
*/
@property(nonatomic, nullable, copy, readonly) NSString *APIKey;

/** @property operation
@brief Returns the mode of oob action. The property will be of FIRActionCodeOperation type.
It will return FIRActionCodeOperationUnknown if no oob action is provided.
*/
@property(nonatomic, readonly) FIRActionCodeOperation operation;

/** @property code
@brief Returns the email action code from the link. nil, if not provided.
*/
@property(nonatomic, nullable, copy, readonly) NSString *code;

/** @property continueURL
@brief Returns the continue URL from the link. nil, if not provided.
*/
@property(nonatomic, nullable, copy, readonly) NSURL *continueURL;

/** @property languageCode
@brief Returns the language code from the link. nil, if not provided.
*/
@property(nonatomic, nullable, copy, readonly) NSString *languageCode;

/** @fn actionCodeURLWithLink:
@brief Construct an FIRActionCodeURL from an oob link.
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
@param link The oob link string used to construct the action code URL.
@return The FIRActionCodeURL object constructed based on the oob link provided.
*/
+ (nullable instancetype)actionCodeURLWithLink:(NSString *)link;

/** @fn init
@brief please use +[FIRActionCodeURL actionCodeURLWithLink:] instead.
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
*/
- (instancetype)init NS_UNAVAILABLE;

@end

/** @typedef FIRCheckActionCodeCallBack
@brief The type of block invoked when performing a check action code operation.

Expand Down
14 changes: 13 additions & 1 deletion Firebase/Auth/Source/Public/FIRMultiFactor.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,22 @@

NS_ASSUME_NONNULL_BEGIN

typedef void (^FIRMultiFactorSessionCallback)(FIRMultiFactorSession *_Nullable, NSError *_Nullable);
/** @typedef FIRMultiFactorSessionCallback
@brief The callback that triggered when a developer calls `getSessionWithCompletion`.
@param session The multi factor session returned, if any.
@param error The error which occurred, if any.
*/
typedef void (^FIRMultiFactorSessionCallback)(FIRMultiFactorSession *_Nullable session, NSError *_Nullable error)
NS_SWIFT_NAME(MultiFactorSessionCallback);

/**
@brief The string identifier for second factors. e.g. “phone”.
*/
extern NSString *const _Nonnull FIRPhoneMultiFactorID NS_SWIFT_NAME(PhoneMultiFactorID);
renkelvin marked this conversation as resolved.
Show resolved Hide resolved

/** @class FIRMultiFactor
@brief The interface defining the multi factor related properties and operations pertaining to a user.
*/
NS_SWIFT_NAME(MultiFactor)
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
@interface FIRMultiFactor : NSObject

Expand Down
3 changes: 3 additions & 0 deletions Firebase/Auth/Source/Public/FIRMultiFactorAssertion.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

NS_ASSUME_NONNULL_BEGIN

/** @class FIRMultiFactorAssertion
@brief The base class for asserting ownership of a second factor. This is equivalent to the AuthCredential class.
*/
NS_SWIFT_NAME(MultiFactorAssertion)
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
@interface FIRMultiFactorAssertion : NSObject

Expand Down
3 changes: 3 additions & 0 deletions Firebase/Auth/Source/Public/FIRMultiFactorInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

NS_ASSUME_NONNULL_BEGIN

/** @class FIRMultiFactorInfo
@brief Safe public structure used to represent a second factor entity from a client perspective.
*/
NS_SWIFT_NAME(MultiFactorInfo)
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
@interface FIRMultiFactorInfo : NSObject

Expand Down
4 changes: 4 additions & 0 deletions Firebase/Auth/Source/Public/FIRMultiFactorResolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

NS_ASSUME_NONNULL_BEGIN

/** @class FIRMultiFactorResolver
@brief The data structure used to help developers resolve 2nd factor requirements on users that
have opted in to 2 factor authentication.
*/
NS_SWIFT_NAME(MultiFactorResolver)
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
@interface FIRMultiFactorResolver : NSObject

Expand Down
4 changes: 4 additions & 0 deletions Firebase/Auth/Source/Public/FIRMultiFactorSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

NS_ASSUME_NONNULL_BEGIN

/** @class FIRMultiFactorSession
@brief Opaque object that identifies the current session to enroll a second factor or to
complete sign in when previously enrolled.
*/
NS_SWIFT_NAME(MultiFactorSession)
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
@interface FIRMultiFactorSession : NSObject

Expand Down
2 changes: 2 additions & 0 deletions Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ NS_SWIFT_NAME(PhoneAuthProvider)

/** @fn verifyPhoneNumber:UIDelegate:multiFactorSession:completion:
@brief Verify ownership of the second factor phone number by the current user.
@param phoneNumber The phone number to be verified.
@param UIDelegate An object used to present the SFSafariViewController. The object is retained
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
by this method until the completion block is executed.
@param session A session to identify the MFA flow. For enrollment, this identifies the user
Expand All @@ -101,6 +102,7 @@ NS_SWIFT_NAME(PhoneAuthProvider)

/** @fn verifyPhoneNumberWithMultiFactorInfo:UIDelegate:multiFactorSession:completion:
@brief Verify ownership of the second factor phone number by the current user.
@param phoneMultiFactorInfo The phone multi factor whose number need to be verified.
@param UIDelegate An object used to present the SFSafariViewController. The object is retained
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
by this method until the completion block is executed.
@param session A session to identify the MFA flow. For enrollment, this identifies the user
Expand Down
Loading