🐷

Amazon Selling-Partner APIを叩くまで by JS

2021/09/04に公開

はじめに

業務にてAmazonのSelling-Partner-APIについて調査していたのですが、認証周りが複雑なのとネット上の情報が少なく、javascriptで APIを叩けるまで、恥ずかしながらものすごく時間がかかってしまいました。そのため、この記事ではjavascript で認証を乗り越え、実際にAPIを叩くところまで紹介していきます。
login with amazon(LWA) という自身のサイトからAmazonのログイン画面にリダイレクトさせる認可サービスがありますが、今回はコードのみで認証を解いていくので使用しません
※別で記事にする予定です!

https://github.com/yuuki008/auth_sp-apiこのリポジトリにて、すぐAPIを叩けるので、とりあえず叩いてみたい方は、どーぞ!

Selling-Partner APIとは

公式ドキュメント抜粋

出品パートナーAPIはRESETベースのAPIで、これにより出品者は出品、注文、支払い、レポートなどのデータにプログラムからアクセスすることができます。出品パートナーAPIを使用するアプリケーションは、販売効率を高め、必要な労働量を削減し、顧客への応答時間を短縮し、出品者がビジネスを成長させるのに役立ちます。出品パートナーAPIは、AmazonマーケットプレイスWebサービス(Amazon MWS)の機能に基づいて構築されており、開発者や出品者パートナーの使いやすさとセキュリティを向上させる機能を提供します。

商品の注文情報、配送情報、決済情報etc...を取得できるようになるようです

あらかじめ

公式ドキュメント出品パートナーAPIアプリケーションの登録のセクションが完了していることが前提として進めていきます!

必要なもの

  1. AWSのアカウント(accesskey, secretkey)
  2. AWSのIAMロールのARN
  3. セラーセントラルで登録したアプリ(client_id, client_secret, refresh_token)

実装

https://storage.googleapis.com/zenn-user-upload/723d5c9cbb77193634ed7e79.png

上記の写真だとシンプルでわかりやすいのですが、いくつか難関があります。

  1. access_tokenを取得
  2. AWS STS(security token service)
  3. AWS Signature(AWS著名)
  4. APIを叩く

1. access_tokenを取得

const getAccessToken = async () => {
  const response = await fetch('https://api.amazon.com/auth/o2/token', {
    method: 'post',
    headers: {
      "Content-Type": "application/json",
      "Content-Length": 524
    },
    body: JSON.stringify({
      grant_type: 'refresh_token',
      client_id: YOUR_APP_CLIENT_ID,
      client_secret: YOUR_APP_CLIENT_SECRET,
      refresh_token: YOUR_APP_REFRESH_TOKEN
    })
  }).then(res => res.json())
  return response
}

client_id, client_secret, refresh_tokenのパラメーターにそれぞれ任意の値を入れてください!
成功レスポンス

{
  access_token: 'Atza|IwEBI...',
  refresh_token: 'Atzr|IwEBI...',
  token_type: 'bearer',
  expires_in: 3600
}

2. AWS STS(security token service)

こちらから抜粋

AWS STSとはAWS Security Token Serviceの略でAWSリソースへアクセスするための一時的なセキュリティ認証情報を提供するためのサービスです。
一時的なセキュリティキーを作成することで、信頼するユーザーへAWSリソースへのアクセスを許可することができます。

const AWS = require('aws-sdk');
const SESConfig = {
  credentials: new AWS.Credentials(YOUR_AWS_ACCESS_KEY, YOUR_AWS_SECRET_KEY),
  region: 'us-west-2',
}
const STS = new AWS.STS();

const createTemporaryAWSCredentials = async () => {
  STS.config.update(SESConfig)
  const response = await STS.assumeRole({
    RoleArn: YOUR_ROLE_ARN,
    RoleSessionName: YOUR_ROLE_ARN_NAME,
  }).promise();
  return response.Credentials
};

  • YOUR_AWS_ACCESS_KEY
  • YOUR_AWS_SECRET_KEY
  • YOUR_ROLE_ARN
  • YOUR_ROLE_ARN_NAME (arnのスラッシュから右)

上記は、任意の値を入れてください
あとaws-sdkを使うので、インストールを、、、

$ npm install aws-sdk

awsのid,secretで値を更新し、STSのassumeRole関数に作成したRoleの値を渡すと、以下のような値が返ってきます!

レスポンス

{
  AccessKeyId: 'AS...',
  SecretAccessKey: '/aJKD...',
  SessionToken: 'FwoGZXIv...',
  Expiration: 2021-09-03T14:13:44.000Z
}

3. AWS Signature(AWS著名)

こちらから抜粋

AWSのAPIを利用するときに、クライアント側からI AMユーザであることを示す為に送付する署名のことです。HTTPリクエストのAuthorizationヘッダーの値に載せて送ります。それを受け取ったAWS側では、同じロジックに従って署名を作り、送られてきた署名と一致するかを確かめることで認証を行います。

以下のコードで Authorization headerを作成します。複雑な処理なのでコピペでいいかと、、、

const qs = require('qs');
const crypto = require('crypto-js');
const fetch = require('node-fetch')

const getAuthorizationHeader = (access_token, role_credentials, req_params) => {
  req_params.query = sortQuery(req_params.query);
  let iso_date = createUTCISODate();

  let encoded_query_string = constructEncodedQueryString(req_params.query);
  let canonical_request = constructCanonicalRequestForAPI(access_token, req_params, encoded_query_string, iso_date);
  let string_to_sign = constructStringToSign('us-west-2', 'execute-api', canonical_request, iso_date);
  let signature = constructSignature('us-west-2', 'execute-api', string_to_sign, role_credentials.secret, iso_date);

  return {
    method:req_params.method,
    url: constructURL(req_params, encoded_query_string),
    body:req_params.body ? JSON.stringify(req_params.body) : null,
    headers: {
      'Authorization':'AWS4-HMAC-SHA256 Credential=' + role_credentials.id + '/' + iso_date.short + '/' + 'us-west-2' + '/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date, Signature=' + signature,
      'Content-Type': 'application/json; charset=utf-8',
      'host': "sellingpartnerapi-fe.amazon.com",
      'x-amz-access-token':access_token,
      'x-amz-security-token':role_credentials.security_token,
      'x-amz-date': iso_date.full
    }
  }
}

const constructURL = (req_params, encoded_query_string) => {
  let url = 'https://sellingpartnerapi-fe.amazon.com' + req_params.api_path;
  if (encoded_query_string !== ''){
    url += '?' + encoded_query_string;
  }
  return url;
}

const sortQuery = (query) => {
  if (query && Object.keys(query).length){
    return Object.keys(query).sort().reduce((r, k) => (r[k] = query[k], r), {});
  }
  return;
}

const createUTCISODate = () => {
  let iso_date = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '');
  return {
    short:iso_date.substr(0,8),
    full:iso_date
  };
}

const constructEncodedQueryString = (query) => {
  if (query){
    query = qs.stringify(query, {arrayFormat:'comma'});
    let encoded_query_obj = {};
    let query_params = query.split('&');
    query_params.map((query_param) => {
      let param_key_value = query_param.split('=');
      encoded_query_obj[param_key_value[0]] = param_key_value[1];
    });
    encoded_query_obj = sortQuery(encoded_query_obj);
    let encoded_query_arr = [];
    for (let key in encoded_query_obj){
      encoded_query_arr.push(key + '=' + encoded_query_obj[key]);
    }
    if (encoded_query_arr.length){
      return encoded_query_arr.join('&');
    }
  }
  return '';
}

const constructCanonicalRequestForAPI = (access_token, params, encoded_query_string, iso_date) => {
  let canonical = [];
  canonical.push(params.method);
  canonical.push(params.api_path);
  canonical.push(encoded_query_string);
  canonical.push('host:sellingpartnerapi-fe.amazon.com');
  canonical.push('x-amz-access-token:' + access_token);
  canonical.push('x-amz-date:' + iso_date.full);
  canonical.push('');
  canonical.push('host;x-amz-access-token;x-amz-date');
  canonical.push(crypto.SHA256(params.body ? JSON.stringify(params.body) : ''));
  return canonical.join('\n');
}

const constructStringToSign = (region, action_type, canonical_request, iso_date) => {
  let string_to_sign = [];
  string_to_sign.push('AWS4-HMAC-SHA256')
  string_to_sign.push(iso_date.full);
  string_to_sign.push(iso_date.short + '/' + region + '/' + action_type + '/aws4_request');
  string_to_sign.push(crypto.SHA256(canonical_request));
  return string_to_sign.join('\n');
}

const constructSignature = (region, action_type, string_to_sign, secret, iso_date) => {
  let signature = crypto.HmacSHA256(iso_date.short, 'AWS4' + secret);
  signature = crypto.HmacSHA256(region, signature);
  signature = crypto.HmacSHA256(action_type, signature);
  signature = crypto.HmacSHA256('aws4_request', signature);
  return crypto.HmacSHA256(string_to_sign, signature).toString(crypto.enc.Hex);
}

AWS Signature(著名)に必要な値

  1. APIを叩く時のクエリパラメーターとbody要素
  2. AWS(account_key, secret_key, security_token)
  3. リージョン(us-west-2)、サービス名(execute-api)
  4. access_token

上記の値を元に著名を作成しています。
AWS側でも著名が作成され、一致すると認証が通ります!!

作成した関数ですが、以下のように呼び出してください

const req_params = {
  api_path: "/reports/2020-09-04/reports",
  method: "GET",
  query: {reportTypes: ["GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE"]},
}

const demoFunc = async () => {
  // 1で作成した関数
  const auth_token = await getAccessToken()
  // 2で作成した関数
  const credentials = await createTemporaryAWSCredentials()
  const role_credentials = {
    id: credentials.AccessKeyId,
    secret: credentials.SecretAccessKey,
    security_token: credentials.SessionToken
  }
  // 3で作成した関数
  let auth = await getAuthorizationHeader(auth_token.access_token, role_credentials, req_params);
}
{
  method: 'GET',
  url: 'https://sellingpartnerapi-fe.amazon.com/reports/2020-09-04/reports?reportTypes=GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE',
  body: null,
  headers: {
    Authorization: 'AWS4-HMAC-SHA256 Credential=AS.../20210904/us-west-2/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date, Signature=442...',
    'Content-Type': 'application/json; charset=utf-8',
    host: 'sellingpartnerapi-fe.amazon.com',
    'x-amz-access-token': 'Atza|Iw...',
    'x-amz-security-token': 'FwoGZXI...'
    'x-amz-date': '20210904T114649Z'
  }
}

APIを叩く

getAuthorizationHeader のレスポンスは、上記のようになるのでこれを使い、いよいよAPIを叩きます!!

// 3で作成した関数
let auth = await getAuthorizationHeader(auth_token.access_token, role_credentials, req_params);
const response = await fetch(auth.url, {
  method: auth.method,
  headers: auth.headers
}).then(res => res.json()).catch(err => console.log(err))
console.log(response)
{
  payload: [
    {
      reportType: 'GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE',
      processingEndTime: '2021-08-31T05:07:49+00:00',
      processingStatus: 'DONE',
      marketplaceIds: [Array],
      reportDocumentId: '...',
      reportId: '...',
      dataEndTime: '2021-08-31T04:26:51+00:00',
      createdTime: '2021-08-31T05:07:49+00:00',
      processingStartTime: '2021-08-31T05:07:49+00:00',
      dataStartTime: '2021-08-17T04:26:52+00:00'
    },
    ...
  ]
}

上記のようなレスポンスが帰ってくれば、成功です!

終わりに

お疲れ様でした!
叩けるまでが長い、認証が難しい、情報が少ない(日本語特に)で自分自身ものすごく苦労しましたが、これを読んだ方がスムーズに認証を乗り越えるようになってくれれば嬉しいです!
コードはこちらからどーぞ!
https://github.com/yuuki008/auth_sp-api

ここまで説明しましたが、amazon-sp-apiという便利なライブラリを使うと、面倒なAWSの著名やaccess_tokenの交換などが不要で比較的簡単にAPIを叩くことができるのでおすすめです!!

参考記事

AWS STSとは?IAMユーザーとの違いと使い方について紹介!
Amazon 出品パートナーAPI開発者ガイド - Qiita

Discussion

ログインするとコメントできます