Authorize an AVS Device Through a Companion App

For a device without a screen, such as a speaker or soundbar, one way you can have a user authorize the Alexa Voice Service (AVS) is through the your companion app on the user's mobile device. The companion app obtains and transfers the authorization code to the device. The device uses this authorization code to obtain access and refresh tokens from Login with Amazon (LWA), which make the calls to AVS. The following instructions describe how to integrate the LWA SDK into your companion app so that you can access AVS with an LWA token.

Prerequisites

Before you integrate the LWA SDK into your companion app, your device must be able generate a code verifier and create a code challenge. LWA uses the code verifier and code challenge to validate requests from your device before exchanging tokens. For more details about the LWA implementation of symmetric proof of possession, see Proof Key for Code Exchange by OAuth Public Clients.

  • Code Verifier – A code verifier is a cryptographically random string generated by your device, which the device hashes (SHA256) and hands off to your companion app. The string should be from 43 through 128 characters long and composed of characters from the URL-safe and file-name-safe alphabet ([A-Z], [a-z], [0-9], "-", "_ ", ".", "~").

  • Code Challenge – Your device must create a code challenge derived from the code verifier with SHA-256:
  • Code Challenge Method – The method used to derive the code challenge. AVS requires SHA-256.

Process overview to obtain refresh and access tokens through a companion app

The following diagram shows a high-level overview of how your device obtains and refreshes LWA access tokens.

LWA Companion App Flow
Click to enlarge

The following numbered steps provide details about the process shown in the previous diagram:

  1. Your companion app requests the following values from your AVS-enabled device:
    • Product ID
    • Device Serial Number (DSN)
    • Code Challenge
  2. Your device sends these values to your companion app, which displays an LWA button to the user:
  3. When the user taps the LWA Button, the app redirects the user the user, Product ID, DSN, and Code Challenge to an Amazon login page and prompts the user to enter their Amazon credentials and consent.
  4. After consenting, LWA redirects the user back to your companion app along with a one-time use LWA authorization code, which is valid for five minutes.
  5. Your companion app securely transmits the following values to your AVS-enabled device:
    • Authorization Code
    • Client ID
    • Redirect URI
  6. Your device calls LWA, and exchanges the Authorization Code, Client ID, Redirect URI and Code Verifier for a refresh token and access token.
  7. LWA returns a refresh token and access token to your device.
  8. Your device uses the access token to make calls to AVS.
  9. When your access token expires, your device exchanges the Client ID and refreshes token to request a new refresh and access token from LWA. Your device establishes a new connection with AVS after the device has received and updated the tokens. If the token exchange fails, the device should retry the exchange with an exponential back-off. Your device should repeat this step every time your access token expires.
  10. LWA returns a new refresh token and access token to your device.

Use the Login with Amazon SDK

For instructions about how to use the LWA SDK to pass an LWA authorization code to your device, click the appropriate tab to select the OS for your mobile device.

Use LWA SDK for Android to pass a LWA authorization code to your device. Your device can then use the code to obtain refresh and access tokens needed to make calls to AVS.

To use the LWA SDK to obtain an access token for your Android device

  1. Register your Android device in the Amazon developer console, and then generate an API key for your device.

    For instructions on how to create an account and register a device, see Register a Product with AVS.

  2. Navigate to Login with Amazon for Android Overview, and then follow the instructions on the Getting Started section.

  3. Import the Login with Amazon API to your source file by adding the following import statements to your source file.

     import com.amazon.identity.auth.device.AuthError;
     import com.amazon.identity.auth.device.api.authorization.AuthCancellation;
     import com.amazon.identity.auth.device.api.authorization.AuthorizationManager;
     import com.amazon.identity.auth.device.api.authorization.AuthorizeListener;
     import com.amazon.identity.auth.device.api.authorization.AuthorizeRequest;
     import com.amazon.identity.auth.device.api.authorization.AuthorizeResult;
     import com.amazon.identity.auth.device.api.authorization.ScopeFactory;
     import com.amazon.identity.auth.device.api.workflow.RequestContext;
    
  4. Initialize the RequestContext by declaring a RequestContext member variable and creating a new instance of the class.

    To create the instance, pass in the current app context to the static factory method. The best place to initialize the RequestContext is in the onCreate method of your Activity. The following example shows the initialization of the RequestContext in the onCreate method.

     private RequestContext mRequestContext;
    
     @Override
     protected void onCreate(Bundle savedInstance) {
         super.onCreate(savedInstance);
         mRequestContext = RequestContext.create(this);
     }
    
  5. Create an AuthorizeListenerImpl.

    AuthorizeListenerImpl extends the AuthorizeListener abstract class and processes the result of the authorize call. It contains three methods: onSuccess, onError, and onCancel. The AuthorizeResult object passed into the onSuccess method contains the values needed to obtain an authorization token.

    private class AuthorizeListenerImpl extends AuthorizeListener {
    
        /* Authorization was completed successfully. */
        @Override
        public void onSuccess(final AuthorizeResult authorizeResult) {
        }
    
        /* There was an error during the attempt to authorize the application. */
        @Override
        public void onError(final AuthError authError) {
        }
    
        /* Authorization was cancelled before it could be completed. */
        @Override
        public void onCancel(final AuthCancellation authCancellation) {
        }
    }
    
  6. Create an instance of your AuthorizeListenerImpl and register it with your RequestContext instance.

    When the AuthorizeManager.authorize call is made, your RequestContext instance invokes the appropriate callback method in your AuthorizeListener with the result of the authorize request. The best place to register your AuthorizeListenerImpl instance is in the onCreate method.

    @Override
    protected void onCreate(Bundle savedInstance) {
      super.onCreate(savedInstance);
      mRequestContext = RequestContext.create(this);
      mRequestContext.registerListener(new AuthorizeListenerImpl());
    }
    
  7. Override the onResume method in your Activity by calling super.onResume() and the onResume method on your RequestContext instance.

    Override the onResume method in your Activity. Call super.onResume() as well as the onResume method on your RequestContext instance. This notifies the RequestContext to invoke your AuthorizeListener when your app resumes if a callback is ready from your AuthorizeManager.authorize call.

    @Override
    protected void onResume() {
        super.onResume();
        mRequestContext.onResume();
    }
    
  8. In the onClick handler for your Login with Amazon button, call AuthorizeManager.authorize to prompt the user to log in and authorize your app.

    This method authorizes the user in one of the following ways:

    • Switches to the system browser, and then lets the user sign in and consent to the requested information.

    • Switches to web view in a secure context to let the user sign in and consent to the requested information.


    This secure context is available through the Amazon Shopping app on Android devices. Amazon devices running Fire OS–for example, Kindle tablets and Fire TV devices–always use this option even if there is no Amazon Shopping app on the device. Because of this, if the user is already signed in to the Amazon Shopping app, this API skips the sign-in page, leading to a Single Sign On experience for the user.

    Amazon authorizes your app for one or more data sets known as scopes. Scopes are an OAuth mechanism for a client to tell an authorization server what resource permissions they need. You pass an array of scopes as a parameter to the authorize method.

    The first time a user logs in to your app, they receive a prompt asking for consent to access to the required data before proceeding. When you use LWA with AVS, LWA requires the alexa:all scope. The alexa:voice_service:pre_auth scope enables the AVS Hosted Splash for the user.

    The call to authorize is asynchronous, and Amazon invokes your AuthorizeListenerImpl instance with the result of your call.

    The following example adds the following options:

    • scopeData is extra data that includes the product type ID (PRODUCT_ID) and unique DSN (PRODUCT_DSN), that binds to the alexa:all scope.
    • A constant which specifies that the authorization code should be returned.
    • The code challenge. For Base64 URL encoding, Amazon recommends you use Android’s Base64 library with the following flags: Base64.NO_PADDING, Base64.URL_SAFE, and Base64.NO_WRAP.
    • The Code Challenge encoding method (S256).
    • The single-use authorization code you’ve obtained is valid for five minutes and a single use.
     private RequestContext mRequestContext;
     private static final String PRODUCT_ID = "INSERT YOUR PRODUCT ID FROM AMAZON DEVELOPER CONSOLE";
     private static final String PRODUCT_DSN = "INSERT UNIQUE DSN FOR YOUR DEVICE";
     private static final String CODE_CHALLENGE = "INSERT CODE CHALLENGE FROM DEVICE FOR THIS REQUEST";
     private static final String CODE_CHALLENGE_METHOD = "S256";
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mRequestContext = RequestContext.create(this);
         mRequestContext.registerListener(new AuthorizeListenerImpl());
    
         // Find the button with the login_with_amazon ID
         // and set up a click handler
         mLoginButton = (Button) findViewById(R.id.login_with_amazon);
         mLoginButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
                 final JSONObject scopeData = new JSONObject();
                 final JSONObject productInstanceAttributes = new JSONObject();
    
                 try {
                     productInstanceAttributes.put("deviceSerialNumber", PRODUCT_DSN);
                     scopeData.put("productInstanceAttributes", productInstanceAttributes);
                     scopeData.put("productID", PRODUCT_ID);
    
                     AuthorizationManager.authorize(new AuthorizeRequest.Builder(mRequestContext)
                         .addScopes(ScopeFactory.scopeNamed("alexa:voice_service:pre_auth"),
                                 ScopeFactory.scopeNamed("alexa:all", scopeData))
                         .forGrantType(AuthorizeRequest.GrantType.AUTHORIZATION_CODE)
                         .withProofKeyParameters(CODE_CHALLENGE, CODE_CHALLENGE_METHOD)
                         .build());
                 } catch (JSONException e) {
                     // handle exception here
                 }
             }
         });
     }
    
  9. Implement onSuccess for your AuthorizeListenerImpl.

    Retrieve the authorization code, redirect URI, and client ID from the AuthorizeResult to be sent securely to your device.

    private class AuthorizeListenerImpl extends AuthorizeListener {
        @Override
        public void onSuccess(final AuthorizeResult authorizeResult) {
            final String authorizationCode = authorizeResult.getAuthorizationCode();
            final String redirectUri = authorizeResult.getRedirectUri();
            final String clientId = authorizeResult.getClientId();
            // Securely send the authorization code, redirectUri, and clientId to the device
        }
        // …
        // Other methods from AuthorizeListener
    }
    

    After the device receives the authorization code, client ID, and redirect URI, the device should call LWA to exchange the authorization code for access and refresh tokens. When making the call, the device needs to send a POST request to https://api.amazon.com/auth/O2/token and pass in the following parameters:

    • grant_type: authorization_code
    • code: The authorization code received from the Android app.
    • redirect_uri: The redirect URI received from the Android app.
    • client_id: The client ID received from the Android app.
    • code_verifier: The code verifier that was initially generated by the device.

    The response includes the following values:

    • access_token: The access token.
    • refresh_token: The refresh token.
    • token_type: bearer
    • expires_in: The number of seconds for which the access token is still valid.

When the access token has expired or is about to expire, your device exchanges the refresh token for new access and refresh tokens. Your device creates a new connection to AVS after receiving and updating the tokens. If the token exchange fails, your device should retry the exchange with an exponential back-off. To do the exchange, your device sends a POST to https://api.amazon.com/auth/O2/token with the following parameters:

* `grant_type`: `refresh_token`
* `refresh_token`: The refresh token.
* `client_id`: The client ID received from the Android app.

The response includes the following values:

*  `access_token`: The access token.
*  `refresh_token`: The refresh token.
*  `token_type`: `bearer`
*  `expires_in`: The number of seconds for which the access token is still valid.  

Use LWA SDK for iOS to pass a LWA authorization code to your device. Your device can then use the code to obtain refresh and access tokens needed to make calls to AVS..

To use the LWA SDK to obtain an access token for your iOS device

  1. Register your iOS device in the Amazon developer console, and then generate an API key for your device.

    For instructions on how to create an account and register a device, see Register a Product with AVS.

  2. Navigate to Login with Amazon for iOS Overview and follow the instructions in the Getting Started section, and then complete the following steps to obtain a Login with Amazon access token to use with AVS:

  3. Import the Login with Amazon API to your source file by adding the following #import statement to your source file.

     #import <LoginWithAmazon/LoginWithAmazon.h>
    
  4. Call authorize:withHandler: in onLoginButtonClicked.

    If you followed the steps to Add a Login with Amazon Button to Your App, you should have an onLoginButtonClicked: method linked to a Login with Amazon button. In that method, call authorize:withHandler: to prompt the user to login and authorize your app.

    This method enables the user to sign-in and consent to the requested information in one of the following ways:

    • Switches to embedded web view through ASWebAuthenticationSession (on iOS 12 and later)
    • Switches to embedded web view through SFAuthenticationSession (on iOS 11)
    • Switches to Safari View Controller (on iOS 9 and iOS 10)
    • Switches to the system browser (on iOS 8 and earlier)

    The first parameter to authorize:withHandler: is an AMZNAuthorizeRequest object that indicates what scope your app is requesting authorization for. A scope encompasses the user data you are requesting from Login with Amazon. The first time a user logs in to your app, they receive a prompt asking for consent to access to the required data before proceeding.

    The second parameter to authorize:withHandler: is AMZNAuthorizationRequestHandler, described in the next step. For a code sample, see step 5.

  5. Create an AMZNAuthorizationRequestHandler block object.

    AMZNAuthorizationRequestHandler processes the result of the authorize:withHandler: call. For details about objective-c blocks, see Working with Blocks on developer.apple.com.

    The first parameter of AMZNAuthorizationRequestHandler is an AMZNAuthorizeResult object. After a user is authorized successfully, AMZNAuthorizeResult contains an access token, which can be used to access the user profile data, and an AMZNUser object, which contains the user profile data.

    The second parameter of AMZNAuthorizationRequestHandler is a Boolean called userDidCancel. This parameter is set to true if the user performs any of the following actions:

    • Closes the Safari View Controller during login and authorization (on iOS 9 and later)
    • Closes the web view in the Amazon Shopping app
    • Cancels the login or reject authorization

    The third parameter of AMZNAuthorizationRequestHandler is an NSError object, which contains error details if the login and authorization fails due to the SDK or authorization server.

    NSString *productId = @"INSERT YOUR PRODUCT ID FROM AMAZON DEVELOPER CONSOLE";
    NSString *productDsn = @"INSERT UNIQUE DSN FOR YOUR DEVICE";
    NSString *codeChallenge = @"INSERT CODE CHALLENGE FROM DEVICE FOR THIS REQUEST";
    
    - (IBAction) onLogInButtonClicked:(id)sender {
      NSDictionary *scopeData = @{@"productID": productId,
                                  @"productInstanceAttributes": @{@"deviceSerialNumber": productDsn}};
    
      id alexaAllScope = [AMZNScopeFactory scopeWithName:@"alexa:all" data:scopeData];
      id<AMZNScope> alexaSplashScope = [AMZNScopeFactory scopeWithName:@"alexa:voice_service:pre_auth"];
    
      AMZNAuthorizeRequest *request = [[AMZNAuthorizeRequest alloc] init];
      request.scopes = @[alexaSplashScope, alexaAllScope];
      request.codeChallenge = codeChallenge;
      request.codeChallengeMethod = @"S256";
      request.grantType = AMZNAuthorizationGrantTypeCode;
    
      AMZNAuthorizationManager *authManager = [AMZNAuthorizationManager sharedManager];
      [authManager authorize:request withHandler:^(AMZNAuthorizeResult *result, BOOL userDidCancel, NSError *error) {
          if (error) {
              // Notify the user that authorization failed
              [[[UIAlertView alloc] initWithTitle:@"" message:[NSString stringWithFormat:@"User authorization failed due to an error: %@", error.localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
          } else if (userDidCancel) {
              // Notify the user that the authorization was cancelled
              [[[UIAlertView alloc] initWithTitle:@"" message:@"Authorization was cancelled prior to completion. To continue, you will need to try logging in again." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
          } else {
              // Fetch the authorization code and return to controller
              self.authCode = result.authorizationCode;
              self.clientId = result.clientId;
              self.redirectUri = result.redirectUri;
              [self userSuccessfullySignedIn];
          }
      }];
    }
    
  6. Securely transfer the authorization code, client ID, and redirect URI to your device.

    After the device receives the authorization code, client ID, and redirect URI, the device should call LWA to exchange the authorization code for access and refresh tokens. When making the call, the device needs to send a POST request to https://api.amazon.com/auth/O2/token and pass in the following parameters:

    • grant_type: authorization_code
    • code: The authorization code received from the iOS app.
    • redirect_uri: The redirect URI received from the iOS app.
    • client_id: The client ID received from the iOS app.
    • code_verifier: The code verifier that was initially generated by the device.

    The response includes the following values:

    • access_token: The access token.
    • refresh_token: The refresh token.
    • token_type: bearer
    • expires_in: The number of seconds for which the access token is still valid.

    When the access token has expired or is about to expire, your device exchanges the refresh token for new access and refresh tokens. Your device creates a new connection to AVS after receiving and updating the tokens. If the token exchange fails, your device retries the exchange with an exponential back-off. To do the exchange, the device sends a POST to https://api.amazon.com/auth/O2/token with the following parameters:

    • grant_type: refresh_token
    • refresh_token: The refresh token.
    • client_id: The client ID received from the iOS app.

    The response includes the following values:

    • access_token: The access token.
    • refresh_token: The refresh token.
    • token_type: bearer
    • expires_in: The number of seconds for which the access token is still valid.