amazon-web-servicesoauth-2.0oauthalexa-skills-kit

How can users grant permission for my website to manage their Amazon Alexa Lists


I want users of my Next.js TypeScript app to grant it permission to manage their Alexa Lists.

I figured this would be possible with OAuth2.

I figured I'd need to create a button in my website that takes the user to an Amazon URL that allows the user to grant my website permission to manage their Alexa lists (and then generates a code that it includes in a GET request that happens as a redirect to a "callback" URL that I registered as the redirect_uri when setting up OAuth2 in Amazon).

I figured the button would be a link to a URL defined like

const url = `${oauth2BaseUrl}?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&response_type=code&scope=${scope}`;

This is generally how OAuth2 works, in my experience.

But I've found Amazon's docs incredibly unhelpful.

I see permissions / scopes mentioned here called alexa::household:lists:read alexa::household:lists:write.

I've set up my API endpoint (which I'll specify at redirectUrl) to exchange the Amazon authorization code for an Amazon access token following the code examples shown there.

I've set oauth2BaseUrl to be 'https://www.amazon.com/ap/oa' (found at https://developer.amazon.com/docs/login-with-amazon/authorization-code-grant.html).

For client ID, I'm using the one for my Alexa skill that I created. Is that correct?

I'm using Next-auth, but I'd be curious if there are any other libraries that could make any of this easier.

Here are permissions I've added in my Skill:

enter image description here

I always get:

400 Bad Request
An unknown scope was requested

But if I just use scopes these different scopes instead, I see it behave how I'd expect (but I lack List permissions): alexa::skills:account_linking postal_code profile:user_id.

P.S. I also started setting up Login With Amazon, but I don't understand why that would be necessary. I'm not looking to offer a federated login feature.


Solution

  • 6 months after asking the question, I finally figured it out.

    Given the meager Amazon docs, I was starting to wonder if this was even possible.

    // Revoke permissions: https://www.amazon.com/ap/adam https://www.amazon.com/gp/help/customer/display.html%3FnodeId%3DGSNRZP4F7NYG36N6
    
    import Alexa from 'ask-sdk-core';
    import * as Model from 'ask-sdk-model';
    import { type Profile, type TokenSet } from 'next-auth';
    import { type OAuthConfig } from 'next-auth/providers';
    
    import { baseUrl } from '../../config/config';
    
    // From https://developer.amazon.com/settings/console/securityprofile/web-settings/update.html:
    const clientId = String(process.env.NEXT_PUBLIC_LWA_CLIENT_ID);
    const clientSecret = String(process.env.LWA_CLIENT_SECRET);
    const oauth2BaseUrl = 'https://www.amazon.com/ap/oa';
    const tokenEndpoint = 'https://api.amazon.com/auth/o2/token';
    const userInfoUrl = 'https://api.amazon.com/user/profile';
    const amazonAlexaApiEndpoint = 'https://api.amazonalexa.com';
    
    const { services } = Model;
    const { LwaServiceClient } = services;
    
    const id = 'alexa';
    
    const authorization = {
      params: {
        redirect_uri: `${baseUrl}/api/auth/callback/${id}`,
        response_type: 'code',
    
        scope: 'profile profile:user_id postal_code', // https://developer.amazon.com/docs/login-with-amazon/customer-profile.html
      },
      url: oauth2BaseUrl,
    };
    
    console.log({ authorization });
    
    /**
     * https://next-auth.js.org/configuration/providers/oauth#userinfo-option
     */
    const userinfo: OAuthConfig<Profile>['userinfo'] = {
      // The result of this method will be the input to the `profile` callback.
      async request(context) {
        const accessToken = String(context.tokens.access_token);
        const input = {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${accessToken}`,
          },
          method: 'GET',
        };
        console.log({ input, userInfoUrl });
        const response = await fetch(userInfoUrl, input); // https://developer.amazon.com/docs/login-with-amazon/obtain-customer-profile.html#call-profile-endpoint
        const json = await response.json();
        const alexaUserId = json?.user_id;
        const profile: Profile = {
          id: alexaUserId,
          ...json,
        };
        console.log({ alexaUserId, json, profile });
    
        return profile;
      },
    };
    
    export const alexaProvider: OAuthConfig<Profile> = {
      authorization,
      clientId,
      clientSecret,
      id,
      idToken: false,
      name: 'Alexa',
      profile(profile: Profile, tokenSet: TokenSet) {
        console.log({ profile, tokenSet });
        profile.tokenSet = tokenSet;
        return profile;
      },
      token: {
        // https://next-auth.js.org/configuration/providers/oauth#token-option
        params: {
          grant_type: 'authorization_code',
        },
        url: tokenEndpoint,
      },
      type: 'oauth' as const,
      userinfo,
    };
    
    export async function getAccessTokenForScope(scope: string) {
      const apiConfiguration = {
        apiClient: new Alexa.DefaultApiClient(),
        apiEndpoint: amazonAlexaApiEndpoint,
        authorizationValue: '',
      };
      const authenticationConfiguration = {
        clientId,
        clientSecret,
      };
    
      const lwaServiceClient = new LwaServiceClient({ apiConfiguration, authenticationConfiguration });
      const accessToken = await lwaServiceClient.getAccessTokenForScope(scope);
      return accessToken;
    }
    

    The insights:

    1. Using Login With Amazon (LWA) is probably required.
    2. As you can see, there are 4 different Amazon endpoints specified above.
    3. No scopes starting with alexa:: exist anywhere within my repo. Those scopes only belong on the AWS sites below.
    4. My Alexa Client ID and Secret do not exist anywhere within my repo. Those only get used on the AWS sites below.
    5. There are 2 allow-lists that you need to add your redirect URL to. One at https://developer.amazon.com/settings/console/securityprofile/web-settings/view.html?identityAppFamilyId= and one at https://developer.amazon.com/alexa/console/ask/amzn1.ask.skill....
    6. After you set up your Security Profile, go to https://developer.amazon.com/loginwithamazon/console/site/lwa/overview.html (Login With Amazon) to create a new "Login with Amazon Configuration" linking to that.
    7. See important AWS dev console settings below.

    Alexa settings Alexa permissions