If a user prematurely cancels a Firebase auth multifactor session, is there any way to reset that session?
The error is FIRAuthErrorCodeWebContextAlreadyPresented; code 17057.
I am creating an asynchronous task that retrieves the multifactor session and attempts to verify that user's phone number but there is a use case that causes the page to unwind and logout to a previous page when the app is pushed to the background. When this happens, there is no way to reset this session and try to verify the phone number except by closing the app and restarting.
The easy answer would be to prevent my app from unwinding and prematurely cancelling this flow but I figured there must be SOME way of resetting this session.
Here is some example code that I have been following: https://cloud.google.com/identity-platform/docs/ios/mfa
mfaEnrollmentTask = Task {() -> String? in
do {
guard let multifactorSession = try? await user.multiFactor.session() else {
print("Unable to configure multi-factor session.")
return nil
}
try Task.checkCancellation()
let verificationId = try await PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil, multiFactorSession: multifactorSession)
return verificationId
} catch {
print(error.localizedDescription)
return nil
}
}
return try? await mfaEnrollmentTask?.value
Related
what I need to do is, I have a lot network requests, and at a time backend will return a token expire error, all the requests will receive this error and they all should be hang up, at this time I need send a refresh token request. after refresh token request finish, all paused network request should relaunch with the new token.
now I use retryWhen operator to handle token expire error, and hang up network. I use share replay operator to send refresh token request only once.
networkReqeust.retryWhen({ (error: Observable<TokenError>) in
error.flatMap{ error -> Observable<()> in
switch error {
case .TokenExpired:
return RefreshTokenObservable.share(replay: 1).flatMap({ (result) -> Observable<()> in
switch result {
case .RefreshSuccess:
return Observable.empty()
case .RefreshFailure:
throw error
}
})
}
}
})
let RefreshTokenObservable: Observable<TokenRefresh> = {
let config = URLSessionConfiguration.default
let session = URLSession.init(configuration: config)
let refreshTokenrequest = URLRequest(url: url!)
return session.rx.response(request: refreshTokenrequest).share(replay: 1).observeOn(MainScheduler.instance).flatMapLatest{ (data, response) -> Observable<TokenRefresh> in
let responseModel = ResponseModel(data:response)
if responseModel.status {
return Observable.just(TokenRefresh.RefreshSuccess)
} else {
return Observable.just(TokenRefresh.RefreshFailure)
}
}.observeOn(MainScheduler.instance)
}()
now refresh token request still launch many times, where I did wrong. why share replayoperator not work
I have a strange bug that is occurring only on few user iPhones, details below -
The app consumes a universal framework (developed by ourself) to save accessToken and refreshToken after successful login to the Keychain. We are using Locksmith to achieve the functionality - Save, load data and delete when the user is logged out.
Everytime when the app is killed and launched or applicationWillEnterForeground, the tokens are refreshed with the help of a service call and are saved to keychain again. When the refreshToken expires (this token is valid for one month), user is notified that the app has not been used for a long time and he is logged out.
The actual problem is here that for only few users, the refresh mechanism fails even when they are using the app daily (i.e. not before completion of one month of the refreshToken). After verification with backend team, the refresh service is always up so I suspect the Locksmith loadDataForUserAccount but unable to reproduce the issue. Also, may users do NOT face the problem. Everything works normally as expected.
Can someone help me move further how to identify the cause?
Below is the code to refresh the accessToken and refreshToken
** Refresh token call From the App when app enters foreground or killed and launched**
if let mySession = ServiceLayer.sharedInstance.session {
mySession.refresh { result in
switch result {
case .failure(.authenticationFailure):
if isBackgroundFetch {
print("👤⚠️ Session refresh failed, user is now logged out.")
self.myService.logoutCurrentUser()
// Logout Current user
mySession.invalidate()
self.showLoginUI()
}
else {
// user accessToken is invalid but provide access to QR
// on the home screen. disable all other actions except logout button
self.showHomeScreen()
}
default:
mySession.getAccessToken { result in
switch result {
case let .success(value):
print("Access Token from App Delegate \(value)")
myAccessToken = value
case let .failure(error):
print("❌ Failed to fetch AccessToken: \(error)")
}
}
}
}
}
From the framework where the refresh mechanism is implemented
public func refresh(_ completion: #escaping (MyResult<String, MyError>) -> (Void)) {
guard isValid else {
completion(.failure(.invalidSession))
return
}
getRefreshToken { result in
switch result {
case let .success(refreshToken):
// Get new tokens.
ServiceClient.requestJSON(ServiceRequest.refreshToken(refreshToken: refreshToken)) { result in
switch result {
case let .success(dictionary):
var newAccessToken: String?
var newRefreshToken: String?
for (key, value) in dictionary {
if key as! String == "access_token" {
newAccessToken = value as? String
}
if key as! String == "refresh_token" {
newRefreshToken = value as? String
}
}
guard newAccessToken != nil && newRefreshToken != nil else {
completion(.failure(.general))
return
}
print("Renewed session tokens.")
do {
try Locksmith.updateData(data: [MySession.accessTokenKeychainKey: newAccessToken!, MySession.refreshTokenKeychainKey: newRefreshToken!],
forUserAccount: MySession.myKeychainAccount)
}
catch {
completion(.failure(.general))
}
completion(.success(newAccessToken!))
case let .failure(error):
if error == MyError.authenticationFailure {
print(“Session refresh failed due to authentication error; invalidating session.")
self.invalidate()
}
completion(.failure(error))
}
}
case let .failure(error):
completion(.failure(error))
}
}
}
The app is likely being launched in the background while the device is locked (for app refresh or other background mode you've configured). Protected data (including Keychain) is not necessarily available at that time. You can check UIApplication.isProtectedDataAvailable to check if it's available, and you can reduce the protection of the item to kSecAttrAccessibleAfterFirstUnlock in order to have background access more reliably (though not 100% promised, even in that mode). Locksmith calls this AfterFirstUnlock.
In my iOS/Swift/Firebase app, I am trying to access the "isNewUser" parameter after a user successfully signs in via email/password so that I can pop a window to compel them to reset their password from the temporary one initially assigned upon user creation.
Any insights would be appreciated. Thanks.
The .isNewUser Bool is available from the FirebaseAuth AdditionalUserInfo class.
Here is the link. In order to utilize this code, please see a demo sign in function I wrote below.
Auth.auth().signIn(with: credential) { (result, error) in
if let error = error {
print("Error: \(error)");
return;
}
// Fetch the user's info
guard let uid = result?.user.uid else {return}
// Safely unwrap the boolean value rather than forcing it with "!" which could crash your app if a nil value is found
guard let newUserStatus = result?.additionalUserInfo?.isNewUser else {return}
// Test the value
print("\nIs new user? \(newUserStatus)\n")
if newUserStatus == true {
// Provide your alert prompt
}
else{
// Transition view to continue into the app
}
}
I have a very weird issue to me right now. Initially I thought it has to do with realm, cos I was initially persisting auth token in realm. I eventually had to move it keychain. The issue is this - when a new user signup successfully an auth token is saved to keychain for resuse to hit authenticated end point. However for every new users that signup and tries to make request to an authenticated route, it keeps saying invalid auth token. But the moment the new user terminates the app and relaunch it the error goes away and the new user who was previously unable to make request to authenticated end point will now be able to. I have research the root countless number of times and still no head way I will appreciate your support and pointers to resolved this issue. when i print the auth token from key chain the moment the user is making the request, the token is available. this is how I am retrieving the token from keychain
class UserData {
static let instance = UserData()
private init() {}
func userAuthToken() -> String {
guard let authToken = KeychainWrapper.standard.string(forKey: Const.authToken) else { return ""}
return authToken
}
func userisAuthenticated() -> Bool {
guard let isEnabled = KeychainWrapper.standard.bool(forKey: Const.isEnabled) else { return false}
return isEnabled
}
func userRefreshToken() -> String {
guard let refreshToken = KeychainWrapper.standard.string(forKey: Const.refreshToken) else { return ""}
return refreshToken
}
}
this is the code for saving to keychain at the time of signup
KeychainWrapper.standard.set(true, forKey: Const.isEnabled)
KeychainWrapper.standard.set("2308r20390f", forKey: Const.authToken)
KeychainWrapper.standard.set("2729894820", forKey: Const.refreshToken)
May be you call API in signup process that store default value to token , in request and not change to new saved one until you close app and open again ,
so you have to change API Manager to deal with this point.
to Test this try to use any token in Any api before Signup process
I’m using Realm Object Server for a simple test project and I’m facing problems synchronizing ROS connection setup and follow up usage of the realm object to access the database.
In viewDidLoad I’m calling connectROS function to initialize realmRos object/connection:
var realmRos: Realm!
override func viewDidLoad() {
connectROS()
if(FBSDKAccessToken.current() != nil){
// logged in
getFBUserData()
}else{
// not logged in
print("didLoad, FB user not logged in")
}
}
func connectROS() {
let username = "realm-admin"
let password = "*********"
SyncUser.logIn(with: .usernamePassword(username: username, password: password, register: false), server: URL(string: "http://146.185.154.***:9080")!)
{ user, error in
print("ROS: checking user credentials")
if let user = user {
print("ROS: user credentials OK")
DispatchQueue.main.async {
// Opening a remote Realm
print("ROS: entering dispatch Q main async")
let realmURL = URL(string: "realm://146.185.154.***:9080/~/***book_test1")!
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: realmURL))
self.realmRos = try! Realm(configuration: config)
// Any changes made to this Realm will be synced across all devices!
}
} else if let error = error {
// handle error
print("ROS: user check FAIL")
fatalError(String(describing: error))
}
}
}
In viewDidLoad function next step is to get FB logged user (in this case I’m using FB authentication). After the logged FB user is fetched, the application perform check is that FB user is new user for my application and my proprietary ROS User’s table.
func checkForExistingProfile(user: User) -> Bool {
var userThatExist: User?
do {
try self.realmRos.write() {
userThatExist = self.realmRos.object(ofType: User.self, forPrimaryKey: user.userName)
}
} catch let error as NSError {
print("ROS is not connected")
print(error.localizedDescription)
}
if userThatExist != nil {
return true
} else {
return false
}
}
At this point checkForExistingProfile usually (not always) crashes at try self.realmRos.write() which happens to be nil.
I think the problem comes from the synchronization between connectROS execution (which is asynchrony) and checkForExistingProfile which execution comes before connectROS completion.
Since you didn't show how checkForExistingProfile() is called after viewDidLoad() this is conjecture, but based on everything else you described it's the likely cause.
What you need to do is not call checkForExistingProfile() until the sync user has logged in and your self.realmRos variable has been initialized. Cocoa Touch does nothing to automatically synchronize code written using an asynchronous pattern (like logIn(), which returns immediately but reports its actual completion state in a callback), so you need to manually ensure that whatever logIn() is supposed to do has been done before you call any additional code that depends on its completion.