Skip to content
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

Sync login state across all Apple user's devices #6924

Merged
merged 8 commits into from
Jan 6, 2021

Conversation

joehinkle11
Copy link
Contributor

This change doesn't effect the public API beyond the ability to flip the shareLoginAcrossDevices flag on the auth object, which will cause the keychain's credentials for the user to be synced across their iCloud, which effectively gives the user a shared login state across all their Apple devices.

Works with anonymous logins too which I thought was pretty cool.

Example usage:

// Firebase Auth
let accessGroup = "TEAM_ID.com.example.app.group"
do {
    let auth = Auth.auth()
    auth.shareLoginAcrossDevices = true
    try auth.useUserAccessGroup(accessGroup)
} catch let error as NSError {
    print("Error changing user access group: %@", error)
}
@morganchen12
Copy link
Contributor

Thanks for the contribution! To fix the CI failure, run ./scripts/style.sh from the root of the repository and commit the result.

@joehinkle11
Copy link
Contributor Author

Thanks for the contribution! To fix the CI failure, run ./scripts/style.sh from the root of the repository and commit the result.

All formatted

@rosalyntan
Copy link
Member

Thanks for this contribution! Since this adds a property to the public API, this will need to go through our internal API review process. I'll go ahead and start this process now :)

@joehinkle11
Copy link
Contributor Author

Hey @rosalyntan just checking to see if there's an update. No rush, but it would be nice to point my project at the real Firebase repo again instead of my version :)

@morganchen12
Copy link
Contributor

Hey @joehinkle11, the proposal for this pull request is still in review but at this point is only missing one or two approvers. It's been taking a while mostly because people are in and out of work due to Thanksgiving.

Copy link
Member

@rosalyntan rosalyntan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your patience! The API proposal has been approved, so we can move forward with this now :)

@rosalyntan rosalyntan requested a review from renkelvin December 16, 2020 15:24
@joehinkle11
Copy link
Contributor Author

@rosalyntan thank you, I'll update the PR with the feedback shortly

@joehinkle11
Copy link
Contributor Author

@rosalyntan Okay I've merge the latest master into this PR, implemented those changes, and linted the code

@Ewarrender
Copy link

Thanks @joehinkle11 this is a really important enhancement for our app. We've long had many users that upgrade to a new device and are disappointed that they are no longer logged in and have lost all their data. Most of these users were either anonymous or have forgotten which provider they linked. We then have to spend a lot of time searching for their data.

Having authentication shared across devices should resolve this and allow Firebase built apps to work like other apps such as Facebook (that keep the user logged in after upgrading to a new phone).

I've been watching this PR for a while, would really appreciate if this could get approved and pushed along.

Copy link
Member

@rosalyntan rosalyntan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the changes! LGTM

@rosalyntan rosalyntan merged commit ae854d8 into firebase:master Jan 6, 2021
@joehinkle11
Copy link
Contributor Author

@Ewarrender same here, I'm going to use it for my app appmakerios.com. I want users to be able to use and pay for products without needing to sign up, and anonymous users that sync across iCloud would make that possible

@xaphod
Copy link

xaphod commented Jan 20, 2021

I tried this in Firebase 7.4 (release) and when signing in I get an error:

(lldb) po error.userInfo
▿ 3 elements
  ▿ 0 : 2 elements
    - key : "NSLocalizedDescription"
    - value : An error occurred when accessing the keychain. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo dictionary will contain more information about the error encountered
  ▿ 1 : 2 elements
    - key : "NSLocalizedFailureReason"
    - value : SecItemCopyMatching (-34018)
  ▿ 2 : 2 elements
    - key : "FIRAuthErrorUserInfoNameKey"
    - value : ERROR_KEYCHAIN_ERROR

Are there any entitlements required for this feature to work?
Is the accessGroup an arbitrary string as the code docs imply, or does it need to created somewhere?
Edit: I also notice that when you enable this setting, it causes a user that's already logged in to become logged out. That's a problem - is this a bug or expected?

@joehinkle11
Copy link
Contributor Author

@xaphod I don't know if one is required, I'll try it tonight and see if what my project entitlements are.

And I haven't seen it log anybody out. It would make sense that it'd log someone out if it is preferring iCloud keys over local ones. Then it would "sync" it to the logged out iCloud state. Maybe we need to make a migration path? So if it doesn't find a key on iCloud it automatically takes the local key and syncs that to iCloud?

@xaphod
Copy link

xaphod commented Jan 20, 2021

@joehinkle11 OK - the access group string that you use, is it arbitrary or does it represent something you created?
Yes, I think a migration path would be necessary for the majority of users to use this feature...

@joehinkle11
Copy link
Contributor Author

@joehinkle11
Copy link
Contributor Author

It allows you to share the login state across the multiple apps on the same device

@rosalyntan
Copy link
Member

rosalyntan commented Jan 20, 2021

Hi @xaphod, you can set up your accessGroup as per the docs here: https://firebase.google.com/docs/auth/ios/single-sign-on#share_auth_state_between_apps

Also, regarding the behavior you observed where a signed in user to become signed out, that is the behavior when switching from an unshared keychain to a shared keychain. You can find migration steps here: https://firebase.google.com/docs/auth/ios/single-sign-on#migrate_a_signed-in_user_to_a_shared_keychain

"Note: Switching from non-sharing to a shared keychain results in clearing the signed-in user. Switching from a shared to unshared keychain will not clear the signed-in user, even when no app uses that access group."

@peterfriese
Copy link
Contributor

Here's what works for one of my sample apps:

  • enable Keychain Sharing
  • define a Keychain Group (I use the bundle ID of my main app both in the app and all extensions, so I can share across the app and the extensions as well)
  • call useUserAccessGroup and provide your bundle ID prefixed with your team ID:
do {
      Auth.auth().shareAuthStateAcrossDevices = true
      try Auth.auth().useUserAccessGroup("YG...HH4.com.yourcoolnewapp")
}
  catch let error as NSError {
    print("Error changing user access group: %@", error)
}

Working on a blog post, should hopefully go out next week.

@xaphod
Copy link

xaphod commented Jan 20, 2021

Thanks everyone for the doc links - that's what I was missing.
My use-case is slightly different perhaps: it's a single app (not multiple apps) across multiple devices.
These tutorials appear to be targeted at the multiple-apps case... will they also work for the single-app case?

@MagicFlow29
Copy link

Hey - thank you so much - such a great implementation and beautiful addition. Worked a charm for me, and i used this to push AUTH to a Watch extension.

@xaphod
Copy link

xaphod commented Jan 22, 2021

Adding my thanks - this works well and is a nice way of reducing onboarding friction.
Here's an example for migrating the user over to shared (from nonshared) when you have multiple Firebase apps, in case it helps anyone.

       let TEAM_ID = "V8U8NNNNNN"
       let KEYCHAIN_ACCESS_GROUP = "com.companyname.arbitrary.string" // must match plist keychain sharing entitlement

        let group = DispatchGroup.init()
        var didMigrate = false
        // app1, app2 are each a firebase app, ie. FirebaseApp.app('appname')
        [app1, app2].forEach { app in
            let auth = Auth.auth(app: app!)
            let userNonShared = auth.currentUser
            auth.shareAuthStateAcrossDevices = true
            try? auth.useUserAccessGroup("\(TEAM_ID).\(KEYCHAIN_ACCESS_GROUP)")
            if auth.currentUser == nil, let u = userNonShared{
                NSLog("*MIGRATING USER* to shared keychain, app=\(app!.name)")
                didMigrate = true
                group.enter()
                auth.updateCurrentUser(u, completion: { _ in group.leave() })
            }
        }
        if didMigrate {
            rootVC.showSpinner() // optionally show some UI to indicate the op will take a moment
        }

        group.notify(queue: .main) {
           // finished
        }
@Ewarrender
Copy link

Ewarrender commented Feb 3, 2021

I used a similar approach to @xaphod for migrating as getStoredUser(forAccessGroup:) wasn't reliable for me.

Add this string to your Info plist to avoid hardcoding the team id and keychain group id:

Key Value
KeychainAccessGroup $(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)

Then call this function on launch to enable the shared auth across devices and use the access group while keeping the user logged in:

private func useDeviceSharing() {
    guard let accessGroup = Bundle.main.infoDictionary?["KeychainAccessGroup"] as? String else {
        return
    }
    let unsharedUser = auth.currentUser
    auth.shareAuthStateAcrossDevices = true
    try? auth.useUserAccessGroup(accessGroup)
    let sharedUser = auth.currentUser
    
    if let unsharedUser = unsharedUser, sharedUser == nil {
        auth.updateCurrentUser(unsharedUser) { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    }
}

auth variable being your instance, e.g. Auth.auth()

@firebase firebase locked and limited conversation to collaborators Feb 6, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.