Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions Parse/Parse/Internal/Commands/PFRESTUserCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ NS_ASSUME_NONNULL_BEGIN
password:(NSString *)password
revocableSession:(BOOL)revocableSessionEnabled
error:(NSError **)error;
/**
Creates a login command with a JSON body, allowing additional parameters such as authData.

This posts to the login route and is required for features like MFA where additional
authentication data must be supplied alongside username/password.
*/
+ (instancetype)logInUserCommandWithParameters:(NSDictionary *)parameters
revocableSession:(BOOL)revocableSessionEnabled
error:(NSError **)error;
+ (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType
authenticationData:(NSDictionary *)authenticationData
revocableSession:(BOOL)revocableSessionEnabled
Expand Down
12 changes: 12 additions & 0 deletions Parse/Parse/Internal/Commands/PFRESTUserCommand.m
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ + (instancetype)logInUserCommandWithUsername:(NSString *)username
error:error];
}

+ (instancetype)logInUserCommandWithParameters:(NSDictionary *)parameters
revocableSession:(BOOL)revocableSessionEnabled
error:(NSError **)error {
// Use POST /login for body parameters like authData
return [self _commandWithHTTPPath:@"login"
httpMethod:PFHTTPRequestMethodPOST
parameters:parameters
sessionToken:nil
revocableSession:revocableSessionEnabled
error:error];
}

+ (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType
authenticationData:(NSDictionary *)authenticationData
revocableSession:(BOOL)revocableSessionEnabled
Expand Down
8 changes: 8 additions & 0 deletions Parse/Parse/Internal/User/Controller/PFUserController.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ NS_ASSUME_NONNULL_BEGIN
- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
password:(NSString *)password
revocableSession:(BOOL)revocableSession;
/**
Logs in the current user using username/password and additional parameters such as authData.
The parameters dictionary can include keys like @"authData": @{ "mfa": @{ ... } } to support MFA flows.
*/
- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
password:(NSString *)password
parameters:(nullable NSDictionary *)parameters
revocableSession:(BOOL)revocableSession;

//TODO: (nlutsenko) Move this method into PFUserAuthenticationController after PFUser is decoupled further.
- (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType
Expand Down
55 changes: 54 additions & 1 deletion Parse/Parse/Internal/User/Controller/PFUserController.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,20 @@ - (BFTask *)logInCurrentUserAsyncWithSessionToken:(NSString *)sessionToken {
message:@"Invalid Session Token."]];
}

PFUser *user = [PFUser _objectFromDictionary:dictionary
// Sanitize response: do not persist transient MFA authData provider
NSMutableDictionary *sanitized = [dictionary mutableCopy];
id authData = sanitized[@"authData"];
if ([authData isKindOfClass:[NSDictionary class]] && authData[@"mfa"]) {
NSMutableDictionary *mutableAuth = [authData mutableCopy];
[mutableAuth removeObjectForKey:@"mfa"]; // transient provider, do not persist
if (mutableAuth.count > 0) {
sanitized[@"authData"] = mutableAuth;
} else {
[sanitized removeObjectForKey:@"authData"];
}
}

PFUser *user = [PFUser _objectFromDictionary:sanitized
defaultClassName:[PFUser parseClassName]
completeData:YES];
// Serialize the object to disk so we can later access it via currentUser
Expand Down Expand Up @@ -113,6 +126,46 @@ - (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
}];
}

- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
password:(NSString *)password
parameters:(NSDictionary *)parameters
revocableSession:(BOOL)revocableSession {
@weakify(self);
return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{
NSError *error = nil;
NSMutableDictionary *merged = [@{ @"username": username ?: @"",
@"password": password ?: @"" } mutableCopy];
if (parameters.count > 0) {
// Prevent authData from being persisted later by only sending it with the request body
// and not mutating the PFUser object here. The server response will drive authData merge.
[merged addEntriesFromDictionary:parameters];
}
PFRESTCommand *command = [PFRESTUserCommand logInUserCommandWithParameters:merged
revocableSession:revocableSession
error:&error];
PFPreconditionReturnFailedTask(command, error);
return [self.commonDataSource.commandRunner runCommandAsync:command
withOptions:PFCommandRunningOptionRetryIfFailed];
}] continueWithSuccessBlock:^id(BFTask *task) {
@strongify(self);
PFCommandResult *result = task.result;
NSDictionary *dictionary = result.result;

if ([dictionary isKindOfClass:[NSNull class]] || !dictionary) {
return [BFTask taskWithError:[PFErrorUtilities errorWithCode:kPFErrorObjectNotFound
message:@"Invalid login credentials."]];
}

PFUser *user = [PFUser _objectFromDictionary:dictionary
defaultClassName:[PFUser parseClassName]
completeData:YES];
PFCurrentUserController *controller = self.coreDataSource.currentUserController;
return [[controller saveCurrentObjectAsync:user] continueWithBlock:^id(BFTask *task) {
return user;
}];
}];
}

- (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType
authData:(NSDictionary *)authData
revocableSession:(BOOL)revocableSession {
Expand Down
18 changes: 18 additions & 0 deletions Parse/Parse/Source/PFUser.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ typedef void(^PFUserLogoutResultBlock)(NSError *_Nullable error);
*/
+ (void)logInWithUsernameInBackground:(NSString *)username password:(NSString *)password block:(nullable PFUserResultBlock)block;

/**
Logs in a user with username and password and additional authentication data (e.g., MFA).

The authData keys must follow the Parse Server spec, for example:
@{ @"mfa": @{ @"token": authCode } }

This data is only sent as part of the login request and is not persisted on the PFUser instance.
*/
+ (BFTask<__kindof PFUser *> *)logInWithUsernameInBackground:(NSString *)username
password:(NSString *)password
authData:(nullable NSDictionary<NSString *, id> *)authData;

/** Block variant of login with additional authData. */
+ (void)logInWithUsernameInBackground:(NSString *)username
password:(NSString *)password
authData:(nullable NSDictionary<NSString *, id> *)authData
block:(nullable PFUserResultBlock)block;

///--------------------------------------
#pragma mark - Becoming a User
///--------------------------------------
Expand Down
32 changes: 32 additions & 0 deletions Parse/Parse/Source/PFUser.m
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,12 @@ - (void)_mergeFromServerWithResult:(NSDictionary *)result decoder:(PFDecoder *)d
// Merge the linked service metadata
NSDictionary *newAuthData = [decoder decodeObject:result[PFUserAuthDataRESTKey]];
if (newAuthData) {
// Remove transient MFA auth provider from persisted state
if ([newAuthData isKindOfClass:[NSDictionary class]] && newAuthData[@"mfa"]) {
NSMutableDictionary *mutable = [newAuthData mutableCopy];
[mutable removeObjectForKey:@"mfa"];
newAuthData = [mutable copy];
}
[self.authData removeAllObjects];
[self.linkedServiceNames removeAllObjects];
[newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id linkData, BOOL *stop) {
Expand Down Expand Up @@ -646,6 +652,12 @@ - (BOOL)mergeFromRESTDictionary:(NSDictionary *)object withDecoder:(PFDecoder *)

if (object[PFUserAuthDataRESTKey] != nil) {
NSDictionary *newAuthData = object[PFUserAuthDataRESTKey];
// Remove transient MFA auth provider from persisted state
if ([newAuthData isKindOfClass:[NSDictionary class]] && newAuthData[@"mfa"]) {
NSMutableDictionary *mutable = [newAuthData mutableCopy];
[mutable removeObjectForKey:@"mfa"];
newAuthData = [mutable copy];
}
[newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
self.authData[key] = obj;
if (obj != nil) {
Expand Down Expand Up @@ -838,6 +850,26 @@ + (void)logInWithUsernameInBackground:(NSString *)username
[[self logInWithUsernameInBackground:username password:password] thenCallBackOnMainThreadAsync:block];
}

+ (BFTask<__kindof PFUser *> *)logInWithUsernameInBackground:(NSString *)username
password:(NSString *)password
authData:(NSDictionary<NSString *,id> *)authData {
NSDictionary *parameters = nil;
if (authData.count > 0) {
parameters = @{ @"authData": authData };
}
return [[self userController] logInCurrentUserAsyncWithUsername:username
password:password
parameters:parameters
revocableSession:[self _isRevocableSessionEnabled]];
}

+ (void)logInWithUsernameInBackground:(NSString *)username
password:(NSString *)password
authData:(NSDictionary<NSString *,id> *)authData
block:(PFUserResultBlock)block {
[[self logInWithUsernameInBackground:username password:password authData:authData] thenCallBackOnMainThreadAsync:block];
}

///--------------------------------------
#pragma mark - Third-party Authentication
///--------------------------------------
Expand Down
19 changes: 19 additions & 0 deletions Parse/Tests/Unit/UserCommandTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ - (void)testLogInCommand {
XCTAssertFalse(command.revocableSessionEnabled);
}

- (void)testLogInCommandWithParametersBody {
NSDictionary *params = @{ @"username": @"a",
@"password": @"b",
@"authData": @{ @"mfa": @{ @"token": @"123456" } } };
PFRESTUserCommand *command = [PFRESTUserCommand logInUserCommandWithParameters:params
revocableSession:YES
error:nil];
XCTAssertNotNil(command);
XCTAssertEqualObjects(command.httpPath, @"login");
XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST);
XCTAssertNotNil(command.parameters);
XCTAssertEqualObjects(command.parameters[@"username"], @"a");
XCTAssertEqualObjects(command.parameters[@"password"], @"b");
XCTAssertEqualObjects(command.parameters[@"authData"], (@{ @"mfa": @{ @"token": @"123456" } }));
XCTAssertEqual(command.additionalRequestHeaders.count, 1);
XCTAssertTrue(command.revocableSessionEnabled);
XCTAssertNil(command.sessionToken);
}

- (void)testServiceLoginCommandWithAuthTypeData {
PFRESTUserCommand *command = [PFRESTUserCommand serviceLoginUserCommandWithAuthenticationType:@"a"
authenticationData:@{ @"b" : @"c" }
Expand Down
51 changes: 51 additions & 0 deletions Parse/Tests/Unit/UserControllerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,57 @@ - (void)testLogInCurrentUserWithUsernamePassword {
OCMVerifyAll(currentUserController);
}

- (void)testLogInCurrentUserWithUsernamePasswordAndAuthData {
id commonDataSource = [self mockedCommonDataSource];
id coreDataSource = [self mockedCoreDataSource];
id commandRunner = [commonDataSource commandRunner];

id commandResult = @{ @"objectId" : @"a",
@"yarr" : @1 };
[commandRunner mockCommandResult:commandResult forCommandsPassingTest:^BOOL(id obj) {
PFRESTCommand *command = obj;

XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST);
XCTAssertNotEqual([command.httpPath rangeOfString:@"login"].location, NSNotFound);
XCTAssertNil(command.sessionToken);
NSDictionary *expected = @{ @"username": @"yolo",
@"password": @"yarr",
@"authData": @{ @"mfa": @{ @"token": @"654321" } } };
XCTAssertEqualObjects(command.parameters, expected);
XCTAssertEqualObjects(command.additionalRequestHeaders, @{ @"X-Parse-Revocable-Session" : @"1" });

return YES;
}];

__block PFUser *savedUser = nil;

id currentUserController = [coreDataSource currentUserController];
[OCMExpect([currentUserController saveCurrentObjectAsync:[OCMArg checkWithBlock:^BOOL(id obj) {
savedUser = obj;
return (savedUser != nil);
}]]) andReturn:[BFTask taskWithResult:nil]];

PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource
coreDataSource:coreDataSource];

XCTestExpectation *expectation = [self currentSelectorTestExpectation];
NSDictionary *params = @{ @"authData": @{ @"mfa": @{ @"token": @"654321" } } };
[[controller logInCurrentUserAsyncWithUsername:@"yolo"
password:@"yarr"
parameters:params
revocableSession:YES] continueWithBlock:^id(BFTask *task) {
PFUser *user = task.result;
XCTAssertNotNil(user);
XCTAssertEqualObjects(user.objectId, @"a");
XCTAssertEqualObjects(user[@"yarr"], @1);
[expectation fulfill];
return nil;
}];
[self waitForTestExpectations];

OCMVerifyAll(currentUserController);
}

- (void)testLogInCurrentUserWithUsernamePasswordNullResult {
id commonDataSource = [self mockedCommonDataSource];
id coreDataSource = [self mockedCoreDataSource];
Expand Down