Step 4: Make an Authorization Request from Your App
In this step, you make an authorization request to the Alexa app to get the user's Amazon authorization code. If the Alexa app isn't installed on the user's mobile device, you make the request to the Login with Amazon (LWA) authorization server. The authorization code is valid for five minutes. For details about the URL parameters, see URIs and endpoints.
Follow these steps to get the user's Amazon authorization code from the Alexa app or the LWA authorization server.
To get the Amazon authorization code
If your backend supports PKCE, generate the code_verifier and derive the code_challenge using SHA-256, as defined in RFC 7636, before constructing the URLs.
From your app backend, construct the Alexa app URL and the LWA fallback URL with the required query parameters. Include the state for cross-site request forgery (CSRF) protection and the PKCE code_challenge and code_challenge_method parameters for enhanced security.
Determine whether the Alexa app is installed on the user's device.
If the Alexa app is installed, open the Alexa app URL. If the Alexa app isn't installed, open the LWA fallback URL in an in-app browser tab, not a native browser app.
After the user acknowledges the linking request, the Alexa app or LWA redirects the user back to your app at your redirect URL with the Amazon authorization code and state.
For details about how to handle the redirect, see the Apple guidelines on Supporting Universal Links in Your App and Android guidelines in Handling Android App Links.
To prevent CSRF attacks, validate the returned state value against the value you sent in step 1. If your backend supports PKCE, use the code_verifier to validate the token endpoint, based on the stored PKCE parameters.
Extract the Amazon authorization code from the redirect.
Success response
After the user acknowledges the linking request, the Alexa app or LWA redirects the user back to your app with the Amazon authorization code. The following example shows a successful redirect to your app's Universal Link or App Link from the Alexa app.
For additional details about OAuth2.0 authorization responses, see Authorization Response.
Error response
If the user denies the request or an error occurs, the Alexa app or LWA redirects the user back to your app with an error. The following example shows an error redirect:
Note: Display a user-friendly error message in your app rather than showing the raw error code.
For additional details about OAuth2.0 error responses, see Error Response.
Browser fallback
If the user's device can't launch your app by using the redirect URL that you configured, the Alexa app redirects the user to that same redirect URL in their default browser.
Fallback might happen, for example, when one of the following occurs:
Universal Links or App Links aren't enabled in your app.
Universal Links or App Links aren't enabled in your app for the redirect URLs that you provided.
The version of the app on the user's device is an older version that doesn't support Universal Links or App Links.
To handle these scenarios, host a web page at the same URL as your Universal Link or App Link to receive the authorization code. When the user opens your web page, you might request them to log in so that you can get the authorization code from your service.
iOS examples
In this example, the app gets 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 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 to 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)
}
In this example, the app opens the Alexa app URL or, the LWA fallback URL if the Alexa app isn't installed.
URLs only open when there is an app (in this case, the Alexa app) that's configured to handle Universal Links with the universalLinksOnly option. You can therefore pass closure expressions in completionHandler to handle the case in which the Alexa app doesn't launch (for example, the Alexa app wasn't installed or is a version that doesn't support Universal Links).
If your app isn't able to launch the Alexa app when using the universalLinksOnly option, your app needs to get the Amazon authorization code by using LWA. For the LWA Fallback URL format, see URLs and endpoints. You can use SFAuthenticationSession on iOS 11 or ASWebAuthenticationSession on iOS 12.0 or higher. This gets the Safari user session and cookies, which avoids an extra login when the user is already logged in to Amazon.com on Safari. This will cause your app to display a window that asks the user's consent to share website information. iOS defines the content of the consent message.
For iOS versions below 11.0, this example uses SFSafariViewController to send users to the LWA page. The SFSafariViewController doesn't ask for the user's consent to share website information; the Safari browser therefore doesn't share cookies with the SFSafariViewController.
import SafariServices
// Open the Alexa URL with option universalLinksOnly
// 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(){}
}
For websites, and as a fallback for when the Alexa app isn't installed on the iOS device, open the LWA URL. In this example, the app opens LWA in a view controller.
import SafariServices
// Open Login with Amazon in Safari View controller
private func openSafariView(lwaFallbackURL: URL) {
let safariViewController = SFSafariViewController(url: lwaFallbackURL)
self.present(safariViewController, animated: true, completion: nil)
}
In this example, the app validates and extracts the parameters from the call that the Alexa app made 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
}
//validated 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 OAuth2 specification
class AuthResponse : ValidatedResponse{
private(set) var code : String
private(set) var state: String
//scope will be returned in Login with Amazon, will not be present in 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 OAuth2 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 examples
Make a request to your backend service to get both the Alexa app URL and the LWA fallback URL. You can implement this request in any way that you prefer, such as 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
}
In this example, the app opens the Alexa app URL or, if the Alexa app is not installed, the LWA fallback URL.
Note the following:
This example uses the PackageManager to verify that the Alexa app is installed and to verify that the installed version of the Alexa app can handle app-to-app account linking.
If the Alexa app isn't installed or can't handle app-to-app account linking, your app must get the Amazon authorization code by using the LWA flow. For the LWA fallback URL format, see URLs and endpoints.
This example opens the URLs by using the Android ACTION_VIEW intent. 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
}
}
}
In this example, the app validates and extracts the parameters from the call that the Alexa app made 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 Enablement 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 example
In this example, the app generates and validates the state between the request and the response. You must validate incoming requests by using the state to prevent 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 state in 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
})
}
}
Errors
If an error occurs when your app tries to get the Amazon authorization code, the redirect_uri that the Alexa app uses to redirect the user back to your app includes an error query parameter, as described by the OAuth 2.0 specification. The Alexa app and the LWA fallback both follow the OAuth 2.0 specification, so they both return the same error codes to your app. For details about why the request to get the Amazon authorization code failed, check the error_description query parameter and the OAuth 2.0 specification description for that error code.
Error code
Message to show to user
Possible causes
invalid_request
We are experiencing a problem connecting with Alexa to link your account. Please try again later.
Request is missing state, scope, and/or response_type
unauthorized_client
We are experiencing a problem connecting with Alexa to link your account. Please try again later.
The provided client_id doesn't have access to request consent to link the user's account
access_denied
None. (The user shouldn't see an error message because they were the one who denied the access.)
The user rejected the request to link their account
unsupported_response_type
We are experiencing a problem connecting with Alexa to link your account. Please try again later.
Parameter response_type isn't equal to code
invalid_scope
We are experiencing a problem connecting with Alexa to link your account. Please try again later.
Invalid scope parameter
server_error
Sorry, Alexa encountered an unexpected error while trying to link your account. Please try again.
Unexpected server error
temporarily_unavailable
Sorry, Alexa encountered a momentary error while trying to link your account. Please try again later.