Front-End Web & Mobile

Mobile token management with Amazon SNS

Introduction

Amazon Simple Notification Service (Amazon SNS) is a fast, flexible, fully managed push messaging service in the cloud. SNS is capable of publishing mobile push notifications to a variety of mobile platforms, including Apple Push Notification service (APNs), Google Cloud Messaging (GCM), Amazon Device Messaging (ADM), Windows Notification Service (WNS), Microsoft Push Notification Service (MPNS), and Baidu Cloud Messaging.

One of the benefits of SNS is that the service proactively manages device tokens on your behalf. Customers have asked us to describe best practices for registering device tokens with SNS. In this blog post, I will describe a recommended approach for registering mobile applications with SNS and maintaining the registration information up to date while minimizing duplicate and erroneous registrations.

Applications, endpoints and tokens

In order to publish a message to a mobile application running on a mobile device, the publisher must have two things:

  • The token of the app/device combination being published to.
  • A valid set of credentials issued by the mobile platform that can be used to connect to it.

The token is a string issued to the application by the operating system of the mobile device.  It uniquely identifies an instance of a mobile app running on a particular mobile device and can be thought of as a unique identifier of this app-device pair.

The exact form the credentials take differs between mobile platforms, but in every case, these credentials must be submitted while making a connection to the platform. One set of credentials is issued per mobile app, and can be used to send a message to any instance of that app.

SNS represents the credentials (plus a few other attributes) as a PlatformApplication object. The tokens (again with some extra data) are represented as objects called PlatformEndpoints. Each PlatformEndpoint belongs to one specific PlatformApplication, and every PlatformEndpoint can be communicated with using the credentials stored in its corresponding PlatformApplication.

Registering an app’s token with Amazon SNS

In order to push mobile notifications to an app using SNS, that app’s token needs to be first registered with SNS using the CreatePlatformEndpoint API method.  This method takes the ARN of the PlatformApplication and the token as parameters and returns the ARN of the created PlatformEndpoint.

The CreatePlatformEndpoint API call is idempotent; it computes what PlatformEndpoint would be created if it were executed and then does the following:

  • If such a PlatformEndpoint already exists, doesn’t create anything; returns the ARN of the already existing PlatformEndpoint.
  • If a PlatformEndpoint with the same token, but different attributes already exists, doesn’t create anything; also does not return anything but throws an exception.
  • If such a PlatformEndpoint does not exist, creates it and returns the ARN of that newly created PlatformEndpoint.

This behavior guarantees that it is always safe to call CreatePlatformEndpoint: it will always either return the PlatformEndpoint you were trying to create, or warn you (via the exception) that you are trying to overwrite an existing PlatformEndpoint that was already modified.

It may be tempting to just call CreatePlatformEndpoint every time at app startup and call it good. In practice this method doesn’t give a working endpoint in some corner cases, such as when an app was uninstalled and reinstalled on the same device and the endpoint for it already exists but is disabled.  A successful registration process should accomplish the following:

  • Ensure a PlatformEndpoint exists for this app/device combination.
  • Ensure the token in the PlatformEndpoint is the latest valid token.
  • Ensure the PlatformEndpoint is enabled and ready to use.

The best practice presented below creates a working, current, enabled endpoint in a wide variety of starting conditions. This approach works whether this is a first time the app is being registered or not, whether or not the PlatformEndpoint for this app already exists, and whether or not the endpoint is enabled or disabled, or has the correct token, and so on.  The approach is also idempotent. It is safe to run it multiple times in a row and it will not create duplicate PlatformEndpoints or alter an existing PlatformEndpoint if it is already up to date and enabled.

retrieve the latest token from the mobile OS
if (endpoint arn not stored)
	# first time registration
	call CreatePlatformEndpoint
	store returned endpoint arn
endif

call GetEndpointAttributes on the endpoint arn 

if (getting attributes encountered NotFound exception)
	#endpoint was deleted 
	call CreatePlatformEndpoint
	store returned endpoint arn
else 
	if (token in endpoint does not match latest) or 
		(GetEndpointAttributes shows endpoint as disabled)
		call SetEndpointAttributes to set the 
                     latest token and enable the endpoint
	endif
endif

This approach can be used any time the app wants to register or re-register itself; it can also be used when notifying SNS of token change.  In that case, you can just invoke it with the latest token value.  Some interesting points to note about this approach are:

  • There are two cases where it may invoke CreatePlatformEndpont. It may be invoked at the very beginning, which handles the case where the app does not know its own PlatformEndpoint ARN, as is the case during a first-time registration. It is also invoked if the initial GetEndpointAttributes call fails with a NotFound exception, as would be the case if the application knows its endpoint ARN but it was deleted.
  • GetEndpointAttributes is called to verify endpoint state even if the endpoint was just created. This handles the case when the PlatformEndpoint already exists but is disabled. In this case, CreatePlatformEndpoint succeeds but does not enable the endpoint, so it is necessary to double-check the state of the endpoint before returning success.

Here is an implementation of the above pseudocode in Java:

 

class RegistrationExample {

AmazonSNSClient client = new AmazonSNSClient(); //provide credentials here


    private void registerWithSNS() {

        String endpointArn = retrieveEndpointArn();
        String token = “retrieved from the mobile os”;

        boolean updateNeeded = false;
        boolean createNeeded = (null == endpointArn);

        if (createNeeded) {
            // No endpoint ARN is stored; need to call CreateEndpoint
            endpointArn = createEndpoint();
            createNeeded = false;
        }

        System.out.println("Retrieving endpoint data...");
        // Look up the endpoint and make sure the data in it is current, even if
        // it was just created
        try {
            GetEndpointAttributesRequest geaReq = 
                    new GetEndpointAttributesRequest()
                    .withEndpointArn(endpointArn);
            GetEndpointAttributesResult geaRes = 
                    client.getEndpointAttributes(geaReq);

            updateNeeded = !geaRes.getAttributes().get("Token").equals(token)
            || !geaRes.getAttributes().get("Enabled").equalsIgnoreCase("true");

        } catch (NotFoundException nfe) {
            // we had a stored ARN, but the endpoint associated with it
            // disappeared. Recreate it.
            createNeeded = true;
        }

        if (createNeeded) {
            createEndpoint();
        }

        System.out.println("updateNeeded=" + updateNeeded);

        if (updateNeeded) {
            // endpoint is out of sync with the current data;
            // update the token and enable it.
            System.out.println("Updating endpoint " + endpointArn);
            Map attribs = new HashMap();
            attribs.put("Token", token);
            attribs.put("Enabled", "true");
            SetEndpointAttributesRequest saeReq = 
                    new SetEndpointAttributesRequest()
                    .withEndpointArn(endpointArn).
                    withAttributes(attribs);
            client.setEndpointAttributes(saeReq);
        }
    }

    /**
     * @return never null
     * */
    private String createEndpoint() {

        String endpointArn = null;
        try {
            System.out.println("Creating endpoint with token " + token);
            CreatePlatformEndpointRequest cpeReq = 
                    new CreatePlatformEndpointRequest()
                    .withPlatformApplicationArn(applicationArn)
                    .withToken(token);
            CreatePlatformEndpointResult cpeRes = client
                    .createPlatformEndpoint(cpeReq);
            endpointArn = cpeRes.getEndpointArn();
        } catch (InvalidParameterException ipe) {
            String message = ipe.getErrorMessage();
            System.out.println("Exception message: " + message);
            Pattern p = Pattern
                    .compile(".*Endpoint (arn:aws:sns[^ ]+) already exists " +
                            "with the same Token.*");
            Matcher m = p.matcher(message);
            if (m.matches()) {
                // the endpoint already exists for this token, but with
                // additional custom data that
                // CreateEndpoint doesn't want to overwrite. Just use the
                // existing endpoint.
                endpointArn = m.group(1);
            } else {
                // rethrow exception, the input is actually bad
                throw ipe;
            }
        }
        storeEndpointArn(endpointArn);
        return endpointArn;
    }

    /**
     * @return the arn the app was registered under previously, or null if no
     *         endpoint arn is stored
     */
    private String retrieveEndpointArn() {
        // retrieve endpointArn from permanent storage,
        // or return null if null stored
        return arnStorage;
    }

    /**
     * Stores the endpoint arn in permanent storage for look up next time
     * */
    private void storeEndpointArn(String endpointArn) {
        // write endpoint arn to permanent storage
        arnStorage = endpointArn;
    }
}

Copyright 2014 Amazon.com, Inc. or its affiliates. 
All Rights Reserved.  Sample code licensed under 
the Apache License, Version 2.0 
(http://aws.amazon.com/apache2.0/).

An interesting thing to note about this implementation is how InvalidParameterException is handled in the createEndpoint() method. SNS rejects CreatePlatformEndpoint requests when an existing endpoint has the same token and a non-null CustomUserData field because the alternative is to overwrite (and therefore lose) the CustomUserData. The createEndpoint() method in the code above captures the InvalidParameterException thrown by SNS, checks whether it was thrown for this particular reason, and if the answer is yes, extracts the ARN of the existing endpoint from the exception. The overall outcome of method invocation can then be treated as a success, since an endpoint with the correct token exists.

Potential pitfalls

Repeatedly calling CreatePlatformEndpoint with an outdated token

Especially in the case of GCM endpoints, it may be tempting to store the first token the application is issued and call CreatePlatformEndpoint with that token every time on application start-up. On the surface, this is an inviting proposition, since it frees the app from having to manage the state of the token and SNS will automatically update the token to its latest value. In practice, however, this solution has a number of serious drawbacks:

  • SNS relies on feedback from GCM to update expired tokens to new tokens. GCM retains information on old tokens for some time, but not indefinitely.  Once GCM forgets about the connection between the old token and the new token, SNS will no longer be able to update the token stored in the endpoint to its correct value; it will just disable the endpoint instead.
  • The platform application will contain multiple endpoints corresponding to the same token.
  • SNS imposes a limit to the number of endpoints that can be created starting with the same token. That limit is three endpoints; eventually creation of new endpoints will fail with an InvalidParameter exception and an error message “This endpoint is already registered with a different token”.

Re-enabling an endpoint associated with an invalid token

When a mobile platform (such as APNS or GCM) informs SNS that the token used in the publish request was invalid, SNS disables the endpoint associated with that token.  SNS will then reject subsequent publishes to that token.  While it may be tempting to simply re-enable the endpoint and keep publishing, in most situations doing this has no beneficial effects: the messages published don’t get delivered and the endpoint becomes disabled again soon after.

The reason for this is that the token associated with the endpoint is genuinely invalid. Deliveries to it cannot succeed because it no longer corresponds to any installed app. The next time it is published to, the mobile platform will again inform SNS that the token is invalid, and SNS will again disable the endpoint.

To re-enable a disabled endpoint, it needs to be associated with a valid token (by using SetEndpointAttributes API call) and then enabled. Only then will deliveries to that endpoint become successful. The only time re-enabling an endpoint without updating its token will work is when a token associated with that endpoint used to be invalid but then became valid again. This can happen, for example, when an app was uninstalled and then re-installed on the same mobile device and received the same mobile token. The approach presented above does this, making sure to only re-enable an endpoint after verifying that the token associated with it is the most current one available.

Our customers tell us SNS is the fastest, most flexible, reliable and most cost-effective way send mobile push notifications, and also deliver notifications to HTTP, SQS, email and SMS endpoints.  We strive to continually improve, and appreciate any feedback you may have to further enhance SNS.  To learn more about getting started with Amazon SNS Mobile Push, and to download our sample apps, visit this link: http://docs.aws.amazon.com/sns/latest/dg/SNSMobilePush.html.