Skip to content

Commit 60b22dd

Browse files
committed
feat: Add login with additional auth data
1 parent 6c3aff0 commit 60b22dd

File tree

8 files changed

+203
-1
lines changed

8 files changed

+203
-1
lines changed

Parse/Parse/Internal/Commands/PFRESTUserCommand.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ NS_ASSUME_NONNULL_BEGIN
2323
password:(NSString *)password
2424
revocableSession:(BOOL)revocableSessionEnabled
2525
error:(NSError **)error;
26+
/**
27+
Creates a login command with a JSON body, allowing additional parameters such as authData.
28+
29+
This posts to the login route and is required for features like MFA where additional
30+
authentication data must be supplied alongside username/password.
31+
*/
32+
+ (instancetype)logInUserCommandWithParameters:(NSDictionary *)parameters
33+
revocableSession:(BOOL)revocableSessionEnabled
34+
error:(NSError **)error;
2635
+ (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType
2736
authenticationData:(NSDictionary *)authenticationData
2837
revocableSession:(BOOL)revocableSessionEnabled

Parse/Parse/Internal/Commands/PFRESTUserCommand.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ + (instancetype)logInUserCommandWithUsername:(NSString *)username
6565
error:error];
6666
}
6767

68+
+ (instancetype)logInUserCommandWithParameters:(NSDictionary *)parameters
69+
revocableSession:(BOOL)revocableSessionEnabled
70+
error:(NSError **)error {
71+
// Use POST /login for body parameters like authData
72+
return [self _commandWithHTTPPath:@"login"
73+
httpMethod:PFHTTPRequestMethodPOST
74+
parameters:parameters
75+
sessionToken:nil
76+
revocableSession:revocableSessionEnabled
77+
error:error];
78+
}
79+
6880
+ (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType
6981
authenticationData:(NSDictionary *)authenticationData
7082
revocableSession:(BOOL)revocableSessionEnabled

Parse/Parse/Internal/User/Controller/PFUserController.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ NS_ASSUME_NONNULL_BEGIN
3838
- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
3939
password:(NSString *)password
4040
revocableSession:(BOOL)revocableSession;
41+
/**
42+
Logs in the current user using username/password and additional parameters such as authData.
43+
The parameters dictionary can include keys like @"authData": @{ "mfa": @{ ... } } to support MFA flows.
44+
*/
45+
- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
46+
password:(NSString *)password
47+
parameters:(nullable NSDictionary *)parameters
48+
revocableSession:(BOOL)revocableSession;
4149

4250
//TODO: (nlutsenko) Move this method into PFUserAuthenticationController after PFUser is decoupled further.
4351
- (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType

Parse/Parse/Internal/User/Controller/PFUserController.m

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,20 @@ - (BFTask *)logInCurrentUserAsyncWithSessionToken:(NSString *)sessionToken {
6666
message:@"Invalid Session Token."]];
6767
}
6868

69-
PFUser *user = [PFUser _objectFromDictionary:dictionary
69+
// Sanitize response: do not persist transient MFA authData provider
70+
NSMutableDictionary *sanitized = [dictionary mutableCopy];
71+
id authData = sanitized[@"authData"];
72+
if ([authData isKindOfClass:[NSDictionary class]] && authData[@"mfa"]) {
73+
NSMutableDictionary *mutableAuth = [authData mutableCopy];
74+
[mutableAuth removeObjectForKey:@"mfa"]; // transient provider, do not persist
75+
if (mutableAuth.count > 0) {
76+
sanitized[@"authData"] = mutableAuth;
77+
} else {
78+
[sanitized removeObjectForKey:@"authData"];
79+
}
80+
}
81+
82+
PFUser *user = [PFUser _objectFromDictionary:sanitized
7083
defaultClassName:[PFUser parseClassName]
7184
completeData:YES];
7285
// Serialize the object to disk so we can later access it via currentUser
@@ -113,6 +126,46 @@ - (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
113126
}];
114127
}
115128

129+
- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username
130+
password:(NSString *)password
131+
parameters:(NSDictionary *)parameters
132+
revocableSession:(BOOL)revocableSession {
133+
@weakify(self);
134+
return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{
135+
NSError *error = nil;
136+
NSMutableDictionary *merged = [@{ @"username": username ?: @"",
137+
@"password": password ?: @"" } mutableCopy];
138+
if (parameters.count > 0) {
139+
// Prevent authData from being persisted later by only sending it with the request body
140+
// and not mutating the PFUser object here. The server response will drive authData merge.
141+
[merged addEntriesFromDictionary:parameters];
142+
}
143+
PFRESTCommand *command = [PFRESTUserCommand logInUserCommandWithParameters:merged
144+
revocableSession:revocableSession
145+
error:&error];
146+
PFPreconditionReturnFailedTask(command, error);
147+
return [self.commonDataSource.commandRunner runCommandAsync:command
148+
withOptions:PFCommandRunningOptionRetryIfFailed];
149+
}] continueWithSuccessBlock:^id(BFTask *task) {
150+
@strongify(self);
151+
PFCommandResult *result = task.result;
152+
NSDictionary *dictionary = result.result;
153+
154+
if ([dictionary isKindOfClass:[NSNull class]] || !dictionary) {
155+
return [BFTask taskWithError:[PFErrorUtilities errorWithCode:kPFErrorObjectNotFound
156+
message:@"Invalid login credentials."]];
157+
}
158+
159+
PFUser *user = [PFUser _objectFromDictionary:dictionary
160+
defaultClassName:[PFUser parseClassName]
161+
completeData:YES];
162+
PFCurrentUserController *controller = self.coreDataSource.currentUserController;
163+
return [[controller saveCurrentObjectAsync:user] continueWithBlock:^id(BFTask *task) {
164+
return user;
165+
}];
166+
}];
167+
}
168+
116169
- (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType
117170
authData:(NSDictionary *)authData
118171
revocableSession:(BOOL)revocableSession {

Parse/Parse/Source/PFUser.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ typedef void(^PFUserLogoutResultBlock)(NSError *_Nullable error);
167167
*/
168168
+ (void)logInWithUsernameInBackground:(NSString *)username password:(NSString *)password block:(nullable PFUserResultBlock)block;
169169

170+
/**
171+
Logs in a user with username and password and additional authentication data (e.g., MFA).
172+
173+
The authData keys must follow the Parse Server spec, for example:
174+
@{ @"mfa": @{ @"token": authCode } }
175+
176+
This data is only sent as part of the login request and is not persisted on the PFUser instance.
177+
*/
178+
+ (BFTask<__kindof PFUser *> *)logInWithUsernameInBackground:(NSString *)username
179+
password:(NSString *)password
180+
authData:(nullable NSDictionary<NSString *, id> *)authData;
181+
182+
/** Block variant of login with additional authData. */
183+
+ (void)logInWithUsernameInBackground:(NSString *)username
184+
password:(NSString *)password
185+
authData:(nullable NSDictionary<NSString *, id> *)authData
186+
block:(nullable PFUserResultBlock)block;
187+
170188
///--------------------------------------
171189
#pragma mark - Becoming a User
172190
///--------------------------------------

Parse/Parse/Source/PFUser.m

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,12 @@ - (void)_mergeFromServerWithResult:(NSDictionary *)result decoder:(PFDecoder *)d
366366
// Merge the linked service metadata
367367
NSDictionary *newAuthData = [decoder decodeObject:result[PFUserAuthDataRESTKey]];
368368
if (newAuthData) {
369+
// Remove transient MFA auth provider from persisted state
370+
if ([newAuthData isKindOfClass:[NSDictionary class]] && newAuthData[@"mfa"]) {
371+
NSMutableDictionary *mutable = [newAuthData mutableCopy];
372+
[mutable removeObjectForKey:@"mfa"];
373+
newAuthData = [mutable copy];
374+
}
369375
[self.authData removeAllObjects];
370376
[self.linkedServiceNames removeAllObjects];
371377
[newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id linkData, BOOL *stop) {
@@ -646,6 +652,12 @@ - (BOOL)mergeFromRESTDictionary:(NSDictionary *)object withDecoder:(PFDecoder *)
646652

647653
if (object[PFUserAuthDataRESTKey] != nil) {
648654
NSDictionary *newAuthData = object[PFUserAuthDataRESTKey];
655+
// Remove transient MFA auth provider from persisted state
656+
if ([newAuthData isKindOfClass:[NSDictionary class]] && newAuthData[@"mfa"]) {
657+
NSMutableDictionary *mutable = [newAuthData mutableCopy];
658+
[mutable removeObjectForKey:@"mfa"];
659+
newAuthData = [mutable copy];
660+
}
649661
[newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
650662
self.authData[key] = obj;
651663
if (obj != nil) {
@@ -838,6 +850,26 @@ + (void)logInWithUsernameInBackground:(NSString *)username
838850
[[self logInWithUsernameInBackground:username password:password] thenCallBackOnMainThreadAsync:block];
839851
}
840852

853+
+ (BFTask<__kindof PFUser *> *)logInWithUsernameInBackground:(NSString *)username
854+
password:(NSString *)password
855+
authData:(NSDictionary<NSString *,id> *)authData {
856+
NSDictionary *parameters = nil;
857+
if (authData.count > 0) {
858+
parameters = @{ @"authData": authData };
859+
}
860+
return [[self userController] logInCurrentUserAsyncWithUsername:username
861+
password:password
862+
parameters:parameters
863+
revocableSession:[self _isRevocableSessionEnabled]];
864+
}
865+
866+
+ (void)logInWithUsernameInBackground:(NSString *)username
867+
password:(NSString *)password
868+
authData:(NSDictionary<NSString *,id> *)authData
869+
block:(PFUserResultBlock)block {
870+
[[self logInWithUsernameInBackground:username password:password authData:authData] thenCallBackOnMainThreadAsync:block];
871+
}
872+
841873
///--------------------------------------
842874
#pragma mark - Third-party Authentication
843875
///--------------------------------------

Parse/Tests/Unit/UserCommandTests.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ - (void)testLogInCommand {
4141
XCTAssertFalse(command.revocableSessionEnabled);
4242
}
4343

44+
- (void)testLogInCommandWithParametersBody {
45+
NSDictionary *params = @{ @"username": @"a",
46+
@"password": @"b",
47+
@"authData": @{ @"mfa": @{ @"token": @"123456" } } };
48+
PFRESTUserCommand *command = [PFRESTUserCommand logInUserCommandWithParameters:params
49+
revocableSession:YES
50+
error:nil];
51+
XCTAssertNotNil(command);
52+
XCTAssertEqualObjects(command.httpPath, @"login");
53+
XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST);
54+
XCTAssertNotNil(command.parameters);
55+
XCTAssertEqualObjects(command.parameters[@"username"], @"a");
56+
XCTAssertEqualObjects(command.parameters[@"password"], @"b");
57+
XCTAssertEqualObjects(command.parameters[@"authData"], (@{ @"mfa": @{ @"token": @"123456" } }));
58+
XCTAssertEqual(command.additionalRequestHeaders.count, 1);
59+
XCTAssertTrue(command.revocableSessionEnabled);
60+
XCTAssertNil(command.sessionToken);
61+
}
62+
4463
- (void)testServiceLoginCommandWithAuthTypeData {
4564
PFRESTUserCommand *command = [PFRESTUserCommand serviceLoginUserCommandWithAuthenticationType:@"a"
4665
authenticationData:@{ @"b" : @"c" }

Parse/Tests/Unit/UserControllerTests.m

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,57 @@ - (void)testLogInCurrentUserWithUsernamePassword {
184184
OCMVerifyAll(currentUserController);
185185
}
186186

187+
- (void)testLogInCurrentUserWithUsernamePasswordAndAuthData {
188+
id commonDataSource = [self mockedCommonDataSource];
189+
id coreDataSource = [self mockedCoreDataSource];
190+
id commandRunner = [commonDataSource commandRunner];
191+
192+
id commandResult = @{ @"objectId" : @"a",
193+
@"yarr" : @1 };
194+
[commandRunner mockCommandResult:commandResult forCommandsPassingTest:^BOOL(id obj) {
195+
PFRESTCommand *command = obj;
196+
197+
XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST);
198+
XCTAssertNotEqual([command.httpPath rangeOfString:@"login"].location, NSNotFound);
199+
XCTAssertNil(command.sessionToken);
200+
NSDictionary *expected = @{ @"username": @"yolo",
201+
@"password": @"yarr",
202+
@"authData": @{ @"mfa": @{ @"token": @"654321" } } };
203+
XCTAssertEqualObjects(command.parameters, expected);
204+
XCTAssertEqualObjects(command.additionalRequestHeaders, @{ @"X-Parse-Revocable-Session" : @"1" });
205+
206+
return YES;
207+
}];
208+
209+
__block PFUser *savedUser = nil;
210+
211+
id currentUserController = [coreDataSource currentUserController];
212+
[OCMExpect([currentUserController saveCurrentObjectAsync:[OCMArg checkWithBlock:^BOOL(id obj) {
213+
savedUser = obj;
214+
return (savedUser != nil);
215+
}]]) andReturn:[BFTask taskWithResult:nil]];
216+
217+
PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource
218+
coreDataSource:coreDataSource];
219+
220+
XCTestExpectation *expectation = [self currentSelectorTestExpectation];
221+
NSDictionary *params = @{ @"authData": @{ @"mfa": @{ @"token": @"654321" } } };
222+
[[controller logInCurrentUserAsyncWithUsername:@"yolo"
223+
password:@"yarr"
224+
parameters:params
225+
revocableSession:YES] continueWithBlock:^id(BFTask *task) {
226+
PFUser *user = task.result;
227+
XCTAssertNotNil(user);
228+
XCTAssertEqualObjects(user.objectId, @"a");
229+
XCTAssertEqualObjects(user[@"yarr"], @1);
230+
[expectation fulfill];
231+
return nil;
232+
}];
233+
[self waitForTestExpectations];
234+
235+
OCMVerifyAll(currentUserController);
236+
}
237+
187238
- (void)testLogInCurrentUserWithUsernamePasswordNullResult {
188239
id commonDataSource = [self mockedCommonDataSource];
189240
id coreDataSource = [self mockedCoreDataSource];

0 commit comments

Comments
 (0)