93

I'm writting an application for iphone, which communicates with my server using REST. The main problem is, I need to identify user somehow. Not so long ago, we were allowed to use UDID, but now its not allowed anymore. So what should I use instead? I need some kind of identifier on iphone, so user will delete application, install it again, and he will get same id.

S.Lott
  • 384,516
  • 81
  • 508
  • 779
Drabuna
  • 1,225
  • 1
  • 14
  • 18

7 Answers7

187

I used CFUUIDCreate() to create a UUID:

+ (NSString *)GetUUID {
  CFUUIDRef theUUID = CFUUIDCreate(NULL);
  CFStringRef string = CFUUIDCreateString(NULL, theUUID);
  CFRelease(theUUID);
  return [(NSString *)string autorelease];
}

Then set the above UUID to my NSString:

NSString *UUID = [nameofclasswhereGetUUIDclassmethodresides UUID];

I then stored that UUID to the Keychain using SSKeyChain

To set the UUID with SSKeyChain:

[SSKeychain setPassword:UUID forService:@"com.yourapp.yourcompany" account:@"user"];

To Retrieve it:

NSString *retrieveuuid = [SSKeychain passwordForService:@"com.yourapp.yourcompany" account:@"user"];

When you set the UUID to the Keychain, it will persist even if the user completely uninstalls the App and then installs it again.

To make sure ALL devices have the same UUID in the Keychain.

  1. Setup your app to use iCloud.
  2. Save the UUID that is in the Keychain to NSUserDefaults as well.
  3. Pass the UUID in NSUserDefaults to the Cloud with Key-Value Data Store.
  4. On App first run, Check if the Cloud Data is available and set the UUID in the Keychain on the New Device.

You now have a Unique Identifier that is persistent and shared/synced with all devices.

Kjuly
  • 34,476
  • 22
  • 104
  • 118
Moomio
  • 2,071
  • 2
  • 13
  • 6
  • 1
    +1 This is the best approach i have seen for unique uuid's per user and not devices only! thx a lot. – d.ennis Jun 06 '12 at 10:39
  • I dont quite get it, could you help me understand it better? Would anyone be open to chat with me on real-time in order for me to understand this? I would greatly appreciate it – Sismetic Jul 04 '12 at 19:35
  • Very nice and easy solution. Works like a charm. Thanks – Stefan Arn Feb 20 '13 at 22:01
  • 1
    I like that more than cheese. Thank-you! – Matthew Jul 19 '13 at 21:25
  • @thejaz, I'm confused since you said you're not storing the UUID to iCloud, but then you said you send the UUID to the server to merge. Where are you getting the UUID if you're not storing it? Can you explain in more detail how you're handling this merging and detecting of multiple devices, but the same user? – user1218464 May 24 '14 at 22:59
  • 4
    Hi, Is there any problem while submitting to appstore? Also can user clear keychain? – Durgaprasad Nov 24 '14 at 12:51
  • someone please reply, Can user clear keychain? – rak appdev Oct 24 '16 at 22:23
  • does anyone know apps, who are using this approach in production? I'd be glad to see how it feels comparing to regular authentication methods – stkvtflw Mar 12 '17 at 04:45
70

Firstly, the UDID is only deprecated in iOS 5. That doesn't mean it's gone (yet).

Secondly, you should ask yourself if you really need such a thing. What if the user gets a new device and installs your app on that? Same user, but the UDID has changed. Meanwhile, the original user might have sold his old device so now a completely new user installs your app and you think it's a different person based on the UDID.

If you don't need the UDID, use CFUUIDCreate() to create a unique ID and save it to the user defaults on the first launch (use CFUUIDCreateString() to convert the UUID to a string first). It will survive backups and restores and even come along with the original user when they switch to a new device. It's in many ways a better option that the UDID.

If you really need a unique device identifier (it doesn't sound like you do), go for the MAC address as pointed out in Suhail's answer.

Kerem Baydoğan
  • 10,475
  • 1
  • 43
  • 50
Ole Begemann
  • 135,006
  • 31
  • 278
  • 256
  • 1
    CFUUIDCreate - if same user, will reinstall my app, will it remain same, or will it change? – Drabuna Sep 01 '11 at 16:09
  • That is pretty useful information, cheers for posting! – Suhail Patel Sep 01 '11 at 16:21
  • 2
    @Drabuna: No, `CFUUIDCreate()` will create a new UUID every time you call it. – Ole Begemann Sep 01 '11 at 16:41
  • 1
    User defaults are backed up and survive from one install to the next though, don't they? – Tommy Sep 01 '11 at 17:48
  • 1
    @Tommy: true. If the UUID was saved to a backup and restored from there, it would survive. – Ole Begemann Sep 01 '11 at 17:54
  • @Ole Begemann for The scenarios u pointed out, its Ok we can use the UDID and save it for the app. but consider an Enterprise app getting data from server and server tracks the Device and data sent to it by its UDID then ? In other words in some scenarios the UDID needs to be added to the server before the app goes to the device so that the Data can be sent to that device. – infiniteLoop Jul 21 '12 at 10:55
  • Had to downvote for the recommendation to use the MAC address. See my answer here: http://stackoverflow.com/a/16230057/112191 – james woodyatt Apr 26 '13 at 06:45
  • NSUserDefaults can be reset by any other application which calls [+ (void)resetStandardUserDefaults](http://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/Reference/Reference.html#//apple_ref/occ/clm/NSUserDefaults/resetStandardUserDefaults) And then you are done. Just tested this on two devices. If data is purged on first device, it is also purged from second. Somebody correct me if i am wrong. – Martin Berger Jun 19 '13 at 13:50
  • How would you use the UUID to identify the user if He switch to a new device? – DrCachetes May 06 '15 at 13:56
  • 1
    A better strategy, especially if you are trying to identify the *user* rather than the *device* is to use run `CFUUIDCreate()` once then store the result in the `NSUbiquitousKeyValueStore` so that it is synced across all iOS devices with the same iCloud account and preserved across **all** reinstalls/device resets no matter if the user used a backup or not. – Garrett May 13 '15 at 22:34
35

I was updating my application that was working based only on Unique Identifier which supported iOS 4.3 and above. So,

1) I was unable to use [UIDevice currentDevice].uniqueIdentifier; as it was no longer available

2) I could not use [UIDevice currentDevice].identifierForVendor.UUIDString because it was Available in iOS 6.0 and later only and was unable to use for lower iOS versions.

3) The mac address was not an option as it wasn't allowed in iOS-7

4) OpenUDID was deprecated some time ago and also had issues with iOS-6.

5) Advertisement identifiers were also not available for iOS-5 and below

Finally this was what i did

a) Added SFHFKeychainUtils to the project

b) Generated CFUUID key String

 CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);
    udidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));

c) Saved it to Key Chain Utils or else it will generate a new Unique Each Time

Final Code

+ (NSString *)GetDeviceID {
    NSString *udidString;
   udidString = [self objectForKey:@"deviceID"];
    if(!udidString)
    {
    CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);
    udidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));
    CFRelease(cfuuid);
        [self setObject:udidString forKey:@"deviceID"];
    }
    return udidString;
}

+(void) setObject:(NSString*) object forKey:(NSString*) key
{
    NSString *objectString = object;
    NSError *error = nil;
    [SFHFKeychainUtils storeUsername:key
                         andPassword:objectString
                      forServiceName:@"LIB"
                      updateExisting:YES
                               error:&error];

    if(error)
        NSLog(@"%@", [error localizedDescription]);
}

+(NSString*) objectForKey:(NSString*) key
{
    NSError *error = nil;
    NSString *object = [SFHFKeychainUtils getPasswordForUsername:key
                                                  andServiceName:@"LIB"
                                                           error:&error];
    if(error)
        NSLog(@"%@", [error localizedDescription]);

    return object;
}

enter image description here

For further Details

Quamber Ali
  • 2,170
  • 25
  • 46
  • 3
    This answer deserves more upvotes – Ratata Tata Mar 28 '14 at 14:33
  • 2
    Nicely Explained. Surely this should be the accepted answer –  Mar 28 '14 at 17:07
  • @OrlandoLeite and user2043155. Thankyou.. I was expecting this should be considered the right answer. :'( – Quamber Ali Apr 15 '14 at 15:49
  • @NSQuamber.java for the time that this question was asked the answer that has been chosen is perfectly acceptable and you shouldn't expect the OP to change their chosen answer just because you have given a more up to date answer, I however do believe this answer does deserve at least a +1 as it is a well explained answer. – Popeye Apr 15 '14 at 15:57
15

Some people want to know more about the different options available, and if you do, take a look at the answer from @NSQuamber.java. If you want to know how to use the NSUUID and sync with iCloud, keep reading. This post ended up being more long-winded than I originally wanted, but I hope that it makes it clear for anyone taking these steps!

Using NSUUID

I use the NSUUID class to create the UUID:

NSUUID *uuid = [NSUUID UUID];

Then to create the string, you only need to call the UUIDString method:

NSString *uuidString = [uuid UUIDString];

or do it in one line:

NSString *uuidString = [[NSUUID UUID] UUIDString];

IMHO, this is much easier than trying to use CFUUIDCreate and have a method you have to maintain.


EDIT: I now use UICKeyChainStore

To set the UUID with UICKeyChainStore:

UICKeyChainStore *keychain = [UICKeyChainStore keyChainStoreWithService:@"com.sample.MyApp"];
keychain[@"com.sample.MyApp.user"] = userID;

To retrieve it:

UICKeyChainStore *keychain = [UICKeyChainStore keyChainStoreWithService:@"com.sample.MyApp"];
NSString *userID = keychain[@"com.sample.MyApp.user"];

I then stored that UUID to the Keychain using SSKeyChain

To set the UUID with SSKeyChain:

[SSKeychain setPassword:userID forService:@"com.sample.MyApp.user" account:@"com.sample.MyApp"];

To retrieve it:

NSString *userID = [SSKeychain passwordForService:@"com.sample.MyApp.user" account:@"com.sample.MyApp"];

When you set the UUID to the Keychain, it will persist even if the user completely uninstalls the App and then installs it again.

Syncing with iCloud

So it's useful to make sure that all the user's devices use the same UUID. This is to ensure that data is synchronized across all the devices, rather than each device thinking it is a unique user.

There were several questions in the comments for my answer on how synchronization would work, so now that I've got it all working, I'll provide more details.

Configuring iCloud/NSUbiquitousKeyValueStore Use

  1. Click on your project at the top of the Project Navigator in Xcode.
  2. Select Capabilities.
  3. Turn on iCloud.

It should now look something like this: Screenshot of iCloud enabled

Using NSUbiquitousKeyValueStore

Using iCloud is fairly simple. To write:

// create the UUID
NSUUID *userUUID = [[NSUUID UUID];
// convert to string
NSString *userID = [userUUID UUIDString];
// create the key to store the ID
NSString *userKey = @"com.sample.MyApp.user";

// Save to iCloud
[[NSUbiquitousKeyValueStore defaultStore] setString:userID forKey:userKey];

To read:

// create the key to store the ID
NSString *userKey = @"com.sample.MyApp.user";

// read from iCloud
NSString *userID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:userKey];

Before you can write the NSUbiquitousKeyValueStore documentation states that you are required to read from iCloud first. To force a read, call the following method:

[[NSUbiquitousKeyValueStore defaultStore] synchronize];

To have your app receive notifications of changes in iCloud, add the following notification:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(iCloudStoreDidChange:)
                                             name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
                                           object:[NSUbiquitousKeyValueStore defaultStore]];

Creating the UUID with iCloud

Combining NSUUID, SSKeychain and NSUbiquityKeyValueStore, here's my method for generating a user ID:

- (NSUUID *)createUserID {
    NSString *userKey = @"com.sample.MyApp.user";
    NSString *KEYCHAIN_ACCOUNT_IDENTIFIER = @"com.sample.MyApp";
    NSString *userID = [SSKeychain passwordForService:userKey account:KEYCHAIN_ACCOUNT_IDENTIFIER];
    if (userID) {
        return [[NSUUID UUID] initWithUUIDString:userID];
    }

    // check iCloud
    userID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:userKey];
    if (!userID) {
        // none in iCloud, create one
        NSUUID *newUUID = [NSUUID UUID];
        userID = [newUUID UUIDString];
        // save to iCloud
        [[NSUbiquitousKeyValueStore defaultStore] setString:userID forKey:userKey];
    }

    // store the user ID locally
    [SSKeychain setPassword:userID forService:userKey account:KEYCHAIN_ACCOUNT_IDENTIFIER];
    return [[NSUUID UUID] initWithUUIDString:userID];
}

How to ensure that your User ID is in sync

Because writing to iCloud requires a download of any data in iCloud first, I put the synchronize call at the top of the (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions method. I also added the notification registration there as well. That allows me to detect any changes from iCloud and handle them appropriately.

Here's a sample:

NSString *const USER_KEY = @"com.sample.MyApp.user";
NSString *const KEYCHAIN_ACCOUNT_IDENTIFIER = @"com.sample.MyApp";

- (void)iCloudStoreDidChange:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSNumber *changeReason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
    NSArray *keysChanged = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey];
    if (changeReason) {
        switch ([changeReason intValue]) {
            default:
            case NSUbiquitousKeyValueStoreServerChange:
            case NSUbiquitousKeyValueStoreInitialSyncChange:
                // check changed keys
                for (NSString *keyChanged in keysChanged) {
                    NSString *iCloudID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:keyChanged];
                    if (![keyChanged isEqualToString:USER_KEY]) {
                        NSLog(@"Unknown key changed [%@:%@]", keyChanged, iCloudID);
                        continue;
                    }

                    // get the local key
                    NSString *localID = [SSKeychain passwordForService:keyChanged account:KEYCHAIN_ACCOUNT_IDENTIFIER];
                    if (!iCloudID) {
                        // no value from iCloud
                        continue;
                    }
                    // local ID not created yet
                    if (!localID) {
                        // save the iCloud value locally
                        [SSKeychain setPassword:iCloudID forService:keyChanged account:KEYCHAIN_ACCOUNT_IDENTIFIER];
                        continue; // continue because there is no user information on the server, so no migration
                    }

                    if ([iCloudID isEqualToString:localID]) {
                        // IDs match, so continue
                        continue;
                    }

                    [self handleMigration:keyChanged from:localID to:iCloudID];
                }

                break;
            case NSUbiquitousKeyValueStoreAccountChange:
                // need to delete all data and download new data from server
                break;
        }
    }
}

When the application is launched or when it comes back to the foreground, I force a synchronization with iCloud and verify the integrity of the UUIDs.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self configureSecKeyWrapper];
    // synchronize data from iCloud first. If the User ID already exists, then we can initialize with it
    [[NSUbiquitousKeyValueStore defaultStore] synchronize];
    [self checkUseriCloudSync];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // synchronize changes from iCloud
    [[NSUbiquitousKeyValueStore defaultStore] synchronize];
    [self checkUseriCloudSync];
}

- (BOOL)checkUseriCloudSync {
    NSString *userKey = @"com.sample.MyApp.user";
    NSString *KEYCHAIN_ACCOUNT_IDENTIFIER = @"com.sample.MyApp";
    NSString *localID = [SSKeychain passwordForService:userKey account:KEYCHAIN_ACCOUNT_IDENTIFIER];
    NSString *iCloudID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:userKey];

    if (!iCloudID) {
        // iCloud does not have the key saved, so we write the key to iCloud
        [[NSUbiquitousKeyValueStore defaultStore] setString:localID forKey:userKey];
        return YES;
    }

    if (!localID || [iCloudID isEqualToString:localID]) {
        return YES;
    }

    // both IDs exist, so we keep the one from iCloud since the functionality requires synchronization
    // before setting, so that means that it was the earliest one
    [self handleMigration:userKey from:localID to:iCloudID];
    return NO;
}

If which UUID came first matters

In my use case of my UserID, I assumed that the value in iCloud is the one to keep, since it would be the first UUID pushed to iCloud, regardless of which device generated the UUID first. Most of you would probably take the same path, since you won't really care which UUID it resolves to, as long as it resolves to a single one. For those of you who actually care about which came first, I suggest you store both the UUID and the timestamp generation ([[NSDate date] timeIntervalSince1970]) so that you can check to see which one is older:

// using dates
NSDate *uuid1Timestamp = [NSDate dateWithTimeIntervalSince1970:timestamp1];
NSDate *uuid2Timestamp = [NSDate dateWithTimeIntervalSince1970:timestamp2];
NSTimeInterval timeDifference = [uuid1 timeIntervalSinceDate:uuid2Timestamp];

// or just subtract
double timeDifference = timestamp1 - timestamp2;
mikeho
  • 6,790
  • 3
  • 34
  • 46
  • How do you handle the case where the iCloud sync may not have occurred yet? Therefore, you would have created a new UUID for a device because there was a delay in syncing? Are you running this in production and have you had any issues regarding sync delay? – user1218464 May 24 '14 at 23:02
  • @user1218464: I made a big edit to my answer to show how I handle syncing. There are no issues because I perform both a migration of data to the correct user locally and also issue a request to my server to do the same. – mikeho Jun 12 '14 at 23:10
  • Thanks for the update. It's very helpful. I haven't tested enough, but before you responded, I tried a slightly different approach of syncing the keychain to iCloud so that you don't need to store in NSUbiquitousKeyValueStore. You can also get a timestamp of when a keychain password was created to make sure you always use the oldest one. I'm not sure if it's the best option, but it definitely cuts down on code. – user1218464 Jun 14 '14 at 01:30
  • @user1218464: what did you end up using? NSUbiquitousKeyValueStore is the way to store key-value pairs in iCloud, so I'm guessing you used the document mechanism? – mikeho Jun 15 '14 at 17:12
  • if you set the kSecAttrSynchronizable attribute when adding a keychain item, it'll sync the keychain item with iCloud as of iOS 7.0.3. So, I'm simply setting that attribute and the keychain item (UUID) is synced across devices using iCloud. – user1218464 Jun 16 '14 at 17:49
  • Any chance you could add your method for `handleMigration`? – ipatch Nov 06 '15 at 01:13
  • @Chris I could, but it wouldn't mean much. The ```handleMigration``` method is just for me to migrate my data from one UUID to the other. Essentially, I'm changing the ownership of all ```CoreData``` entities that have a ```user``` field to the new UUID. I'm then deleting the old user entity. Then I push all changed objects up to my server to make modifications on the server side. – mikeho Nov 06 '15 at 01:26
10

There is a nice alternative on Github which generates a Unique Identifier based on a combination of Mac Address and the Bundle Identifier which works pretty well: UIDevice-with-UniqueIdentifier-for-iOS-5

Suhail Patel
  • 13,644
  • 2
  • 44
  • 49
  • Thanks Patel Ji, this is better options I guess, Coz there may be some scenarios where we want the perticular App allways emit the same UIID for that Device. – infiniteLoop Jul 21 '12 at 10:59
  • 1
    The MAC address does not have the uniqueness property required to make it a suitable replacement for the UDID. See my answer here: http://stackoverflow.com/a/16230057/112191 – james woodyatt Apr 26 '13 at 06:43
  • 9
    MAC address is now unavailable in iOS 7. – MusiGenesis Aug 06 '13 at 16:48
1

In iOS7 Apple has introduced a read only property called "identifierForVendor" in the UIDevice class. If you decide to use it you should make note of the following,

  • This value could be nil if it is accessed before the user unlocks the device
  • The value changes when the user deletes all of that vendor’s apps from the device and subsequently reinstalls one or more of them.
  • The value can also change when installing test builds using Xcode or when installing an app on a device using ad-hoc distribution.

If you need an identifier for advertising purposes, use the advertisingIdentifier property of ASIdentifierManager. However make note that point one discussed above is still true for this as well.

Source: https://developer.apple.com/library/ios/documentation/uikit/reference/UIDevice_Class/Reference/UIDevice.html#//apple_ref/occ/instp/UIDevice/identifierForVendor

ThE uSeFuL
  • 1,456
  • 1
  • 16
  • 29
0

This is a hot topic indeed. I have an app that I have to migrate because it used the UDID to name an XML file to be stored on a server. Then the device with the app would connect to the server and download its specific udid.xml and parse it to work.

Ive been thinking that indeed if the user moves to a new device, the app will break. So I really should use something else. The thing is, I don't use a database for the data. The data is simply stored in an xml format, one xml file per device stored on the cloud.

Im thinking the best thing would be to have the user fill out the data on the web, have php create a token on the fly which will not be stored in a database but rather sent to the user. The user can then input the token on the target device and retrieve the xml in question.

That would be my solution to the problem. Not sure how to implement the whole 'creating unique tokens' thing though.

marciokoko
  • 4,988
  • 8
  • 51
  • 91