App-to-App Account Linking (Configuration guide for your app)

更新时间:
复制 MD 格式

This topic describes how to develop an account linking service that allows users to navigate from your app to the Alexa app to link their accounts and enable skills.

Background information

To use an Alexa-enabled device to control devices in your app, users must link their accounts. They can navigate from your app to the Amazon Alexa voice platform to log on. You can develop an App-to-App Account Linking service to link accounts, retrieve skills, and enable them directly from your app. This service saves users from the need to manually find the skill in the Alexa app and enter their account credentials.

Overview

You can develop an App-to-App Account Linking service to allow users to link to their Alexa account and obtain skills directly from your app. You can open the link in one of two ways:

  • Start the Alexa app from your iOS or Android application.

  • Use Login with Amazon as a fallback for iOS and Android.

App-to-App Account Linking allows a user to start from your app or website to link their Alexa user identity with their identity in another service. When you start App-to-App Account Linking from your app, your users can:

  • Discover your Alexa skill through your app.

  • Enable the skill and link their account from your app.

  • If they are logged on to both your app and the Alexa app on their mobile device, they can link their accounts directly without entering their passwords.

  • If the Alexa app is not installed on their mobile device, they can use Login with Amazon (LWA) to link their accounts from your app. For more information, see Login with Amazon (LWA).

There are two other workflows to implement App-to-App Account Linking:

  • Alexa app only (browser opens within the app): The user completes the account linking entirely within the Alexa app. This is the most common flow. For more information, see Add Account Linking to Your Alexa Skill.

Note

If you have an application or website, we recommend that you implement an App-to-App Account Linking service that does not use the Alexa app browser. This topic describes this type of account linking, which starts from your app.

Target platforms

The Alexa app workflow is available for iOS and Android. The process you implement depends on whether you are developing for an iOS application, an Android application, or a website:

  • iOS and Android

    Use the Alexa app flow as the primary flow. Use the LWA flow as a fallback if the Alexa app is not installed.

  • Websites

    Implement the LWA flow.

User flow

During App-to-App Account Linking, the user follows this workflow.

Your app offers the user the option to enable your skill and link their account to Alexa. If the user chooses to link their account, one of two things happens. The outcome depends on whether the Alexa app is installed on the mobile device.

  • Alexa app

    If the Alexa app is installed on the device, the Alexa app starts. It asks the user to confirm the account linking request. After confirming the request, the user returns to your application.

  • LWA

    If the Alexa app is not installed on the device, a browser window opens with LWA. The user can enter their Amazon account information or create an Amazon account. The user is then asked to grant your skill permission to link accounts. After confirming the request, the user returns to your app.

After the accounts are linked and the user uses your skill, the skill uses the same workflow as the Alexa-app-only flow. For more information, see Alexa app only. In any case, disabling the skill unlinks the accounts.

The following are example screenshots of App-to-App Account Linking. As described, the App-to-App Account Linking workflow depends on whether the user has the Alexa app installed.

Flow design

App-to-App Account Linking works using OAuth 2.0.

The underlying interaction flow is as follows:

  1. The user installs the app on their mobile device and logs in.

  1. Your app offers the user the option to enable your skill and link their account to Alexa. It provides example information, such as "You can now order smart light bulbs through the Cloud Intelligence App with Alexa voice commands." The user confirms the linking request.

  2. What happens next depends on whether the Alexa app is installed on the user's device.

    • If the Alexa app is installed:

      • Your app starts the Alexa app using the Alexa app URL and authorization request parameters.

      • The Alexa app starts and asks the user if they want to link Alexa to your service.

      • The user confirms the linking request.

      • The Alexa app uses a redirect URL to return the user to your app. It sends the user's Amazon authorization code as part of the redirect URL.

    • If the Alexa app is not installed:

      • Your app starts LWA in an in-app browser tab, not the native browser app. It uses the LWA fallback URL and the relevant authorization request parameters.

      • LWA starts and asks the user to log on to their Amazon account.

      • LWA asks the user if they want to link Alexa to your service.

      • The user confirms the linking request.

      • LWA uses your redirect URL to send the user back to your application. It sends the user's Amazon authorization code as part of the redirect URL.

  3. Your backend server calls the LWA authorization service URL. It exchanges the Amazon authorization code retrieved in the previous step for an Amazon access token.

  4. Your backend server calls your authorization server to obtain the user's authorization code for their account in your service.

  5. The backend server calls the Alexa Skill Activation API with the user's Amazon access token and the user's service authorization code. This enables the skill and links the accounts.

  6. Alexa goes to your app's access token URL. It exchanges your service's user authorization code for your service's access token. This completes the App-to-App Account Linking.

Development steps

We recommend that you use the following libraries and languages to provide code samples for iOS apps (10.0 or later) and Android apps:

  1. Display the account linking option in your app.

    Your app needs a start page from which the user can start skill enablement and account linking. This page should show what the user can do by linking their account to Alexa, such as ordering smart lights by voice. How you allow the user to start account linking depends on the experience you want to create. You can offer account linking in multiple places. For example, you can offer the option after the user registers for an account in your app. You can provide a button on a "Link to Alexa" page in the app's settings. In any case, explain the convenience and benefits of linking their account to Alexa.

    • To add an App-to-App Account Linking button to your iOS app:

      1. Open the storyboard in the Xcode integrated development environment (IDE).

      2. Add a UIButton to the view controller and name it "Link your account with Alexa". Consider a name that shows the benefit to the user, such as "Listen to your music with Alexa" or "Order a smart oven that understands you".

      3. In the upper-left corner of the Xcode IDE, click "Show the assistant editor".

      4. Create an action connection for the button by dragging the button to the code editor.

    • To add an App-to-App Account Linking button to your Android app:

      1. In Android Studio, in Design Mode, open the page layout where you want to add the App-to-App Account Linking button.

      2. From the palette, drag a new button to the layout.

      3. Define an ID for the button.

      4. In the page's code, initialize the App-to-App Account Linking button. Define its onClick listener behavior to start the process, as shown in the following example.

        class AppToAppFragment : Fragment() {
          private lateinit var appToAppButton: Button
          override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
            ): View? {
              val root = inflater.inflate(R.layout.appToAppPage, container, false)
              initComponents(root)
              appToApp.setOnClickListener {
                doAppToApp() 
              }
          }
          private fun initComponents(root: View) {
            appToAppButton = root.findViewById(R.id.appToAppAccountLinkingButton)
          }
        }

  1. Enable Universal Links (iOS) or App Links (Android) for your app. To allow the Alexa app to redirect users back to your app, you must enable Universal Links for your iOS application or App Links for your Android application.

    Note

    Universal Links and App Links must comply with the specified URI syntax. Remember the domain and path you add. You will need them when you add the redirect URL to the skill details in the Alexa developer console.

  1. Configure your skill to use App-to-App Account Linking.

    You can use the Alexa developer console, the Alexa Skills Kit command-line interface (ASK CLI), or the Alexa Skill Management API (SMAPI) to configure your skill. During this configuration, you can specify your app's redirect URL, access token URL, and other details.

    • Configure using the Alexa developer console.

      1. Log on to the Alexa developer console.

      2. Find your skill in the list. Under your skill, select Edit.

      3. On the left, click TOOLS, and then click Account Linking.

      4. If you have previously configured Account Linking for your skill, enable "Allow users to link their account to your skill from within your application or website".

      5. For Authorization Grant Type, select Authorization Code Grant if it is not already selected. App-to-App Account Linking only supports the authorization code grant type.

      6. For the other settings, use the settings described in Configure Account Linking. Note the following additional instructions when you fill in the Redirect URL field:

        • For Redirect URL, add the redirect URL described in URLs and parameters.

        • The redirect URL must comply with the specified URI syntax. The Alexa app will open this Universal Link or App Link URL to start your app.

        • You can add multiple redirect URLs to handle different scenarios.

        • If your app does not support Universal Links and App Links, you should have a valid web page that the redirect URL points to. This should support the same process for receiving the user's Amazon authorization code, as described in Flow design.

        • Remember to add redirect URLs for your iOS application, Android application, and website.

    • Configure using the ASK CLI.

      To enable App-to-App Account Linking using the ASK CLI, use the subcommand in Update account linking info and follow these steps:

      • To allow users to enable the skill without account linking, select Y or N.

      • For Authorization URL, enter the URL of your authorization server. This URL is used for regular account linking. The authorization server must accept the user's credentials, authenticate the user, and generate an authorization code. The Alexa app can later pass this code to your authorization server to retrieve an access token that uniquely identifies the user with your service.

      • For Client ID, enter the identifier on your logon page that will be used to identify that the request is from your skill.

      • For Scopes (comma-separated), enter the characters that represent the required access permissions for the user's account, such as the user ID. This is required for smart home skills. You can specify up to 15 scopes.

      • For Domains (comma-separated), enter a list of additional domains from which the logon page retrieves content. You can specify up to 15 domains.

      • For Authorization Grant Type, select AUTH_CODE.

      • For Access Token URI, enter the access token URL. For more information, see URLs and parameters.

      • For Client Secret, enter a credential that allows the Alexa service to authenticate with the access token URI. This is combined with the Client ID to identify requests from Alexa.

      • For Client Authentication Scheme, select HTTP_BASIC or REQUEST_BODY_CREDENTIALS.

      • (Optional) For Default access token expiration time in seconds, enter the time in seconds that the access token is valid.

      • (Optional) For Interactive access token URL, enter the URI that will be called with authorization codes that can be exchanged for Alexa access tokens.

      • (Optional) For Redirect URLs for App-to-App Account Linking, enter the app redirect URL described in URLs and parameters. This setting is required for App-to-App Account Linking.

    • Configure using the Alexa Skill Management API (SMAPI).

      To configure App-to-App Account Linking using the SMAPI, use the structure in this Account linking request.

      The following is an example request to add a redirect URL using SMAPI.

      Name

      Description

      Type

      skipOnEnablement

      Set to true to allow users to enable the skill without starting the account linking flow. Set to false to require the normal account linking flow when a user enables the skill. For more information, see Let users enable your skill without linking an account.

      Boolean

      type

      Specifies the OAuth authorization grant type. For App-to-App Account Linking, you must use AUTH_CODE.

      String

      authorizationUrl

      The URL of your authorization server. It must accept the user's credentials, authenticate the user, and generate an authorization code. The Alexa app can later pass this code to your authorization server to retrieve an access token that uniquely identifies the user with your service.

      String

      domains

      A list of additional domains from which your logon page gets content. You can specify up to 15 domains.

      Array of String

      clientId

      The identifier your logon page uses to identify that the request is from your skill.

      String

      scopes

      A string that indicates the required access permissions for the user's account, such as user_id. This field is required for smart home skills. You can specify up to 15 scopes.

      Array of String

      accessTokenUrl

      The URI used to request an authorization token. This is required only when type is AUTH_CODE.

      String

      clientSecret

      The credential you provide that allows the Alexa service to authenticate with the access token URI. This is combined with the clientId to identify requests from Alexa.

      String

      accessTokenScheme

      The type of authentication used. For example, HTTP_BASIC or REQUEST_BODY_CREDENTIALS. This is required for App-to-App Account Linking because the authorization grant type is AUTH_CODE.

      String

      redirectUrls

      To start your app with a Universal Link or App Link, the redirectURL must comply with the specified URI syntax.

      Array of String

      {
         "accountLinkingRequest": {
            "accessTokenScheme": "HTTP_BASIC",
            "accessTokenUrl": "https://api.amazon.com/auth/o2/token/",
            "authorizationUrl": "https://www.amazon.com/ap/oa/",
            "clientId": "yourSMAPIClientId",
            "clientSecret": "yourSMAPIClientSecret",
            "domains": [],
            "redirectUrls": ["yourRedirectURL1","yourRedirectURL2"],
            "scopes": ["profile"],
            "skipOnEnablement": true,
            "type": "AUTH_CODE"
          }
      }

  1. Obtain the user's Amazon authorization code.

    Your app needs to obtain the user's Amazon authorization code so that it can be exchanged for an Amazon access token later. The Amazon access token will enable your app to enable the skill and complete the account linking. The Amazon authorization code is valid for 5 minutes.

    You can obtain the user's Amazon authorization code by opening the Alexa app URL (for the Alexa app flow) or by making an HTTP GET request to the LWA authorization server (for the LWA flow). For iOS and Android, we strongly recommend that you implement the Alexa app flow and use LWA only as a fallback.

    To obtain the user's Amazon authorization code, assemble the authorization request. Then, open the Alexa app URL (or the LWA fallback URL if the Alexa app is not installed) with the parameters described in URLs and parameters. We strongly recommend that you assemble these URLs on your backend server and pass the assembled URLs to your application. This way, you can quickly change parameters, such as the stage, without rebuilding your application.

    When the user confirms the linking request, the Alexa app redirects the user to your application's redirect URL, which is a parameter in the Alexa or LWA fallback URL. The redirect URL includes the success or error parameters described next. If the user's device cannot start your application using the redirect URL you provided, the user is sent to the same redirect URL in their default browser.

    For example, this can happen in the following situations:

    • Universal Links or App Links are not enabled in your application.

    • Universal Links or App Links are not enabled in your application for the redirect URL you provided.

    • The application version on the user's device is an older version that does not support Universal Links or App Links.

In these cases, you should have a web page (with the same URL as the Universal Link or App Link) to receive the authorization code. When the user goes to your web page, they may need to log on so that you can obtain an authorization code from your service.

For possible errors and the corresponding messages to show the user, see Errors when obtaining the Amazon authorization code.

Successful response to the HTTP GET request for the Alexa app URLIf successful, the response is a redirect URL that includes the user's Amazon authorization code and other parameters shown next. To handle this, see Apple's guidelines on supporting Universal Links in your app and the Android guide for handling Android App Links.

// Example success response from the Alexa app
https://yourRedirectURL?code={Amazon Authorization code}&state={your state}
// Example success response from LWA
https://yourRedirectURL?code={Amazon Authorization code}&scope={permission scope}&state={your state}

Address

  • code: The Amazon authorization code returned by the Alexa app or LWA.

  • state: The state you passed in the authorization request. You should validate the state to prevent cross-site request forgery.

  • scope: The permission scope. LWA returns the alexa::skills:account_linking scope. The Alexa app does not return a scope parameter.

For more details on OAuth 2.0 authorization responses, see Authorization Response.

Error response to the HTTP GET request for the Alexa app URL

If an error occurs, the response is a redirect URL with an error and other parameters, as shown next. Your application must display an appropriate error message (error page, UIAlertController, etc.). For the error messages to display, see Error codes when obtaining the Amazon authorization code.

// Example error response
HTTP 200
https://yourRedirectURL?error_description={Error description}&state={your state}&error={OAuth Error Code}

Additional information

  • error: A single ASCII error code. Possible values: invalid_request, unauthorized_client, access_denied, unsupported_response_type, invalid_scope, server_error, temporarily_unavailable.

  • error_description: A description of the error.

For more details on OAuth 2.0 error responses, see Error Response.

iOS example

Example: Obtain URL (iOS)

In this example, the application obtains both the Alexa app URL and the LWA fallback URL.

import Alamofire
import SwiftyJSON
class AlamofireHelper {
    // Your backend base URL from which you're requesting the URL
    static let BASE_URL = "https://exampleBackEndURL"
    /** Get the Alexa app Universal Link and LWA fallback URL
     @param userAccessToken for your backend server
     @param state maintained between client and server, caller should generate it 
     */
    static func getAlexaAppUrl(userAccessToken:String, state: String) -> DataRequest {
        let alexaAppUrl = "/alexaurl"
        let inputstate = "?state=" + state
        var urlRequest = URLRequest(url: URL(string: BASE_URL + alexaAppUrl + inputstate)!)
        urlRequest.addValue("Bearer " + userAccessToken, forHTTPHeaderField: "Authorization")
        return Alamofire.request(urlRequest)
    }
    private init() {}
}
// Call your backend to get the authorization request URLs (Alexa app / LWA)
AlamofireHelper.getAlexaAppUrl(userAccessToken: savedSession.accessToken, state: StateHelper.generateState()).responseJSON {
    response in
    guard let status = response.response?.statusCode, 200..<300 ~= status else {
        self.createAlert(title: "Failed to get Alexa URL", message: "Failed to get Alexa URL")
        return
    }
    guard let responseValue = response.result.value else {
        self.createAlert(title: "Invalid response", message: "Invalid response")
        return
    }
    let reponseInJSON = JSON(responseValue)
    guard let companionApp = reponseInJSON["companionAppURL"].string,let lwaFallback = reponseInJSON["LWAFallBackURL"].string else {
        self.createAlert(title: "Error response", message: "Error response")
        return
    }
    guard let companionAppURL = URL(string: companionApp), let lwaFallbackURL = URL(string: lwaFallback) else {
        self.createAlert(title: "Incorrect URL format", message: "Incorrect URL format")
        return
    }
    // The openUniversalLinks code snippet can be found in another example
    self.openUniversalLinks(companionAppURL: companionAppURL, lwaFallbackURL: lwaFallbackURL)
}

Example: Open URL (iOS)

In this example, the application opens the Alexa app URL, or the LWA fallback URL if the Alexa app is not installed.

Note the following:

  • The URL opens only if the application (in this case, the Alexa app) is configured to handle Universal Links with the universalLinksOnly option. Therefore, you can handle the case where the Alexa app fails to start using the completionHandler closure expression. For more information about closure expressions, see Closure Expressions. For example, the Alexa app is not installed or the version does not support Universal Links.

  • If your application cannot start the Alexa app when using the universalLinksOnly option, your application needs to use LWA to obtain the Amazon authorization code. You can use SFAuthenticationSession on iOS 11 or ASWebAuthenticationSession on iOS 12.0 or later. This retrieves the Safari user session and cookies, which avoids an extra logon when the user is already logged on to Amazon.com in Safari. This causes your application to display a window asking the user to consent to sharing website information. The content of this message is defined by iOS.

import SafariServices
// Open the Alexa URL with the universalLinksOnly option.
// If it doesn't open the Universal Link, open the LWA fallback. 
private func openUniversalLinks(companionAppURL: URL, lwaFallbackURL: URL) {
    UIApplication.shared.open(companionAppURL, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly:true]) {
        companionAppLaunched in
        if !companionAppLaunched {
            if #available(iOS 12.0, *) {
                WebSession.initWebAuthenticationSession(authURL: lwaFallbackURL)
            } else if #available(iOS 11.0, *) {
                WebSession.initAuthenticationSession(authURL: lwaFallbackURL)
            } else {
                let safariViewController =  SFSafariViewController(url: lwaFallbackURL)
                self.present(safariViewController, animated: true, completion: nil)}
        }
    }
}
// Web session
class WebSession {
    @available(iOS, introduced: 11.0, deprecated: 12.0)
    private static var authenticationSession: SFAuthenticationSession?
    @available(iOS 12.0, *)
    private static var webAuthenticationSession : ASWebAuthenticationSession?
    private static let CALLBACK_URL_SCHEME = "https://yourAppsUniversalLinkURL"
    @available(iOS, introduced: 11.0, deprecated: 12.0)
    static func initAuthenticationSession(authURL:URL) {
        if let session = authenticationSession {
            session.cancel()
        }
        self.authenticationSession = SFAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{
            (callBack:URL?, error:Error?) in
            //Callback and error handling
        })
        self.authenticationSession?.start()
    }
    @available(iOS 12.0, *)
    static func initWebAuthenticationSession(authURL:URL) {
        if let session = webAuthenticationSession {
            session.cancel()
        }
        self.webAuthenticationSession = ASWebAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{
            (callBack:URL?, error:Error?) in
            // Callback and error handling
        })
        self.webAuthenticationSession?.start()
    }
    private init(){}
}

Example: Open LWA URL (iOS)

Opens the LWA URL in the case of a website, as a fallback for when the Alexa app is not installed on an iOS device.

In this example, the application opens LWA in a view controller.

import SafariServices
// Open Login with Amazon in a Safari View controller
private func openSafariView(lwaFallbackURL: URL) {
    let safariViewController =  SFSafariViewController(url: lwaFallbackURL)
    self.present(safariViewController, animated: true, completion: nil)}
}

Example: Handle the Universal Link response (iOS)

In this example, the application validates and extracts the parameters from the Alexa app's call to its Universal Link.

// Handler for Universal Links in AppDelegate
// Add this method to handle incoming Universal Links.
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
        let incomingURL = userActivity.webpageURL else {
            return false
    }
    let handlers : [UniversalLinkHandler] = [AuthResponseHandler(), ErrorResponseHandler()]
    var validatedResponse : ValidatedResponse?
    for handler in handlers {
        if handler.canHandle(incomingURL: incomingURL) {
            validatedResponse = handler.getValidatedResponse()
            let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController
            initialViewController.response = validatedResponse
            self.window?.rootViewController = initialViewController
            self.window?.makeKeyAndVisible()
            return true
        }
    }
    return false
}
// Incoming Universal Links handler protocol
protocol UniversalLinkHandler {
    //Validate the incoming URL scheme.
    func canHandle(incomingURL:URL)-> Bool
    //Get the validated response.
    func getValidatedResponse() -> ValidatedResponse?
}
extension UniversalLinkHandler {
    func validateState(state:String)->Bool {
        return StateHelper.validateState(state: state)
    }
}
// Successfully receiving the Amazon Authorization code
class AuthResponseHandler : UniversalLinkHandler {
    private let AUTH_RESPONSE_PARAMETERS: Set = ["code", "state", "scope"]
    private var validatedResponse: ValidatedResponse?
    func canHandle(incomingURL: URL) -> Bool {
        guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
            let queryParameters = components.queryItems else {
                return false
        }
        var validatedParameters : [String: String] = [:]
        for queryParameter in queryParameters {
            //Duplicated parameters in URL
            if validatedParameters.keys.contains(queryParameter.name) {
                return false
            }
            //Unrecognized parameters in URL
            if !AUTH_RESPONSE_PARAMETERS.contains(queryParameter.name) {
                return false
            }
            validatedParameters[queryParameter.name] = queryParameter.value
        }
        guard let code = validatedParameters["code"], let state = validatedParameters["state"] else {
            return false
        }
        //Validate the state.
        if !validateState(state: state) {
            self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state")
            return true
        }
        self.validatedResponse = AuthResponse(url: incomingURL,code: code, state: state, scope: validatedParameters["scope"])
        return true
    }
    func getValidatedResponse() -> ValidatedResponse? {
        return validatedResponse
    }
}
// Error authorization response handler
class ErrorResponseHandler : UniversalLinkHandler {
    private let ERROR_RESPONSE_PARAMETERS: Set = ["error", "error_description", "state"]
    private var validatedResponse: ValidatedResponse?
    func canHandle(incomingURL: URL) -> Bool {
        guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
            let queryParameters = components.queryItems else {
                return false
        }
        // Validate all query parameters. 
        var validatedParameters : [String: String] = [:]
        for queryParameter in queryParameters {
            // Duplicated parameters in URL
            if validatedParameters.keys.contains(queryParameter.name) {
                return false
            }
            // Unrecognized parameters in URL
            if !ERROR_RESPONSE_PARAMETERS.contains(queryParameter.name) {
                return false
            }
            validatedParameters[queryParameter.name] = queryParameter.value
        }
        guard let error = validatedParameters["error"], let errorDescription = validatedParameters["error_description"], let state = validatedParameters["state"] else {
            return false
        }
        // Validate the state.
        if !validateState(state: state) {
            self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state")
            return true
        }
        self.validatedResponse = ErrorResponse(url: incomingURL, error: error, errorDescription: errorDescription)
        return true
    }
    func getValidatedResponse() -> ValidatedResponse? {
        return validatedResponse
    }
}
// Validated response
class ValidatedResponse {
    // Universal Link URL that launched the app
    private(set) var url: URL
    init (url: URL) {
        self.url = url
    }
}
// Amazon authorization code response per the OAuth 2.0 specification 
class AuthResponse : ValidatedResponse{
    private(set) var code : String
    private(set) var state: String
    // The scope will be returned in Login with Amazon, but will not be present in the Alexa Companion flow.
    private(set) var scope: String?
    init(url:URL, code:String, state:String, scope: String?) {
        self.code = code
        self.state = state
        self.scope = scope
        super.init(url:url)
    }
}
// Error response per the OAuth 2.0 specification 
class ErrorResponse : ValidatedResponse {
    private(set) var error : String
    private(set) var errorDescription: String
    init(url: URL, error:String, errorDescription:String) {
        self.error = error
        self.errorDescription = errorDescription
        super.init(url: url)
    }
}

Android example

Example: Obtain URL (Android)

Make a request to your backend service to obtain the Alexa app URL and the LWA fallback URL. You can implement this request in any way you like, for example, using Retrofit or any other Android library.

The following code is a simple example.

fun doAppToApp(){
  val appToAppUrls: AppToAppUrls = yourBackendService.getAppToAppUrls();
  // This function is shown in a different example. 
  openAlexaAppToAppUrl(appToAppUrls.alexaAppUrl, appToAppUrls.lwaFallBackUrl)
}
data class AppToAppUrls(
  @SerializedName("alexaAppUrl")
  val alexaAppUrl: String,
  @SerializedName("lwaFallBackUrl")
  val lwaFallBackUrl : String
)
interface BackendService {
  fun getAppToAppUrls() : AppToAppUrls
}

Example: Open URL (Android)

In this example, the application opens the Alexa app URL, or the LWA fallback URL if the Alexa app is not installed.

Note the following:

  • This example uses PackageManager to verify that the Alexa app is installed and that the installed version can handle App-to-App Account Linking.

  • If the Alexa app is not installed or cannot handle App-to-App Account Linking, your application must use the LWA flow to obtain the Amazon authorization code. For the LWA fallback URL format, see URLs and parameters below.

  • This example uses the Android ACTION_VIEW intent to open the URL. The App-to-App URL will open in the Alexa app, and the LWA fallback URL will open in the user's default browser.

private fun openAlexaAppToAppUrl(alexaAppUrl: String, lwaFallbackUrl: String){
    if (AlexaAppUtil.isAlexaAppSupportAppLink(fragmentContext!!)) {
        val alexaAppToAppIntent = getAppToAppIntent(alexaAppUrl);
        startActivity(alexaAppToAppIntent)
    } else {
        val lwaAppToAppIntent = getAppToAppIntent(lwaFallbackUrl);
        startActivity(lwaAppToAppIntent)
    }
}
private fun getAppToAppIntent(appToAppUrl: String): Intent {
    return Intent(Intent.ACTION_VIEW, Uri.parse(appToAppUrl))
}
/**
 * Utility to check if the Alexa app is installed and supports app-to-app.
 */
object AlexaAppUtil {
    private const val ALEXA_PACKAGE_NAME = "com.amazon.dee.app"
    private const val ALEXA_APP_TARGET_ACTIVITY_NAME = "com.amazon.dee.app.ui.main.MainActivity"
    private const val REQUIRED_MINIMUM_VERSION_CODE = 866607211 
    /**
     * Check if the Alexa app is installed and supports App Links.
     *
     * @param context Application context.
     */
    @JvmStatic
    fun doesAlexaAppSupportAppToApp(context: Context): Boolean {
        try {
            val packageManager: PackageManager = context.packageManager
            val packageInfo = packageManager.getPackageInfo(ALEXA_PACKAGE_NAME, 0)
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                packageInfo.longVersionCode > REQUIRED_MINIMUM_VERSION_CODE
            } else {
                packageInfo != null
            }
        } catch (e: PackageManager.NameNotFoundException) {
            // The Alexa app is not installed.
            return false
        }
    }
}

Example: Handle the App Link response (Android)

In this example, the application validates and extracts the parameters from the Alexa app's call to its App Link.

/**
 * App-to-app result page landing activity.
 */
class AppToAppResultPageActivity : AppCompatActivity() {
  private lateinit var statusText: TextView
  private lateinit var goBackButton: Button
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val appLinkData = intent.data
    setContentView(R.layout.app_to_app_result)
    initializeComponents()
    statusText.text = "Linking your account"
    // Send the data returned from the Alexa app / LWA to your backend 
    // to call the Skill Activation API to link the accounts. 
    val accountLinking = linkAccountInBackend(appLinkData)
    displayAccountLinkingStatus(result); 
  }
  private fun initializeComponents() {
    statusText = findViewById(R.id.text_status)
    goBackButton = findViewById(R.id.btn_goback)
    goBackButton.setOnClickListener {
      val intent = Intent(this, MainActivity::class.java)
      startActivity(intent)
    }
  }
}

State generation and validation

Example: State Helper

In this example, the application generates and validates the state between the request and the response. You must use the state to validate incoming requests to prevent cross-site request forgery. For more information, see Cross-Site Request Forgery.

import Foundation
import Security
class StateHelper {
    private static let STATE_VALID_FOR = 3600
    // Base64 (iOS + TimeStamp + Secure Random UUID)
    private static let NUMBER_OF_PARAMETER = 3
    static func generateState(session: Session)-> String {
        var buffer = Data(count: 30)
        let _ = buffer.withUnsafeMutableBytes {
            SecRandomCopyBytes(kSecRandomDefault, 30, $0)
        }
        let state = ("iOS." + String(Int64(Date().timeIntervalSince1970)) + "." + buffer.base64EncodedString()).encodeToURISafeBase64()
        // Store the state in the session manager for future validation.
        session.state = state
        LocalSessionManager.saveSession(session: session)
        return state
    }
    static func validateState(state: String) -> Bool {
        guard let originalState = LocalSessionManager.loadSession()?.state  else {
            return false
        }
        if state != originalState {
            return false
        }
        if let timeStampString = originalState.decodeFromURISafeBase64()?.split(separator: "."), timeStampString.count == NUMBER_OF_PARAMETER, let timeStamp = Int64(timeStampString[1]){
            if Int64(Date().timeIntervalSince1970) - timeStamp <= STATE_VALID_FOR {
                return true
            }
        }
        if let session = LocalSessionManager.loadSession() {
            session.state = nil
            LocalSessionManager.saveSession(session: session)
        }
        return false
    }
    private init() {}
}
extension String {
    func decodeFromURISafeBase64() -> String? {
        let base64String = String(self.map {
            // Replace unsafe characters. 
            character in
            if character == "." {
                return  "+"
            } else if character == "_" {
                return "/"
            } else if character == "-" {
                return "="
            }
            return character
        })
        guard let data = Data(base64Encoded: base64String) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
    func encodeToURISafeBase64() -> String {
        return String(Data(self.utf8).base64EncodedString().map{
            // Replace back unsafe characters. 
            character in
            if character == "+" {
                return  "."
            } else if character == "/" {
                return "_"
            } else if character == "=" {
                return "-"
            }
            return character
        })
    }
}

  1. Exchange the Amazon authorization code for an Amazon access token.

    After your app receives the user's Amazon authorization code, you need to use it to obtain an Amazon access token. You need the access token to call the Alexa service to enable the skill and complete the account linking.

    Make an HTTP POST request in the format shown below. For possible errors and the corresponding messages to show the user, see Error codes when exchanging the Amazon authorization code for an Amazon access token.

    Request parameters

    POST /auth/o2/token HTTP/1.1
    Host: api.amazon.com
    Content-Type: application/x-www-form-urlencoded;charset=UTF-8
    grant_type=authorization_code
    &code=amazon authorization code
    &client_id=yourClientId
    &client_secret=yourClientSecret
    &redirect_uri=yourRedirectUri

    Response parameters

    HTTP 200
    Content-Type: application/json
    {
      "access_token": "AmazonAccessToken",
      "expires_in": "3600",
      "refresh_token": "YourRefreshToken",
      "token_type": "bearer"
    }

    For more details on obtaining an access token in the LWA case, see Access Token Request, Access Token Response, and Access Token Errors.

    Example: Obtain the user's Amazon access token

    In this example, the application exchanges the user's Amazon authorization code for an Amazon access token.

    const axios = require('axios');
    // Amazon OAuth token request in the backend
    const OAuthRequests = (function () {
        const header = {
            "headers": {
                "Content-Type": "application/json"
            }
        };
        const accessTokenBody = function (amazonAuthCode, state) {
            return {
                "grant_type": "authorization_code",
                "code": amazonAuthCode,
                "redirect_uri": config.skillConfig.redirect_uri,
                "client_id": config.skillConfig.client_id,
                "client_secret": config.skillConfig.client_secret
            };
        };
        return {
            accessTokenRequest: (authcode, state) => {
                return {
                    header: header,
                    body: accessTokenBody(authcode, state)
                }
            }
        }
    })();
    // Request the Amazon access/refresh tokens from the Amazon OAuth token server
    function getAccessTokenByAuthcode(amazonAuthCode, state) {
        const accessTokenRequest = OAuthRequests.accessTokenRequest(amazonAuthCode, state);
        return axios.post(config.endpoints.oauthEndpoint, accessTokenRequest.body, accessTokenRequest.header);
    }
  2. Enable the skill and complete account linking.

    When your app has the user's Amazon access token, your backend server can call the Alexa Skill Activation API to enable the skill and complete the account linking.

    Make a request to the Alexa Skill Activation API using the following URL format: [BASEURL]/v1/users/~current/skills/{yourSkillId}/enablement, where [BASE URL] depends on the user's region: https://api.amazonalexa.com, https://api.eu.amazonalexa.com, or https://api.fe.amazonalexa.com. The security provider you use must authorize calls from your application to the application's backend server. It must also provide a server-side API to obtain the authorization code for the user currently logged on to your application. That is, it needs to provide an authorization code for the user's account in your service. Alexa will use this code to exchange for a user access token to access the user's resources. For possible errors and the corresponding messages to show the user, see Errors when calling the Alexa Skill Activation API. For more details on the Alexa Skill Activation API, see Alexa Skill Activation API.

    Note
    • If you use LWA as your security provider, remember that the LWA Client ID you use to obtain the userAuthCode is different from the Alexa Client ID shown in the Alexa developer console when configuring the App-to-App Account Linking skill. You cannot use the Alexa Client ID used to obtain the Amazon access token (Step 5) to generate an authCode for your user. To generate a new userAuthCode, use the LWA Client ID associated with your LWA security profile.

    • If you have a smart home skill and have enabled the Send Alexa Events permission for it, your Lambda function must handle the AcceptGrant directive. Otherwise, account linking fails when the user tries to enable your skill. For more information, see Authenticate a Customer to Alexa with Permissions. Note that, in general, sending events to the event gateway still works as it does today. That is, after the skill is enabled, it can obtain an access token from the directive.

    To enable the skill, make the following request:

    Request parameters

    POST /v1/users/~current/skills/{skillId}/enablement HTTP/1.1
    Host: api.amazonalexa.com, api.eu.amazonalexa.com, api.fe.amazonalexa.com
    Content-Type: application/json
    Authorization: "Bearer {Amazon Access Token}"
    {
        "stage": "skill stage",
        "accountLinkRequest": {
          "redirectUri": "https://yourRedirectURI",
          "authCode": "Your user's authorization code from your authorization server",
          "type": "AUTH_CODE"
        }
    }

    In the next request shown, set HOST to one of the following, depending on the user's region: api.amazonalexa.com, api.eu.amazonalexa.com, or api.fe.amazonalexa.com

    Successful response

    HTTP 201
    Content-Type: application/json
    {
      "skill": {
        "stage": "your skill stage",
        "id": "your skill id"
      },
      "user": {
        "id": "User ID"
      },
      "accountLink": {
        "status": "LINKED"
      },
      "status": "ENABLED"
    }

    Note that you can disable the skill and unlink the user's account by making a DELETE request to the Alexa Skill Activation API.

    Example: Enable the skill and complete account linking

    In this example, the application enables the skill and completes the account linking.

    const _ = require('lodash');
    const axios = require('axios');
    const {config} = require('../config/skillconfig');
    const {getAccessTokenByAuthcode} = require('./oauthrequests');
    // Request to the Alexa Skill Activation API in the backend
    const AlexaServiceRequest = (() => {
        const header = (amazonAccessToken) => {
            return {
                "headers": {
                    "Content-Type": "application/json",
                    "Authorization": "Bearer " + amazonAccessToken
                }
            };
        };
        const body = (userAuthCode) => {
            return {
                "stage": config.skillConfig.stage,
                "accountLinkRequest": {
                    "redirectUri": config.skillConfig.redirect_uri,
                    "authCode": userAuthCode,
                    "type": "AUTH_CODE"
                }
            }
        }
        return {
            createEnablement: (amazonAccessToken, userAuthCode) => {
                return {
                    header: header(amazonAccessToken),
                    body: body(userAuthCode)
                }
            }
        }
    })();
    function createEnablementWithAccountLink(amazonAuthCode, userAuthcode, state) {
        // Exchange the Amazon OAuth Code for an Amazon Access Token.
        const tokenPromise = getAccessTokenByAuthcode(amazonAuthCode, state);
        return tokenPromise.then((res) => {
            if (!_.has(res, 'data.access_token')) {
                throw Error("Amazon AccessToken is invalid");
            }
            console.log(res);
            const amazonAccessToken = res.data.access_token;
            // Call the Alexa Skill Activation API to create the enablement.
            const createEnablementRequest = AlexaServiceRequest.createEnablement(amazonAccessToken, userAuthcode);
            return sendPostRequests(config.endpoints.alexaServiceEndpoints, createEnablementRequest.body, createEnablementRequest.header);
        });
    }
    // Post the enablement to the Alexa Skill Activation API.
    function sendPostRequests(endpoints, body, header) {
        var alexaServicePromises = []
        // Post for each Alexa Skill Activation API regional endpoint for the user.
        endpoints.forEach((endpoint)=> {
            alexaServicePromises.push(axios.post(endpoint, body, header));
        });
        return new Promise((resolve, reject)=> {
            var failures = 0;
            alexaServicePromises.forEach((promise)=> {
                promise.then((res)=> {
                    if (res.status == 201) {
                        resolve(res.data);
                    } else {
                        if (++failures == alexaServicePromises.length) {
                            reject(res.data);
                        }
                    }
                }).catch((err)=> {
                    if (++failures == alexaServicePromises.length) {
                        reject(err.data);
                    }
                })
            })
        });
    }
  3. Display the account linking status in your app.

    Keep the user informed of the account linking status by displaying it in your app, for example, in the screenshots in "User experience and errors". You can use different user interface components, such as a table or a page view, to display the account linking status.

    Example: Display account linking status

    In this example, the application uses a UIViewController to display the account linking status.

    1. In the Xcode IDE, go to the main storyboard and add a new view controller. The storyboard ID for this view controller is "linkingStatusScreen".

    2. Add a new Cocoa Touch class file, select a subclass of UIViewController, and connect your class file with the UIViewController in the Identity Inspector.

    3. Add a title, a UILabel to display the account linking status, a UIButton to close this page, and a UIActivityIndicatorView to show a loading animation while the application calls the Alexa service.

      import UIKit
      class LinkingStatusViewController: UIViewController {
          @IBOutlet weak var header: UILabel!
          @IBOutlet weak var status: UILabel!
          @IBOutlet weak var closeButton: UIButton!
          @IBOutlet weak var indicator: UIActivityIndicatorView!
          var response : ValidatedResponse?
          override func viewDidLoad() {
              super.viewDidLoad()
              if let res = self.response {
                  passValidatedResponse(validatedResponse: res)
              }
          }
          private func updateView(header:String, status:String, animating: Bool,closeButton:Bool) {
              if animating {
                  self.indicator?.startAnimating()
              } else {
                  self.indicator?.stopAnimating()
              }
              self.indicator?.isHidden = !animating
              self.header?.text = header
              self.status?.text = status
              self.closeButton.isEnabled = closeButton
              if closeButton {
                  self.closeButton.backgroundColor = #colorLiteral(red: 0.1960784314, green: 0.6039215686, blue: 0.8392156863, alpha: 1)
              } else {
                  self.closeButton.backgroundColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1)
              }
          }
      }
      extension LinkingStatusViewController {
          func passValidatedResponse(validatedResponse: ValidatedResponse) {
              if validatedResponse is AuthResponse {
                  let response: AuthResponse = validatedResponse as! AuthResponse;
                  updateView(header: "Status", status: "Loading", animating: true, closeButton: false)
                  usingAuthCodeToGetAccessToken(amazonAuthCode: response.code)
              } else if (validatedResponse is ErrorResponse) {
                  let response: ErrorResponse = validatedResponse as! ErrorResponse;
                  updateView(header: response.error, status: response.errorDescription, animating: false, closeButton: true)
              } else {
                  updateView(header: "Unknown request", status: validatedResponse.url.absoluteString, animating: false, closeButton: true)
              }
          }
          // Complete account linking.
          private func usingAuthCodeToGetAccessToken(amazonAuthCode: String) {
              guard let session = LocalSessionManager.loadSession(), session.sessionValid  else {
                  updateView(header: "Status", status: "User not logged in", animating: false, closeButton: true)
                  return;
              }
              AlamofireHelper.completeAccountLinking(userAccessToken: LocalSessionManager.loadSession()!.accessToken, amazonAuthCode: amazonAuthCode, state: StateHelper.generateState(session: session)).responseJSON(queue: DispatchQueue.global(qos: .userInitiated), options: []) {
                  response in
                  if let status = response.response?.statusCode, 200..<300 ~= status {
                      DispatchQueue.main.sync {
                          self.updateView(header: "Status", status: "Account Linking Succeeded", animating: false, closeButton: true)
                      }
                      return
                  } else {
                      DispatchQueue.main.sync {
                          self.updateView(header: "Failed", status: "Account Linking failed", animating: false, closeButton: true)
                      }
                  }
              }
          }
      }
      // Open the account linking status page.
      let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController
      initialViewController.response = validatedResponseself.window?.rootViewController = initialViewController
      self.window?.makeKeyAndVisible()
  4. Use the access token in your skill.

    After the user successfully enables your skill and links Alexa to your service, requests sent to your skill include the user's access token. Your skill code needs to retrieve the access token from the request, validate it, and use it to retrieve the necessary user information from the resource server. For more details on how to validate and use the access token, see the documentation for your skill type:

Example: Cloud Intelligence app (Android and iOS)

Cloud Intelligence Android app

App-Alexa-App

  1. Obtain Alexa App-to-App parameters.

    Call the cloud API /living/voice/alexa/apptoapp/config/get to obtain the Alexa configuration.

    Returned entity object:

    public class AlexaApptoappConfigGetDTO {
        private String alexaAppUrl;
        private String lwaFallbackUrl;
    
  2. Launch Alexa.

     private void openAlexaAppToAppUrl(String alexaAppUrl, String lwaFallbackUrl) {
            Intent alexaAppToAppIntent;
            if (doesAlexaAppSupportAppToApp(getApplicationContext())) {
                alexaAppToAppIntent = getAppToAppIntent(alexaAppUrl);
            } else {
                alexaAppToAppIntent = getAppToAppIntent(lwaFallbackUrl);
            }
            startActivity(alexaAppToAppIntent);
            finish();
        }
    private Intent getAppToAppIntent(String appToAppUrl) {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(appToAppUrl));
            return intent;
        }
  3. Receive the skill binding result from Alexa.

    <activity
                android:name=".activity.AuthActivity"
                android:launchMode="singleTask"
                android:screenOrientation="portrait">
    
                <!--Amazon Alexa App-->
                <intent-filter
                    android:autoVerify="true"
                    tools:ignore="UnusedAttribute">
    
                    <action android:name="android.intent.action.VIEW" />
    
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
    
                    <data
                        android:host="yourHost"
                        android:scheme="yourScheme"
                        android:pathPrefix="yourPathPrefix" />
                </intent-filter>
            </activity>
      private void handleIntent(@NonNull Intent intent) {
            ALog.d(TAG, "handleIntent: ");
            // Callback from Amazon Alexa
            if (intent.getData() == null) return;
            String appLinkData = intent.getData().toString();
            ILog.d(TAG, "appLinkData:" + appLinkData);
            // After the callback from Alexa, authenticate with the cloud. Set a tag to prevent the current Cloud Intelligence account from being logged out.
    
            String domain = "https://open-living.iot.aliyun.com";
            if (GlobalConfig.API_ENV_PRE.equals(GlobalConfig.getInstance().getApiEnv())) {
                domain = "https://open-living-beta.iot.aliyun.com";
            }
            if (appLinkData.startsWith(domain) && !appLinkData.contains("error=") && !appLinkData.contains("error_description=")) {
                if (null == mHandler) {
                    mHandler = new TmallGenieHandler(this);
                }
                // Link callback
                parsingUrl(appLinkData);
            }
        }
    
    private void parsingUrl(String url) {
            String redirect_uri;
            String code = "";
            String state = "";
    
            url = url.trim();
    
            String[] urlParts = url.split("\\?");
            redirect_uri = urlParts[0];
            if (urlParts.length > 1) {
                String[] params = urlParts[1].split("&");
                Map<String, String> map = new HashMap<>();
                for (String param : params) {
                    String[] keyValue = param.split("=");
                    if (keyValue.length > 1) {
                        map.put(keyValue[0], keyValue[1]);
                    }
                }
                state = map.get("state");
                code = map.get("code");
            }
            ILog.e(TAG, " redirect_uri = " + redirect_uri + ",state = " + state, ",code = " + code);
       
    
        }
  4. Query the cloud to check if the skill was bound successfully.

    public void queryAlexaApptoappLaunchGet(String state, String redirectUri, String code) {
            ILog.d("TmallGenieBusiness", "queryAlexaApptoappLaunchGet");
            if (null == mIoTAPIClient) {
                return;
            }
    
            Map<String, Object> params = new HashMap<>();
            params.put("state", state);
            params.put("redirectUri", redirectUri);
            params.put("code", code);
            IoTRequest request = buildRequest(params, "/living/voice/alexa/apptoapp/launch");
    
            ILog.d("TmallGenieBusiness", "requestMap:" + JSON.toJSONString(params));
    
            IoTAPIClient mIoTAPIClient = new IoTAPIClientFactory().getClient();
            mIoTAPIClient.send(request, mListener);
        }

Alexa-to-App-to-Alexa

  1. Receive the authentication request from Alexa.

    <activity
                android:name=".activity.AuthActivity"
                android:launchMode="singleTask"
                android:screenOrientation="portrait">
    
                <!--Amazon Alexa App-->
                <intent-filter
                    android:autoVerify="true"
                    tools:ignore="UnusedAttribute">
    
                    <action android:name="android.intent.action.VIEW" />
    
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
    
                    <data
                        android:host="yourHost"
                        android:scheme="yourScheme"
                        android:pathPrefix="yourPathPrefix" />
                </intent-filter>
            </activity>

    private void handleIntent(@NonNull Intent intent) {
            needKeepUserClick = false;
            ALog.d(TAG, "action: " + intent.getAction());
            ALog.d(TAG, "data: " + intent.getData());
            String clientId = null;
            String state = null;
            final ArrayList<String> scopeList = new ArrayList<>();
            String redirectUri = null;
    
            Uri appLinkData = intent.getData();
            if (appLinkData == null) {
                Log.w(TAG, "handleIntent: ", new Throwable("Cloud Intelligence: empty app link data"));
                finish();
                return;
            }
    
    
            if (appLinkData.getPathSegments().contains("oauth2") || appLinkData.getPathSegments().contains("alexa")) {
                if (appLinkData.getPathSegments().contains("alexa")) {
                    needKeepUserClick = true;
                }
               
                mScope = Scope.Alexa;
                clientId = appLinkData.getQueryParameter("client_id");
                state = appLinkData.getQueryParameter("state");
                String _scope = appLinkData.getQueryParameter("scope");
                if (_scope != null) {
                    scopeList.add(_scope);
                }
                redirectUri = appLinkData.getQueryParameter("redirect_uri");
            } else {
                Log.w(TAG, "handleIntent: ", new Throwable("Cloud Intelligence: unknown path segments"));
            }
    
    
            if (mScope == null) {
                Log.w(TAG, "handleIntent: ", new Throwable("Cloud Intelligence: wrong scope"));
                finish();
                return;
            }
    
    
            String finalClientId = clientId;
            String finalState = state;
            String finalRedirectUri = redirectUri;
        
  2. Obtain the code from the cloud.

        private void accountAuth2CodeGet(@NonNull String clientId, @NonNull String state, @NonNull String redirectUri, @Nullable List<String> scope) {
            showLoading();
            JSONObject params = new JSONObject();
            params.put("clientId", clientId);
            params.put("state", state);
            params.put("redirectUri", redirectUri);
            if (scope != null) {
                params.put("scope", scope);
            }
    
            IoTRequestBuilder builder = new IoTRequestBuilder().setAuthType(MineConstants.APICLIENT_IOTAUTH).setApiVersion(LIVING_ACCOUNT_OAUTH2_CODE_GET_VERSION);
            IoTRequest ioTRequest = builder
                    .setPath("/living/account/oauth2/code/get")
                    .setParams(params.getInnerMap())
                    .setScheme(Scheme.HTTPS)
                    .build();
    
    
            new IoTAPIClientFactory().getClient().send(ioTRequest, new IoTCallback() {
                @Override
                public void onFailure(IoTRequest request, Exception e) {
                    ALog.w(TAG, "onFailure: " + (e == null ? "" : e.getMessage()));
                    String message = e != null ? e.getMessage() : null;
                    if (message == null) {
                        message = e != null ? e.getLocalizedMessage() : null;
                    }
                    onAuthFailed(message, redirectUri, state);
                }
    
                @Override
                public void onResponse(IoTRequest request, IoTResponse response) {
                    if (response != null && response.getData() != null && response.getCode() == 200) {
                        ALog.d(TAG, "onResponse succeed");
                        onAuthSucceed(response.getData().toString(), redirectUri, state);
                    } else {
                        String message = response != null ? response.getMessage() : null;
                        if (message == null) {
                            message = response != null ? response.getLocalizedMsg() : null;
                        }
                        ALog.d(TAG, "onResponse error; " + message);
                        onAuthFailed(message, redirectUri, state);
                    }
                }
            });
    
        }

    Cloud authentication successful.

    private void onAuthSucceed(@NonNull String code, @NonNull String redirectUri, @NonNull String state) {
            Intent intent = new Intent();
            ThreadPool.MainThreadHandler.getInstance().post(() -> {
                if (isFinishing() || isDestroyed()) return;
                dismissLoading();
                switch (mScope) {
                    case Alexa:
                        intent.setAction(Intent.ACTION_VIEW);
                        intent.setData(getAlexaSuccessfulUri(redirectUri, code, state));
                        try {
                            if (intent.resolveActivity(AApplication.getInstance().getPackageManager()) != null) {
                                startActivity(intent);
                            } else {
                                ALog.d(TAG, "not systemAction->" + intent.getAction());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
    
                        break;
                    default:
                        break;
                }
                finish();
            });
        }

    Cloud authentication failed.

    private void onAuthSucceed(@NonNull String code, @NonNull String redirectUri, @NonNull String state) {
            Intent intent = new Intent();
            ThreadPool.MainThreadHandler.getInstance().post(() -> {
                if (isFinishing() || isDestroyed()) return;
                dismissLoading();
                switch (mScope) {
                    case Alexa:
                        intent.setAction(Intent.ACTION_VIEW);
                        intent.setData(getAlexaSuccessfulUri(redirectUri, code, state));
                        try {
                            if (intent.resolveActivity(AApplication.getInstance().getPackageManager()) != null) {
                                startActivity(intent);
                            } else {
                                ALog.d(TAG, "not systemAction->" + intent.getAction());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
    
                        break;
                    default:
                        break;
                }
                finish();
            });
        }

Cloud Intelligence iOS app

Development steps - Cloud Intelligence iOS app

We recommend that you use the following libraries and languages to provide iOS apps (9.0 or later):

  1. Display the account linking option in the Cloud Intelligence iOS app.

    The Cloud Intelligence iOS app needs a start page from which the user can start skill enablement and account linking. This page should show what the user can do by linking their account to Alexa, such as ordering smart lights by voice.

    How you allow the user to start account linking depends on the experience you want to create. You can offer account linking in multiple places. For example, you can offer the option after the user registers for a Cloud Intelligence iOS app account. You can provide a button on a "Link to Alexa" page in the app's settings. In any case, explain the convenience and benefits of linking their account to Alexa.

    To add an App-to-App Account Linking button to your iOS app:

    1. Open the storyboard in the Xcode IDE.

    2. Add a UIButton to the view controller and name it "Link your account with Alexa". Consider a name that shows the benefit to the user, such as "Listen to your music with Alexa" or "Order a smart oven that understands you".

    3. In the upper-left corner of the Xcode IDE, click "Show the assistant editor".

    4. Create an action connection for the button by dragging the button to the code editor.

  2. Enable Universal Links for the Cloud Intelligence iOS app.

    To allow the Alexa app to redirect users back to the Cloud Intelligence iOS app, you must enable Universal Links for your iOS application.

    Note
    • Universal Links and App Links must comply with the specified URI syntax. Remember the domain and path you add. You will need them when you add the redirect URL to the skill details in the Alexa developer console.

    • To enable Universal Links, see the examples and instructions in the iOS linking development documentation.

  3. Configure your skill to use App-to-App Account Linking.

    You can use the Alexa developer console, the ASK CLI, or the SMAPI to configure your skill. During this configuration, you can specify the Cloud Intelligence iOS app's redirect URL, access token URL, and other details.

    • Configure using the Alexa developer console.

      1. Log on to the Alexa developer console.

      2. Find your skill in the list. Under your skill, select Edit.

      3. On the left, click TOOLS, and then click Account Linking.

      4. If you have previously configured Account Linking for your skill, enable "Allow users to link their account to your skill from within your application or website".

      5. For Authorization Grant Type, select Authorization Code Grant if it is not already selected. App-to-App Account Linking only supports the authorization code grant type.

      6. For the other settings, use the settings described in Configure Account Linking. Note the following additional instructions when you fill in the Redirect URL field:

        • For Redirect URL, add the redirect URL described in URLs and parameters.

        • The redirect URL must comply with the specified URI syntax. The Alexa app will open this Universal Link or App Link URL to start the Cloud Intelligence iOS app.

        • You can add multiple redirect URLs to handle different scenarios.

        • If your app does not support Universal Links and App Links, you should have a valid web page that the redirect URL points to. This should support the same process for receiving the user's Amazon authorization code, as described in Flow design.

        • Remember to add redirect URLs for your iOS application, Android application, and website.

    • Configure using the ASK CLI.

      To enable App-to-App Account Linking using the ASK CLI, use the subcommand in Update account linking info and follow these steps:

      • To allow users to enable the skill without account linking, select Y or N.

      • For Authorization URL, enter the URL of your authorization server. This URL is used for regular account linking. The authorization server must accept the user's credentials, authenticate the user, and generate an authorization code. The Alexa app can later pass this code to your authorization server to retrieve an access token that uniquely identifies the user with your service.

      • For Client ID, enter the identifier on your logon page that will be used to identify that the request is from your skill.

      • For Scopes (comma-separated), enter the characters that represent the required access permissions for the user's account, such as the user ID. This is required for smart home skills. You can specify up to 15 scopes.

      • For Domains (comma-separated), enter a list of additional domains from which the logon page retrieves content. You can specify up to 15 domains.

      • For Authorization Grant Type, select AUTH_CODE.

      • For Access Token URI, enter the access token URL. For more information, see URLs and parameters.

      • For Client Secret, enter a credential that allows the Alexa service to authenticate with the access token URI. This is combined with the Client ID to identify requests from Alexa.

      • For Client Authentication Scheme, select HTTP_BASIC or REQUEST_BODY_CREDENTIALS.

      • (Optional) For Default access token expiration time in seconds, enter the time in seconds that the access token is valid.

      • (Optional) For Interactive access token URL, enter the URI that will be called with authorization codes that can be exchanged for Alexa access tokens.

      • (Optional) For Redirect URLs for App-to-App Account Linking, enter the app redirect URL described in URLs and parameters. This setting is required for App-to-App Account Linking.

    • Configure using the Alexa Skill Management API (SMAPI).

      To configure App-to-App Account Linking using the SMAPI, use the structure in this Account linking request.

      The following is an example request to add a redirect URL using SMAPI.

      Name

      Description

      Type

      skipOnEnablement

      Set to true to allow users to enable the skill without starting the account linking flow. Set to false to require the normal account linking flow when a user enables the skill. For more information, see Let users enable your skill without linking an account.

      Boolean

      type

      Specifies the OAuth authorization grant type. For App-to-App Account Linking, you must use AUTH_CODE.

      String

      authorizationUrl

      The URL of your authorization server. It must accept the user's credentials, authenticate the user, and generate an authorization code. The Alexa app can later pass this code to your authorization server to retrieve an access token that uniquely identifies the user with your service.

      String

      domains

      A list of additional domains from which your logon page gets content. You can specify up to 15 domains.

      Array of String

      clientId

      The identifier your logon page uses to identify that the request is from your skill.

      String

      scopes

      A string that indicates the required access permissions for the user's account, such as user_id. This field is required for smart home skills. You can specify up to 15 scopes.

      Array of String

      accessTokenUrl

      The URI used to request an authorization token. This is required only when type is AUTH_CODE.

      String

      clientSecret

      The credential you provide that allows the Alexa service to authenticate with the access token URI. This is combined with the clientId to identify requests from Alexa.

      String

      accessTokenScheme

      The type of authentication used. For example, HTTP_BASIC or REQUEST_BODY_CREDENTIALS. This is required for App-to-App Account Linking because the authorization grant type is AUTH_CODE.

      String

      redirectUrls

      To start the Cloud Intelligence iOS app with a Universal Link or App Link, the redirectURL must comply with the specified URI syntax.

      Array of String

      {
         "accountLinkingRequest": {
            "accessTokenScheme": "HTTP_BASIC",
            "accessTokenUrl": "https://api.amazon.com/auth/o2/token/",
            "authorizationUrl": "https://www.amazon.com/ap/oa/",
            "clientId": "yourSMAPIClientId",
            "clientSecret": "yourSMAPIClientSecret",
            "domains": [],
            "redirectUrls": ["yourRedirectURL1","yourRedirectURL2"],
            "scopes": ["profile"],
            "skipOnEnablement": true,
            "type": "AUTH_CODE"
          }
      }
  4. Obtain the user's Amazon authorization code.

    The Cloud Intelligence iOS app needs to obtain the user's Amazon authorization code so that it can be exchanged for an Amazon access token later. The Amazon access token will enable the Cloud Intelligence iOS app to enable the skill and complete the account linking. The Amazon authorization code is valid for 5 minutes.

    You can obtain the user's Amazon authorization code by opening the Alexa app URL (for the Alexa app flow) or by making an HTTP GET request to the LWA authorization server (for the LWA flow). For iOS and Android, we strongly recommend that you implement the Alexa app flow and use LWA only as a fallback.

    To obtain the user's Amazon authorization code, assemble the authorization request. Then, open the Alexa app URL (or the LWA fallback URL if the Alexa app is not installed) with the parameters described in URLs and parameters. We strongly recommend that you assemble these URLs on your backend server and pass the assembled URLs to your application. This way, you can quickly change parameters, such as the stage, without rebuilding your application.

    When the user confirms the linking request, the Alexa app redirects the user to your application's redirect URL, which is a parameter in the Alexa or LWA fallback URL. The redirect URL includes the success or error parameters described next.

    If the user's device cannot start your application using the redirect URL you provided, the user is sent to the same redirect URL in their default browser. For example, this can happen in the following situations:

    • Universal Links or App Links are not enabled in your application.

    • Universal Links or App Links are not enabled in your application for the redirect URL you provided.

    • The application version on the user's device is an older version that does not support Universal Links or App Links.

    In these cases, you should have a web page (with the same URL as the Universal Link or App Link) to receive the authorization code. When the user goes to your web page, they may need to log on so that you can obtain an authorization code from your service.

    For possible errors and the corresponding messages to show the user, see Errors when obtaining the Amazon authorization code.

    Successful response to the HTTP GET request for the Alexa app URLIf successful, the response is a redirect URL that includes the user's Amazon authorization code and other parameters shown next. To handle this, see Apple's guidelines on supporting Universal Links in your app and the Android guide for handling Android App Links.

    // Example success response from the Alexa app
    https://yourRedirectURL?code={Amazon Authorization code}&state={your state}
    // Example success response from LWA
    https://yourRedirectURL?code={Amazon Authorization code}&scope={permission scope}&state={your state}

    Address

    • code: The Amazon authorization code returned by the Alexa app or LWA.

    • state: The state you passed in the authorization request. You should validate the state to prevent cross-site request forgery.

    • scope: The permission scope. LWA returns the alexa::skills:account_linking scope. The Alexa app does not return a scope parameter.

    For more details on OAuth 2.0 authorization responses, see Authorization Response.Error response to the HTTP GET request for the Alexa app URLIf an error occurs, the response is a redirect URL with an error and other parameters, as shown next. Your application must display an appropriate error message (error page, UIAlertController, etc.). For the error messages to display, see Error codes when obtaining the Amazon authorization code.

    // Example error response
    HTTP 200
    https://yourRedirectURL?error_description={Error description}&state={your state}&error={OAuth Error Code}

    Additional information

    • error: A single ASCII error code. Possible values: invalid_request, unauthorized_client, access_denied, unsupported_response_type, invalid_scope, server_error, temporarily_unavailable.

    • error_description: A description of the error.

    For more details on OAuth 2.0 error responses, see Authorization Response.

    Example: Obtain URL (iOS)

    In this example, the application obtains both the Alexa app URL and the LWA fallback URL.

    import Alamofire
    import SwiftyJSON
    class AlamofireHelper {
        // Your backend base URL from which you're requesting the URL
        static let BASE_URL = "https://exampleBackEndURL"
        /** Get the Alexa app Universal Link and LWA fallback URL
         @param userAccessToken for your backend server
         @param state maintained between client and server, caller should generate it 
         */
        static func getAlexaAppUrl(userAccessToken:String, state: String) -> DataRequest {
            let alexaAppUrl = "/alexaurl"
            let inputstate = "?state=" + state
            var urlRequest = URLRequest(url: URL(string: BASE_URL + alexaAppUrl + inputstate)!)
            urlRequest.addValue("Bearer " + userAccessToken, forHTTPHeaderField: "Authorization")
            return Alamofire.request(urlRequest)
        }
        private init() {}
    }
    // Call your backend to get the authorization request URLs (Alexa app / LWA).
    AlamofireHelper.getAlexaAppUrl(userAccessToken: savedSession.accessToken, state: StateHelper.generateState()).responseJSON {
        response in
        guard let status = response.response?.statusCode, 200..<300 ~= status else {
            self.createAlert(title: "Failed to get Alexa URL", message: "Failed to get Alexa URL")
            return
        }
        guard let responseValue = response.result.value else {
            self.createAlert(title: "Invalid response", message: "Invalid response")
            return
        }
        let reponseInJSON = JSON(responseValue)
        guard let companionApp = reponseInJSON["companionAppURL"].string,let lwaFallback = reponseInJSON["LWAFallBackURL"].string else {
            self.createAlert(title: "Error response", message: "Error response")
            return
        }
        guard let companionAppURL = URL(string: companionApp), let lwaFallbackURL = URL(string: lwaFallback) else {
            self.createAlert(title: "Incorrect URL format", message: "Incorrect URL format")
            return
        }
        // The openUniversalLinks code snippet can be found in another example.
        self.openUniversalLinks(companionAppURL: companionAppURL, lwaFallbackURL: lwaFallbackURL)
    }

    Example: Open URL (iOS)

    In this example, the application opens the Alexa app URL, or the LWA fallback URL if the Alexa app is not installed.

    Note the following:

    • This example uses the UIApplication.shared.open API with the universalLinksOnly option to open the URL.

    • The URL opens only if the application (in this case, the Alexa app) is configured to handle Universal Links with the universalLinksOnly option. Therefore, you can handle the case where the Alexa app fails to start using the completionHandler closure expression. For more information about closure expressions, see Closure Expressions. For example, the Alexa app is not installed or the version does not support Universal Links.

    • If your application cannot start the Alexa app when using the universalLinksOnly option, your application needs to use LWA to obtain the Amazon authorization code. You can use SFAuthenticationSession on iOS 11 or ASWebAuthenticationSession on iOS 12.0 or later. This retrieves the Safari user session and cookies, which avoids an extra logon when the user is already logged on to Amazon.com in Safari. This causes your application to display a window asking the user to consent to sharing website information. The content of this message is defined by iOS.

    • For iOS versions earlier than 11.0, this example uses SFSafariViewController to send the user to the LWA page. SFSafariViewController does not require the user to consent to sharing website information. Therefore, the Safari browser does not share cookies with SFSafariViewController.

    • You can also open the LWA fallback URL through WKWebView. After authorization is complete in the WebView, your app will be opened via a Universal Link. Intercept this URL to obtain the authorization parameters.

      import SafariServices
      // Open the Alexa URL with the universalLinksOnly option.
      // If it doesn't open the Universal Link, open the LWA fallback. 
      private func openUniversalLinks(companionAppURL: URL, lwaFallbackURL: URL) {
          UIApplication.shared.open(companionAppURL, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly:true]) {
              companionAppLaunched in
              if !companionAppLaunched {
                  if #available(iOS 12.0, *) {
                      WebSession.initWebAuthenticationSession(authURL: lwaFallbackURL)
                  } else if #available(iOS 11.0, *) {
                      WebSession.initAuthenticationSession(authURL: lwaFallbackURL)
                  } else {
                      let safariViewController =  SFSafariViewController(url: lwaFallbackURL)
                      self.present(safariViewController, animated: true, completion: nil)}
              }
          }
      }
      // Web session
      class WebSession {
          @available(iOS, introduced: 11.0, deprecated: 12.0)
          private static var authenticationSession: SFAuthenticationSession?
          @available(iOS 12.0, *)
          private static var webAuthenticationSession : ASWebAuthenticationSession?
          private static let CALLBACK_URL_SCHEME = "https://yourAppsUniversalLinkURL"
          @available(iOS, introduced: 11.0, deprecated: 12.0)
          static func initAuthenticationSession(authURL:URL) {
              if let session = authenticationSession {
                  session.cancel()
              }
              self.authenticationSession = SFAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{
                  (callBack:URL?, error:Error?) in
                  //Callback and error handling
              })
              self.authenticationSession?.start()
          }
          @available(iOS 12.0, *)
          static func initWebAuthenticationSession(authURL:URL) {
              if let session = webAuthenticationSession {
                  session.cancel()
              }
              self.webAuthenticationSession = ASWebAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{
                  (callBack:URL?, error:Error?) in
                  // Callback and error handling
              })
              self.webAuthenticationSession?.start()
          }
          private init(){}
      }

      Example: Open LWA URL (iOS)

      Opens the LWA URL in the case of a website, as a fallback for when the Alexa app is not installed on an iOS device.

      In this example, the application opens LWA in a view controller.

      import SafariServices
      // Open Login with Amazon in a Safari View controller.
      private func openSafariView(lwaFallbackURL: URL) {
          let safariViewController =  SFSafariViewController(url: lwaFallbackURL)
          self.present(safariViewController, animated: true, completion: nil)}
      }

      Example: Handle the Universal Link response (iOS)

      In this example, the application validates and extracts the parameters from the Alexa app's call to its Universal Link.

      // Handler for Universal Links in AppDelegate
      // Add this method to handle incoming Universal Links.
      func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
          guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let incomingURL = userActivity.webpageURL else {
                  return false
          }
          let handlers : [UniversalLinkHandler] = [AuthResponseHandler(), ErrorResponseHandler()]
          var validatedResponse : ValidatedResponse?
          for handler in handlers {
              if handler.canHandle(incomingURL: incomingURL) {
                  validatedResponse = handler.getValidatedResponse()
                  let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController
                  initialViewController.response = validatedResponse
                  self.window?.rootViewController = initialViewController
                  self.window?.makeKeyAndVisible()
                  return true
              }
          }
          return false
      }
      // Incoming Universal Links handler protocol
      protocol UniversalLinkHandler {
          //Validate the incoming URL scheme.
          func canHandle(incomingURL:URL)-> Bool
          //Get the validated response.
          func getValidatedResponse() -> ValidatedResponse?
      }
      extension UniversalLinkHandler {
          func validateState(state:String)->Bool {
              return StateHelper.validateState(state: state)
          }
      }
      // Successfully receiving the Amazon Authorization code
      class AuthResponseHandler : UniversalLinkHandler {
          private let AUTH_RESPONSE_PARAMETERS: Set = ["code", "state", "scope"]
          private var validatedResponse: ValidatedResponse?
          func canHandle(incomingURL: URL) -> Bool {
              guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
                  let queryParameters = components.queryItems else {
                      return false
              }
              var validatedParameters : [String: String] = [:]
              for queryParameter in queryParameters {
                  //Duplicated parameters in URL
                  if validatedParameters.keys.contains(queryParameter.name) {
                      return false
                  }
                  //Unrecognized parameters in URL
                  if !AUTH_RESPONSE_PARAMETERS.contains(queryParameter.name) {
                      return false
                  }
                  validatedParameters[queryParameter.name] = queryParameter.value
              }
              guard let code = validatedParameters["code"], let state = validatedParameters["state"] else {
                  return false
              }
              //Validate the state.
              if !validateState(state: state) {
                  self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state")
                  return true
              }
              self.validatedResponse = AuthResponse(url: incomingURL,code: code, state: state, scope: validatedParameters["scope"])
              return true
          }
          func getValidatedResponse() -> ValidatedResponse? {
              return validatedResponse
          }
      }
      // Error authorization response handler
      class ErrorResponseHandler : UniversalLinkHandler {
          private let ERROR_RESPONSE_PARAMETERS: Set = ["error", "error_description", "state"]
          private var validatedResponse: ValidatedResponse?
          func canHandle(incomingURL: URL) -> Bool {
              guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
                  let queryParameters = components.queryItems else {
                      return false
              }
              // Validate all query parameters. 
              var validatedParameters : [String: String] = [:]
              for queryParameter in queryParameters {
                  // Duplicated parameters in URL
                  if validatedParameters.keys.contains(queryParameter.name) {
                      return false
                  }
                  // Unrecognized parameters in URL
                  if !ERROR_RESPONSE_PARAMETERS.contains(queryParameter.name) {
                      return false
                  }
                  validatedParameters[queryParameter.name] = queryParameter.value
              }
              guard let error = validatedParameters["error"], let errorDescription = validatedParameters["error_description"], let state = validatedParameters["state"] else {
                  return false
              }
              // Validate the state.
              if !validateState(state: state) {
                  self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state")
                  return true
              }
              self.validatedResponse = ErrorResponse(url: incomingURL, error: error, errorDescription: errorDescription)
              return true
          }
          func getValidatedResponse() -> ValidatedResponse? {
              return validatedResponse
          }
      }
      // Validated response
      class ValidatedResponse {
          // Universal Link URL that launched the app
          private(set) var url: URL
          init (url: URL) {
              self.url = url
          }
      }
      // Amazon authorization code response per the OAuth 2.0 specification 
      class AuthResponse : ValidatedResponse{
          private(set) var code : String
          private(set) var state: String
          // The scope will be returned in Login with Amazon, but will not be present in the Alexa Companion flow.
          private(set) var scope: String?
          init(url:URL, code:String, state:String, scope: String?) {
              self.code = code
              self.state = state
              self.scope = scope
              super.init(url:url)
          }
      }
      // Error response per the OAuth 2.0 specification 
      class ErrorResponse : ValidatedResponse {
          private(set) var error : String
          private(set) var errorDescription: String
          init(url: URL, error:String, errorDescription:String) {
              self.error = error
              self.errorDescription = errorDescription
              super.init(url: url)
          }
      }

      State generation and validation

      Example: State Helper

      In this example, the application generates and validates the state between the request and the response. You must use the state to validate incoming requests to prevent cross-site request forgery. For more information, see Cross-Site Request Forgery.

      import Foundation
      import Security
      class StateHelper {
          private static let STATE_VALID_FOR = 3600
          // Base64 (iOS + TimeStamp + Secure Random UUID)
          private static let NUMBER_OF_PARAMETER = 3
          static func generateState(session: Session)-> String {
              var buffer = Data(count: 30)
              let _ = buffer.withUnsafeMutableBytes {
                  SecRandomCopyBytes(kSecRandomDefault, 30, $0)
              }
              let state = ("iOS." + String(Int64(Date().timeIntervalSince1970)) + "." + buffer.base64EncodedString()).encodeToURISafeBase64()
              // Store the state in the session manager for future validation.
              session.state = state
              LocalSessionManager.saveSession(session: session)
              return state
          }
          static func validateState(state: String) -> Bool {
              guard let originalState = LocalSessionManager.loadSession()?.state  else {
                  return false
              }
              if state != originalState {
                  return false
              }
              if let timeStampString = originalState.decodeFromURISafeBase64()?.split(separator: "."), timeStampString.count == NUMBER_OF_PARAMETER, let timeStamp = Int64(timeStampString[1]){
                  if Int64(Date().timeIntervalSince1970) - timeStamp <= STATE_VALID_FOR {
                      return true
                  }
              }
              if let session = LocalSessionManager.loadSession() {
                  session.state = nil
                  LocalSessionManager.saveSession(session: session)
              }
              return false
          }
          private init() {}
      }
      extension String {
          func decodeFromURISafeBase64() -> String? {
              let base64String = String(self.map {
                  // Replace unsafe characters. 
                  character in
                  if character == "." {
                      return  "+"
                  } else if character == "_" {
                      return "/"
                  } else if character == "-" {
                      return "="
                  }
                  return character
              })
              guard let data = Data(base64Encoded: base64String) else {
                  return nil
              }
              return String(data: data, encoding: .utf8)
          }
          func encodeToURISafeBase64() -> String {
              return String(Data(self.utf8).base64EncodedString().map{
                  // Replace back unsafe characters. 
                  character in
                  if character == "+" {
                      return  "."
                  } else if character == "/" {
                      return "_"
                  } else if character == "=" {
                      return "-"
                  }
                  return character
              })
          }
      }

  5. Exchange the Amazon authorization code for an Amazon access token, enable the skill, and complete account linking.

    Pass the code, redirectUri, and state to the cloud through the /living/voice/alexa/apptoapp/launch API to complete the account linking.

  6. Display the account linking status in the Cloud Intelligence iOS app. Keep the user informed of the account linking status by displaying it in the Cloud Intelligence iOS app, for example, in the screenshots in "User experience and errors". You can use different user interface components, such as a table or a page view, to display the account linking status.

    Example: Display account linking status.

    In this example, the application uses a UIViewController to display the account linking status.

    1. In the Xcode IDE, go to the main storyboard and add a new view controller. The storyboard ID for this view controller is linkingStatusScreen.

    2. Add a new Cocoa Touch class file, select a subclass of UIViewController, and connect your class file with the UIViewController in the Identity Inspector.

    3. Add a title, a UILabel to display the account linking status, a UIButton to close this page, and a UIActivityIndicatorView to show a loading animation while the application calls the Alexa service.

      import UIKit
      class LinkingStatusViewController: UIViewController {
          @IBOutlet weak var header: UILabel!
          @IBOutlet weak var status: UILabel!
          @IBOutlet weak var closeButton: UIButton!
          @IBOutlet weak var indicator: UIActivityIndicatorView!
          var response : ValidatedResponse?
          override func viewDidLoad() {
              super.viewDidLoad()
              if let res = self.response {
                  passValidatedResponse(validatedResponse: res)
              }
          }
          private func updateView(header:String, status:String, animating: Bool,closeButton:Bool) {
              if animating {
                  self.indicator?.startAnimating()
              } else {
                  self.indicator?.stopAnimating()
              }
              self.indicator?.isHidden = !animating
              self.header?.text = header
              self.status?.text = status
              self.closeButton.isEnabled = closeButton
              if closeButton {
                  self.closeButton.backgroundColor = #colorLiteral(red: 0.1960784314, green: 0.6039215686, blue: 0.8392156863, alpha: 1)
              } else {
                  self.closeButton.backgroundColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1)
              }
          }
      }
      extension LinkingStatusViewController {
          func passValidatedResponse(validatedResponse: ValidatedResponse) {
              if validatedResponse is AuthResponse {
                  let response: AuthResponse = validatedResponse as! AuthResponse;
                  updateView(header: "Status", status: "Loading", animating: true, closeButton: false)
                  usingAuthCodeToGetAccessToken(amazonAuthCode: response.code)
              } else if (validatedResponse is ErrorResponse) {
                  let response: ErrorResponse = validatedResponse as! ErrorResponse;
                  updateView(header: response.error, status: response.errorDescription, animating: false, closeButton: true)
              } else {
                  updateView(header: "Unknown request", status: validatedResponse.url.absoluteString, animating: false, closeButton: true)
              }
          }
          // Complete account linking.
          private func usingAuthCodeToGetAccessToken(amazonAuthCode: String) {
              guard let session = LocalSessionManager.loadSession(), session.sessionValid  else {
                  updateView(header: "Status", status: "User not logged in", animating: false, closeButton: true)
                  return;
              }
              AlamofireHelper.completeAccountLinking(userAccessToken: LocalSessionManager.loadSession()!.accessToken, amazonAuthCode: amazonAuthCode, state: StateHelper.generateState(session: session)).responseJSON(queue: DispatchQueue.global(qos: .userInitiated), options: []) {
                  response in
                  if let status = response.response?.statusCode, 200..<300 ~= status {
                      DispatchQueue.main.sync {
                          self.updateView(header: "Status", status: "Account Linking Succeeded", animating: false, closeButton: true)
                      }
                      return
                  } else {
                      DispatchQueue.main.sync {
                          self.updateView(header: "Failed", status: "Account Linking failed", animating: false, closeButton: true)
                      }
                  }
              }
          }
      }
      // Open the account linking status page.
      let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController
      initialViewController.response = validatedResponseself.window?.rootViewController = initialViewController
      self.window?.makeKeyAndVisible()
  7. Use the access token in your skill.

    After the user successfully enables your skill and links Alexa to your service, requests sent to your skill include the user's access token. Your skill code needs to retrieve the access token from the request, validate it, and use it to retrieve the necessary user information from the resource server. For more details on how to validate and use the access token, see the documentation for your skill type:

Cloud API reference

/living/voice/alexa/apptoapp/config/get

Obtains the redirect parameters for Alexa App-to-App.

Version: 1.0.0

Request parameters

No request parameters.

Parameter

Data Type

Structure

Required

Default Value

Example input parameters

Description

Child Parameter Type

No data

Response parameters

Standard:

Parameter Name

Data Type

Structure

Backend Parameter Name

Description

Child Parameter Type

code

Integer

code

The response code. A value of 200 indicates success.

message

String

message

The error message.

localizedMsg

String

localizedMsg

The localized error message.

data

JSON

Struct

data

The response data.

lwaFallbackUrl

String

lwaFallbackUrl

The LWA fallback URL. If the user does not have the Alexa app installed, a webview opens this URL for LWA authorization.

alexaAppUrl

String

alexaAppUrl

The URL to open when the user has the Alexa app installed.

/living/account/oauth2/code/get

Generates an external authorization AuthCode for the current logon account based on OAuth 2.0 rules.

Version: 1.0.0

Request parameters

Parameter

Data Type

Structure Type

Required

Default Value

Sample input parameters

Description

Child Parameter Type

redirectUri

String

Yes

The callback URL for the third party that requested authorization after the OAuth 2.0 authentication process is complete.

state

String

No

The state carried by the third party that requested authorization during the OAuth 2.0 authentication process.

clientId

String

Yes

The client_id of the third-party service provider that requested authorization during the OAuth 2.0 authentication process.

scope

JSON

Array

No

The scope of permissions requested by the third-party service provider during the OAuth 2.0 authorization process.

String

Response parameters

Standard:

Parameter name

Data Type

Structure

Backend Parameter Name

Description

Child Parameter Type

code

Integer

code

The response code. A value of 200 indicates success.

message

String

message

The error message.

localizedMsg

String

localizedMsg

The localized error message.

data

String

data

The generated AuthCode.

/living/voice/alexa/apptoapp/launch

After the Alexa App-to-App flow obtains the configuration and redirects to the Alexa app or LWA page to obtain the AuthCode issued by Alexa, this API is called to submit the code to the cloud and complete the binding process.

Version: 1.0.1

Request parameters

Parameter

Data Type

Structure

Required

Default Value

Sample input parameters

Description

Child Parameter Type

code

String

Yes

The Alexa LWA authorization authCode.

state

String

Yes

The OAuth 2.0 authentication parameter state.

Response parameters

Standard:

Parameter name

Data Type

Structure

Backend Parameter Name

Description

Child Parameter Type

code

Integer

code

The response code. A value of 200 indicates success.

message

String

message

The error message.

localizedMsg

String

localizedMsg

The localized error message.

Appendix: Glossary

Terms

  • Service

    The service you provide. For example, you might have a web-based service "Ride Hailer" that allows users to order a taxi.

  • App

    The application your users use to interact with your service. Continuing the previous example, you might have a "Ride Hailer" app.

  • Skill

    An Alexa skill that enables users to interact with your service using Alexa.

  • Alexa app

    The Amazon Alexa app that users can download for their mobile devices. To download, see Alexa App.

  • Login with Amazon (LWA)

    An authentication system that allows users to log on and grant access to their user profile data. For App-to-App Account Linking, LWA is a fallback that you can implement to handle cases where the user does not have the Alexa app installed on their mobile device. For general information about LWA, see Login with Amazon (LWA).

  • OAuth 2.0

    An authentication standard through which your service can allow Alexa to access information in the user's account with your service, with the user's permission. For the OAuth 2.0 standard, see OAuth 2.0.

  • App Link

    A deep link on Android that a user clicks to start an application. For more details on App Links, see the Android documentation.

  • Universal Link

    A deep link on iOS that a user clicks to start an application. For more details on Universal Links, see the iOS documentation.

Name

Explanation

Example

Alexa App URL

Your app uses this Universal Link (iOS) or App Link (Android) to redirect the user to the Alexa app to confirm the linking request. For general information about Universal Links (iOS), see Universal Link Content

. You can store the Alexa app URL and LWA fallback URL in your backend and retrieve them with a GET

You must send a request to retrieve them. You can store the URL in local storage to avoid additional network calls. If you modify the skill security configuration file, a rebuild may be required.

https://alexa.amazon.com/spa/skill-account-linking-consent?fragment=skill-account-linking-consent&client_id={ClientId}&scope=alexa::skills:account_linking&skill_stage={skillStage}&response_type=code&redirect_uri={yourRedirectUrl}&state={yourState}

LWA fallback URL

A link that redirects the user to LWA to enter their Amazon account information. It works for iOS, Android, and websites. Your app uses this option if the Alexa app is not installed on the user's device.

https://www.amazon.com/ap/oa?client_id={ClientId}&scope=alexa::skills:account_linking&response_type=code&redirect_uri={yourRedirectUrl}&state={yourState}

App redirect URLs

The Alexa app (or LWA, if the Alexa app is not installed) uses this Universal Link (iOS) or App Link (Android) to redirect the user back to your app after the user confirms the linking request in the Alexa app or LWA. The redirect

returns a parameter that provides your app with the user's Amazon authorization code, which is valid for 5 minutes.

You can specify one or more of these using the Alexa developer console, ASK CLI, or SMAPI. For more information, see the URI specification.

Authorization URL

The URL of your authorization server. The authorization server must accept the user's credentials, authenticate the user, and generate an authorization code. The Alexa app can later pass this code to the authorization server to retrieve an access token that uniquely identifies the user with your service.

This link is only for regular (non-App-to-App) account linking. You can specify this using the developer console, ASK CLI, or SMAPI.

Access token URL

Alexa uses your token server to exchange the user's authorization code (for your service) for an access token to complete the account linking.

You can specify this using the Alexa developer console, ASK CLI, or SMAPI.

LWA authorization service

Your backend server uses this to exchange the user's Amazon authorization code for an Amazon access token. This enables your app to call the Alexa service to enable the skill and link the account for the user.

Host: https://api.amazon.com, Port request: /auth/o2/token.

Alexa Skill Activation API

Your backend server calls this function to enable the user's skill. For more details on the Alexa Skill Activation API, see Alexa Skill Activation API.

https://api.amazonalexa.com/v1/users/~current/skills/{yourSkillId}/enablement

Depending on whether the user activation is local, the original URL can be https://api.amazonalexa.com, https://api.eu.amazonalexa.com, or https://api.fe.amazonalexa.com

URLs and parameters

Parameters for the Alexa app URL and LWA fallback URL

Field

Description

Client ID

The client ID provided by the Alexa developer console when you enable App-to-App Account Linking.

Client secret

The client secret provided by the Alexa developer console when you enable App-to-App Account Linking.

Redirect URL

The Universal Link or App Link specified when you enable App-to-App Account Linking in the Alexa developer console. The Alexa app and LWA redirect the user back to this URL after they confirm the account linking request.

Scope

The scope. Your app must set this to alexa::skills:account_linking to enable the skill.

Skill ID

The unique identifier for your skill. You can find it in the developer console.

Stage

The skill stage. This depends on whether your skill has been published. Before your skill is published, set Stage to development. After your skill is published, use live.

response type

The response type must be code, because App-to-App Account Linking currently only supports the authorization code grant.

state

An opaque value used by the application to maintain the state between the current request and response. The state value is required for the Alexa app URL. Alexa includes this state in the response when it redirects the user back to your app using the redirect URL. You must use the state to validate incoming requests to prevent cross-site request forgery. For more information, see Cross-Site Request Forgery. Make sure the generated state does not contain the following characters: &, =, ', /, \, <, >, ", \"|. You can use base64 URI as a safe way to do this. For an example implementation, see the state helper code example in development step 4: Get the user's Amazon authorization code. You can generate the state using a secure random number generator and save it to the user session for validation. Alternatively, you can generate the state from your backend server and encrypt and decrypt it with a key.