TL;DR
とある目的でZendeskのAPIを叩くアプリにOauth2を使おうとしたら、callback先のアプリのURLがdynamicで詰んだ。
しかたなくcallbackのURLにproxyを挟んでURLを強制的に一意にし、Stateパラメータ使ってdynamic URLの情報をproxyに渡して、proxyでparameterを読んで加工して正しいcallback URLへリダイレクトということをやった。
callback先のアプリ = Google App Script
アプリとは言ったものの、実は今回Oauth2を使いたかったのはGoogle App Script(GAS)です。※Googleが公開している素晴らしいOauth2用のLibraryがあるので、GASからOauth2で外部サービスと繋ぎたい場合は迷わず使いましょう!
Zendeskの基本的なOauth2のフロー
例えばGoogle App Scriptのコードが紐付いたSpreadsheetが1つあるとして、その1つだけが対象であればcallback URLは変わらないので問題ないですが、今回のケースではユーザがテンプレートのSpreadsheetを各自Copyして使う想定がされていて、Copyした先のSpreadsheetのcallback URLは変わってしまいます(callback URLがscript IDを含んでいるため)。
GASのcallback URL
https://script.google.com/macros/d/{SCRIPT ID}/usercallback
先に説明したように、callback URLは{SCRIPT ID}
を含んでいます。
ZendeskにOAuth clientを登録する
この時、当然事前にcallback URLを登録しておく必要がありますが、未来の{SCRIPT ID}
を知らないのでこの方法は使えません。
proxyを挟んでみる(Fastly as a reverse proxy)
ここで普通に考えると思いつくのは間にproxyを挟んでなんとかしよう、という結論に至りますが、如何せんこれだけのためにproxyをわざわざ用意したくない。面倒も見たくない。じゃあどうするか、とりあえずFastlyをreverse proxy代わりにして、zendeskからcallbackのリクエストを受けた時にそいつを正しいGASのcallback URLにリダイレクトさせてあげればいいのでは、という事になりました。
Fastlyにはsynthetic
レスポンスというカスタムレスポンスを返す事ができる機能があり、これを使うとoriginレスで任意のレスポンスをclientに返すことができます。
今回の要件だと、clientに正しいcallback URLへの301 リダイレクトを返したいだけなので、まさにピッタリです。
Oauth2 State parameter に思いの丈を綴る
先の方法を実現する上で唯一残る問題は、どうやって最終的なGASのcallback URLをZendesk, ないしはproxyのFastlyへ伝えるか、という点です。堂々とオススメできる方法ではもちろんありませんが、Oauth2でCSRF攻撃を防ぐためのOPTIONALなパラメータとして定義されているState
に一時的に任意の値を付け加えて、それをproxy上で読み込み、切り取り、元のstateに修正してリダイレクトしてしまえば原理上は問題ないはず、です。
Stateパラメータについては下記のリンクなどが参考になると思います。
一番はじめに紹介したGASのOauth2用Libraryのコードを見ると、Stateパラメータを追加しているのは下記の部分です。
https://github.com/googlesamples/apps-script-oauth2/blob/24/dist/OAuth2.gs#L340-L352
これを、思い切ってこうしちゃいます。
var redirectUri = getRedirectUri(this.scriptId_);
var state = eval('Script' + 'App').newStateToken()
.withMethod(this.callbackFunctionName_)
.withArgument('serviceName', this.serviceName_)
.withTimeout(3600)
.createToken();
var params = {
client_id: this.clientId_,
response_type: 'code',
redirect_uri: redirectUri,
// add script_id to 'state'
state: state + '&' + this.scriptId_
};
params = _.extend(params, this.params_);
こうすることで、zendeskに渡されるState
パラメータは、
state=${generated_original_state_strings}&{gas_script_id}
となります。これをFastlyのVCLで読み取ってうまいことします。(※正確には送られる際にurlencode
されるので、&
は%26
に置き換わります)
Fastly のVCLでparameterをいじる
Fastlyで行う処理は至ってシンプルです。originは必要ないので、適当に127.0.0.1
などをbackendとして登録し、ドメインを設定します(ドメイン持ってなくても無料TLSを使えば問題ないです)。次に、vcl_recv
とvcl_error
にVCLスニペットを使ってコードを足していきます。
vcl_recv
# Snippet callback
if (req.url ~ "^/usercallback") {
# state parameter 以外の全てのクエリを含めたリクエストURLをいったん保存
set req.http.org-url = querystring.filter(req.url, "state");
# 送られてきたクエリからstate parameterの値だけを抜き出す
set req.http.oauth2-state = subfield(req.url.qs, "state", "&");
# 正規表現を使って元のstate値と付け足したstate値を切り出す
if (req.http.oauth2-state ~ "^(.*)%2526(.*)$") {
set req.http.x-org-state = re.group.1;
set req.http.x-gas-url = re.group.2;
}
# synthetic レスポンスを返すために vcl_error へジャンプ
error 900;
}
vcl_error
# Snippet callback_error
if (obj.status == 900) {
set obj.status = 301;
set obj.response = "Moved Permanently";
# vcl_recv で切り出した値を使って Location ヘッダを組み立てる
set obj.http.Location = "https://script.google.com/macros/d/" req.http.x-gas-url req.http.org-url "&state=" req.http.x-org-state;
synthetic {""};
return (deliver);
}
必要なコードは最低限以上になります。
最後に、zendeskのOauth clientのredirect urlに、Fastlyに設定したドメインを登録します。
https://<name>.global.ssl.fastly.net/usercallback
GASのOauth2 Library側でもredirect urlを書き換える。
最後に忘れてはいけないのが、先に紹介したLibrary側でもZendeskへ送るredirect urlをproxyのドメインに書き換えないといけない点です。そうしないとLibraryは通常のGASにcallback URLをparameterに追加して送るため、redirect urlの不一致で認証が失敗します。
もう一度コードに戻ると、redirect urlを設定しているのは下記の部分なので、無理やりだと例えばここを単にFastlyに設定したURLを返すように書き換えるとよいと思います。
https://github.com/googlesamples/apps-script-oauth2/blob/24/dist/OAuth2.gs#L71-L73
function getRedirectUri(scriptId) {
return Utilities.formatString('https://script.google.com/macros/d/%s/usercallback', scriptId);
// return "https://<name>.global.ssl.fastly.net/usercallback" とか。
}
もしくは、getService()
内で
.setParam('redirect_uri', "https://<name>.global.ssl.fastly.net/usercallback")
して、さらに.setTokenPayloadHandler()
を使ってHandler先の関数で
function zendeskTokenHandler(payload) {
payload.redirect_uri = 'https://<name>.global.ssl.fastly.net/usercallback';
return payload;
}
という手順を踏むとか。
以上です。もっと良い方法を知っている方がいたら是非教えてください〜!