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:
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.
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:
alexa::
exist anywhere within my repo. Those scopes only belong on the AWS sites below.