Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

APNS token collision, stored in Postgres

I use push notifications and store device tokens like I assume everyone else does. First I transform them into a string my app:

NSString *deviceTokenString = [[[token description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]
                               stringByReplacingOccurrencesOfString:@" " withString:@""];

Then I PUT them to my server, where ActiveRecord stores them in a character varying(255) column:

Device.where(:token => device_token, :username => username).first_or_create!(:model => model)

I have a validation that ensures no two tokens are the same, which I understand should always be the case:

class Device < ActiveRecord::Base
  belongs_to :user
  validates_uniqueness_of :token
end

However, I've started to see validation errors for token uniqueness:

ActiveRecord::RecordInvalid: Validation failed: Token has already been taken

Manual query in psql confirms that a device is trying to register with a token already in the table under a different user. Isn't this supposed to be impossible? Is something in the way I'm transforming tokens truncating them? I checked every code example I could find when the problem first occurred and everyone seems to use the method I've listed in the first code sample.

like image 222
kevboh Avatar asked Mar 02 '13 18:03

kevboh


2 Answers

It can happen that a device tries to register with a token already in the table under a different user if someone logs out and then logs in with a different account.

I would do the following on the server for a user user and a token string token (assuming that only one user can be logged in on one device at a time):

  1. Check if there is a Device for token_string.
  2. If there is no device, create one for token_string and user.
  3. If there is a device and its user is not user, update its user to user.

That way, the push notifications will be sent for the last user that logged in on the device.

Concerning your way of transforming the NSData to a hex string on the device, you should not rely on -[NSData description]. Better do it programmatically (typed in, not tested):

- (NSString *)hexStringForData:(NSData *)data
{
    NSUInteger length = data.length;
    const char *bytes = data.bytes;
    NSMutableString *result = [NSMutableString stringWithCapacity:length * 2];
    for (int i = 0; i < length; i++) {
        [result appendFormat:@"%02x", bytes[i] & 0xff];
    }
    return [result copy];
}
like image 155
Tammo Freese Avatar answered Oct 21 '22 07:10

Tammo Freese


I'll wager a guess at this one, but take it for what it's for, a guess.

When iOS devices are restored from backups, or when they are "restored" onto new devices, say, someone upgrading from a iPhone 4 to iPhone 5, or when someone gives their iPhone to their wife or sells it on eBay, you will get duplicated/redundant/confusing device data. I've definitely seen that happen, but not specifically with APNS tokens.

Here is what the APNS docs have to say about it:

By requesting the device token and passing it to the provider every time your application launches, you help to ensure that the provider has the current token for the device. If a user restores a backup to a device or computer other than the one that the backup was created for (for example, the user migrates data to a new device or computer), he or she must launch the application at least once for it to receive notifications again. If the user restores backup data to a new device or computer, or reinstalls the operating system, the device token changes. Moreover, never cache a device token and give that to your provider; always get the token from the system whenever you need it. If your application has previously registered, calling registerForRemoteNotificationTypes: results in the operating system passing the device token to the delegate immediately without incurring additional overhead.

So, I'm not looking at your code, but it seems likely that your "duplicate" tokens have to do with some combination of not registering every time, some kind of caching, and device restoration.

like image 43
slf Avatar answered Oct 21 '22 06:10

slf