<下記は「試みたものの、結果としてはうまくいなかった例」になりますが、試行の過程として残しています。修正後の内容は、後述の「追記2」を参照してください>
また、以下は、Amazon SP-API上での検証ではなく、他の一般的なOAuth2.0アプリケーションでの検証になります。
コピー元のスクリプトに doGet() を実装してWebアプリとしてデプロイし、コピー先からコピー元に対して、認証に必要な処理を呼び出せるようにしてはいかがでしょうか。
- アクセストークンを使うときは、その都度、getToken() を呼び出してください。
- セラーセントラルのAPI登録画面でのリダイレクトURIは、コピー元のcallbackから変更する必要はありません。
- 複数ユーザーの同時アクセスに対応するためgetService関数の中でロックを設定しています。( 参照 )
※実際にはその他の処理でも複数ユーザーによる競合が想定される部分には、ロックをかける必要があるかもしれません。
DEPLOY_URLを追加しているほか、showDialog() も一部修正しています。
js
1AUTH_ENDPOINT = 'https://XXX';
2他...
3略
4
5// ☆追加:コピー元のデプロイURL
6const DEPLOY_URL = 'https://script.google.com/macros/s/***/exec';
7
8// OAuth2ライブラリ使用
9function getService() {
10 略
11 .setLock(LockService.getUserLock()); // 複数ユーザーが同時にトークンをリフレッシュしようとする可能性があるためロックを設定。
12}
13
1415 * 最初のOAuth2承認の際のコールバック処理
16
17function authCallback(request) {
18 略
19}
20
2122 * 認証用ダイアログ(html)表示
23 * セラーセントラルの認証URLへリンクさせる
24
25function showDialog() {
26 const respjson = UrlFetchApp.fetch(DEPLOY_URL + '?authmode=true').getContentText();
27 const response = JSON.parse(respjson);
28 const url = response.authorizationUrl;
29 const tag_text = `<button type="button" class="btn btn-primary" onclick="window.open('${url}')">セラーセントラルで認証する</button>`;
30 const html = HtmlService.createHtmlOutput(tag_text);
31 SpreadsheetApp.getUi().showModalDialog(html, "認証ダイアログ");
32}
33
3435 * アクセストークンを取得する。
3637 * 認可済の場合:アクセストークン
38 * 未認可の場合:'UNAUTHORIZED'
39
40function getToken() {
41 const respjson = UrlFetchApp.fetch(DEPLOY_URL).getContentText();
42 const response = JSON.parse(respjson);
43 if (response.authorized) {
44 return response.token;
45 } else {
46 return "UNAUTHORIZED"
47 }
48}
49
5051 * クライアントからGETリクエストを受けた時に呼ばれる。
5253 * 呼び出し元に「authmode」パラメータが存在する場合
5455 * 呼び出し元に「authmode」パラメータが存在せず、認可済みの場合
5657 * 呼び出し元に「authmode」パラメータが存在せず、未認可の場合
5859
60function doGet(e) {
61 if (e.parameter.authmode)
62 return toJson({ authorizationUrl: getService.getAuthorizationUrl() })
63
64 if (getService.hasAccess())
65 return toJson({ authorized: true, token: getService.getAccessToken() })
66
67 return toJson({ authorized: false })
68}
69
70function toJson(content) {
71 const output = ContentService.createTextOutput();
72 output.setMimeType(ContentService.MimeType.JSON);
73 output.setContent(JSON.stringify(content));
74 return output;
75}
76
77/*個別処理を行う場合に必要となるアクセストークンは、getToken() で取得する。
78 * 下記は注文を取得する例。
79*/
80function getOrders(){
81 const access_token = getToken(); // アクセストークンの取得
82 if (access_token==='UNAUTHORIZED) {
83 console.log('認証が完了していないためアクセストークンを取得できません。' +
84 管理者に連絡して、認証・認可ファイルで再認証を行ってもらってください。');
85 }
86 略
87
88 const end_point = 'https://sellingpartnerapi-eu.amazon.com';
89 略
90 const options = {
91 'method': 'GET',
92 'headers': {
93 'x-amz-access-token': access_token,
94 'x-amz-date': isoDate,
95 'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
96 }
97 }
98 const orders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
99 console.log(orders);
100}
説明
上記は、実質的には下記の2つのコードを合体したものになります。
① original.gs
・認証・認可の処理を行う専用のファイル。
・アクセストークンを保管するとともに、②からの要求に応じて authorizationUrl やアクセストークンを渡す。
js
1AUTH_ENDPOINT = 'https://XXX';
2TOKEN_ENDPOINT = 'https://YYY';
3CLIENT_ID = 'AAA';
4CLIENT_SECRET = 'BBB';
5APP_ID = 'CCC';
6
7// OAuth2ライブラリ使用
8function getService() {
9 略
10}
11
12function authCallback(request) {
13 略
14}
15
16function doGet(e) {
17 if (e.parameter.authmode)
18 return toJson({ authorizationUrl: getService.getAuthorizationUrl() })
19
20 if (getService.hasAccess())
21 return toJson({ authorized: true, token: getService.getAccessToken() })
22
23 return toJson({ authorized: false })
24}
25
26function toJson(content) {
27 略
28}
② copy.gs
・実際の処理を行うファイル
js
1const DEPLOY_URL = 'https://script.google.com/macros/s/***/exec'; // ①のデプロイURL
2
34 * 認証用ダイアログ(html)表示
5 * セラーセントラルの認証URLへリンクさせる
6
7function showDialog() {
8 const respjson = UrlFetchApp.fetch(DEPLOY_URL + '?authmode=true').getContentText();
9 const response = JSON.parse(respjson);
10 const url = response.authorizationUrl;
11 const tag_text = `<button type="button" class="btn btn-primary" onclick="window.open('${url}')">セラーセントラルで認証する</button>`;
12 const html = HtmlService.createHtmlOutput(tag_text);
13 SpreadsheetApp.getUi().showModalDialog(html, "認証ダイアログ");
14}
15
1617 * アクセストークンを取得する。
18
19function getToken() {
20 const respjson = UrlFetchApp.fetch(DEPLOY_URL).getContentText();
21 const response = JSON.parse(respjson);
22 if (response.authorized) {
23 return response.token;
24 } else {
25 return "UNAUTHORIZED"
26 }
27}
28
29/*個別処理を行う場合に必要となるアクセストークンは、getToken() で取得する。
30 * 下記は注文を取得する例。
31*/
32function getOrders(){
33 const access_token = getToken(); // アクセストークンの取得
34 if (access_token==='UNAUTHORIZED) {
35 console.log('認証が完了していないためアクセストークンを取得できません。' +
36 管理者に連絡して、認証・認可ファイルで再認証を行ってもらってください。');
37 }
38 略
39
40 const end_point = 'https://sellingpartnerapi-eu.amazon.com';
41 略
42 const options = {
43 'method': 'GET',
44 'headers': {
45 'x-amz-access-token': access_token,
46 'x-amz-date': isoDate,
47 'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
48 }
49 }
50 const orders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
51 console.log(orders);
52}
上記のように2つに分けた場合は、①単独では動かず、②の copy.js を複製して使用することを想定しています。
(冒頭に記載したコードは、①と②を合体したものですので、単一のファイルだけで動くはずです)
②で認証・認可が必要になる場面で ①を呼び出して認証・認可に必要な処理を肩代わりさせます。
この過程でアクセストークンを ①の中に保管しておきます。
実際に②がいろいろな処理を行う場面でアクセストークンが必要になった場合は、getToken()によって①からアクセストークンを取得する、という考え方です。
追記
他の人にも協力してもらい、いろいろ試しましたが、このスクリプトは下記のような根本的な問題点があるらしいことが分かりました。
- ①originalを所有(作成)しているGoogleアカウント以外のアカウントが、コピーしたファイルから showDialog を実行した場合、認可フローを完了させることができない(トークンが無効または期限が切れていると表示される)
(①[オリジナル]を所有(作成)しているGoogleアカウントであれば ②[コピー]のshowDialogを実行しても問題なく認証フローを完了できる)
これは、オリジナルとコピー両方公開ファイルにしても変わりません。
(おそらくGASまたはOAuth2.0の仕組み上不可避と思われます)
一方、一部ではうまく行っています。
- ①[オリジナル]での認証・認可後、オリジナルを所有(作成)しているアカウント以外のアカウントが ②[コピー]を利用して①[オリジナル]からアクセストークンを受け取り、アクセストークンを利用した処理を行うことは可能。
- ①からもらったアクセストークンが揮発した場合でも、①から自動的に更新後のアクセストークンを取得できる。
したがって、少なくとも最初の認証・認可フローは①を所有しているアカウント(①を作成したアカウント)で行う必要がありそうです。
追記2
結局、運用方針としては、下記のように①と②を分離し、
- ①は開発者/管理者が専有してメンテナンス(最初だけ認証・認可を行ってリフレッシュトークン・アクセストークンをキャッシュしておく)
- 利用者側には②をコピーして使ってもらう(①がアクセストークンを保持している限り②側での認証・認可フローは不要)
というのが現実的かもしれませんね。
crient_secret が書かれたファイルをむやみに渡す必要がなくなるというメリットもあると思います。
①改(認証・認可用ファイル。showDialogを元に戻した)。デプロイし、デプロイURLを②改に記載する。
js
1AUTH_ENDPOINT = 'https://XXX';
2TOKEN_ENDPOINT = 'https://YYY';
3CLIENT_ID = 'AAA';
4CLIENT_SECRET = 'BBB';
5APP_ID = 'CCC';
6
7// OAuth2ライブラリ使用
8function getService() {
9 略
10 .setLock(LockService.getUserLock());
11}
12
13function authCallback(request) {
14 略
15}
16
17function doGet(e) {
18 if (e.parameter.authmode)
19 return toJson({ authorizationUrl: getService.getAuthorizationUrl() })
20
21 if (getService.hasAccess())
22 return toJson({ authorized: true, token: getService.getAccessToken() })
23
24 return toJson({ authorized: false })
25}
26
27function toJson(content) {
28 略
29}
30
3134
35function showDialog(){
36 const service = getService();
37 const url = service.getAuthorizationUrl();
38 const tag_text = `<button type="button" class="btn btn-primary" onclick="window.open('${url}')">セラーセントラルで認証する</button>`;
39 const html = HtmlService.createHtmlOutput(tag_text);
40 SpreadsheetApp.getUi().showModalDialog(html, "認証ダイアログ");
41}
②改
実際の処理を行うファイル。こちらを複製してセラーにつかってもらう。
利用者セラーは認証・認可フローを行わない前提のため、showDialogは①へ移動した。
こちらは特にデプロイする必要はない。
js
1const DEPLOY_URL = 'https://script.google.com/macros/s/***/exec'; // ①のデプロイURL
2
34 * アクセストークンを取得する。
5
6function getToken() {
7 const respjson = UrlFetchApp.fetch(DEPLOY_URL).getContentText();
8 const response = JSON.parse(respjson);
9 if (response.authorized) {
10 return response.token;
11 } else {
12 return "UNAUTHORIZED"
13 }
14}
15
16/*個別処理を行う場合に必要となるアクセストークンは、getToken() で取得する。
17 * 下記は注文を取得する例。
18*/
19function getOrders(){
20 const access_token = getToken(); // アクセストークンの取得
21 if (access_token==='UNAUTHORIZED) {
22 console.log('認証が完了していないためアクセストークンを取得できません。' +
23 管理者に連絡して、認証・認可ファイルで再認証を行ってもらってください。');
24 }
25 略
26
27 const end_point = 'https://sellingpartnerapi-eu.amazon.com';
28 略
29 const options = {
30 'method': 'GET',
31 'headers': {
32 'x-amz-access-token': access_token,
33 'x-amz-date': isoDate,
34 'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
35 }
36 }
37 const orders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
38 console.log(orders);
39}
ただし、現状の単純なコードでは、DEPLOY_URLを第三者に知られてしまうと、その人がアクセストークンを取得して勝手にいろいろできてしまうので、何らかの管理のしくみ(GET時のパラメータに特別なトークンが埋め込まれていないとアクセストークンを発行できないようにする等)を実装することが必要です。
(この点は冒頭に記載した単一ファイルを運用する場合でも同じです。さらに単一ファイルだと認証に必要なclient_id や client_secret まで利用者セラーに知られてしまうのが問題だと思います)