How Do I Use Login With Amazon to Link A User Account to My Skill? - amazon-web-services

I am having a hard time getting this to work by following along with Amazon's Alexa documentation. I'm running aground on Account Linking because I can't figure out how to get Login with Amazon (LWA) to ask for alexa::skills:account_linking scope.
I've included the Amazon API library in my application and set that all up correctly and I'm invoking the process using the (globally available) amazon object as follows (typescript):
const options: any = {};
options.scope = ['profile', 'alexa::skills:account_linking'];
options.scope_data = {
profile : {essential: false}
};
options.response_type = 'code';
const self = this;
amazon.Login.authorize(options, (response) => {
if (!response || !response.code) {
throw { error: response };
}
// ... send the response code to my server
// ... to be exchanged for bearer and refresh tokens
});
What I would expect to happen from that is a popup Amazon login process to be spawned which (1) has the user log in to Amazon, and (2) collects the user's consent to link their Amazon account to my Alexa skill (i.e. linked to my credentialed hosted service), so that we get back (in the browser) an authorization code that we can exchange (on our server) for bearer and refresh tokens to act on behalf of the user.
The problem is, that code above immediately fails and never pops up a process. The message that is thrown says: "An unknown scope was requested". If I remove the 'alexa::skills:account_linking' string from the options.scope array, I get to an Amazon login screen, and if I log in to Amazon, my server does get an authorization code, etc. But no Account Linking has taken place, so I'm stuck.
I've tried to reconcile this documentation (which also talks about including a Skill ID somehow), with this documentation but I'm just not seeing how to make it work. Can anyone please help point me in the right direction about what I'm doing wrong here? It must be something pretty fundamental.

If your goal is to use Login with Amazon for account linking only for the skill and to not store the tokens on your own server, you can set up the skill and Login with Amazon with the below configurations. The advantage of this approach is that you don't need to stand up your own web server to just handle the LwA flow. This approach also handles all the flow out of the box, including refreshing tokens.
If you're using these tokens for another purpose, you may want to look into something like AWS Cognito to simplify the process.
Skill Account Linking Configuration
Replace Your Client ID with the LwA Client ID, replace Your Secret with the LwA Client Secret, and copy your redirect URIs
LwA Configuration
Paste your Alexa redirect URLs here. These will be specific to your vendor account so it's important to have the right ones.
Source: This is what I do for my Aberto Sonorus skill: https://www.amazon.com/WBPhoto-Aberto-Sonorus/dp/B078W199Z3 (edited screenshots attached)

Related

Authcode for Alexa BDD Tests using LWA

During Accountlinking, Alexa server initiates a Acceptgrant API with its Authcode so that it can be exchanged to Access Token. I want to test my API which accepts the Authcode
(postman or BDD Tests). How can we generate this ? It would be really helpful for automated tests.
I have tried LWA with AWS JS SDK. We need to whitelist the server in security profile. This basically opens a browser instance and appends a Authcode to the redirect_uri. Is there any other way than this? I read about CLI where we can get accesstoken but not authcode.
Also when we create a skill it generates a ClientId. Are there any security profiles for this ClientId ? How can I whitelist my server for this ClientId ? I didnt find a way to link this client ID to security profile.
Some more info:
I will elaborate a bit more. Basically Alexa sends a AcceptGrant directive during account linking when user enables the skill from alexa UI. AcceptGrant consists of Authcode so that User/bussiness(smart device manufacturer cloud) can get accesstoken using LWA. So now the bussiness has API exposed which accepts a Authcode. Now i am trying to test this API. Currently i dont find a way to generate this token.
If i generate authcode with LWA flow , this requires a redirect_uri. But if i use the code which is sent by Alexa (during accountlinking flow) , it doesn't require redirect_uri. From this page
https://developer.amazon.com/docs/login-with-amazon/authorization-code-grant.html#access-token-request
if i use AWS JS SDK it doesnt require a redirect_uri for access_token request. When i tried AWS JS to generate authcode, i get an secuirty error asking user to whitelist the domain. And i dont find a way to whitelist the client_id of Alexa to a security profile.
In skill linking, linking out from a skill to a 3p server, Alexa handles the auth code exchange and retrieves the access token for you. The auth code is never exposed to the skill because you don't need it.
It's only good once. After that, you use the refresh token returned when the auth code was used.
Maybe if you describe the use case you're trying to accomplish and why you believe you need the code, it'll be possible to give you more useful help.

How do you connect Cognito to Pinpoint for analytics?

I'm trying to get some basic analytics for a Cognito user pool. It seemed simple enough to do, I created a project in Pinpoint, then I went to my user pool, went to General settings > Analytics, clicked the Add Analytics button, choose the Amazon Cognito app client that my app uses, pointed to the Pinpoint project I just created, checked the "Share user profile data" checkbox (though I assume that's not absolutely necessary), and it had the message telling me if would use the IAM role and so on. Clicked Save, got no error, I assumed at this point I would start seeing Analytics in Pinpoint, but there's absolutely nothing showing up.I do have a message saying I haven't enabled any features yet, but I don't see any features I'd need to enable. I don't care about the campaigns as of now, and then under Application analytics it seems geared to you manually updating your mobile or web app to send something, but I thought that was if you need to customize something. Am I mistaken? Will this integration only work if you change your web app to explicitly send things to Pinpoint? I just assumed if I connected Cognito and Pinpoint the analytics would show since Cognito obviously knows people are logging in without you needing to manually make some extra request.
From my research, I found out that since you are using a Web Application without using AWS Amplify framework, you need to add additional lines of code to your application in order to send Cognito authentication analytics data to your Amazon Pinpoint project.
If you are using the Javascript SDK you may add the initate-Auth property code snippet to your front-end application:
var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
var params = {
AuthFlow: "USER_PASSWORD_AUTH",
ClientId: 'STRING_VALUE', /* the client ID attached to the Pinpoint project */
AuthParameters: {
'USERNAME': 'STRING_VALUE',
'PASSWORD': 'STRING_VALUE'
},
AnalyticsMetadata: {
AnalyticsEndpointId: 'STRING_VALUE' /* the Pinpoint project ID */
},
};
cognitoidentityserviceprovider.initiateAuth(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
In the above code snippet, the Pinpoint project/application ID is added as part of the "AnalyticsMetadata" parameter when the client makes the API calls (e.g. sign-in, sign-up, etc.) to the Cognito user pool. Therefore, the API calls will have the pinpoint project ID attached to them and Cognito can use that information to send the data to your Pinpoint project. Without this crucial step, analytics data will not be sent to Pinpoint and will result in the behavior you have described.
If using CLI (for verification/testing purpose), you may execute the following AWS CLI initiate-auth command below :
$ aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --auth-parameters USERNAME=STRING_VALUE,PASSWORD=STRING_VALUE --client-id STRING_VALUE --analytics-metadata AnalyticsEndpointId=STRING_VALUE
The take away point is that :
Amazon Cognito integration with Amazon Pinpoint only works for apps (clients) that are using Cognito SDK for Mobile Apps (AWS Mobile SDKs or JavaScript - AWS Amplify JavaScript library).
Just a note to syumaK's response, yes you need to update your code's initAuth call to include the AnalyticsMetadat property. However, I found out that the AnalyticsEndpointId should NOT be the Pinpoint project ID.
I believe that since you've configured Cognito already to integrate with the pinpoint project, it knows which pinpoint project you are sending metrics to already. The AnalyticsEndpointId should be a uniquely identifier for the login.
The documentation is definitely flaky here. The cli one from syumaK's seems to describe it differently than Cognito API Reference
What happened to me was that I used the pinpoint project id, and when I log in with multiple users, it thinks it's the same one. The daily active users remains at one. AWS Support clarified that it should be a unique identifier for each user. So after changing the id to be the user's email, I am starting to get accurate numbers for the daily active users.
It also seems like this is just about as useful as it is going to be for not using the Cognito SDK for Mobile Apps or Amplify. You can only get information like daily/monthly active users, authentication metrics. You don't have the ability to further break down by segments, etc.

3-legged OAuth and one-time-code flow using google-auth-library-ruby with google-api-ruby-client

Quick Overview: I have a ruby app that runs nightly and does something with a user's google calendar. The user has already given access via a separate react app. I'm having trouble getting the ruby app to access the user's calendar with the authorization code from the react app.
Details: I have a React front-end that can sign in a user using gapi and subsequently sign the user into Firebase. Here is how I configure the gapi obj:
this.auth2 = await loadAuth2WithProps({
apiKey: config.apiKey, // from firebase
clientId: config.clientId, // from gcp
// ....
access_type: "offline", // so we get get authorization code
})
Here is sign in:
doSignInWithGoogle = async () => {
const googleUser = await this.auth2.signIn();
const token = googleUser.getAuthResponse().id_token;
const credential = app.auth.GoogleAuthProvider.credential(token);
return this.auth.signInWithCredential(credential);
};
The user's next step is to grant the app offline access to their calendar:
doConnectGoogleCalendar = async () => {
const params = {scope:scopes};
const result = await this.auth2.grantOfflineAccess(params);
console.log(result.code); // logs: "4/ygFsjdK....."
};
At this point the front end has the authorization code that can be passed to a server-side application to be exchanged for access and refresh tokens. I haven't been able to find a good way to use a user supplied auth-code to make calls to available scopes. This is how I've configured the oauth client:
auth_client = Google::APIClient::ClientSecrets.load(
File.join(Rails.root,'config','client_secrets.json') // downloaded from GCP
).to_authorization
^ I'm using the same GCP Credentials on the backend that I'm using for the frontend. It is a "OAuth 2.0 Client ID" type of credential. I'm unsure if this is good practice or not. Also, do I need to define the same config that I do on the frontend (like access_type and scope)?.
Next I do what the docs say to get the access and refresh tokens(click Ruby):
auth_client.code = authorization_code_from_frontend
auth_client.fetch_access_token!
---------
Signet::AuthorizationError (Authorization failed. Server message:)
{
"error": "invalid_grant",
"error_description": "Bad Request"
}
Is there something I'm missing in setting up a separate backend application that can handle offline access to a user granted scope? There is so much different information on these libraries but I haven't been able to distill it down to something that works.
UPDATE
I found this page describing the "one-time-code flow" which I haven't found anywhere else is all of the docs I've gone through. It does answer one of my minor questions above: Yes, you can use the same client secrets as the web application for the backend. (see the full example at the bottom where they do just that). I'll explore it more and see if my bigger problem can be resolved. Also going to update the title to include one-time-code flow.
After a good amount of digging through code samples and source code, I have a clean working solution. Once I found the page in my "update" it led me to finding out that ClientSecrets way I was doing things had been deprecated in favor of the google-auth-library-ruby project. I'm glad I found it because it seems to be a more complete solution as it handles all of the token management for you. Here is code to setup everything:
def authorizer
client_secrets_path = File.join(Rails.root,'config','client_secrets.json')
client_id = Google::Auth::ClientId.from_file(client_secrets_path)
scope = [Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY]
redis = Redis.new(url: Rails.application.secrets.redis_url)
token_store = Google::Auth::Stores::RedisTokenStore.new(redis: redis)
Google::Auth::WebUserAuthorizer.new(client_id, scope, token_store, "postmessage")
end
and then this is how I use the authorization code:
def exchange_for_token(user_id,auth_code)
credentials_opts = {user_id:user_id,code:auth_code}
credentials = authorizer.get_and_store_credentials_from_code(credentials_opts)
end
after calling that method the library will store the exchanged tokens in Redis (you can configure where to store) for later use like this:
def run_job(user_id)
credentials = authorizer.get_credentials(user_id)
service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = credentials
calendar_list = service.list_calendar_lists.items
# ... do more things ...
end
There is so much info out there that it is difficult to isolate what applies to each condition. Hopefully this helps anyone else that gets stuck with the "one-time-code flow" so they don't spend days banging their head on their desk.

Alexa Skill with Azure AD B2C Auth expires after an hour

I am currently building an Alexa skill backed by Azure Functions (.NET Core/C#) and Azure AD B2C for authentication.
For the initial setup, I used mostly used the instructions found in this arcticle. Since, the article was written a couple of years ago, I had to make a few changes. In the end, I landed on the following configuration:
Azure Active Directory B2C
As I mentioned, we are using AAD B2C for authentication. Users of a related application are able to sign-up and sign-in to a React application. The idea is to provide an alternative interface for said users through Alexa intents + utterances.
I created an application for Alexa in AAD B2C with the following settings:
Properties
Web App / Web API: Yes
Allow implicit flow: Yes
Reply URLs: I entered the values provided by the Alexa skill setup (e.g. https://pitangui.amazon.com/api/skil/link/...); there are three different ones. I also added one for my azure function app (this is something that could be incorrect. It was part of what I did while diagnosing other earlier problems); it's in the format: https://myfuncname.azurewebsites.net/.auth/login/aad/callback (Do I even need this???)
App ID URI: https://myorg.onmicrosoft.com/alexa
Include native client: No
Keys
I generated a single App Key, which I'm using as the Secret in the Account Linking section in the Alexa Developer Console.
Many of the examples online mention setting an explicit expiration date here of 1 or 2 years; however, I am not presented with any options at all (i.e. no expiration option), just the code. Could this be part of the problem???
API Access
In the Published scopes section, the Scope's name is user_impersonation. The description is "Access this app on behalf of the signed-in user". The full scope value is: https://myorgsname.onmicrosoft.com/alexa/user_impersonation.
For API Access, I have to API entries here:
One that uses the user_impersonation scope mentioned above.
The second, titled "Access the user's profile", uses:
Acquire an id_token for users (openid)
Acquire a refresh_token for users (offline_access)
AAD B2C User Flow
The user flow that I'm using allows signing up and signing in, it utilizes the following configuration:
Properties
Misc
Enable JavaScript enforcing page layout (preview): On
Token lifetime
Access & ID token lifetimes (minutes): 60
Refresh token lifetime (days): 14
Refresh token sliding window lifetime: "Bounded".
Lifetime length (days): 90
Token compatibility settings
Issuer (iss) claim: https://<domain>/<b2c-tenant-guid>
Subject (sub) claim: ObjectID
Claim representing user flow: tfp
Session behavior
Web app session lifetime (minutes): 1440
Web app session timeout: Rolling
Single sign-on configuration: Tenant
Require ID Token in logout requests: No
Azure Function Authentication Middleware
For the authentication layer within the Azure Function, I'm utilizing the method described in the article mentioned above.
Alexa Developer Console
On the Alexa side of things, I have a really simple skill setup with the following settings:
Endpoint
My endpoint uses the HTTPS option with the default region set to the fully-qualified HTTPS endpoint of my Azure Function App's handler function.
The certificate set to "My development endpoint is a sub-domain of a domain that has a wildcard ..."
Account Linking
The account linking settings are as outlined below:
Do you allow uses to create an account or link to ...: Toggled On
Allow users to enable skill without account linking: Toggled On
Allow users to link their account to your skill from within your application or website: Toggled Off
Auth Code Grant: On
Authorization URI: https://myorg.b2clogin.com/myorg.onmicrosoft.com/oauth2/v2.0/authorize?p=<sign-in-user-flow-policy-name>
Access Token URI: https://myorg.b2clogin.com/myorg.onmicrosoft.com/oauth2/v2.0/token?p=<sign-in-user-flow-policy-name>
Your Client ID: AAD B2C App GUID
Your Secret: Key generated in App settings in AAD B2C for my Alexa Skill App (mentioned in the AAD B2C setup info above).
Your Authentication Scheme: HTTP Basic
Scope: openid and https://myorg.onmicrosoft.com/alexa/user_impersonation
Domain List: login.microsoftonline.com and myorg.b2clogin.com Note: This is probably wrong as I didn't know what to put here. The article above doesn't mention this setting at all
Default Access Token Expiration Time: 3600
Note: The Alexa Redirect URLS at the bottom are what I put in AAD B2C for the Reply URL section.
The Problem
Now for the most important part, The Problem. Everything seems to work at first...I'm able to go to alexa.amazon.com and utilize Link Account (which redirects me to and from my AAD B2C-driven login screen). Once I link accounts, I'm able to successfully utilize an utterance and receive a reply.
The problems starts when I wait an hour (I believe it's an hour). Attempting to initiate the Intent after an hour yields an error on the Azure Function app side of things when it tries to validate the Auth Token.
Can anyone provide me some guidance as to what I may have setup incorrectly or at least some things that I should look into? As I mentioned at the start of this question, many of the references that I'm finding online are out-of-date and do not cover all of the settings that I'm expected to utilize. Many of them are still using microsoftonline.com authority vs. b2clogin.com.
At a glance, I would assume that the problem is that the Alexa skill is failing to refresh its token after it expires after an hour. What do I need to do to ensure that it refreshes correctly?
I think that I have enough information at this point to go ahead and answer my own question. What I found was that the offline_access scope is necessary for Token Refresh to be possible.
Per Microsoft, "The offline_access scope gives your app access to resources on behalf of the user for an extended time. On the consent page, this scope appears as the "Maintain access to data you have given it access to" permission. When a user approves the offline_access scope, your app can receive refresh tokens from the Microsoft identity platform token endpoint. Refresh tokens are long-lived. Your app can get new access tokens as older ones expire.".
You can read more about it here.
To resolve the issue, I ensured that this scope was available in AAD B2C and added it as a referenced scope in the Alexa developer console.
Thanks for giving insight on offline_access. It took few hours to figure out how to implement offline_access. Interestingly offline_access works only with Azure AD, OAuth 1.0 endpoint and not with 2.0.
While trying with 2.0 it kept failing while account linking when multiple scopes were mentioned in Alexa configurations. The scopes I tried were as follows.
https://samplealexabackendapi/
https://graph.microsoft.com/offline_access
Finally I ended up working with OAuth 1.0 endpoint and using the scope https://samplealexabackendapi/.default which considers all scopes available to the app registered.

GCP Pub/Sub authentication token not appearing in the request

I'm setting up cloud pub/sub service to receive push subscription messages on my backend by following the developer documentation.
For some reasons, the push subscription requests sent to my backend never have the "token" query parameter included, so my backend can't verify whether the push requests coming from my application or not. The following code from the developer doc is used to achieve the verification purpose:
// Verify that the request originates from the application.
if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0)
{
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
According to the doc, I've verified that the "Service Account Token Creator" permission is granted to pub/sub's signing service, as well as enabled "push endpoint authentication" option and assigned an unique string to the "Audience(optional)" field in my push subscription in pub/sub.
I wonder if I'm still missing something here, or I've mistaken the way how the token audience works? Why does the "token" value not appearing in the requests my backend received? Any inputs would be appreciated!
As John Hanley said, the token is in the header. However, when I look your code, I don't know what do you want to "compare". The provided token change very often (because its life is very short) and you can't compare it to a reference token
But, you can extract claim information from token and then validate them. Here a snippet of Python code that I use for extracting email.
authorization = request.headers['Authorization'][7:]
b64_profile = authorization.split('.')[1]
profile = base64.b64decode(b64_profile + '=' * (-len(b64_profile) % 4))
print(json.loads(profile)['email'])
The code snippet quoted here is meant to verify the application token. This token is defined by the application developer as an environmental variable in appengine-web.xml (line 7).
The token that properly verifies the origin of the push request is a different one. It is generated by Cloud Pub/Sub and comes in the authorization header of the push request, in the form of a JWT. This bit of code shows how to verify the JWT (you can also check out the full sample):
// Verify and decode the JWT.
GoogleIdToken idToken = verifier.verify(authorization);
Hope this helps clear the confusion. :-)