You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1348 lines
52 KiB
1348 lines
52 KiB
/*
|
|
* Copyright 2017 Google
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import "FIRUser_Internal.h"
|
|
|
|
#import "FIRAdditionalUserInfo_Internal.h"
|
|
#import "FIRAuth.h"
|
|
#import "FIRAuthCredential_Internal.h"
|
|
#import "FIRAuthDataResult_Internal.h"
|
|
#import "FIRAuthErrorUtils.h"
|
|
#import "FIRAuthGlobalWorkQueue.h"
|
|
#import "FIRAuthSerialTaskQueue.h"
|
|
#import "FIRAuthOperationType.h"
|
|
#import "FIRAuth_Internal.h"
|
|
#import "FIRAuthBackend.h"
|
|
#import "FIRAuthRequestConfiguration.h"
|
|
#import "FIRAuthTokenResult_Internal.h"
|
|
#import "FIRDeleteAccountRequest.h"
|
|
#import "FIRDeleteAccountResponse.h"
|
|
#import "FIREmailAuthProvider.h"
|
|
#import "FIREmailPasswordAuthCredential.h"
|
|
#import "FIRGetAccountInfoRequest.h"
|
|
#import "FIRGetAccountInfoResponse.h"
|
|
#import "FIRGetOOBConfirmationCodeRequest.h"
|
|
#import "FIRGetOOBConfirmationCodeResponse.h"
|
|
#import <FirebaseCore/FIRLogger.h>
|
|
#import "FIRSecureTokenService.h"
|
|
#import "FIRSetAccountInfoRequest.h"
|
|
#import "FIRSetAccountInfoResponse.h"
|
|
#import "FIRUserInfoImpl.h"
|
|
#import "FIRUserMetadata_Internal.h"
|
|
#import "FIRVerifyAssertionRequest.h"
|
|
#import "FIRVerifyAssertionResponse.h"
|
|
#import "FIRVerifyCustomTokenRequest.h"
|
|
#import "FIRVerifyCustomTokenResponse.h"
|
|
#import "FIRVerifyPasswordRequest.h"
|
|
#import "FIRVerifyPasswordResponse.h"
|
|
#import "FIRVerifyPhoneNumberRequest.h"
|
|
#import "FIRVerifyPhoneNumberResponse.h"
|
|
|
|
#if TARGET_OS_IOS
|
|
#import "FIRPhoneAuthProvider.h"
|
|
#import "AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h"
|
|
#endif
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
/** @var kUserIDCodingKey
|
|
@brief The key used to encode the user ID for NSSecureCoding.
|
|
*/
|
|
static NSString *const kUserIDCodingKey = @"userID";
|
|
|
|
/** @var kHasEmailPasswordCredentialCodingKey
|
|
@brief The key used to encode the hasEmailPasswordCredential property for NSSecureCoding.
|
|
*/
|
|
static NSString *const kHasEmailPasswordCredentialCodingKey = @"hasEmailPassword";
|
|
|
|
/** @var kAnonymousCodingKey
|
|
@brief The key used to encode the anonymous property for NSSecureCoding.
|
|
*/
|
|
static NSString *const kAnonymousCodingKey = @"anonymous";
|
|
|
|
/** @var kEmailCodingKey
|
|
@brief The key used to encode the email property for NSSecureCoding.
|
|
*/
|
|
static NSString *const kEmailCodingKey = @"email";
|
|
|
|
/** @var kPhoneNumberCodingKey
|
|
@brief The key used to encode the phoneNumber property for NSSecureCoding.
|
|
*/
|
|
static NSString *const kPhoneNumberCodingKey = @"phoneNumber";
|
|
|
|
/** @var kEmailVerifiedCodingKey
|
|
@brief The key used to encode the isEmailVerified property for NSSecureCoding.
|
|
*/
|
|
static NSString *const kEmailVerifiedCodingKey = @"emailVerified";
|
|
|
|
/** @var kDisplayNameCodingKey
|
|
@brief The key used to encode the displayName property for NSSecureCoding.
|
|
*/
|
|
static NSString *const kDisplayNameCodingKey = @"displayName";
|
|
|
|
/** @var kPhotoURLCodingKey
|
|
@brief The key used to encode the photoURL property for NSSecureCoding.
|
|
*/
|
|
static NSString *const kPhotoURLCodingKey = @"photoURL";
|
|
|
|
/** @var kProviderDataKey
|
|
@brief The key used to encode the providerData instance variable for NSSecureCoding.
|
|
*/
|
|
static NSString *const kProviderDataKey = @"providerData";
|
|
|
|
/** @var kAPIKeyCodingKey
|
|
@brief The key used to encode the APIKey instance variable for NSSecureCoding.
|
|
*/
|
|
static NSString *const kAPIKeyCodingKey = @"APIKey";
|
|
|
|
/** @var kTokenServiceCodingKey
|
|
@brief The key used to encode the tokenService instance variable for NSSecureCoding.
|
|
*/
|
|
static NSString *const kTokenServiceCodingKey = @"tokenService";
|
|
|
|
/** @var kMetadataCodingKey
|
|
@brief The key used to encode the metadata instance variable for NSSecureCoding.
|
|
*/
|
|
static NSString *const kMetadataCodingKey = @"metadata";
|
|
|
|
/** @var kMissingUsersErrorMessage
|
|
@brief The error message when there is no users array in the getAccountInfo response.
|
|
*/
|
|
static NSString *const kMissingUsersErrorMessage = @"users";
|
|
|
|
/** @typedef CallbackWithError
|
|
@brief The type for a callback block that only takes an error parameter.
|
|
*/
|
|
typedef void (^CallbackWithError)(NSError *_Nullable);
|
|
|
|
/** @typedef CallbackWithUserAndError
|
|
@brief The type for a callback block that takes a user parameter and an error parameter.
|
|
*/
|
|
typedef void (^CallbackWithUserAndError)(FIRUser *_Nullable, NSError *_Nullable);
|
|
|
|
/** @typedef CallbackWithUserAndError
|
|
@brief The type for a callback block that takes a user parameter and an error parameter.
|
|
*/
|
|
typedef void (^CallbackWithAuthDataResultAndError)(FIRAuthDataResult *_Nullable,
|
|
NSError *_Nullable);
|
|
|
|
/** @var kMissingPasswordReason
|
|
@brief The reason why the @c FIRAuthErrorCodeWeakPassword error is thrown.
|
|
@remarks This error message will be localized in the future.
|
|
*/
|
|
static NSString *const kMissingPasswordReason = @"Missing Password";
|
|
|
|
/** @fn callInMainThreadWithError
|
|
@brief Calls a callback in main thread with error.
|
|
@param callback The callback to be called in main thread.
|
|
@param error The error to pass to callback.
|
|
*/
|
|
static void callInMainThreadWithError(_Nullable CallbackWithError callback,
|
|
NSError *_Nullable error) {
|
|
if (callback) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
callback(error);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** @fn callInMainThreadWithUserAndError
|
|
@brief Calls a callback in main thread with user and error.
|
|
@param callback The callback to be called in main thread.
|
|
@param user The user to pass to callback if there is no error.
|
|
@param error The error to pass to callback.
|
|
*/
|
|
static void callInMainThreadWithUserAndError(_Nullable CallbackWithUserAndError callback,
|
|
FIRUser *_Nonnull user,
|
|
NSError *_Nullable error) {
|
|
if (callback) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
callback(error ? nil : user, error);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** @fn callInMainThreadWithUserAndError
|
|
@brief Calls a callback in main thread with user and error.
|
|
@param callback The callback to be called in main thread.
|
|
@param result The result to pass to callback if there is no error.
|
|
@param error The error to pass to callback.
|
|
*/
|
|
static void callInMainThreadWithAuthDataResultAndError(
|
|
_Nullable CallbackWithAuthDataResultAndError callback,
|
|
FIRAuthDataResult *_Nullable result,
|
|
NSError *_Nullable error) {
|
|
if (callback) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
callback(result, error);
|
|
});
|
|
}
|
|
}
|
|
|
|
@interface FIRUserProfileChangeRequest ()
|
|
|
|
/** @fn initWithUser:
|
|
@brief Designated initializer.
|
|
@param user The user for which we are updating profile information.
|
|
*/
|
|
- (nullable instancetype)initWithUser:(FIRUser *)user NS_DESIGNATED_INITIALIZER;
|
|
|
|
@end
|
|
|
|
@implementation FIRUser {
|
|
/** @var _hasEmailPasswordCredential
|
|
@brief Whether or not the user can be authenticated by using Firebase email and password.
|
|
*/
|
|
BOOL _hasEmailPasswordCredential;
|
|
|
|
/** @var _providerData
|
|
@brief Provider specific user data.
|
|
*/
|
|
NSDictionary<NSString *, FIRUserInfoImpl *> *_providerData;
|
|
|
|
/** @var _taskQueue
|
|
@brief Used to serialize the update profile calls.
|
|
*/
|
|
FIRAuthSerialTaskQueue *_taskQueue;
|
|
|
|
/** @var _tokenService
|
|
@brief A secure token service associated with this user. For performing token exchanges and
|
|
refreshing access tokens.
|
|
*/
|
|
FIRSecureTokenService *_tokenService;
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
// Explicitly @synthesize because these properties are defined in FIRUserInfo protocol.
|
|
@synthesize uid = _userID;
|
|
@synthesize displayName = _displayName;
|
|
@synthesize photoURL = _photoURL;
|
|
@synthesize email = _email;
|
|
@synthesize phoneNumber = _phoneNumber;
|
|
|
|
#pragma mark -
|
|
|
|
+ (void)retrieveUserWithAuth:(FIRAuth *)auth
|
|
accessToken:(NSString *)accessToken
|
|
accessTokenExpirationDate:(NSDate *)accessTokenExpirationDate
|
|
refreshToken:(NSString *)refreshToken
|
|
anonymous:(BOOL)anonymous
|
|
callback:(FIRRetrieveUserCallback)callback {
|
|
FIRSecureTokenService *tokenService =
|
|
[[FIRSecureTokenService alloc] initWithRequestConfiguration:auth.requestConfiguration
|
|
accessToken:accessToken
|
|
accessTokenExpirationDate:accessTokenExpirationDate
|
|
refreshToken:refreshToken];
|
|
FIRUser *user = [[self alloc] initWithTokenService:tokenService];
|
|
user.auth = auth;
|
|
user.requestConfiguration = auth.requestConfiguration;
|
|
[user internalGetTokenWithCallback:^(NSString *_Nullable accessToken, NSError *_Nullable error) {
|
|
if (error) {
|
|
callback(nil, error);
|
|
return;
|
|
}
|
|
FIRGetAccountInfoRequest *getAccountInfoRequest =
|
|
[[FIRGetAccountInfoRequest alloc] initWithAccessToken:accessToken
|
|
requestConfiguration:auth.requestConfiguration];
|
|
[FIRAuthBackend getAccountInfo:getAccountInfoRequest
|
|
callback:^(FIRGetAccountInfoResponse *_Nullable response,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
// No need to sign out user here for errors because the user hasn't been signed in yet.
|
|
callback(nil, error);
|
|
return;
|
|
}
|
|
user->_anonymous = anonymous;
|
|
[user updateWithGetAccountInfoResponse:response];
|
|
callback(user, nil);
|
|
}];
|
|
}];
|
|
}
|
|
|
|
- (instancetype)initWithTokenService:(FIRSecureTokenService *)tokenService {
|
|
self = [super init];
|
|
if (self) {
|
|
_providerData = @{ };
|
|
_taskQueue = [[FIRAuthSerialTaskQueue alloc] init];
|
|
_tokenService = tokenService;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - NSSecureCoding
|
|
|
|
+ (BOOL)supportsSecureCoding {
|
|
return YES;
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
|
|
NSString *userID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserIDCodingKey];
|
|
BOOL hasAnonymousKey = [aDecoder containsValueForKey:kAnonymousCodingKey];
|
|
BOOL anonymous = [aDecoder decodeBoolForKey:kAnonymousCodingKey];
|
|
BOOL hasEmailPasswordCredential =
|
|
[aDecoder decodeBoolForKey:kHasEmailPasswordCredentialCodingKey];
|
|
NSString *displayName =
|
|
[aDecoder decodeObjectOfClass:[NSString class] forKey:kDisplayNameCodingKey];
|
|
NSURL *photoURL =
|
|
[aDecoder decodeObjectOfClass:[NSURL class] forKey:kPhotoURLCodingKey];
|
|
NSString *email =
|
|
[aDecoder decodeObjectOfClass:[NSString class] forKey:kEmailCodingKey];
|
|
NSString *phoneNumber =
|
|
[aDecoder decodeObjectOfClass:[NSString class] forKey:kPhoneNumberCodingKey];
|
|
BOOL emailVerified = [aDecoder decodeBoolForKey:kEmailVerifiedCodingKey];
|
|
NSSet *providerDataClasses = [NSSet setWithArray:@[
|
|
[NSDictionary class],
|
|
[NSString class],
|
|
[FIRUserInfoImpl class]
|
|
]];
|
|
NSDictionary<NSString *, FIRUserInfoImpl *> *providerData =
|
|
[aDecoder decodeObjectOfClasses:providerDataClasses forKey:kProviderDataKey];
|
|
FIRSecureTokenService *tokenService =
|
|
[aDecoder decodeObjectOfClass:[FIRSecureTokenService class] forKey:kTokenServiceCodingKey];
|
|
FIRUserMetadata *metadata =
|
|
[aDecoder decodeObjectOfClass:[FIRUserMetadata class] forKey:kMetadataCodingKey];
|
|
NSString *APIKey =
|
|
[aDecoder decodeObjectOfClass:[FIRUserMetadata class] forKey:kAPIKeyCodingKey];
|
|
if (!userID || !tokenService) {
|
|
return nil;
|
|
}
|
|
self = [self initWithTokenService:tokenService];
|
|
if (self) {
|
|
_userID = userID;
|
|
// Previous version of this code didn't save 'anonymous' bit directly but deduced it from
|
|
// 'hasEmailPasswordCredential' and 'providerData' instead, so here backward compatibility is
|
|
// provided to read old format data.
|
|
_anonymous = hasAnonymousKey ? anonymous : (!hasEmailPasswordCredential && !providerData.count);
|
|
_hasEmailPasswordCredential = hasEmailPasswordCredential;
|
|
_email = email;
|
|
_emailVerified = emailVerified;
|
|
_displayName = displayName;
|
|
_photoURL = photoURL;
|
|
_providerData = providerData;
|
|
_phoneNumber = phoneNumber;
|
|
_metadata = metadata ?: [[FIRUserMetadata alloc] initWithCreationDate:nil lastSignInDate:nil];
|
|
_requestConfiguration = [[FIRAuthRequestConfiguration alloc] initWithAPIKey:APIKey];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
|
[aCoder encodeObject:_userID forKey:kUserIDCodingKey];
|
|
[aCoder encodeBool:_anonymous forKey:kAnonymousCodingKey];
|
|
[aCoder encodeBool:_hasEmailPasswordCredential forKey:kHasEmailPasswordCredentialCodingKey];
|
|
[aCoder encodeObject:_providerData forKey:kProviderDataKey];
|
|
[aCoder encodeObject:_email forKey:kEmailCodingKey];
|
|
[aCoder encodeObject:_phoneNumber forKey:kPhoneNumberCodingKey];
|
|
[aCoder encodeBool:_emailVerified forKey:kEmailVerifiedCodingKey];
|
|
[aCoder encodeObject:_photoURL forKey:kPhotoURLCodingKey];
|
|
[aCoder encodeObject:_displayName forKey:kDisplayNameCodingKey];
|
|
[aCoder encodeObject:_metadata forKey:kMetadataCodingKey];
|
|
[aCoder encodeObject:_auth.requestConfiguration.APIKey forKey:kAPIKeyCodingKey];
|
|
[aCoder encodeObject:_tokenService forKey:kTokenServiceCodingKey];
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)setAuth:(nullable FIRAuth *)auth {
|
|
_auth = auth;
|
|
_tokenService.requestConfiguration = auth.requestConfiguration;
|
|
}
|
|
|
|
- (NSString *)providerID {
|
|
return @"Firebase";
|
|
}
|
|
|
|
- (NSArray<id<FIRUserInfo>> *)providerData {
|
|
return _providerData.allValues;
|
|
}
|
|
|
|
/** @fn getAccountInfoRefreshingCache:
|
|
@brief Gets the users's account data from the server, updating our local values.
|
|
@param callback Invoked when the request to getAccountInfo has completed, or when an error has
|
|
been detected. Invoked asynchronously on the auth global work queue in the future.
|
|
*/
|
|
- (void)getAccountInfoRefreshingCache:(void(^)(FIRGetAccountInfoResponseUser *_Nullable user,
|
|
NSError *_Nullable error))callback {
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken, NSError *_Nullable error) {
|
|
if (error) {
|
|
callback(nil, error);
|
|
return;
|
|
}
|
|
FIRGetAccountInfoRequest *getAccountInfoRequest =
|
|
[[FIRGetAccountInfoRequest alloc] initWithAccessToken:accessToken
|
|
requestConfiguration:self->_auth.requestConfiguration];
|
|
[FIRAuthBackend getAccountInfo:getAccountInfoRequest
|
|
callback:^(FIRGetAccountInfoResponse *_Nullable response,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
callback(nil, error);
|
|
return;
|
|
}
|
|
[self updateWithGetAccountInfoResponse:response];
|
|
if (![self updateKeychain:&error]) {
|
|
callback(nil, error);
|
|
return;
|
|
}
|
|
callback(response.users.firstObject, nil);
|
|
}];
|
|
}];
|
|
}
|
|
|
|
- (void)updateWithGetAccountInfoResponse:(FIRGetAccountInfoResponse *)response {
|
|
FIRGetAccountInfoResponseUser *user = response.users.firstObject;
|
|
_userID = user.localID;
|
|
_email = user.email;
|
|
_emailVerified = user.emailVerified;
|
|
_displayName = user.displayName;
|
|
_photoURL = user.photoURL;
|
|
_phoneNumber = user.phoneNumber;
|
|
_hasEmailPasswordCredential = user.passwordHash.length > 0;
|
|
_metadata =
|
|
[[FIRUserMetadata alloc]initWithCreationDate:user.creationDate
|
|
lastSignInDate:user.lastLoginDate];
|
|
NSMutableDictionary<NSString *, FIRUserInfoImpl *> *providerData =
|
|
[NSMutableDictionary dictionary];
|
|
for (FIRGetAccountInfoResponseProviderUserInfo *providerUserInfo in user.providerUserInfo) {
|
|
FIRUserInfoImpl *userInfo =
|
|
[FIRUserInfoImpl userInfoWithGetAccountInfoResponseProviderUserInfo:providerUserInfo];
|
|
if (userInfo) {
|
|
providerData[providerUserInfo.providerID] = userInfo;
|
|
}
|
|
}
|
|
_providerData = [providerData copy];
|
|
}
|
|
|
|
/** @fn executeUserUpdateWithChanges:callback:
|
|
@brief Performs a setAccountInfo request by mutating the results of a getAccountInfo response,
|
|
atomically in regards to other calls to this method.
|
|
@param changeBlock A block responsible for mutating a template @c FIRSetAccountInfoRequest
|
|
@param callback A block to invoke when the change is complete. Invoked asynchronously on the
|
|
auth global work queue in the future.
|
|
*/
|
|
- (void)executeUserUpdateWithChanges:(void(^)(FIRGetAccountInfoResponseUser *,
|
|
FIRSetAccountInfoRequest *))changeBlock
|
|
callback:(nonnull FIRUserProfileChangeCallback)callback {
|
|
[_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
|
|
[self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
complete();
|
|
callback(error);
|
|
return;
|
|
}
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
complete();
|
|
callback(error);
|
|
return;
|
|
}
|
|
FIRAuthRequestConfiguration *configuration = self->_auth.requestConfiguration;
|
|
// Mutate setAccountInfoRequest in block:
|
|
FIRSetAccountInfoRequest *setAccountInfoRequest =
|
|
[[FIRSetAccountInfoRequest alloc] initWithRequestConfiguration:configuration];
|
|
setAccountInfoRequest.accessToken = accessToken;
|
|
changeBlock(user, setAccountInfoRequest);
|
|
// Execute request:
|
|
[FIRAuthBackend setAccountInfo:setAccountInfoRequest
|
|
callback:^(FIRSetAccountInfoResponse *_Nullable response,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
complete();
|
|
callback(error);
|
|
return;
|
|
}
|
|
if (response.IDToken && response.refreshToken) {
|
|
FIRSecureTokenService *tokenService = [[FIRSecureTokenService alloc]
|
|
initWithRequestConfiguration:configuration
|
|
accessToken:response.IDToken
|
|
accessTokenExpirationDate:response.approximateExpirationDate
|
|
refreshToken:response.refreshToken];
|
|
[self setTokenService:tokenService callback:^(NSError *_Nullable error) {
|
|
complete();
|
|
callback(error);
|
|
}];
|
|
return;
|
|
}
|
|
complete();
|
|
callback(nil);
|
|
}];
|
|
}];
|
|
}];
|
|
}];
|
|
}
|
|
|
|
/** @fn updateKeychain:
|
|
@brief Updates the keychain for user token or info changes.
|
|
@param error The error if NO is returned.
|
|
@return Whether the operation is successful.
|
|
*/
|
|
- (BOOL)updateKeychain:(NSError *_Nullable *_Nullable)error {
|
|
return [_auth updateKeychainWithUser:self error:error];
|
|
}
|
|
|
|
/** @fn setTokenService:callback:
|
|
@brief Sets a new token service for the @c FIRUser instance.
|
|
@param tokenService The new token service object.
|
|
@param callback The block to be called in the global auth working queue once finished.
|
|
@remarks The method makes sure the token service has access and refresh token and the new tokens
|
|
are saved in the keychain before calling back.
|
|
*/
|
|
- (void)setTokenService:(FIRSecureTokenService *)tokenService
|
|
callback:(nonnull CallbackWithError)callback {
|
|
[tokenService fetchAccessTokenForcingRefresh:NO
|
|
callback:^(NSString *_Nullable token,
|
|
NSError *_Nullable error,
|
|
BOOL tokenUpdated) {
|
|
if (error) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
self->_tokenService = tokenService;
|
|
if (![self updateKeychain:&error]) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
callback(nil);
|
|
}];
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
/** @fn updateEmail:password:callback:
|
|
@brief Updates email address and/or password for the current user.
|
|
@remarks May fail if there is already an email/password-based account for the same email
|
|
address.
|
|
@param email The email address for the user, if to be updated.
|
|
@param password The new password for the user, if to be updated.
|
|
@param callback The block called when the user profile change has finished. Invoked
|
|
asynchronously on the auth global work queue in the future.
|
|
@remarks May fail with a @c FIRAuthErrorCodeRequiresRecentLogin error code.
|
|
Call @c reauthentateWithCredential:completion: beforehand to avoid this error case.
|
|
*/
|
|
- (void)updateEmail:(nullable NSString *)email
|
|
password:(nullable NSString *)password
|
|
callback:(nonnull FIRUserProfileChangeCallback)callback {
|
|
if (password && ![password length]){
|
|
callback([FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:kMissingPasswordReason]);
|
|
return;
|
|
}
|
|
BOOL hadEmailPasswordCredential = _hasEmailPasswordCredential;
|
|
[self executeUserUpdateWithChanges:^(FIRGetAccountInfoResponseUser *user,
|
|
FIRSetAccountInfoRequest *request) {
|
|
if (email) {
|
|
request.email = email;
|
|
}
|
|
if (password) {
|
|
request.password = password;
|
|
}
|
|
}
|
|
callback:^(NSError *error) {
|
|
if (error) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
if (email) {
|
|
self->_email = email;
|
|
}
|
|
if (self->_email && password) {
|
|
self->_anonymous = NO;
|
|
self->_hasEmailPasswordCredential = YES;
|
|
if (!hadEmailPasswordCredential) {
|
|
// The list of providers need to be updated for the newly added email-password provider.
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
FIRAuthRequestConfiguration *requestConfiguration = self->_auth.requestConfiguration;
|
|
FIRGetAccountInfoRequest *getAccountInfoRequest =
|
|
[[FIRGetAccountInfoRequest alloc] initWithAccessToken:accessToken
|
|
requestConfiguration:requestConfiguration];
|
|
[FIRAuthBackend getAccountInfo:getAccountInfoRequest
|
|
callback:^(FIRGetAccountInfoResponse *_Nullable response,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
callback(error);
|
|
return;
|
|
}
|
|
[self updateWithGetAccountInfoResponse:response];
|
|
if (![self updateKeychain:&error]) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
callback(nil);
|
|
}];
|
|
}];
|
|
return;
|
|
}
|
|
}
|
|
if (![self updateKeychain:&error]) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
callback(nil);
|
|
}];
|
|
}
|
|
|
|
- (void)updateEmail:(NSString *)email completion:(nullable FIRUserProfileChangeCallback)completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self updateEmail:email password:nil callback:^(NSError *_Nullable error) {
|
|
callInMainThreadWithError(completion, error);
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (void)updatePassword:(NSString *)password
|
|
completion:(nullable FIRUserProfileChangeCallback)completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self updateEmail:nil password:password callback:^(NSError *_Nullable error){
|
|
callInMainThreadWithError(completion, error);
|
|
}];
|
|
});
|
|
}
|
|
|
|
#if TARGET_OS_IOS
|
|
/** @fn internalUpdateOrLinkPhoneNumberCredential:completion:
|
|
@brief Updates the phone number for the user. On success, the cached user profile data is
|
|
updated.
|
|
|
|
@param phoneAuthCredential The new phone number credential corresponding to the phone number
|
|
to be added to the Firebase account, if a phone number is already linked to the account this
|
|
new phone number will replace it.
|
|
@param isLinkOperation Boolean value indicating whether or not this is a link operation.
|
|
@param completion Optionally; the block invoked when the user profile change has finished.
|
|
Invoked asynchronously on the global work queue in the future.
|
|
*/
|
|
- (void)internalUpdateOrLinkPhoneNumberCredential:(FIRPhoneAuthCredential *)phoneAuthCredential
|
|
isLinkOperation:(BOOL)isLinkOperation
|
|
completion:(FIRUserProfileChangeCallback)completion {
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
completion(error);
|
|
return;
|
|
}
|
|
FIRAuthOperationType operation =
|
|
isLinkOperation ? FIRAuthOperationTypeLink : FIRAuthOperationTypeUpdate;
|
|
FIRVerifyPhoneNumberRequest *request = [[FIRVerifyPhoneNumberRequest alloc]
|
|
initWithVerificationID:phoneAuthCredential.verificationID
|
|
verificationCode:phoneAuthCredential.verificationCode
|
|
operation:operation
|
|
requestConfiguration:self->_auth.requestConfiguration];
|
|
request.accessToken = accessToken;
|
|
[FIRAuthBackend verifyPhoneNumber:request
|
|
callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
completion(error);
|
|
return;
|
|
}
|
|
// Get account info to update cached user info.
|
|
[self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
completion(error);
|
|
return;
|
|
}
|
|
self->_anonymous = NO;
|
|
if (![self updateKeychain:&error]) {
|
|
completion(error);
|
|
return;
|
|
}
|
|
completion(nil);
|
|
}];
|
|
}];
|
|
}];
|
|
}
|
|
|
|
- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneAuthCredential
|
|
completion:(nullable FIRUserProfileChangeCallback)completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self internalUpdateOrLinkPhoneNumberCredential:phoneAuthCredential
|
|
isLinkOperation:NO
|
|
completion:^(NSError *_Nullable error) {
|
|
callInMainThreadWithError(completion, error);
|
|
}];
|
|
});
|
|
}
|
|
#endif
|
|
|
|
- (FIRUserProfileChangeRequest *)profileChangeRequest {
|
|
__block FIRUserProfileChangeRequest *result;
|
|
dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
|
|
result = [[FIRUserProfileChangeRequest alloc] initWithUser:self];
|
|
});
|
|
return result;
|
|
}
|
|
|
|
- (void)setDisplayName:(NSString *)displayName {
|
|
_displayName = [displayName copy];
|
|
}
|
|
|
|
- (void)setPhotoURL:(NSURL *)photoURL {
|
|
_photoURL = [photoURL copy];
|
|
}
|
|
|
|
- (NSString *)rawAccessToken {
|
|
return _tokenService.rawAccessToken;
|
|
}
|
|
|
|
- (NSDate *)accessTokenExpirationDate {
|
|
return _tokenService.accessTokenExpirationDate;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
|
|
NSError *_Nullable error) {
|
|
callInMainThreadWithError(completion, error);
|
|
}];
|
|
});
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential
|
|
completion:(nullable FIRUserProfileChangeCallback)completion {
|
|
FIRAuthDataResultCallback callback = ^(FIRAuthDataResult *_Nullable authResult,
|
|
NSError *_Nullable error) {
|
|
completion(error);
|
|
};
|
|
[self reauthenticateAndRetrieveDataWithCredential:credential completion:callback];
|
|
}
|
|
|
|
- (void)
|
|
reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
|
|
completion:(nullable FIRAuthDataResultCallback) completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self->_auth internalSignInAndRetrieveDataWithCredential:credential
|
|
isReauthentication:YES
|
|
callback:^(FIRAuthDataResult *_Nullable
|
|
authResult,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
// If "user not found" error returned by backend, translate to user mismatch error which is
|
|
// more accurate.
|
|
if (error.code == FIRAuthErrorCodeUserNotFound) {
|
|
error = [FIRAuthErrorUtils userMismatchError];
|
|
}
|
|
callInMainThreadWithAuthDataResultAndError(completion, authResult, error);
|
|
return;
|
|
}
|
|
if (![authResult.user.uid isEqual:[self->_auth getUID]]) {
|
|
callInMainThreadWithAuthDataResultAndError(completion, authResult,
|
|
[FIRAuthErrorUtils userMismatchError]);
|
|
return;
|
|
}
|
|
// Successful reauthenticate
|
|
[self setTokenService:authResult.user->_tokenService callback:^(NSError *_Nullable error) {
|
|
callInMainThreadWithAuthDataResultAndError(completion, authResult, error);
|
|
}];
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (nullable NSString *)refreshToken {
|
|
__block NSString *result;
|
|
dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
|
|
result = self->_tokenService.refreshToken;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion {
|
|
// |getIDTokenForcingRefresh:completion:| is also a public API so there is no need to dispatch to
|
|
// global work queue here.
|
|
[self getIDTokenForcingRefresh:NO completion:completion];
|
|
}
|
|
|
|
- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh
|
|
completion:(nullable FIRAuthTokenCallback)completion {
|
|
[self getIDTokenResultForcingRefresh:forceRefresh
|
|
completion:^(FIRAuthTokenResult *_Nullable tokenResult,
|
|
NSError *_Nullable error) {
|
|
|
|
if (completion) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
completion(tokenResult.token, error);
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)getIDTokenResultWithCompletion:(nullable FIRAuthTokenResultCallback)completion {
|
|
[self getIDTokenResultForcingRefresh:NO
|
|
completion:^(FIRAuthTokenResult *_Nullable tokenResult,
|
|
NSError *_Nullable error) {
|
|
if (completion) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
completion(tokenResult, error);
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)getIDTokenResultForcingRefresh:(BOOL)forceRefresh
|
|
completion:(nullable FIRAuthTokenResultCallback)completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self internalGetTokenForcingRefresh:forceRefresh
|
|
callback:^(NSString *_Nullable token, NSError *_Nullable error) {
|
|
FIRAuthTokenResult *tokenResult;
|
|
if (token) {
|
|
tokenResult = [self parseIDToken:token error:&error];
|
|
}
|
|
if (completion) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
completion(tokenResult, error);
|
|
});
|
|
}
|
|
}];
|
|
});
|
|
}
|
|
|
|
/** @fn parseIDToken:error:
|
|
@brief Parses the provided IDToken and returns an instance of FIRAuthTokenResult containing
|
|
claims obtained from the IDToken.
|
|
|
|
@param token The raw text of the Firebase IDToken encoded in base64.
|
|
@param error An out parameter which would contain any error that occurs during parsing.
|
|
@return An instance of FIRAuthTokenResult containing claims obtained from the IDToken.
|
|
|
|
@remarks IDToken returned from the backend in some cases is of a length that is not a multiple
|
|
of 4. In these cases this function pads the token with as many "=" characters as needed and
|
|
then attempts to parse the token. If the token cannot be parsed an error is returned via the
|
|
"error" out parameter.
|
|
*/
|
|
- (FIRAuthTokenResult *)parseIDToken:(NSString *)token error:(NSError **)error {
|
|
*error = nil;
|
|
NSArray *tokenStringArray = [token componentsSeparatedByString:@"."];
|
|
|
|
// The token payload is always the second index of the array.
|
|
NSString *idToken = tokenStringArray[1];
|
|
|
|
// Convert the base64URL encoded string to a base64 encoded string.
|
|
// Replace "_" with "/"
|
|
NSMutableString *tokenPayload =
|
|
[[idToken stringByReplacingOccurrencesOfString:@"_" withString:@"/"] mutableCopy];
|
|
|
|
// Replace "-" with "+"
|
|
tokenPayload =
|
|
[[tokenPayload stringByReplacingOccurrencesOfString:@"-" withString:@"+"] mutableCopy];
|
|
|
|
// Pad the token payload with "=" signs if the payload's length is not a multiple of 4.
|
|
while ((tokenPayload.length % 4) != 0) {
|
|
[tokenPayload appendFormat:@"="];
|
|
}
|
|
NSData *decodedTokenPayloadData =
|
|
[[NSData alloc] initWithBase64EncodedString:tokenPayload
|
|
options:NSDataBase64DecodingIgnoreUnknownCharacters];
|
|
if (!decodedTokenPayloadData) {
|
|
*error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:token];
|
|
return nil;
|
|
}
|
|
NSDictionary *tokenPayloadDictionary =
|
|
[NSJSONSerialization JSONObjectWithData:decodedTokenPayloadData
|
|
options:NSJSONReadingMutableContainers|NSJSONReadingAllowFragments
|
|
error:error];
|
|
if (*error) {
|
|
return nil;
|
|
}
|
|
|
|
if (!tokenPayloadDictionary) {
|
|
*error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:token];
|
|
return nil;
|
|
}
|
|
|
|
NSDate *expDate =
|
|
[NSDate dateWithTimeIntervalSinceNow:[tokenPayloadDictionary[@"exp"] doubleValue]];
|
|
NSDate *authDate =
|
|
[NSDate dateWithTimeIntervalSinceNow:[tokenPayloadDictionary[@"auth_time"] doubleValue]];
|
|
NSDate *issuedDate =
|
|
[NSDate dateWithTimeIntervalSinceNow:[tokenPayloadDictionary[@"iat"] doubleValue]];
|
|
FIRAuthTokenResult *result =
|
|
[[FIRAuthTokenResult alloc] initWithToken:token
|
|
expirationDate:expDate
|
|
authDate:authDate
|
|
issuedAtDate:issuedDate
|
|
signInProvider:tokenPayloadDictionary[@"sign_in_provider"]
|
|
claims:tokenPayloadDictionary];
|
|
return result;
|
|
}
|
|
|
|
/** @fn internalGetTokenForcingRefresh:callback:
|
|
@brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
|
|
@param callback The block to invoke when the token is available. Invoked asynchronously on the
|
|
global work thread in the future.
|
|
*/
|
|
- (void)internalGetTokenWithCallback:(nonnull FIRAuthTokenCallback)callback {
|
|
[self internalGetTokenForcingRefresh:NO callback:callback];
|
|
}
|
|
|
|
- (void)internalGetTokenForcingRefresh:(BOOL)forceRefresh
|
|
callback:(nonnull FIRAuthTokenCallback)callback {
|
|
[_tokenService fetchAccessTokenForcingRefresh:forceRefresh
|
|
callback:^(NSString *_Nullable token,
|
|
NSError *_Nullable error,
|
|
BOOL tokenUpdated) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
callback(nil, error);
|
|
return;
|
|
}
|
|
if (tokenUpdated) {
|
|
if (![self updateKeychain:&error]) {
|
|
callback(nil, error);
|
|
return;
|
|
}
|
|
}
|
|
callback(token, nil);
|
|
}];
|
|
}
|
|
|
|
- (void)linkWithCredential:(FIRAuthCredential *)credential
|
|
completion:(nullable FIRAuthResultCallback)completion {
|
|
FIRAuthDataResultCallback callback = ^(FIRAuthDataResult *_Nullable authResult,
|
|
NSError *_Nullable error) {
|
|
completion(authResult.user, error);
|
|
};
|
|
[self linkAndRetrieveDataWithCredential:credential completion:callback];
|
|
}
|
|
|
|
- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
|
|
completion:(nullable FIRAuthDataResultCallback)completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
if (self->_providerData[credential.provider]) {
|
|
callInMainThreadWithAuthDataResultAndError(completion,
|
|
nil,
|
|
[FIRAuthErrorUtils providerAlreadyLinkedError]);
|
|
return;
|
|
}
|
|
FIRAuthDataResult *result =
|
|
[[FIRAuthDataResult alloc] initWithUser:self additionalUserInfo:nil];
|
|
if ([credential isKindOfClass:[FIREmailPasswordAuthCredential class]]) {
|
|
if (self->_hasEmailPasswordCredential) {
|
|
callInMainThreadWithAuthDataResultAndError(completion,
|
|
nil,
|
|
[FIRAuthErrorUtils providerAlreadyLinkedError]);
|
|
return;
|
|
}
|
|
FIREmailPasswordAuthCredential *emailPasswordCredential =
|
|
(FIREmailPasswordAuthCredential *)credential;
|
|
[self updateEmail:emailPasswordCredential.email
|
|
password:emailPasswordCredential.password
|
|
callback:^(NSError *error) {
|
|
if (error) {
|
|
callInMainThreadWithAuthDataResultAndError(completion, nil, error);
|
|
} else {
|
|
callInMainThreadWithAuthDataResultAndError(completion, result, nil);
|
|
}
|
|
}];
|
|
return;
|
|
}
|
|
|
|
#if TARGET_OS_IOS
|
|
if ([credential isKindOfClass:[FIRPhoneAuthCredential class]]) {
|
|
FIRPhoneAuthCredential *phoneAuthCredential = (FIRPhoneAuthCredential *)credential;
|
|
[self internalUpdateOrLinkPhoneNumberCredential:phoneAuthCredential
|
|
isLinkOperation:YES
|
|
completion:^(NSError *_Nullable error) {
|
|
if (error){
|
|
callInMainThreadWithAuthDataResultAndError(completion, nil, error);
|
|
} else {
|
|
callInMainThreadWithAuthDataResultAndError(completion, result, nil);
|
|
}
|
|
}];
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
[self->_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
|
|
CallbackWithAuthDataResultAndError completeWithError =
|
|
^(FIRAuthDataResult *result, NSError *error) {
|
|
complete();
|
|
callInMainThreadWithAuthDataResultAndError(completion, result, error);
|
|
};
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
completeWithError(nil, error);
|
|
return;
|
|
}
|
|
FIRAuthRequestConfiguration *requestConfiguration = self->_auth.requestConfiguration;
|
|
FIRVerifyAssertionRequest *request =
|
|
[[FIRVerifyAssertionRequest alloc] initWithProviderID:credential.provider
|
|
requestConfiguration:requestConfiguration];
|
|
[credential prepareVerifyAssertionRequest:request];
|
|
request.accessToken = accessToken;
|
|
[FIRAuthBackend verifyAssertion:request
|
|
callback:^(FIRVerifyAssertionResponse *response, NSError *error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
completeWithError(nil, error);
|
|
return;
|
|
}
|
|
FIRAdditionalUserInfo *additionalUserInfo =
|
|
[FIRAdditionalUserInfo userInfoWithVerifyAssertionResponse:response];
|
|
FIRAuthDataResult *result =
|
|
[[FIRAuthDataResult alloc] initWithUser:self additionalUserInfo:additionalUserInfo];
|
|
// Update the new token and refresh user info again.
|
|
self->_tokenService = [[FIRSecureTokenService alloc]
|
|
initWithRequestConfiguration:requestConfiguration
|
|
accessToken:response.IDToken
|
|
accessTokenExpirationDate:response.approximateExpirationDate
|
|
refreshToken:response.refreshToken];
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
completeWithError(nil, error);
|
|
return;
|
|
}
|
|
FIRGetAccountInfoRequest *getAccountInfoRequest =
|
|
[[FIRGetAccountInfoRequest alloc] initWithAccessToken:accessToken
|
|
requestConfiguration:requestConfiguration];
|
|
[FIRAuthBackend getAccountInfo:getAccountInfoRequest
|
|
callback:^(FIRGetAccountInfoResponse *_Nullable response,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
completeWithError(nil, error);
|
|
return;
|
|
}
|
|
self->_anonymous = NO;
|
|
[self updateWithGetAccountInfoResponse:response];
|
|
if (![self updateKeychain:&error]) {
|
|
completeWithError(nil, error);
|
|
return;
|
|
}
|
|
completeWithError(result, nil);
|
|
}];
|
|
}];
|
|
}];
|
|
}];
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (void)unlinkFromProvider:(NSString *)provider
|
|
completion:(nullable FIRAuthResultCallback)completion {
|
|
[_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
|
|
CallbackWithError completeAndCallbackWithError = ^(NSError *error) {
|
|
complete();
|
|
callInMainThreadWithUserAndError(completion, self, error);
|
|
};
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
completeAndCallbackWithError(error);
|
|
return;
|
|
}
|
|
FIRAuthRequestConfiguration *requestConfiguration = self->_auth.requestConfiguration;
|
|
FIRSetAccountInfoRequest *setAccountInfoRequest =
|
|
[[FIRSetAccountInfoRequest alloc] initWithRequestConfiguration:requestConfiguration];
|
|
setAccountInfoRequest.accessToken = accessToken;
|
|
BOOL isEmailPasswordProvider = [provider isEqualToString:FIREmailAuthProviderID];
|
|
if (isEmailPasswordProvider) {
|
|
if (!self->_hasEmailPasswordCredential) {
|
|
completeAndCallbackWithError([FIRAuthErrorUtils noSuchProviderError]);
|
|
return;
|
|
}
|
|
setAccountInfoRequest.deleteAttributes = @[ FIRSetAccountInfoUserAttributePassword ];
|
|
} else {
|
|
if (!self->_providerData[provider]) {
|
|
completeAndCallbackWithError([FIRAuthErrorUtils noSuchProviderError]);
|
|
return;
|
|
}
|
|
setAccountInfoRequest.deleteProviders = @[ provider ];
|
|
}
|
|
[FIRAuthBackend setAccountInfo:setAccountInfoRequest
|
|
callback:^(FIRSetAccountInfoResponse *_Nullable response,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
completeAndCallbackWithError(error);
|
|
return;
|
|
}
|
|
if (isEmailPasswordProvider) {
|
|
self->_hasEmailPasswordCredential = NO;
|
|
} else {
|
|
// We can't just use the provider info objects in FIRSetAcccountInfoResponse because they
|
|
// don't have localID and email fields. Remove the specific provider manually.
|
|
NSMutableDictionary *mutableProviderData = [self->_providerData mutableCopy];
|
|
[mutableProviderData removeObjectForKey:provider];
|
|
self->_providerData = [mutableProviderData copy];
|
|
|
|
#if TARGET_OS_IOS
|
|
// After successfully unlinking a phone auth provider, remove the phone number from the
|
|
// cached user info.
|
|
if ([provider isEqualToString:FIRPhoneAuthProviderID]) {
|
|
self->_phoneNumber = nil;
|
|
}
|
|
#endif
|
|
}
|
|
if (response.IDToken && response.refreshToken) {
|
|
FIRSecureTokenService *tokenService = [[FIRSecureTokenService alloc]
|
|
initWithRequestConfiguration:requestConfiguration
|
|
accessToken:response.IDToken
|
|
accessTokenExpirationDate:response.approximateExpirationDate
|
|
refreshToken:response.refreshToken];
|
|
[self setTokenService:tokenService callback:^(NSError *_Nullable error) {
|
|
completeAndCallbackWithError(error);
|
|
}];
|
|
return;
|
|
}
|
|
if (![self updateKeychain:&error]) {
|
|
completeAndCallbackWithError(error);
|
|
return;
|
|
}
|
|
completeAndCallbackWithError(nil);
|
|
}];
|
|
}];
|
|
}];
|
|
}
|
|
|
|
- (void)sendEmailVerificationWithCompletion:(nullable FIRSendEmailVerificationCallback)completion {
|
|
[self sendEmailVerificationWithNullableActionCodeSettings:nil completion:completion];
|
|
}
|
|
|
|
- (void)sendEmailVerificationWithActionCodeSettings:(FIRActionCodeSettings *)actionCodeSettings
|
|
completion:(nullable FIRSendEmailVerificationCallback)
|
|
completion {
|
|
[self sendEmailVerificationWithNullableActionCodeSettings:actionCodeSettings
|
|
completion:completion];
|
|
}
|
|
|
|
/** @fn sendEmailVerificationWithNullableActionCodeSettings:completion:
|
|
@brief Initiates email verification for the user.
|
|
|
|
@param actionCodeSettings Optionally, a @c FIRActionCodeSettings object containing settings
|
|
related to the handling action codes.
|
|
*/
|
|
- (void)sendEmailVerificationWithNullableActionCodeSettings:(nullable FIRActionCodeSettings *)
|
|
actionCodeSettings
|
|
completion:
|
|
(nullable FIRSendEmailVerificationCallback)
|
|
completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
callInMainThreadWithError(completion, error);
|
|
return;
|
|
}
|
|
FIRAuthRequestConfiguration *configuration = self->_auth.requestConfiguration;
|
|
FIRGetOOBConfirmationCodeRequest *request =
|
|
[FIRGetOOBConfirmationCodeRequest verifyEmailRequestWithAccessToken:accessToken
|
|
actionCodeSettings:actionCodeSettings
|
|
requestConfiguration:configuration];
|
|
[FIRAuthBackend getOOBConfirmationCode:request
|
|
callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable
|
|
response,
|
|
NSError *_Nullable error) {
|
|
[self signOutIfTokenIsInvalidWithError:error];
|
|
callInMainThreadWithError(completion, error);
|
|
}];
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
|
|
dispatch_async(FIRAuthGlobalWorkQueue(), ^{
|
|
[self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
|
|
NSError *_Nullable error) {
|
|
if (error) {
|
|
callInMainThreadWithError(completion, error);
|
|
return;
|
|
}
|
|
FIRDeleteAccountRequest *deleteUserRequest =
|
|
[[FIRDeleteAccountRequest alloc] initWitLocalID:self->_userID
|
|
accessToken:accessToken
|
|
requestConfiguration:self->_auth.requestConfiguration];
|
|
[FIRAuthBackend deleteAccount:deleteUserRequest callback:^(NSError *_Nullable error) {
|
|
if (error) {
|
|
callInMainThreadWithError(completion, error);
|
|
return;
|
|
}
|
|
if (![self->_auth signOutByForceWithUserID:self->_userID error:&error]) {
|
|
callInMainThreadWithError(completion, error);
|
|
return;
|
|
}
|
|
callInMainThreadWithError(completion, error);
|
|
}];
|
|
}];
|
|
});
|
|
}
|
|
|
|
/** @fn signOutIfTokenIsInvalidWithError:
|
|
@brief Signs out this user if the user or the token is invalid.
|
|
@param error The error from the server.
|
|
*/
|
|
- (void)signOutIfTokenIsInvalidWithError:(nullable NSError *)error {
|
|
NSInteger errorCode = error.code;
|
|
if (errorCode == FIRAuthErrorCodeUserNotFound ||
|
|
errorCode == FIRAuthErrorCodeUserDisabled ||
|
|
errorCode == FIRAuthErrorCodeInvalidUserToken ||
|
|
errorCode == FIRAuthErrorCodeUserTokenExpired) {
|
|
FIRLogNotice(kFIRLoggerAuth, @"I-AUT000016",
|
|
@"Invalid user token detected, user is automatically signed out.");
|
|
[_auth signOutByForceWithUserID:_userID error:NULL];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation FIRUserProfileChangeRequest {
|
|
/** @var _user
|
|
@brief The user associated with the change request.
|
|
*/
|
|
FIRUser *_user;
|
|
|
|
/** @var _displayName
|
|
@brief The display name value to set if @c _displayNameSet is YES.
|
|
*/
|
|
NSString *_displayName;
|
|
|
|
/** @var _displayNameSet
|
|
@brief Indicates the display name should be part of the change request.
|
|
*/
|
|
BOOL _displayNameSet;
|
|
|
|
/** @var _photoURL
|
|
@brief The photo URL value to set if @c _displayNameSet is YES.
|
|
*/
|
|
NSURL *_photoURL;
|
|
|
|
/** @var _photoURLSet
|
|
@brief Indicates the photo URL should be part of the change request.
|
|
*/
|
|
BOOL _photoURLSet;
|
|
|
|
/** @var _consumed
|
|
@brief Indicates the @c commitChangesWithCallback: method has already been invoked.
|
|
*/
|
|
BOOL _consumed;
|
|
}
|
|
|
|
- (nullable instancetype)initWithUser:(FIRUser *)user {
|
|
self = [super init];
|
|
if (self) {
|
|
_user = user;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (nullable NSString *)displayName {
|
|
return _displayName;
|
|
}
|
|
|
|
- (void)setDisplayName:(nullable NSString *)displayName {
|
|
dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
|
|
if (self->_consumed) {
|
|
[NSException raise:NSInternalInconsistencyException
|
|
format:@"%@",
|
|
@"Invalid call to setDisplayName: after commitChangesWithCallback:."];
|
|
return;
|
|
}
|
|
self->_displayNameSet = YES;
|
|
self->_displayName = [displayName copy];
|
|
});
|
|
}
|
|
|
|
- (nullable NSURL *)photoURL {
|
|
return _photoURL;
|
|
}
|
|
|
|
- (void)setPhotoURL:(nullable NSURL *)photoURL {
|
|
dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
|
|
if (self->_consumed) {
|
|
[NSException raise:NSInternalInconsistencyException
|
|
format:@"%@",
|
|
@"Invalid call to setPhotoURL: after commitChangesWithCallback:."];
|
|
return;
|
|
}
|
|
self->_photoURLSet = YES;
|
|
self->_photoURL = [photoURL copy];
|
|
});
|
|
}
|
|
|
|
/** @fn hasUpdates
|
|
@brief Indicates at least one field has a value which needs to be committed.
|
|
*/
|
|
- (BOOL)hasUpdates {
|
|
return _displayNameSet || _photoURLSet;
|
|
}
|
|
|
|
- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
|
|
dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
|
|
if (self->_consumed) {
|
|
[NSException raise:NSInternalInconsistencyException
|
|
format:@"%@",
|
|
@"commitChangesWithCallback: should only be called once."];
|
|
return;
|
|
}
|
|
self->_consumed = YES;
|
|
// Return fast if there is nothing to update:
|
|
if (![self hasUpdates]) {
|
|
callInMainThreadWithError(completion, nil);
|
|
return;
|
|
}
|
|
NSString *displayName = [self->_displayName copy];
|
|
BOOL displayNameWasSet = self->_displayNameSet;
|
|
NSURL *photoURL = [self->_photoURL copy];
|
|
BOOL photoURLWasSet = self->_photoURLSet;
|
|
[self->_user executeUserUpdateWithChanges:^(FIRGetAccountInfoResponseUser *user,
|
|
FIRSetAccountInfoRequest *request) {
|
|
if (photoURLWasSet) {
|
|
request.photoURL = photoURL;
|
|
}
|
|
if (displayNameWasSet) {
|
|
request.displayName = displayName;
|
|
}
|
|
}
|
|
callback:^(NSError *_Nullable error) {
|
|
if (error) {
|
|
callInMainThreadWithError(completion, error);
|
|
return;
|
|
}
|
|
if (displayNameWasSet) {
|
|
[self->_user setDisplayName:displayName];
|
|
}
|
|
if (photoURLWasSet) {
|
|
[self->_user setPhotoURL:photoURL];
|
|
}
|
|
if (![self->_user updateKeychain:&error]) {
|
|
callInMainThreadWithError(completion, error);
|
|
return;
|
|
}
|
|
callInMainThreadWithError(completion, nil);
|
|
}];
|
|
});
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|