33

I'm implementing Sign in with Apple and noticed that the email and fullName properties of the returned ASAuthorizationAppleIDCredential are only filled on the very first Sign-In for this Apple ID. On all subsequent Sign-Ins those properties are nil.

Is this a bug on iOS 13 or expected behaviour?

Here is the code I'm using to start the request:

@available(iOS 13.0, *)
dynamic private func signInWithAppleClicked() {
    let request = ASAuthorizationAppleIDProvider().createRequest()
    request.requestedScopes = [.fullName, .email]

    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

I'm receiving the credential in this delegate method:

public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }

    let userIdentifier = credential.user
    let token = credential.identityToken
    let authCode = credential.authorizationCode
    let realUserStatus = credential.realUserStatus
    let mail = credential.email // nil
    let name = credential.fullName // nil
}
mhaddl
  • 885
  • 1
  • 9
  • 18
  • 2
    According to this thread (https://forums.developer.apple.com/message/377782#377437) this seems to be expected behaviour. – mhaddl Aug 22 '19 at 06:18
  • Similar question https://stackoverflow.com/q/57714339/2595805 – Eugene Berdnikov Sep 12 '19 at 07:09
  • 1
    I have spent nearly 3 hours on this, and it seems like it's a bug. The first time when the user creates the account - only then I am able to grab the fullName and email. On subsequent sign in tries, it's only returning me with .user (id) and everything else is returning nil. – Osama Naeem Aug 21 '19 at 19:47
  • This is expected behavior as detailed in Apples documentation - https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple – spig Mar 22 '20 at 03:10
  • 1
    So it sounds like if the user signs in again on a different device, to the same app (or the same device, but the app was deleted), that the info for the user (e.g., full name) has to be propagated down to the app from a server. (Or as indicated below, perhaps from decoding the JWT locally?) – Chris Prince Jan 11 '21 at 01:43
  • Hi, can you please tell me wether the userIdentifier for a particular user will be the same for every time the user logs in using Apple ID or not? – Ankit Jul 05 '21 at 05:06

9 Answers9

66

Seems like a bug but after reading different posts on apple forums it looks like this seems to be the expected behaviour.

So some takeaways.

  1. On the first time sign in with apple (signup) make sure to create user account on your backend.
  2. In case of any connection error with your servers, make sure you save the user details locally (because you are not getting this next time) and keep retrying to create account on your backend.

  3. For testing on device you can revoke your apple ID login for your app. After revoking it will work like a signup next time and you will get the details like (email, name, etc).

To revoke access on your device with IOS 13.

iPhone Settings > Apple Id > Password & Security > Apple ID logins > {YOUR APP} > Stop using Apple ID
Bilal
  • 18,478
  • 8
  • 57
  • 72
  • This make the screen of touch id or any other screen to authenticate it, not call anymore... just doesn't work anymore nothing... any thoughts ?? – Daniel Arantes Loverde Feb 07 '20 at 22:00
  • 1
    If I can only get email and full name on the first call, can I get notified if full name has changed afterwards? – mdonati Mar 25 '20 at 08:35
  • @mdonati : Since iOS 14, you may get this informations by implementing S2S notifications. For now, we don't have more informations about these notifications. – vmeyer Aug 07 '20 at 10:51
12

In case you're wondering how to retrieve email second and subsequent times, here's a hint: use identityToken which contains encoded in JWT user authorisation data including email.

  1. Import this library to decode JWT: https://github.com/auth0/JWTDecode.swift
  2. try this code
    import JWTDecode
    // ...
    if let identityTokenData = appleIDCredential.identityToken,
    let identityTokenString = String(data: identityTokenData, encoding: .utf8) {
    print("Identity Token \(identityTokenString)")
    do {
       let jwt = try decode(jwt: identityTokenString)
       let decodedBody = jwt.body as Dictionary<String, Any>
       print(decodedBody)
       print("Decoded email: "+(decodedBody["email"] as? String ?? "n/a")   )
    } catch {
       print("decoding failed")
    }

Or decode it at PHP backend like this:

    print_r(json_decode(base64_decode(str_replace('_', '/', str_replace('-','+',explode('.', $identityTokenString)[1])))));
XY L
  • 25,431
  • 14
  • 84
  • 143
Ilya Shevyryaev
  • 756
  • 6
  • 8
  • 2
    Only during the first login the JWT includes Email. And never the name or other informations about the user. https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user – Hamoonist May 18 '20 at 13:58
  • 1
    Incorrect. I was able to have email all the time in identityToken in encoded representation. – Ilya Shevyryaev May 19 '20 at 14:05
  • I copy pasted your code, only the first time it included the email. – Hamoonist May 20 '20 at 08:34
  • 2
    When I tried it, the email was also sent in subsequent identityToken responses. Maybe it's worth to note that I chose to hide my email address and that the token contains that apple relay mail address... – user2549803 May 26 '20 at 22:48
  • Well, thanks to guys like you I still have job :). Copy pasting often does not work, you have to dig a bit. This token works perfectly on production app now with JWKFactory php library at backend. – Ilya Shevyryaev Jun 08 '20 at 09:47
  • I replicated the php logic in javascript and it worked perfectly. i was able to get the email address. – Rajesh Jul 07 '20 at 08:20
  • I have given my answer above & it is worked but, @IlyaShevyryaev, You nailed it. If you want to do server-side validation then I must suggest to go with this answer. – Hitesh Surani Jul 08 '20 at 12:51
  • This is good, but it does not get you the full name. I think at the top of the post the person is asking about getting email and full name – LilMoke Sep 20 '20 at 21:08
  • Underrated answer!! Gets you the email when the user has already signed up using Apple Sign in and is trying to login again. Thank you!! – Antoine Neidecker Sep 06 '21 at 15:24
  • I think I noticed what's up... If your first request doesn't include the `.email` scope, subsequent tokens may not contain the email. Likewise, if your first request didn't have it, even if you include it in subsequent requests, the email isn't included in the token's payload. Otherwise, in Apple's own Swift library, even if `.email` is null, if you decode the token, it'll be included in the JWT's payload. – Mismatch Nov 16 '22 at 21:13
  • Tested Aug 2024, the JWT doesn't include the body section in subsequent calls. This would explain it missing on credentials response. – RobMac Aug 06 '23 at 10:51
  • @RobMac bro lives in the future.. – Ilya Shevyryaev Aug 07 '23 at 04:39
  • LOL, I meant 2023. But I bet it will still be the same in 2024. – RobMac Aug 07 '23 at 14:38
5

It is a correct behavior when implementing SignIn with Apple.

This behaves correctly, user info is only sent in the ASAuthorizationAppleIDCredential upon initial user sign up. Subsequent logins to your app using Sign In with Apple with the same account do not share any user info and will only return a user identifier in the ASAuthorizationAppleIDCredential. It is recommended that you securely cache the initial ASAuthorizationAppleIDCredential containing the user info until you can validate that an account has successfully been created on your server.

To overcome this issue we can store all the required information in Keychain. I have created Singleton class for SignIn With Apple. I am sure it will help you.

Git source: https://github.com/IMHitesh/HSAppleSignIn

You need to follow below steps:

Step:1

Add the AppleSignIn folder into your project.

Step:2

Enable SignIn with Apple in Capabilities.

Step:3 -> IMPORTANT

Goto your UIViewController and Add Container view for SignIn with apple.

if #available(iOS 13.0, *) {
    appleSignIn.loginWithApple(view:viewAppleButton, completionBlock: { (userInfo, message) in
        if let userInfo = userInfo{
            print(userInfo.email)
            print(userInfo.userid)
            print(userInfo.firstName)
            print(userInfo.lastName)
            print(userInfo.fullName)
        }else if let message = message{
            print("Error Message: \(message)")
        }else{
            print("Unexpected error!")
        }
    })
}else{
    viewAppleButton.isHidden = true
}
Hitesh Surani
  • 12,733
  • 6
  • 54
  • 65
4

This seems to be the expected behaviour:

This behaves correctly, user info is only sent in the ASAuthorizationAppleIDCredential upon initial user sign up. Subsequent logins to your app using Sign In with Apple with the same account do not share any user info and will only return a user identifier in the ASAuthorizationAppleIDCredential. It is recommened that you securely cache the initial ASAuthorizationAppleIDCredential containing the user info until you can validate that an account has succesfully been created on your server.

Source https://forums.developer.apple.com/thread/121496#379297

Juan de la Torre
  • 1,297
  • 9
  • 19
4

This is not bug but it will indicate that your authentication successfully store to your device setting.

if you want to that all information again then you need to following this states.

  1. go to your device -> Settings -> Apple ID -> Password & Security -> Apps Using your Apple ID -> you get list of apps used sign in with apple {find your app} -> swift left of your apps row {show Delete option} -> click on Delete

  2. restart your app or repress sign in with apple button

Now you can get all information

Rahul Patel
  • 179
  • 1
  • 5
1

In https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple it says:

Because the user’s information isn’t shared with your app in any subsequent API calls, your app should store it locally, immediately after you receive it from the API response. In case of subsequent failures in your process or network, you can read the information from local storage and try processing it again.

spig
  • 1,667
  • 6
  • 22
  • 29
0

The email would be given on the first time sign in. If the user do not "revoke" the apple sign in of your app (which is in the user's Apple ID of system setting page) the callback for signing in would be returned with a nil email value. You could save the user id and email info of the first time sign-in successful result, and when the next time sign in to judge the difference between the return and the saved info.

A better practice is to judge the the value of ASAuthorizationAppleIDProvider.getCredentialState while your app is being "active" for syncing the sign-in state with back-end server in time.

Please refer to: How to Sign Out of Apple After Being Authenticated

Shrdi
  • 359
  • 4
  • 13
0

I wrote a Helper class specific for this issue. This Helper class can help to save and retrieve the user info securely to and from keyChain. I am using SwiftKeychainWrapper library to do the heavy task for me. Feel free to copy paste the helper class in your code.You might need to add any other extra information depending on your need.

import Foundation
import SwiftKeychainWrapper

/// A Helper class which abstract Keychain API related calls.
final class KeyChainService {
    // MARK: - Properties
    static let shared = KeyChainService()
    
    /// Returns previous saved user name if available.
    var appleUserName: String? {
        return KeychainWrapper
            .standard
            .string(forKey: .appAppleUserName)
    }
    
    /// Returns previous saved user appleId/email  if available.
    var appleUserEmail: String? {
        return KeychainWrapper
            .standard
            .string(forKey: .appAppleEmailId)
    }
    
    
    /// Saves the apple user name into keychain.
    /// - Parameter name: Apple user name retrieved form AppleLogin.
    /// - Returns: true if succeed otherwise false.
    @discardableResult
    func saveAppleUserName(name: String?) -> Bool {
        guard let name = name else {return false}
        return KeychainWrapper.standard.set(
            name,
            forKey: KeychainWrapper.Key.appAppleUserName.rawValue
        )
    }
    
    /// Saves the apple user email into keychain.
    /// - Parameter email: Apple userId/email  retrieved form AppleLogin.
    /// - Returns: true if succeed otherwise false.
    @discardableResult
    func saveAppleEmail(email: String?) -> Bool {
        guard let email = email else {return false}
        return KeychainWrapper.standard.set(
            email,
            forKey: KeychainWrapper.Key.appAppleEmailId.rawValue
        )
    }
    

    /// Deletes both apple user name and saved Id from keyChain.
    func deleteSavedAppleUserInfo(){
        KeychainWrapper.standard.remove(forKey: .appAppleUserName)
        KeychainWrapper.standard.remove(forKey: .appAppleEmailId)
    }
}

// MARK: - KeychainWrapper + Extensions
extension KeychainWrapper.Key {
    /// A random string used to identify saved user apple name from keychain.
    static let appAppleUserName: KeychainWrapper.Key = "appAppleUserName"
    
    /// A random string used to identify saved user apple email /Id from keychain.
    static let appAppleEmailId:KeychainWrapper.Key = "appAppleEmailId"
}

Mussa Charles
  • 4,014
  • 2
  • 29
  • 24
0

Aug 2023 update: This is still happening. As mentioned here before, this is expected behavior. When decoding the JWT on subsequent calls, the JWT body (section) is missing.

The only consistent way I've been able to get the name/email again, is by signing out in Settings, removing the app and restarting the device.

The correct way to handle this is by caching the credentials, either on the device or server side (the recommended way, doing S2S updates).

RobMac
  • 793
  • 9
  • 13