質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

OAuth 2.0

OAuth 2.0(Open Authorization 2.0)は、APIを通して保護されたリソース(サードパーティのアプリケーション)へアクセスする為のオープンプロトコルです。

Q&A

1回答

3247閲覧

複製したGASで、AmazonSPAPIの認可フローを完了する

thimone

総合スコア1

Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

OAuth 2.0

OAuth 2.0(Open Authorization 2.0)は、APIを通して保護されたリソース(サードパーティのアプリケーション)へアクセスする為のオープンプロトコルです。

0グッド

1クリップ

投稿2022/04/29 07:31

編集2022/04/29 21:36

前提

GASでAmazonSP-APIと連携している、スプレッドシートがあります。
これを、スプレッドシートをコピーしてそのまま配布しても同様に動作するようにしたいです。

しかし、SPAPIの認証方法(利用者セラーからアプリケーションへの認可)で詰まっている状態で、
認証ワークフローからすると、下記2点により難しいのではないかと考えています。
①複製後のスプレッドシートは最初Webアプリとしてデプロイされていないため、認証完了後のリダイレクトを受けられない
②リダイレクトURIが用意できたとしても、それをセラーセントラルのアプリケーション登録画面から設定しなければならない
→①は手順追加で対応できますが、②は運用上現実的ではない状態です

実現したいこと

コピー後も認証フローを完了させられるようなスプレッドシートにしたいので、
そのための仕組みづくりが可能であれば教えていただきたいです。
(セラーセントラルのアプリ登録でリダイレクトURIはこれにする、リダイレクト用アプリを用意する、設定するstateパラメータはこれにする等)
コピー後スプレッドシートに対しての個別設定は、Webアプリデプロイの操作のみ許容できます。

発生している問題・エラーメッセージ

単純にスプレッドシートをコピーしたもので認証しようとすると、
「SP-APIを承認」画面のConfirm後に下記メッセージが表示されます。

問題が発生しました 無効なリダイレクトURLを受信しました。使用されたリダイレクトURLを確認してください。 Amazonテクニカルサポートに連絡して、以下の情報をお知らせください。 App ID: amzn1.sp.solution.XXX Error Code: MD5101

該当のソースコード

javascript

1AUTH_ENDPOINT = 'https://XXX'; 2TOKEN_ENDPOINT = 'https://YYY'; 3CLIENT_ID = 'AAA'; 4CLIENT_SECRET = 'BBB'; 5APP_ID = 'CCC'; 6 7// OAuth2ライブラリ使用 8function getService() { 9 // OAuth 2.0承認サービスの生成 10 return OAuth2.createService('spapi') 11 12 .setAuthorizationBaseUrl(AUTH_ENDPOINT) // 認可エンドポイント 13 .setTokenUrl(TOKEN_ENDPOINT) // トークンエンドポイント 14 15 .setClientId(CLIENT_ID) // OAuthクライアントID 16 .setClientSecret(CLIENT_SECRET) // OAuthクライアントシークレット 17 18 .setParam('application_id', APP_ID) 19 .setParam('version', 'beta') 20 21 // OAuth2.0 承認後のコールバック関数名の設定 22 .setCallbackFunction('authCallback') 23 24 // 承認トークンを維持するプロパティーストアの設定 25 .setPropertyStore(PropertiesService.getUserProperties()) 26} 27 28/** 29 * 最初のOAuth2承認の際のコールバック処理 30 */ 31function authCallback(request) { 32 var service = getService(); 33 // トークン取得用にパラメータを追加 34 request.parameter['code'] = request.parameter.spapi_oauth_code; 35 36 var authorized = service.handleCallback(request); 37 if (authorized) { 38 service.getAccessToken(); 39 return HtmlService.createHtmlOutput(`認証に成功しました。`); 40 } 41 return HtmlService.createHtmlOutput('認証に失敗しました。処理をやり直してください。'); 42} 43 44/** 45 * 認証用ダイアログ(html)表示 46 * セラーセントラルの認証URLへリンクさせる 47 */ 48function showDialog(){ 49 const service = getService(); 50 const url = service.getAuthorizationUrl(); 51 const tag_text = `<button type="button" class="btn btn-primary" onclick="window.open('${url}')">セラーセントラルで認証する</button>`; 52 const html = HtmlService.createHtmlOutput(tag_text); 53 SpreadsheetApp.getUi().showModalDialog(html, "認証ダイアログ"); 54}
//セラーセントラルのアプリ登録画面「OAuthリダイレクトURI」 https://script.google.com/macros/d/{コピー元スプレッドシートのスクリプトID}/usercallback

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2022/04/29 09:35

・AUTH_ENDPOINT、TOKEN_ENDPOINT、CLIENT_ID、CLIENT_SECRET、APP_ID はソースコードに直書きされている、という理解でよろしいでしょうか。 ・コピー元ファイルとコピー先ファイルで、CLIENT_ID、CLIENT_SECRET等は同一の値のままで運用する、という理解でよろしいでしょうか?
thimone

2022/04/29 13:13

>・AUTH_ENDPOINT、TOKEN_ENDPOINT、CLIENT_ID、CLIENT_SECRET、APP_ID はソースコードに直書きされている、という理解でよろしいでしょうか。  →はい、コードに直書きされています。 >・コピー元ファイルとコピー先ファイルで、CLIENT_ID、CLIENT_SECRET等は同一の値のままで運用する、という理解でよろしいでしょうか?  →はい、同一の値のまま運用します。 よろしくお願いいたします。
guest

回答1

0

<下記は「試みたものの、結果としてはうまくいなかった例」になりますが、試行の過程として残しています。修正後の内容は、後述の「追記2」を参照してください>
また、以下は、Amazon SP-API上での検証ではなく、他の一般的なOAuth2.0アプリケーションでの検証になります。


コピー元のスクリプトに doGet() を実装してWebアプリとしてデプロイし、コピー先からコピー元に対して、認証に必要な処理を呼び出せるようにしてはいかがでしょうか。

  • DEPLOY_URL は、一度コピー元をデプロイして確定してしまえば、変更がない限りコピー先で変更する必要はありません。(コピー先ファイルごとのデプロイURLに変える必要はありません。)

  • 逆に、コピー元のデプロイURLが変わってしまった場合は、コピー先ファイルに記載されているDEPLOY_URLを全部そのURLに修正しなければなりません。

 

  • アクセストークンを使うときは、その都度、getToken() を呼び出してください

 

  • セラーセントラルのAPI登録画面でのリダイレクトURIは、コピー元のcallbackから変更する必要はありません。

 

  • 複数ユーザーの同時アクセスに対応するためgetService関数の中でロックを設定しています。( 参照 )

※実際にはその他の処理でも複数ユーザーによる競合が想定される部分には、ロックをかける必要があるかもしれません。
DEPLOY_URLを追加しているほか、showDialog() も一部修正しています。

js

1AUTH_ENDPOINT = 'https://XXX'; 2... 34 5// ☆追加:コピー元のデプロイURL 6const DEPLOY_URL = 'https://script.google.com/macros/s/***/exec'; 7 8// OAuth2ライブラリ使用 9function getService() { 10 略 11 .setLock(LockService.getUserLock()); // 複数ユーザーが同時にトークンをリフレッシュしようとする可能性があるためロックを設定。 12} 13 14/** 15 * 最初のOAuth2承認の際のコールバック処理 16 */ 17function authCallback(request) { 18 略 19} 20 21/** 22 * 認証用ダイアログ(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 34/** 35 * アクセストークンを取得する。 36 * @return : string 37 * 認可済の場合:アクセストークン 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 50/** 51 * クライアントからGETリクエストを受けた時に呼ばれる。 52 * @return : JSON string 53 * 呼び出し元に「authmode」パラメータが存在する場合 54 *  {"authorizationUrl : 認可URL} 55 * 呼び出し元に「authmode」パラメータが存在せず、認可済みの場合 56 *   {"anthorized" : true, "token" : アクセストークン} 57 * 呼び出し元に「authmode」パラメータが存在せず、未認可の場合 58 * {"authorized" : false} 59 */ 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 } 8687 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 3/** 4 * 認証用ダイアログ(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 16/** 17 * アクセストークンを取得する。 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 } 3839 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 31/** 32 * 認証用ダイアログ(html)表示 33 * セラーセントラルの認証URLへリンクさせる 34 */ 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 3/** 4 * アクセストークンを取得する。 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 } 2526 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 まで利用者セラーに知られてしまうのが問題だと思います)

投稿2022/04/30 10:28

編集2022/05/03 20:10
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

thimone

2022/05/03 16:48

丁寧な説明に加え、実装と検証までしていただき、非常にありがたいです。 追記2について疑問点なのですが、 ②改のスプレッドシートで認可フローを実行しないとしているため、 以下2つのどちらかの運用となるのかなと思っています。どちらでしょうか。 (1)①改で所有者のユーザが最初に認可フローを完了させる。   ②改を利用する各ユーザは、①改で認可されたセラーアカウント1つのみ使用しAPIを利用する。 (2)所有者でないユーザでも、①改を(②を経由せず)直接実行すれば認可できる?   ②改を利用する各ユーザは、①改を利用して各自セラーアカウントを認可し、   各自のセラーアカウントでAPIを利用する。 「専有」等書かれてますので(1)なのだろうと思っていますが、 気になっているのは利用者ごとに各自のセラーアカウントを利用してAPIを叩けるか、という点になります。
退会済みユーザー

退会済みユーザー

2022/05/03 20:13

後付けで申し訳ございませんが、上記は、Amazon SP-APIでの検証ではなく、他の一般的なOAuth2.0のアプリケーション(具体的にはBASE API)での検証になっています。(Amazon SP-APIの開発者アカウントや出品アカウントを持っていないため) したがって、各自のセラーアカウントを利用してAPIを叩けるかについては、実際にやってみていただかないことにはわからないと思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問