Step 3: Handle the Authorization Request in Your App


To start account linking, the Alexa service opens the configured Universal Link or App Link and makes an authorization request. In response, your app must show a consent screen to the user and get their consent to link their account. The authorization request follows the OAuth 2.0 specification for the authorization code grant type or the implicit grant type.

Authorization request to your app

The authorization request that opens your app looks like the following example. For a description of the parameters, see Query parameters.

https://yourAppAuthUrl?
response_type={Response Type}&
client_id={Client ID}&
scope={Scope}&
state={State}&
code_challenge={unique-id}&
code_challenge_method=S256&
redirect_uri={Redirect URI}

On receipt of the authorization request to your app, obtain the user's consent for account linking and assemble the authorization response parameters.

To handle the authorization request in your app

  1. Extract the authorization request parameters from the Universal Link or App Link request.
  2. Present a screen in your app where the user can grant or deny access to the specific scopes requested.
  3. When the user accepts the authorization request, make a request to your authorization server to generate an authorization code (for the authorization code grant type) or an access token (for the implicit grant type).
  4. Include the authorization code or access token in the redirect that you use to send the user back to the Alexa app in Step 4: Redirect to the Alexa App.
  5. If the user denies the request, include the error in the redirect.

If the user doesn't have your app installed on their device, the authorization request URI opens in their browser. Here, use the same log-in and consent flow that you set up for Standard Account Linking.

iOS example

// Handler for Universal Links in AppDelegate
// Add this method to handle incoming Universal Links

static let EXPECTED_CODE_CHALLENGE_METHOD = "S256"

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
        let incomingURL = userActivity.webpageURL else {
            return false
    }
    
    guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
        let queryParameters = components.queryItems else {
            return false
    }
    
    // If your skill opted-in to PKCE for an Authorization Code Grant, 
    // Alexa WILL include `code_challenge` and `code_challenge_method` in the `incomingURL`.
    let AUTH_REQUEST_PARAMETERS: Set<String> = [
        "redirect_uri", "state", "scope", "client_id", "response_type",
        "code_challenge", "code_challenge_method"]
    
    var validatedParameters : [String: String] = [:]
    for queryParameter in queryParameters {
        if AUTH_REQUEST_PARAMETERS.contains(queryParameter.name) {
            validatedParameters[queryParameter.name] = queryParameter.value
        }
    }
    
    // Validate PKCE parameters if Authorization Code Grant
    if validatedParameters["response_type"] == "code" {
        guard validatePkceParameters(parameters: validatedParameters) else {
            Logger.error("Invalid PKCE parameters")
            // Fail request since Alexa only supports S256 as code_challenge_method
        }
    }

    let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "appToAppConsentScreen") as! AppToAppScreenViewController
            initialViewController.authorizationRequest = validatedParameters
            self.window?.rootViewController = initialViewController
            self.window?.makeKeyAndVisible()
            return true
}

private func validatePkceParameters(parameters: [String: String]) -> Bool {
    // If code challenge is present, validate the method
    if let codeChallenge = parameters["code_challenge"],
        !codeChallenge.isEmpty {
        guard let codeChallengeMethod = parameters["code_challenge_method"],
                codeChallengeMethod == EXPECTED_CODE_CHALLENGE_METHOD else {
            logger.warning("Invalid code_challenge_method")
            return false
        }
    }
    return true
}
    
// Create this controller for the consent screen
class AppToAppScreenViewController: UIViewController {
    
    var authorizationRequest : [String: String]?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    
    @IBAction func acceptButtonClicked(_ sender: UIButton) {
    
        // ** GUIDANCE FOR YOUR AUTHORIZATION SERVER (on user ACCEPT): **
        // This applies if `response_type` indicates an Authorization Code Grant (e.g., "code").
        // PKCE (RFC 7636) protects the Authorization Code Grant; it's NOT used for Implicit Grant.
        //
        // 1. If PKCE Opted-In (for Auth Code Grant): Alexa sends `code_challenge` & `code_challenge_method`.
        //    Your server MUST handle them as defined in RFC 7636 (store with auth code for token endpoint verification).
        // 2. If PKCE Not Opted-In OR Implicit Grant: Proceed with standard grant flow (RFC 6749).
        // 3. On Success: Issue auth code (for Auth Code Grant) or access token (for Implicit Grant).
        //    Then, redirect to `redirect_uri` with appropriate params & original `state`.
        // See RFC 6749 & RFC 7636.
    
        guard let responseUri = 
            callBackendForAuthorization(authorizationRequest: authorizationRequest, "ACCEPT") else {
                return
        }
        
        // implementation of this method shown in the following step
        openUrl(alexaUrl: responseUri)
    }
    
    @IBAction func denyButtonClicked(_ sender: UIButton) {
        
        guard let responseUri = 
            callBackendForAuthorization(authorizationRequest: authorizationRequest, "DENY") else {
                return
        }
        
        // implementation of this method shown in the following step
        openUrl(alexaUrl: responseUri)
    }
}

Android example

class AppToAppConsent : AppCompatActivity() {
    
        // Constants
        private val QUERY_PARAMETER_KEY_CLIENT_ID = "client_id"
        private val QUERY_PARAMETER_KEY_RESPONSE_TYPE = "response_type"
        private val QUERY_PARAMETER_KEY_STATE = "state"
        private val QUERY_PARAMETER_KEY_SCOPE = "scope"
        private val QUERY_PARAMETER_KEY_REDIRECT_URI = "redirect_uri"
        private val QUERY_PARAMETER_KEY_CODE_CHALLENGE = "code_challenge";
        private val QUERY_PARAMETER_KEY_CODE_CHALLENGE_METHOD = "code_challenge_method";
        private val EXPECTED_CODE_CHALLENGE_METHOD = "S256";
    
        private val USER_ACCEPT = "ACCEPT";
        private val USER_DENY = "DENY";
    
        // Incoming Query Parameters
        private var clientId: String? = null
        private var responseType: String? = null
        private var state: String? = null
        private var scope: String? = null
        private var redirectUri: String? = null
        private var url: String? = null
        private var codeChallenge: String? = null
        private var codeChallengeMethod: String? = null
    
        private lateinit var acceptButton: Button
        private lateinit var denyButton: Button
    
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val appLinkData = intent.data
    
            // Get values from App Link
            clientId = appLinkData.getQueryParameter(QUERY_PARAMETER_KEY_CLIENT_ID)
            responseType = appLinkData.getQueryParameter(QUERY_PARAMETER_KEY_RESPONSE_TYPE)
            state = appLinkData.getQueryParameter(QUERY_PARAMETER_KEY_STATE)
            scope = appLinkData.getQueryParameter(QUERY_PARAMETER_KEY_SCOPE)
            redirectUri = appLinkData.getQueryParameter(QUERY_PARAMETER_KEY_REDIRECT_URI)
    
            // Get PKCE (RFC 7636) values from App Link data if present.
            // Alexa will send these if your skill opted-in to PKCE for an Authorization Code Grant.
            codeChallenge = appLinkData.getQueryParameter(QUERY_PARAMETER_KEY_CODE_CHALLENGE);
            codeChallengeMethod = appLinkData.getQueryParameter(QUERY_PARAMETER_KEY_CODE_CHALLENGE_METHOD);
    
            // Validate PKCE parameters if Authorization Code Grant
            if (responseType == "code" && !validatePkceParameters()) {
              // Fail request since Alexa only uses S256 as code_challenge_method
            }

            setContentView(R.layout.activity_consent_screen)
            initializeComponents()
        }
    
        private fun validatePkceParameters(): Boolean {
        // If code challenge is present, validate the method
            if (!codeChallenge.isNullOrEmpty()) {
                if (codeChallengeMethod != EXPECTED_CODE_CHALLENGE_METHOD) {
                    logger.warning("Invalid code_challenge_method: $codeChallengeMethod")
                    return false
                }
            }
            return true
        }

        private fun initializeComponents() {
    
            acceptButton = findViewById(R.id.btn_accept_linking)
            acceptButton.setOnClickListener {
                
                // ** GUIDANCE FOR YOUR AUTHORIZATION SERVER (on user ACCEPT): **
                // Applies if `response_type` is for Authorization Code Grant (e.g., "code").
                // PKCE (RFC 7636) is for Auth Code Grant; NOT for Implicit Grant.
                //
                // 1. If PKCE Opted-In (for Auth Code Grant): Alexa sends `code_challenge` & `code_challenge_method`.
                //    Your server MUST handle as defined in RFC 7636 (store with auth code for token endpoint verification).
                // 2. If PKCE Not Opted-In OR Implicit Grant: Standard grant flow (RFC 6749).
                // 3. On Success: Issue auth code (Auth Code Grant) or access token (Implicit Grant).
                //    Redirect to `redirect_uri` with params & original `state`.
                // See RFC 6749 & RFC 7636.    
    
                val responseUri = callBackendForAuthorization(clientId, responseType, state, scope, redirectUri, codeChallenge, codeChallengeMethod, USER_ACCEPT)
    
                // implementation of this method shown in the following step
                openUrl(responseUri)
            }
    
            denyButton = findViewById(R.id.btn_decline_linking)
            denyButton.setOnClickListener {
                val responseUri = callBackendForAuthorization(clientId, responseType, state, scope, redirectUri, USER_DENY)
    
                // implementation of this method shown in the following step
                openUrl(responseUri)   
            }
        }
}

Was this page helpful?

Last updated: frontmatter-missing