Front-End Web & Mobile

Integrating Amazon Cognito using developer authenticated identities: An end-to-end example

In September, we introduced developer authenticated identities, a new feature that allows you to utilize your own end-user identities with Amazon Cognito (read our announcement post). The purpose of this post is to show an end-to-end sample that demonstrates how to integrate this feature with an existing authentication system.

Using developer authenticated identities involves interaction between the end-user device, your backend for authentication, and Amazon Cognito. The following diagram shows the flow of calls between the various systems involved.

This sample has two components: a server-side authentication application written in Java (you can find the code here) and updated Cognito Android and iOS sample apps that have added functionality to interact with this backend.

Authentication Server

The authentication server is a simple application designed to store user credentials in a secure manner and provides an OpenID Connect token to authenticated users. The application provides three basic functionalities:

  1. Register Users: You can register a username-password combination that can be used to login to the application.
  2. Login: The server authenticates the user credentials and provides a key for further communication.
  3. GetToken: The client requests an OpenId connect from the server. The server validates the client is authenticated and then contacts Amazon Cognito for the OpenId connect token.

Mobile Application

We have updated the Amazon Cognito samples to interact with this server-side application. The following sections of this post will walk you through the code changes to the sample.

Implementing the custom identity provider

To use developer authenticated identities you need to implement a custom identity provider. The identity provider facilitates the interaction between your mobile app and your authentication system and helps you manage the OpenID connect tokens needed by Cognito.

Android

The DeveloperAuthenticationProvider class extends AWSAbstractCognitoDeveloperIdentityProvider, which contains the getProviderName, login, refresh, and getIdentityId methods.

The first step is to override the getProviderName method to return the your custom provider name. This should be the same name that you chose while configuring your identity pool on the Amazon Cognito console.

The login function makes an asynchronous login request to the server with the credentials entered by the user.

public void login(String userName, String password, Context context) {
  new DeveloperAuthenticationTask(context).execute(new LoginCredentials(userName, password));
}

The DeveloperAuthenticationTask class makes the actual request to the server application. If the request is successful, then it adds your provider name and user identifier to the logins map. The developer user identifier can be an alphanumeric string that uniquely identifies a user in your backend. In the sample, we use username as a developer user identifier.

@Override
protected void onPostExecute(Void result) {
    // Be sure to update the logins map on successful login operation
    if (isSuccessful) {
        CognitoSyncClientManager
                .addLogins(
                        ((DeveloperAuthenticationProvider) CognitoSyncClientManager.provider
                                .getIdentityProvider()).getProviderName(),
                        userName);
    } else {
        new AlertDialog.Builder(context).setTitle("Login error")
                .setMessage("Username or password do not match!!").show();
    }
}

iOS

The DeveloperAuthenticatedIdentityProvider class extends the AWSAbstractCognitoIdentityProvider, which is the custom developer provider we have implemented.  The sample sets the developer provider name as one of the properties of the class in its constructor. The class DeveloperAuthenticationClient implements login and getToken requests to the server application.

- (BFTask *)login:(NSString *)username password:(NSString *)password;

If the login request is successful, then add the developer provider name and developer user identifier to the logins dictionary. The developer user identifier is any identifier that identifies a user uniquely. Again the username is used as a developer user identifier.

[self completeLogin:@{ProviderName: username}];

Implementing the refresh method

The sample app supports the following use cases for authentication: Developer authenticated identities, public providers, and unauthenticated users. For the app to use developer authenticated identities the app should contact the server application, for the last two cases the mobile application instead interacts directly with Amazon Cognito. For this reason, the refresh method has two separate flows depending on the contents of the logins.

If the end user is using your authentication system, then the logins map will have a key for the developer provider name. In this scenario, the communication with Amazon Cognito will be through the server application using the GetOpenIdTokenForDeveloperIdentity API. This API will return an identityId and OpenId connect token. The sample app calls the GetToken functionality of the backend server. This call verifies the authentication and then calls the Amazon Cognito API. Be sure to update the stored identityId and token with the one that you received from the server application using the update function.

If the loginsMap does not contain a key for developer provider name, then the mobile app calls the getIdentityId method and returns null.

Android

public String refresh() {
        setToken(null);
        // If there is a key with developer provider name in the logins map, it
        // means the app user has used developer credentials
        if (getProviderName() != null && !this.loginsMap.isEmpty()
                && this.loginsMap.containsKey(getProviderName())) {
            GetTokenResponse getTokenResponse = (GetTokenResponse) devAuthClient.getToken(
                    this.loginsMap,
                    identityId);
            update(getTokenResponse.getIdentityId(),
                    getTokenResponse.getToken());
            return getTokenResponse.getToken();
        } else {
            this.getIdentityId();
return null;
        }
    }

iOS

- (BFTask *)refresh {
    if (![self authenticatedWithProvider]) {
        return [super getIdentityId];
    }
    else {
        return [[self.client getToken:self.identityId logins:self.logins] continueWithSuccessBlock:^id(BFTask *task) {
            if (task.result) {
                DeveloperAuthenticationResponse *response = task.result;
                if (![self.identityPoolId isEqualToString:response.identityPoolId]) {
                    return [BFTask taskWithError:[NSError errorWithDomain:DeveloperAuthenticationClientDomain
                                                                     code:DeveloperAuthenticationClientInvalidConfig
                                                                 userInfo:nil]];
                }
                
                // potential for identity change here
                self.identityId = response.identityId;
                self.token = response.token;
            }
            return [BFTask taskWithResult:self.identityId];
        }];
    }
}

Implementing the getIdentityId method

Finally, we override the getIdentityId method because once again it will have two potential flows depending on the provider used by the end user. When using the developer authentication system, the identityId will be obtained using the GetToken call of the server application. For other providers or unauthenticated access, we just call the getIdentityId method of the parent class.

Android

@Override
public String getIdentityId() {
    identityId = CognitoSyncClientManager.provider.getCachedIdentityId();
    if (identityId == null) {
        if (getProviderName() != null && !this.loginsMap.isEmpty()
                && this.loginsMap.containsKey(getProviderName())) {
            GetTokenResponse getTokenResponse = (GetTokenResponse) devAuthClient.getToken(
                    this.loginsMap, identityId);
            update(getTokenResponse.getIdentityId(),
                    getTokenResponse.getToken());
            return getTokenResponse.getIdentityId();
        } else {
            return super.getIdentityId();
        }
    } else {
        return identityId;
    }
}

iOS

- (BFTask *)getIdentityId {
    // already cached the identity id, return it
    if (self.identityId) {
        return [BFTask taskWithResult:nil];
    }
    // not authenticated with our developer provider
    else if (![self authenticatedWithProvider]) {
        return [super getIdentityId];
    }
    // authenticated with our developer provider, use refresh logic to get id/token pair
    else {
        return [[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
            if (!self.identityId) {
                return [self refresh];
            }
            return [BFTask taskWithResult:self.identityId];
        }];
    }
}

Summary

We hope this blog post and the sample apps help you better understand the developer authenticated identities flow and make it easy for you to integrate your own authentication system with Amazon Cognito.  If you have any comments, questions, or feedback, feel free to leave a comment here or visit our forums and we will try to assist you.