Skip to content

feat: Add login with additional auth data #1858

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
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
56 changes: 56 additions & 0 deletions Parse/Tests/Unit/UserControllerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,62 @@ - (void)testLogInCurrentUserWithUsernamePassword {
OCMVerifyAll(currentUserController);
}

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

id commandResult = @{ @"objectId" : @"a",
@"yarr" : @1,
@"authData" : @{ @"mfa" : @{ @"status" : @"enabled" }, @"other" : @{ @"k" : @"v" } } };
[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);
// Assert transient MFA data was not persisted on PFUser (via public API)
XCTAssertFalse([user isLinkedWithAuthType:@"mfa"]);
// Non-MFA auth providers should still be present if provided
XCTAssertTrue([user isLinkedWithAuthType:@"other"]);
[expectation fulfill];
return nil;
}];
[self waitForTestExpectations];

OCMVerifyAll(currentUserController);
}

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