From 13deb1144c8180904bcd7f4218cd9bcf55139292 Mon Sep 17 00:00:00 2001 From: Steffen Keller Date: Tue, 4 Mar 2025 03:23:13 +0100 Subject: [PATCH 1/2] feat: Add login with additional auth data --- .../Internal/Commands/PFRESTUserCommand.h | 5 +++ .../Internal/Commands/PFRESTUserCommand.m | 20 +++++++++++ .../User/Controller/PFUserController.h | 4 +++ .../User/Controller/PFUserController.m | 28 ++++++++++++++++ Parse/Parse/Source/PFUser.h | 33 +++++++++++++++++++ Parse/Parse/Source/PFUser.m | 24 +++++++++++++- 6 files changed, 113 insertions(+), 1 deletion(-) diff --git a/Parse/Parse/Internal/Commands/PFRESTUserCommand.h b/Parse/Parse/Internal/Commands/PFRESTUserCommand.h index 8e2eb0e1c..40eb275d5 100644 --- a/Parse/Parse/Internal/Commands/PFRESTUserCommand.h +++ b/Parse/Parse/Internal/Commands/PFRESTUserCommand.h @@ -23,6 +23,11 @@ NS_ASSUME_NONNULL_BEGIN password:(NSString *)password revocableSession:(BOOL)revocableSessionEnabled error:(NSError **)error; ++ (instancetype)logInUserCommandWithUsername:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData + revocableSession:(BOOL)revocableSessionEnabled + error:(NSError **)error; + (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType authenticationData:(NSDictionary *)authenticationData revocableSession:(BOOL)revocableSessionEnabled diff --git a/Parse/Parse/Internal/Commands/PFRESTUserCommand.m b/Parse/Parse/Internal/Commands/PFRESTUserCommand.m index b5126c447..f3781d941 100644 --- a/Parse/Parse/Internal/Commands/PFRESTUserCommand.m +++ b/Parse/Parse/Internal/Commands/PFRESTUserCommand.m @@ -65,6 +65,26 @@ + (instancetype)logInUserCommandWithUsername:(NSString *)username error:error]; } ++ (instancetype)logInUserCommandWithUsername:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData + revocableSession:(BOOL)revocableSessionEnabled + error:(NSError **) error { + NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:@{ + @"username" : username, + @"password" : password, + @"authData" : authData + }]; + + return [self _commandWithHTTPPath:@"login" + httpMethod:PFHTTPRequestMethodGET + parameters:parameters + sessionToken:nil + revocableSession:revocableSessionEnabled + error:error]; +} + + + (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType authenticationData:(NSDictionary *)authenticationData revocableSession:(BOOL)revocableSessionEnabled diff --git a/Parse/Parse/Internal/User/Controller/PFUserController.h b/Parse/Parse/Internal/User/Controller/PFUserController.h index 6b55f6815..b1bddbb43 100644 --- a/Parse/Parse/Internal/User/Controller/PFUserController.h +++ b/Parse/Parse/Internal/User/Controller/PFUserController.h @@ -38,6 +38,10 @@ NS_ASSUME_NONNULL_BEGIN - (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username password:(NSString *)password revocableSession:(BOOL)revocableSession; +- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData + revocableSession:(BOOL)revocableSession; //TODO: (nlutsenko) Move this method into PFUserAuthenticationController after PFUser is decoupled further. - (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType diff --git a/Parse/Parse/Internal/User/Controller/PFUserController.m b/Parse/Parse/Internal/User/Controller/PFUserController.m index 6b75673be..b79e88601 100644 --- a/Parse/Parse/Internal/User/Controller/PFUserController.m +++ b/Parse/Parse/Internal/User/Controller/PFUserController.m @@ -141,6 +141,34 @@ - (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType }]; } +- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData + revocableSession:(BOOL)revocableSession { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + NSError *error; + PFRESTCommand *command = [PFRESTUserCommand logInUserCommandWithUsername:username + password:password + authData:authData + revocableSession:revocableSession + error:&error]; + PFPreconditionReturnFailedTask(command, error); + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + PFUser *user = [PFUser _objectFromDictionary:result.result + defaultClassName:[PFUser parseClassName] + completeData:YES]; + @synchronized ([user lock]) { + [user startSave]; + return [user _handleServiceLoginCommandResult:result]; + } + }]; +} + ///-------------------------------------- #pragma mark - Reset Password ///-------------------------------------- diff --git a/Parse/Parse/Source/PFUser.h b/Parse/Parse/Source/PFUser.h index 4338ec19c..b69d0e77e 100644 --- a/Parse/Parse/Source/PFUser.h +++ b/Parse/Parse/Source/PFUser.h @@ -167,6 +167,39 @@ typedef void(^PFUserLogoutResultBlock)(NSError *_Nullable error); */ + (void)logInWithUsernameInBackground:(NSString *)username password:(NSString *)password block:(nullable PFUserResultBlock)block; +/** + Makes an *asynchronous* request to login a user with specified credentials and additional data. + + Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to `+currentUser` will use the latest logged in user. + + @param username The username of the user. + @param password The password of the user. + @param authData Additional data to be sent with the login request. + + @return The task, that encapsulates the work being done. + */ ++ (BFTask<__kindof PFUser *> *)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData; + +/** + Makes an *asynchronous* request to log in a user with specified credentials and additional data. + + Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to `+currentUser` will use the latest logged in user. + + @param username The username of the user. + @param password The password of the user. + @param authData Additional data to be sent with the login request. + @param block The block to execute. + It should have the following argument signature: `^(PFUser *user, NSError *error)`. + */ ++ (void)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData + block:(nullable PFUserResultBlock)block; + ///-------------------------------------- #pragma mark - Becoming a User ///-------------------------------------- diff --git a/Parse/Parse/Source/PFUser.m b/Parse/Parse/Source/PFUser.m index 8e0787a33..bd064053f 100644 --- a/Parse/Parse/Source/PFUser.m +++ b/Parse/Parse/Source/PFUser.m @@ -184,7 +184,13 @@ - (NSMutableDictionary *)_convertToDictionaryForSaving:(PFOperationSet *)changes @synchronized([self lock]) { NSMutableDictionary *serialized = [super _convertToDictionaryForSaving:changes withObjectEncoder:encoder error:error]; if (self.authData.count > 0) { - serialized[PFUserAuthDataRESTKey] = [self.authData copy]; + NSMutableDictionary *authDataCopy = [self.authData mutableCopy]; + // Remove 'mfa' from authData to prevent it from being saved to the server. + // This ensures that MFA setup changes are not unintentionally triggered. + [authDataCopy removeObjectForKey:@"mfa"]; + if (authDataCopy.count > 0) { + serialized[PFUserAuthDataRESTKey] = [authDataCopy copy]; + } } return serialized; } @@ -838,6 +844,22 @@ + (void)logInWithUsernameInBackground:(NSString *)username [[self logInWithUsernameInBackground:username password:password] thenCallBackOnMainThreadAsync:block]; } ++ (BFTask *)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData { + return [[self userController] logInCurrentUserAsyncWithUsername:username + password:password + authData:authData + revocableSession:[self _isRevocableSessionEnabled]]; +} + ++ (void)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + authData:(NSDictionary *)authData + block:(PFUserResultBlock)block { + [[self logInWithUsernameInBackground:username password:password authData:authData] thenCallBackOnMainThreadAsync:block]; +} + ///-------------------------------------- #pragma mark - Third-party Authentication ///-------------------------------------- From 0270169795b2c98976af85595129e5daef6f1963 Mon Sep 17 00:00:00 2001 From: Steffen Keller Date: Tue, 4 Mar 2025 05:22:31 +0100 Subject: [PATCH 2/2] ci: Add tests for login with additional authData --- Parse/Tests/Unit/UserCommandTests.m | 29 ++++++++++++++++++ Parse/Tests/Unit/UserControllerTests.m | 41 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/Parse/Tests/Unit/UserCommandTests.m b/Parse/Tests/Unit/UserCommandTests.m index 688b0e83a..830a3aee1 100644 --- a/Parse/Tests/Unit/UserCommandTests.m +++ b/Parse/Tests/Unit/UserCommandTests.m @@ -41,6 +41,35 @@ - (void)testLogInCommand { XCTAssertFalse(command.revocableSessionEnabled); } +- (void)testLogInCommandWithAuthData { + NSDictionary *authData = @{ @"mfa" : @{ @"token" : @"000000" } }; + PFRESTUserCommand *command = [PFRESTUserCommand logInUserCommandWithUsername:@"a" + password:@"b" + authData:authData + revocableSession:YES + error:nil]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"login"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNotNil(command.parameters); + XCTAssertNotNil(command.parameters[@"username"]); + XCTAssertNotNil(command.parameters[@"password"]); + XCTAssertNotNil(command.parameters[@"authData"]); + XCTAssertEqualObjects(command.parameters[@"authData"], authData); + XCTAssertEqual(command.additionalRequestHeaders.count, 1); + XCTAssertTrue(command.revocableSessionEnabled); + XCTAssertNil(command.sessionToken); + + command = [PFRESTUserCommand logInUserCommandWithUsername:@"a" + password:@"b" + authData:authData + revocableSession:NO + error:nil]; + XCTAssertNotNil(command); + XCTAssertEqual(command.additionalRequestHeaders.count, 0); + XCTAssertFalse(command.revocableSessionEnabled); +} + - (void)testServiceLoginCommandWithAuthTypeData { PFRESTUserCommand *command = [PFRESTUserCommand serviceLoginUserCommandWithAuthenticationType:@"a" authenticationData:@{ @"b" : @"c" } diff --git a/Parse/Tests/Unit/UserControllerTests.m b/Parse/Tests/Unit/UserControllerTests.m index 7b76b051e..97a4e375e 100644 --- a/Parse/Tests/Unit/UserControllerTests.m +++ b/Parse/Tests/Unit/UserControllerTests.m @@ -219,6 +219,47 @@ - (void)testLogInCurrentUserWithUsernamePasswordNullResult { OCMVerifyAll(commandRunner); } +- (void)testLogInCurrentUserWithUsernamePasswordAuthData { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + id commandRunner = [commonDataSource commandRunner]; + + NSDictionary *authData = @{ @"mfa" : @{ @"token" : @"000000" } }; + id commandResult = @{ @"objectId" : @"a", + @"yarr" : @1 }; + [commandRunner mockCommandResult:commandResult forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNotEqual([command.httpPath rangeOfString:@"login"].location, NSNotFound); + XCTAssertNil(command.sessionToken); + XCTAssertEqualObjects(command.parameters, (@{ @"username" : @"yolo" , @"password" : @"yarr", @"authData" : authData })); + XCTAssertEqualObjects(command.additionalRequestHeaders, @{ @"X-Parse-Revocable-Session" : @"1" }); + + return YES; + }]; + + id currentUserController = [coreDataSource currentUserController]; + PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller logInCurrentUserAsyncWithUsername:@"yolo" + password:@"yarr" + authData:authData + 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)testRequestPasswordReset { id commonDataSource = [self mockedCommonDataSource]; id coreDataSource = [self mockedCoreDataSource];