How to convert Amazon MWS credentials to SP-API creds - amazon-web-services

Here are the seemingly clear instructions from Amazon.
Simply send the following: sellingPartnerId, developerId, and mwsAuthToken
I do this with httparty like so:
query = {
sellingPartnerId: "A3Kxxxxx",
developerId: "753xxxx",
mwsAuthToken: "amzn.mws.8abxxxxx-xxxx-xxxx-xxxx-xxxxxx",
}
and then
send = HTTParty.get("https://sellingpartnerapi-na.amazon.com/authorization/v1/authorizationCode",
query: query
)
This returns the following error:
{"errors"=>
[{"message"=>"Access to requested resource is denied.",
"code"=>"MissingAuthenticationToken"}]}
I've adjusted the call everyway I've seen. I've read the following articles:
This
This
Paged through the 695 issues on github for this API and still no luck.. I've adjusted my query to this with no luck either:
query = {
grant_type: "client_credentials",
sellingPartnerId: "A3K98Oxxxxxx",
developerId: "753xxxxxxxx",
mwsAuthToken: "amzn.mws.8abxxxxxxx-xxxx-xxxx-xxxx-xxxxxxx",
client_id: "amzn1.application-oa2-client.xxxxxxxxxxxxxxxxxxxxxxxx",
client_secret: "a473e76XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
scope: "sellingpartnerapi::migration"
}
Nothing I've tried has worked.. Any suggestions? Has anyone actually migrated their MWS to SP-API credential successfully?

Unfortunately the specific Amazon docs that you link to don't tell the whole story. There are a few other requirements you'll need in order to get the authorizationCode response that you're looking for:
Amazon OAuth Token
You'll need an access token from Amazon's OAuth API (an entirely different API). You can use the grantless workflow for this, since in your case the user hasn't actually authorized the SP-API yet:
POST https://api.amazon.com/auth/o2/token
body: {
grant_type: 'client_credentials',
scope: 'sellingpartnerapi::migration',
client_id: 'amzn1.application-oa2-client.xxxxxxxxxxxxxxxxxxxxxxxx',
client_secret: 'a473e76XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
}
This will return an access_token that you'll need for your actual migration request to https://sellingpartnerapi-na.amazon.com/authorization/v1/authorizationCode. The response will look something like:
{
"access_token": "Atc|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"scope": "sellingpartnerapi::migration",
"token_type": "bearer",
"expires_in": 3600
}
Important: Take the access_token value from that response and add it as an x-amz-access-token header to your /authorization/v1/authorizationCode request.
Sign Your Request
This is the actual reason behind the error you're receiving. An unsigned request will not include the "authorization token" that you're being prompted for.
You'll need to sign your request using Amazon's SigV4 signing mechanism. It looks like you're using Ruby (HTTParty), so you can use the aws-sdk's Aws::Sigv4::Signer for this. You'll need to have setup IAM credentials as documented in the generic developer guide, and those credentials being provided to your Aws::Sigv4::Signer somehow (hardcoding, env vars, Aws::SharedCredentials, etc.)
Request signing will result in a few proprietary headers being added to your request. Once this is done, you should have all that you need to make the request successfully.

Related

Using Postman to access Google Indexing APIs

I would like to test Google Indexing API with Postman.
I have already configured the api correctly on https://console.cloud.google.com/ for using it on a wordpress site with the plugin Instant indexing and everything works ok.
Now I simply want to do a POST call with Postman, in particular I did not found anything clear about the type of authentication (Oauth 2) to use for the call.
{
"error": {
"code": 401,
"message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED",
"details": [
{
"#type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "CREDENTIALS_MISSING",
"domain": "googleapis.com",
"metadata": {
"method": "google.indexing.v3.UrlService.PublishUrlNotification",
"service": "indexing.googleapis.com"
}
}
]
}
}
Anyone can provide me an example? Thank you.
As stated in the comments, Google API's normally use OAuth2. Some exceptions might use an API key, but even then most of those still limit the data you can access with an API key versus what you can access using OAuth2
This particular API does indeed use OAuth2 as per the docs. This means you need an access token (as the error message states), so you will need to generate one.
Easiest would be to use a library for one of the many programming languages, as per the docs here.
I'll give a simple example in python as that is my preferred language. The following code uses the google-auth library (docs). It's the base library to deal with credential objects. You will need to download the JSON representation for the service account you created. See the relevant docs. Note that you should handle this file as if it were a password, same as with the access token itself, although the access token has a default lifetime of 3600 seconds, AKA 1h.
If you get an error message that the token is expired, create a new one. Read up on refresh tokens for that, but that's beyond the scope of this question.
Code was tested on a virtualenv for python 3.10
requirements.txt
google-auth
requests
main.py
from google.oauth2 import service_account
import google.auth.transport.requests
# create credentials object
creds= service_account.Credentials.from_service_account_file("/path/to/serviceaccount.json")
# we need a scoped credentials object as per
# https://developers.google.com/search/apis/indexing-api/v3/prereqs#requirements
scoped_creds = creds.with_scopes(['https://www.googleapis.com/auth/indexing'])
# create a request object for the refresh method
request = google.auth.transport.requests.Request()
# make the library do it's magic and get a token
scoped_creds.refresh(request)
print(scoped_creds.token)
This will print out an access token. (if it prints with dots (....) at the end, remove them.
Another easy option would be to (ab)use the gcloud command line tool. There's a couple of steps to this to get to work. You can use the cloud shell as an easy way to do this.
Download or copy the serviceaccount JSON file I mentioned above.
Activate the serviceaccount in gcloud using:
gcloud auth activate-service-account SERVICE_ACCOUNT#DOMAIN.COM --key-file=/path/key.json
Print the access token with this command:
gcloud auth print-access-token
The hard and masochistic way would be to use something like CURL or HTTPS requests manually to generate the token. Feel free to do so, but I'm just going to point you to the docs for that. It's a bit of a pain in the posterior.
You can test the token as explained in this answer.
Now that you have the access token, you can use it in POSTMAN by setting it in the header for the call. See this nice answer, but basically add the following request header key/value pair, replacing TOKEN with the generated token.
KEY: Authorization
VALUE: Bearer TOKEN
For anyone interested I managed to make it work by following these steps:
To obtain the authentication token with Postman first go to https://console.cloud.google.com/apis/credentials and in your web application ID client set https://oauth.pstmn.io/v1/callback as Authorized redirect URIs
Now in Postman Application in Authorization tab select OAuth 2.0 and configure the fields according to your client_secret.json (image)
Now click on 'Get new access Token' and you should get it
Specify the url and type as raw json body (image)

AWS Amplify React GET request error - missing authentication token

I am using AWS Amplify in my react app to call my API hosted in API Gateway with AWS_IAM authorization. When I set authorization to NONE, everything works fine in my react app.
However, when I set authorization to AWS_IAM and execute my API using API.get() from Amplify like the code below:
const notes = await API.get('notes', '/notes', init);
I get an error message like:
{
"message": "Missing Authentication Token",
"err": "missing auth"
}
So I tried to use aws-api-gateway-cli-test to test my API gateway. Through the script, I was able to get a valid credential, get authenticated and correct response. I have also tried the POSTMAN with my admin credentials and it worked as well.
After doing some research, I saw people referring this to CORS issue. I have double checked my API gateway settings and confirmed that I have enabled CORS. The same issue persists.
Through the debugging feature of aws-amplify, I was able to see the signing process in my Chrome inspector. The signing process was performed properly with some accessKey and secretKey. I pulled those keys out of the inspector into my POSTMAN and tried to get.
These credentials are not valid and I received the following error message:
{
"message": "The security token included in the request is invalid.",
"err:": "default"
}
Update: I forgot to copy session token into POSTMAN. Now with all credentials generated by my app I am able to get correct result from my API in POSTMAN. Just not in my app.
At this point, it is pretty obvious to me that it is an auth problem. However, I have been using aws-amplify for sign-in and get process. Pretty sure the signing process is done by amplifying behind the scenes.
I have spent 3 days on this problem and basically tried everything... Any ideas on why it doesn't work?
P.S. A lot of people have suggested that typos in the invoke URL could lead to this issue. I have double checked and there is no typo. Below is my amplify configure code:
Amplify.configure({
Auth: {
mandatorySignIn: true,
region: config.cognito.REGION,
userPoolId: config.cognito.USER_POOL_ID,
identityPoolId: config.cognito.IDENTITY_POOL_ID,
userPoolWebClientId: config.cognito.APP_CLIENT_ID
},
Storage: {
region: config.s3.REGION,
bucket: config.s3.BUCKET,
identityPoolId: config.cognito.IDENTITY_POOL_ID
},
API: {
endpoints: [
{
name: "notes",
endpoint: config.apiGateway.URL,
region: config.apiGateway.REGION
}
]
}
});
Just resolved my problem - I have had Authorization settings for OPTIONS method to be AWS_IAM under Resources in API Gateway. However when my browser send a request it will send one to OPTIONS first to check for certain headers without the credentials in this request.
Since I set OPTIONS with IAM authorization, the OPTIONS method then checked against IAM with this request without my credentials. This is why I received "Missing Authentication Token".
The problem was this in my case:
import { Auth } from 'aws-amplify';
import { API } from 'aws-amplify';
I know, I now it's banal. This why I should't code when I am exausted.

AWS Cognito; unauthorized_client error when hitting /oauth2/token

Steps taken so far:
Set up new user pool in cognito
Generate an app client with no secret; let's call its id user_pool_client_id
Under the user pool client settings for user_pool_client_id check the "Cognito User Pool" box, add https://localhost as a callback and sign out url, check "Authorization Code Grant", "Implicit Grant" and everything under "Allowed OAuth Scopes"
Create a domain name; let's call it user_pool_domain
Create a new user with a username/password
Now, I can successfully go to:
https://{{user_pool_domain}}.auth.us-east-2.amazoncognito.com/oauth2/authorize?response_type=code&client_id={{user_pool_client_id}}&redirect_uri=https%3A%2F%2Flocalhost
This presents me with a login page and I am able to login as my user which returns me to https://localhost/?code={{code_uuid}}
I then try the following:
curl -X POST https://{{user_pool_domain}}.auth.us-east-2.amazoncognito.com/oauth2/token -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=authorization_code&redirect_uri=https%3A%2F%2Flocalhost&code={{code_uuid}}&client_id={{user_pool_client_id}}'
However, this just returns back the following:
{"error":"unauthorized_client"}
The token endpoint docs say that unauthorized_client is because "Client is not allowed for code grant flow or for refreshing tokens." which is confusing because I checked the boxes allowing the client to use the code grant flow.
So, it turns out that the user pool has to have a trailing slash (https://localhost/) and then that trailing slash has to be used in all of the callback URLs. Then it decides to work!
Everything looks OK to me. I think it may be complaining about the Authorization header missing but not sure. You could try a few things:
1) According to this page (https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html), you shouldn't need to send the Authorization header in the token request, but maybe it is still needed. You could try either passing just the client ID in it (Authorization [client ID]) or configure a secret and try passing Authorization [client ID:client secret] like it says). It usually makes sense to use a client secret for authorization code flow anyway since in this flow, there is a server side component that can securely handle the token exchange.
2) Try using Implicit Flow instead to see if that works. Implicit Flow makes sense for single page apps with no server side component. For that, no client secret is needed.
If you are using amplify and have it configured outside of the CLI and the other answers aren't working for you, one last fix you can try is to ensure you have responseType: 'token' if you are using implicit flow. Fixed things for me.
Auth: {
oauth: {
domain : 'your-app.auth.your-region.amazoncognito.com',
redirectSignIn: environment.cognito.oauthCallbackLogin,
redirectSignOut: environment.cognito.oauthCallbackLogout,
responseType: 'token',
scope : ['email', 'openid', 'profile'],
}
}
I had this error for another reason: I had response_type=token in the request URL, but the implicit OAuth flow was not enabled in the user pool client, so I needed to change it to response_type=code.
I agree with #rioastamal and #kiran01bm
as well. I did not need a trailing slash and it has to be verbatim as configured for the callbacks.
In my case I had my Redirect URI encoded at definition like this const redirectUri = encodeURIComponent(REDIRECT_URI).
Later, when it was used in the POST call to the /token endpoint as part of the params, it resulted as a double-encoded string.
A facepalm moment, but could happen to anyone.
Getting rid of one of the encoding fixed it for me.
Make sure to also include the scope in the request. Like the following
https://domain.auth.eu-central-1.amazoncognito.com/signup?client_id={}&response_type=token&scope=aws.cognito.signin.user.admin+email+openid+phone+profile&redirect_uri=https://www.google.com/
I my case, the issue came from the ACS URL that was incorrect, but so close that I did not see it. It was redirecting my to a page with this error "An error was encountered with the requested page."
I configured the UserPoolClient via cloudformation and had the AllowOAuthFlows set to implicit, where to work with amplify/cognito I needed that value to be code.
GoogleUserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: GoogleUserPoolIdentityProvider
Properties:
UserPoolId:!Ref MyUserPool
AllowedOAuthFlowsUserPoolClient: true
GenerateSecret: false
CallbackURLs:
- http://localhost:8080
LogoutURLs:
- http://localhost:8080
AllowedOAuthFlows:
- code
AllowedOAuthScopes:
- email
- openid
SupportedIdentityProviders:
- Google
Authorization code grant means you get a code at the end of that redirect and you have to exchange that code for the respective tokens, and the response Type will be code.
And the Implicit grant type is the equivalent of response type token, where in you will get the tokens on the first step itself.
So check if you have the correct response type as per your auth flow set in the cognito console.
In my case, I updated the localhost:port in Allowed callback URLs of cognito app client setting but failed to add localhost:port to Allowed sign-out URLs

AWS: getting limited IAM credentials for DynamoDB without JS SDK

What I want to achieve:
My case described in this article but I do not want to use SDK on my webpage - I want to keep my app simple and fast, but as it turned out - it is hard to find an example that does not use SDK. I stumped with http call from which I need to get limited IAM credentials for DynamoDB. In my investigation of the documentation, I found method GetCredentialsForIdentity, description:
Returns credentials for the provided identity ID. Any provided logins
will be validated against supported login providers. If the token is
for cognito-identity.amazonaws.com, it will be passed through to AWS
Security Token Service with the appropriate role for the token.
But on this page not mentioned url for that endpoint. I tried
https://cognito-idp.us-east-1.amazonaws.com
https://cognito-identity.us-east-1.amazonaws.com
https://<mydomain>.auth.us-east-1.amazoncognito.com
I getting 400 error now, maybe because of incorrect endpoint url. My current code:
fetch('https://cognito-identity.us-east-1.amazonaws.com', {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-amz-json-1.1',
'X-Amz-Target': 'AWSCognitoIdentityService.GetCredentialsForIdentity',
},
'body': '{"IdentityId": "us-east-1:<GUID of the user>"}'
});
1) What is the http endpoint?
2) Am I digging in the right direction?
Correct endpoint is
https://cognito-identity.us-east-1.amazonaws.com
according to this page.
Also, I found that I need to provide not a sub from id_token but firstly GetId of the user.

AWS cognito returning - 'Invalid Login Token. Not a Cognito Token'

I am able to successfully retrieve an identity token for my Custom Authentication Provider (Developer Authentication). This is implemented using the Cognito devauth demo servlet.
This token is returned to a browser running AWS JS SDK. When I make a call to getCredentialsForIdentiy, I receive the 'Invalid Login Token' error.
POST https://cognito-identity.us-west-2.amazonaws.com/ 400 (Bad Request)
app.js:150 Error: Invalid login token. Not a Cognito token.
at constructor.a (aws-sdk-2.58.0.min.js:41)
at constructor.callListeners (aws-sdk-2.58.0.min.js:41)
at constructor.emit (aws-sdk-2.58.0.min.js:41)
at constructor.emitEvent (aws-sdk-2.58.0.min.js:41)
at constructor.e (aws-sdk-2.58.0.min.js:41)
at i.runTo (aws-sdk-2.58.0.min.js:43)
at aws-sdk-2.58.0.min.js:43
at constructor.<anonymous> (aws-sdk-2.58.0.min.js:41)
at constructor.<anonymous> (aws-sdk-2.58.0.min.js:41)
at constructor.callListeners (aws-sdk-2.58.0.min.js:41) "NotAuthorizedException: Invalid login token. Not a Cognito token.
at constructor.a (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:615)
at constructor.callListeners (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:30513)
at constructor.emit (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:30224)
at constructor.emitEvent (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:16590)
at constructor.e (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:12285)
at i.runTo (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:43:7277)
at https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:43:7482
at constructor.<anonymous> (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:12495)
at constructor.<anonymous> (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:16645)
at constructor.callListeners (https://sdk.amazonaws.com/js/aws-sdk-2.58.0.min.js:41:30619)"
I am passing to getCredentialsForIdentity the following params.
identityId: <returned from servlet in region:guid format>
customRoleArn: <that maps to authenticated role>
Logins: <cognito-identity.amazonaws.com = token returned by congito>
I notice that new congnito identity was created by using id browser. So, the token interaction with the servlet seems correct. But the token is being rejected.
What am I missing here? How can I troubleshoot it further?
Edit:
Browser client: javascript sdk, version 2.58.
Essentially writing a JS client to this demo . The modifications is only to get token as part of login call itself. The generated token corresponds to a role that only has IOT client access. (I wonder if that policy needs to expand). Eventually the users will be validated against an internal ID store instead of this demo.
Edit 2:
Instead of calling getCredentialsForIdentiy against Cognito Service, I invoked assumeRoleWithWebIdentity against STS and that worked. The use of Basic Flow instead of Enhanced flow here. . Really not sure why enhanced flow did not work but will take the basic flow approach for now.
So, the AWS documentation here is either inaccurate or documents an inefficient scenario.
Using JavaScript Cognito API, my Developer Provider returned token could not be used with GetCredentialsForIdentity against Cognito API. (this is what prompted me to ask this question).
However, I can use the same token against STS API and invoke AssumeRoleWithWebIdentity which essentially returns the same response parameters as GetCredentialsForIdentity above. This is the approach I took.
Basic Auth Flow documentation in the first link seems inaccurate. Because it lists 'get credentials' and 'assume role' as required steps but only one is actually needed. Also, from a browser, this is an API call out to an Amazon service. So using Enhanced Flow instead of Basic does not seem like an advantage if it worked.
I had this same problem, using V3 of the Javascript SDK, and struggled for a very long time! The currently accepted answer worked for me - using the basic authflow instead of enhanced.
I am hoping to supplement the above with some information that other present-day readers using V3 of the SDK may find useful:
On the server, use GetOpenIdTokenForDeveloperIdentityCommand from #aws-sdk/client-cognito-identity to generate the token and send it to the client.
On the client, I was unable to successfully generate credentials with any of the following:
fromCognitoIdentity, from #aws-sdk/credential-providers
GetCredentialsForIdentityCommand, from #aws-sdk/client-cognito-identity
GetCredentialsForIdentityPoolCommand, from #aws-sdk/client-cognito-identity
The following did work, using #aws-sdk/client-sts:
const { STSClient, AssumeRoleWithWebIdentityCommand } = require("#aws-sdk/client-sts")
const client = new STSClient({region: cognitoRegion})
const assumeRoleCommand = new AssumeRoleWithWebIdentityCommand({
WebIdentityToken: openIdToken, // the token your server sends to the client
RoleArn: authRoleARN, // the ARN of the role you set up on AWS Cognito / IAM
RoleSessionName: userId // See note below
})
const credentialsResponse = await client.send(assumeRoleCommand)
// the client apparently expects slightly different property names than the received credentials object
const credentials = {
accessKeyId: credentialsResponse.Credentials.AccessKeyId,
expiration: credentialsResponse.Credentials.Expiration,
secretAccessKey: credentialsResponse.Credentials.SecretAccessKey,
sessionToken: credentialsResponse.Credentials.SessionToken
}
// Example using SES, you may be using different services
const sesClient = new awsSes.SESClient({
apiVersion: "2010-12-01", // yours may be different
region: "us-east-1", // yours may be different
credentials: credentials
})
Note: The AssumeRoleWithWebIdentityCommand docs discuss what you should use for the RoleSessionName.