まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:oauth

最近関わったプロジェクトの中で「ウェブアプリケーションにユーザー管理機能/ログイン機能を付与する」ことを検討する機会がありました。これ自体は特別珍しいことではないと思っています。


こういったユーザー管理機能を実装しようとした場合、例えばログイン単体機能だけを考えればいい場合はデータベースに users テーブルを用意して、ログイン画面を作って認証、、とすればすぐに実現できそうです。が、現実的にはユーザー登録していない人向けにオンラインサインアップ機能を付けたり(指定されたメールアドレスにメールを送信して、そのメール内のリンクからユーザー登録の続きを行う、ということになるのでメール送信機能が必要になります)、パスワード変更を望むユーザーのための画面を作ったり、パスワードを忘れてしまった人のためのリセット機能を作ったり(これもメール送信が必要になりますね)、、それほど単純な機能追加だけにとどまらないことになります。加えて後述の「開発者を信用していいか?」という問題にも関わることになります。詳しくは以下でも紹介しますが、セキュリティ面まで考慮するとユーザー管理機能は「自前で用意すべきではない」機能の1つだと思っています。

というわけでアプリケーションにユーザー管理機能/ログイン機能を付与する場合の選択肢としては「専用サービスを使う(専用サービスの認証機能を自分のアプリケーション内に組み込む)」のが現実的ではないかと思っています。上述に挙げたような面倒そうな機能が一通り準備された状態で使うことができ、OAuth 2.0 をベースとしたセキュリティ要件もかなり高く設定できます。具体的には Auth0IBM AppIDAmazon CognitoGoogle Firebase などが候補になると思っています(以後、これらのサービスのことを ID as a Services という意味で "IDaaS" と呼ぶことにします)。

一方で、ログイン画面はそのアプリケーションの一部となるので、当然のように「ログイン画面をアプリケーションと(統一された CSS やヘッダー、フッダーという意味で)同じデザインにしたい」という要求もでてきます。各種 IDaaS には初めから用意されたログイン画面というものもありますが、その画面をそのまま使うのではなく、カスタマイズされたログイン画面からログインしつつ、API を使って認証機能そのものは IDaaS のものを使う、というものです。具体的な実現手順に関しては組み込み先のアプリケーションで使われているフレームワークや IDaaS によって異なりますが、OAuth 2.0 を使った SDK や API によって実現そのものは可能なものばかりです。自分も以前に IBM AppID を使ってログイン画面のカスタマイズを紹介したこともありました。


さて本ブログエントリのテーマとなる問題はここからです。IDaaS を使って自分のアプリケーションにログイン機能を組み込んだり、その画面をカスタマイズしたりすること自体は可能そうに思えるのですが、ここに「セキュリティ」という新たな要素を含めて考えてみます。一言でセキュリティと言ってもその観点はいくつもあるので「どういった観点で考えた時にどういうケースを心配する必要があるか?」を分けて考える必要があります。要は単純に1つの答にたどり着くのが難しく、ケース・バイ・ケースで考えないといけないような結構面倒な話なのでした。作成しているアプリケーションの特性や公開範囲、優先事項とその順位も考慮した上で「何をどこまで妥協できるか?」という観点でも考える必要があると思っています。以下、その優先事項の例を挙げつつ、何を優先するとどういった影響が起こりうるか、という視点をかいつまんで紹介していきます。なお、以下の説明では IBM AppID を使った場合で説明しますが、他の IDaaS を使っている場合も同様と考えてください。


【フロントエンドフレームワーク】
まずはクラウドのメリットを活かしやすい、と言われるフロントエンドフレームワーク(React, Angular, Vue など)を使ってウェブアプリケーションを開発しているか? という観点を考えてみます。このフロントエンドフレームワークを採用することでサーバーサイドにアプリケーションサーバーが不要になり(HTTP サーバーでよくなり)、負荷軽減やセキュリティ脆弱の可能性が下がるというメリットがあります。

一方でフロントエンドに全てのリソースを用意する必要があるため「アプリケーションを構成している要素(HTML や JavaScript)が全てウェブブラウザ経由で見えてしまう」という制約があります。IDaaS を使った認証ではこの制約が問題になるケースを考慮する必要があります。

より具体的な内容を説明します。たとえば通常の(フロントエンドフレームワークを使わない)サーバーサイドアプリケーションで AppID を画面カスタマイズ無しで認証機能として使う場合(サーバーサイド SDK を使う場合)、AppID から以下の情報を取得する必要があります:
  • リージョン(IDaaS インスタンスのあるロケーション)
  • テナントID(IDaaS インスタンスを特定する ID)
  • クライアントID(アプリケーションを特定するID)
  • シークレット
  • コールバック URL

実装手段にもよりますが、サーバーサイドサイドアプリケーションではこれらの情報を外部に開示する必要はなく、認証に関わる処理を全てサーバー側で行うように設計することでこれらの情報を外部から参照できないように作ることができます。重要性にランクを付けるつもりはありませんが、特にこの中でもシークレットと呼ばれる情報は(パスワードに相当するもので)機密性が高く、特定の人だけが知りうるように運用する必要があるものです。

一方でフロントエンドフレームワークを使ったアプリケーションから AppID の認証機能を使う場合(フロントエンド向け SDK を使う場合)、AppID から以下の情報を取得する必要があります:
  • クライアントID(アプリケーションを特定するID)
  • エンドポイント URL

サーバーサイドの時と比べて必要なパラメータ数は少なくなります。これは認証に用いるプロトコルが異なるためです(OAuth 2.0 コールバックと、OAuth 2.0 PKCE の違い)。ただフロントエンドフレームワークの場合はサーバーサイドの場合と異なり、これらのパラメータを全て JavaScript 内に記載する必要がある、という点が異なります。実際のプログラミング時ではこれらのパラメータを全て環境変数化してコードとは分離するのが一般的ですが、フロンドエンドアプリケーションとしてビルドする際に同環境変数も取り込まれ、静的なファイルとなった中にはプレーンテキストで含まれることになります(つまりプレーンテキストで外部公開されます)。

「とはいえサーバーサイドの時と違ってシークレットが不要だから(つまりシークレットは公開されないから)いいのでは?」と考えることもできます(実際、そのような考えでこの仕組みが提供されています)。たしかに最も機密性の高いシークレットが開示されるわけではない、と考えることはできますが、一方で「OAuth 2.0 PKCE ではこれらの情報だけで SDK を利用することができる」わけです。つまり「このアプリケーションの認証機能を実装する上で必要になる情報全て」が公開されている、とも言えます。

これはちょっと怖い事実といえます。現実的にはベンダー側(AppID 側)の設定でログイン URL を指定したりすることで情報の不正利用を防くことができるのですが、(詳しくは書きませんが)悪さをする側でも更に対策の対策のようなことができたりして、これだけでも好ましくなかったりします。なお、この件は後述の画面カスタマイズの時にも関わってくる、ということを先に記載しておきます。

ここで言いたかったのは「フロントエンドフレームワークには運用上の大きなメリットはあるが、認証セキュリティの件では通常のウェブアプリケーションよりも低くなってしまう」ということです。「低いからダメ」とまでは言いませんが、どういうことが起こり得て、それを正しく理解した上でそれは許容できるものかどうかを判断する必要があるもの、ということになります。


【認証画面のカスタマイズ】
上述の「フロントエンドフレームワークを採用するか否か」とは別に、認証画面を(ベンダー提供の標準画面を使うのではなく)自分達でカスタマイズしたものを使いたいかどうか、という観点を考えてみます。他の条件が変わらないのであれば、当然ログイン画面も(ベンダーから提供されるものではなく)アプリケーションテーマに合うようなものにカスタマイズしたくなると思っています。

ただこのカスタマイズには技術的なものとは異なる大きな壁があります。ログイン画面を開発するということは、入力したユーザー ID やパスワードをサーバーに送って、、、という処理も伴うわけですが、この処理部分を担当するアプリケーション開発者を信用してよいのか? という壁です。

「え?でもログイン画面を実装するなら当然じゃないの?」と考える人もいると思うので補足しておきます。上でも書いた OAuth 2.0 のコールバックや PKCE といったプロトコルを使い、画面カスタマイズなしで(ベンダーが用意したログイン画面を使って)認証する場合、利用者が入力したユーザー ID やパスワードをサーバーに送る、という処理は認証サービス側で用意することになります。つまりアプリケーション開発者からは手が出せない処理ということになります。この方法であれば利用者が入力した ID やパスワードが正しいかどうかを判断することも、正しかった場合に正しかったという結果を含めて次の処理に移ったり(リダイレクトしたり)、またオンラインサインアップやパスワードリセット時の処理に関しても同様にベンダーが用意済みの処理をそのまま利用することになるので、アプリケーション開発者からは手を出したくても出せない処理部分ということになります。したがってアプリケーション開発者は悪いことをしない、という前提に立つ必要もないのでした(IDaaS ベンダー側を信用する必要はありますけど、それはカスタマイズする場合も同様です)。


これがログイン画面を自分達で用意する場合、確かにユーザーインターフェース的にはアプリケーションに合ったものが用意できるというメリットがある一方、アプリケーション開発者が(例えば正しいことが分かった ID とパスワードを記録する、といった)処理を行う余地が残ってしまいます。これが「アプリケーション開発者を信用してよいか?」という壁になるのです。 上述した「ユーザー管理機能は自前で用意するべきではない」といったのはこのような背景があってのものでした。性善説に基づいて考えれば、そんな悪い開発者ばかりではないでしょうし、社内の者がそんなことをするメリットもない、と考えることはできます。ただ「ではそういう設計でよいか?」というのは別の問題であって、アプリケーションのセキュリティ設計の責任を持つ立場の人が考えることになると思っています(繰り返しますが「絶対ダメ」ではないのです。アプリケーションの特性やメリット・デメリット・優先順位を考慮した上で最終的には個別に判断するべきものだと思っています)


少しややこしい話をします。この「開発者を信用する/しない」という問題は、上で紹介した「フロントエンドフレームワークを使った場合、同じ認証機能を別のアプリで実装するために必要な情報が全てインターネット上に公開されてしまう」という問題と絡めて考えるべきとも思われます。「開発者を信用できるならよい」だけかもしれなかった話が、「信用できない開発者が作ったかもしれないログイン画面」を許すかどうかという問題も併せて考える必要が出てきてしまうのでした。


なお IDaaS を使わずに認証機能を自前で用意する場合に関しては選択肢がなく、「開発者を信用しないといけない」ことになります。試験的なアプリケーションであればこれが大きな問題とはならないと考えることもできますが、コンシューマー向けに公開して個人情報を集めるようなアプリケーションの場合、その設計は考え物だと思っています。


ここまでに紹介したようにアプリケーションの認証は「フロントエンドフレームワークを採用するか」、「ログイン画面のカスタマイズをするか」という2つの軸だけで考えても結構複雑な問題となります。現実は更にアプリケーションの特性(ユーザーは誰なのか? どの程度の負荷が想定され、どの程度の安定性が求められるか? 運用予算はどの程度か? など)も絡めて考慮する必要があるので、明確な基準を作るのが難しく、どうしても個別判断が多くなってしまうと思われます。


【フロントエンドフレームワークで IDaaS を使って画面をカスタマイズする場合】
では(ある程度のリスクを承知の上で)「フロントエンドフレームワークを使って開発するウェブアプリで、IDaaS を使ってユーザー管理を行うようなケースで、ログイン画面もカスタマイズしたい」という決定がなされた場合(作らないといけない立場になった場合)、どのような点に気を付けて実装すべきでしょうか。

※アプリをゼロから作るケースで、もし自分に設計の決定権があった場合は、おそらくこのような決定はしないと思う、ということは最初に言っておきます。わざわざフロントエンドでは作らない、たぶん。。


まず上で書いたような理由があるため、ログイン画面自体はサーバーサイドアプリケーションとして(フロントエンドフレームワークのアプリケーションとは別に)用意するべきです。そして フロントエンド → ログイン画面 → 認証 → ログイン画面で結果を取得 → フロントエンドに結果を返す  といった(リダイレクトによる)連携を行う設計にします:
img01


ここで特に気を付けるべきは最後の「ログイン画面で結果を取得 → フロントエンドに結果を返す」部分です。フロントエンドは HTTP GET リクエストしか受け付けることができないので、リダイレクト時に何か情報を渡そうとすると GET リクエストのパラメータで渡すしかありません。そのパラメータのフォーマットが推測可能な形になっていると認証を回避できてしまう可能性もあります:
img02


一例としてはこのような方法が考えられます。フロントエンドからログイン画面にリダイレクトする前に乱数を使ってランダムな値を生成し、その値を保持した上で、そのランダムな値をパラメータにログイン画面へリダイレクトします(ログイン画面でも同じ値を保持させます)。その上で IDaaS に対して認証を行い、ログインが成功した場合はログイン成功を示す情報(ユーザーIDなど)を保持していたランダム値で暗号化した上で、結果の暗号化文字列を URL パラメータに含めてフロントエンドに戻ります。最後にフロントエンドは自分が保持していたランダム値を使ってパラメータを復号化してユーザー情報を取り出す、といった具合です。この方法でもフロントエンド側で値を保持する必要があるので、そこで localStorage や sessionStorage などが必要になり、使わない場合と比べてセキュリティリスクが高くなってしまうのですが、一方でこの方法であればログイン認証を回避して(認証結果だけを偽造して)ログインしたかのように見せかける手法は使えず、また URL の一部にパラメータが表示されることがあってもその内容は暗号化されているので、ぱっと見ではその内容が推測できない、という要件はクリアできます。この方法で充分なセキュリティ要件を満たしているかどうかはアプリケーション次第の所もあり、個別に判断する必要があります。が、「この方法でも大丈夫」と判断されるケースであれば、このようなやり方もある、という例として紹介しました:
img03


【サンプルコード】
こういうケースで実装しなければならなくなった場合の1つのケースとして紹介しました。IDaaS やフロントエンドフレームワークの種類によって実装の実現方法も異なるし、ログイン画面のアプリとして作るウェブアプリのパフォーマンスも変わってくるとは思うのですが、仮に IDaaS として IBM AppID を、フロントエンドフレームワークとして ReactJS を使う場合のサンプルソースコードを作って公開してみたので紹介しておきます。Auth0 などでも同様に使えると思います:
https://github.com/dotnsf/appid-spa-customlogin


まず "git clone" などで上記 URL からソースコードを取得します。中には "spa", "web", "spa-normal" という3つのフォルダがあり、それぞれがアプリケーションになっていますが、サンプルとして使うのは "spa" と "web" の2つだけです:
2023031901


なお、spa-normal/ は React から AppID をカスタマイズ無しで使う場合のサンプルとなっています。こちらをセットアップして使ってみるとわかるのですが、AppID のログイン画面をカスタマイズ無しで使うとこのような画面になります(後述のものと比較してください)。またカスタマイズ無しの場合、この画面は別ウィンドウで開きます(この挙動も変更できません):
2023031903


ではサンプルを動かしてみましょう。web/.env ファイルだけは実行前に情報を入力しておく必要があります。IBM AppID のインスタンスを作成し、ユーザーを登録し、webapp(ウェブアプリケーション)を登録した結果として得られる Key, Secret, Client_ID, Tenant_ID, Region といった情報を書き写します(SPA_URL はこのままで構いません)。また AppID に登録したアプリの Redirect URI に http://localhost:8080/appid/callback を登録しておいてください:
2023031902


そして2つのアプリを実行します。ターミナルを2つ用意してください。まずは web/ フォルダ側を実行します:
$ cd web
$ npm install
$ npm run start

8080 番ポートでアプリケーションが起動します。そのまま spa/ フォルダ側も(別のターミナルで)実行します:
$ cd spa
$ npm install
$ npm run start

こちらは 3000 番ポートでアプリケーションが実行し、ウェブブラウザが自動で起動します(しない場合は http://localhost:3000 にアクセスします)。以下のような React の画面が表示されれば成功です:

2023031901


この時点ではまだログインできていないので "Login" ボタンが表示されています。このボタンをクリックすると web/ のログイン画面に転送されます(標準の別ウィンドウではなく、同じウィンドウで以下の画面が開きます。上述のカスタマイズ前のログイン画面の UI とも違っていることが確認できます):
2023031902


AppID 標準ではない、カスタマイズされた(日本語の)ログイン画面に遷移しました(ここでは紹介しませんが、オンラインサインアップやパスワード忘れ画面も日本語のカスタマイズ画面を実現しています)。ここで AppID に登録したユーザーの ID とパスワードを入力してログインします:
2023031903


ログインに成功すると元のアドレスに戻ります。今度はログインできているのでログインしたユーザーの情報と "Logout" ボタンが表示されています:
2023031904


Logout ボタンを押すとログイン情報が消え、同じアドレスのまま最初の(ログイン前の)画面に戻ります:
2023031905


フロントエンドフレームワークのアプリで IDaaS を使い、カスタマイズされたログイン画面で認証する、という目的は達成できていると思います。詳しくはソースコード内を参照いただきたいのですが、上述のようなアルゴリズムを実装して、(比較的)セキュアな方法で情報をやり取りしつつ、ログイン画面は標準のものではなく、カスタマイズされた画面を使えています。

注意していただきたいのは、この実現方法は充分にセキュアとは言えない点を持っています。アプリケーション開発者が信用できて、2つのアプリケーション間で情報をやり取りする方法(独自に実装したチャレンジ機能)が「この方法でもよい」と判断された場合のサンプルである、と理解した上で参照していただきたいです。





React.js をはじめとする、いわゆる「フロントエンドフレームワーク」の勉強をしながら色んな機能を実装している中で「あれ?これって・・・」と違和感のように感じた実装内容がありました。自分なりに色々調べたり、ベンダーのサポートに問い合わせたりした結果として「どうやらこれは問題ではないらしい」という結論に落ち着きました。

ただ「これは問題点と分類されることではない」という説明を受けている一方で、「本当に大丈夫なんだろうか?」という違和感は今でもまだ少し残っています。内容が内容だけにどこまで詳しく書くべきかも迷っていましたが、「ベンダーが問題ではないと言っているのだからいいだろう」と割り切って書くことにしました。簡単にいうと「フロントエンドフレームワークを使ったアプリケーションでの認証機能の安全性」についてです。

以下では Auth0 を使ったサンプルを紹介していますが、同様の現象が AWS CognitoIBM AppID でも再現することを確認していることをはじめに記載しておきます(Auth0 を例として選んだのは、この件でのやり取りの中で最も闊達な議論が出来、様々な情報をいただくことができたのでその内容を交えて紹介したいためです。悪意はありません)。細かな実装の違いはありますが、IDaaS に分類される ID 管理のマネージドサービスほぼ全てに共通の事象であると考えていただいていいと思います。


【通常の認証実装と、フロントエンドフレームワークを使った場合の実装との違い】
認証の実装手段による違いを説明するために、以下3種類のアプリケーション例を使うことにします:

A ユーザー管理機能ごと自分で実装し、サーバーサイドでユーザー認証を行うパターン

ある意味で最もシンプルなアーキテクチャです。自分たちで用意するデータベース等でユーザー情報(IDやパスワードハッシュ)も管理する形です。ログイン時はアプリケーションのログイン画面から入力された ID とパスワード(のハッシュ)がデータベース内に格納されているものと一致するかどうかを調べ、一致していればログインを許可する、という流れになります:
a_


これだけなら非常にシンプルとも言えますが、オンラインサインアップやパスワード変更、パスワード忘れといった機能も自分達で用意する必要があったり、既存の他のユーザー情報(社内ユーザーDBなど)との連携時なども自分たちで機能を実装する必要があります。そこまで意識すると必ずしもシンプルな開発が実現できる、とはいえなくなります。


B ユーザー管理機能にマネージドサービス(今回の例では Auth0)を使い、サーバーサイドでユーザー認証を行うパターン

ユーザー管理にマネージドサービスを使うパターンです。Auth0 や AWS Cognito、IBM AppID などが代表的であると思っています。多くの場合で OAuth 2.0 の仕組みが用意されていて、ログイン時のみマネージドサービスのサイトへ飛んでログインし、正しい ID &パスワード情報が入力された場合のみコールバック関数と呼ばれる事前に登録された URL が呼び出される形でアプリケーションの認証が実現できるものです:
b_


上述のAで問題になる、オンラインサインアップやパスワード変更といった面倒な手間も予め用意されているので、ユーザー管理機能まで含めた形で比較的簡単に実装ができるものです。また多くの場合でプログラミング言語ごとの SDK が提供されており、比較的簡単にログイン機能を実装することができます。


C ユーザー認証機能にマネージドサービス(今回の例では Auth0)を使い、クライアントサイドでユーザー認証を行うパターン

Bの仕組みをフロントエンドフレームワーク内で実現するものです。詳しくは後述しますが、フロントエンドフレームワークではコールバック機能が使えないため、PKCE と呼ばれるプロトコルを実装した専用の JavaScript SDK が提供されていることが多いです。今回、自分が問題が発生するのではないかと思ったのはこのパターンです:
c_



ではA、B、Cそれぞれのケースごとに実装やアーキテクチャの違いをセキュリティ面を意識しながら改めて説明します。

まずAのケース。このケースでは利用者が入力した ID とパスワード(ハッシュ)の組み合わせがデータベースのユーザーテーブル内に存在するかどうかを調べることになります。ログイン処理自体はバックエンド側で実装されるケースが多く、フロントエンド側から ID とパスワードを受け取ってパスワードをハッシュ化し、データベースのユーザーテーブル内で同じ ID とパスワードハッシュを持つレコードが存在するかどうかを調べます。存在していた場合は正しい ID とパスワードが入力されたことになるためログインを許可します。この ID とパスワードを受け取った後の一連の処理はバックエンド側で実装される形になるため、利用者からは何をしているのか、どのホストで動いているデータベースにどのような認証情報やプロトコルで接続して、どのような SQL を実行して情報が正しいかどうかを調べたか、といった裏側の情報は一切見ることができません。

一方、悪意を持った利用者がログイン画面から当てずっぽうに ID やパスワードを何度も試す、といった攻撃行為を行った場合でも、そのような記録を(どの IP アドレスから利用しているか、も含めて)アプリケーション・サーバー側のログに残すことができます。したがって「どこまでセキュリティを意識して実装するかにもよるが、比較的安全な実装ができる」と考えることができます。


次にBのケースです。こちらは OAuth 2.0 のコールバックという仕組みを使って実装する想定で説明します。利用者がログインする時に、一時的にプロバイダー(例えば Auth0)が用意するログイン画面に行きます。ここでプロバイダーに登録されたユーザーの ID とパスワード等を入力することが出来た場合にアクセストークンを含めた形で元のアプリケーションの画面に戻ります。アプリケーションではアクセストークンがあるので各種 API を実行できるようになり、ユーザーの詳細情報を取得することもできるようになります。

つまりログイン時に入力される情報は(プロバイダーの画面で入力されるため)アプリケーションからも知ることはできません。またこのログイン画面にリダイレクトする機能や、ログイン成功後のコールバックを受け取る機能については、バックエンド側で ClientID や ClientSecret 、コールバック URL を正しく設定することで可能になります。つまりアプリケーション側でもこの認証部分についてはバックエンド側で処理されることになるため、Aのケースと同様に利用者からは裏側の情報は一切見ることができません

悪意を持った利用者が何度も当てずっぽうにログインを試みる攻撃(ブルートフォースアタック)については、プロバイダー側で「どの IP アドレスから頻繁にログインを試みているか」を特定することができるため、例えばその IP アドレスからのリクエストを遮断する、といった対処が可能になります。


最後のCのケースはどうでしょう?こちらはフロントエンドフレームワークを使っているため、バックエンド側で処理を行うことはできなくなります。そこで PKCE という仕組みを使うのですが、この仕組は充分に安全なのでしょうか? PKCE は悪意のあるアプリケーションからアクセストークンを横取りされないようにするための仕組みでもあるのですが、ではアクセストークンを横取りされなければ大丈夫なのか?という観点で考えてみました。

このCのケースで自分が気になったのは「認証は(アプリケーションから見て外部にある)認証サーバーで」行いつつ、「アプリケーション側にはバックエンドの仕組みがない」という点だと思っています。したがって認証サーバーで認証を行う際に必要な client_id などの情報を安全なバックエンドに持たせることができない、ということになります。

ではどうしているか、というと、実はこれらの情報は全く隠されていません。ビルド時に JavaScript の難読化処理は行われていますが、HTTP でアクセス可能な JavaScript 内に平文で保存されています。この点がフロントエンドフレームワークを使った場合の実装が他のケースと大きく異なる点といえます。


【フロントエンドフレームワークを使った場合に漏れる情報を確認】
実際にこれらの情報が平文で格納されていることを確認してみます。例として実証用に用意したこのサイトを使います:


まず上述のサイトはこちらのサンプルをベースに作っています。このサンプルからはアプリケーション画面の背景色を変えた程度のものを公開しています:


また上述のサイトは Auth0 と、Auth0 が提供するフロントエンドフレームワーク用の SDK を使って実装しています。Auth0 側の設定ではログイン/ログアウトの許可 URL およびコールバック URL は全て同じで、上述の URL そのものを指定しています(これらは本来管理者以外からは見えていないはずのものです)。localhost など、開発/テスト時に指定しがちなもの(=推測される可能性の高いもの)は今回使っていません:
2022050400


このアプリケーションサイトの URL にアクセスすると、以下のようなページが表示されます(元々のサンプルの背景は黒でしたが、後述の比較のため青に変更しています):
2022050401


"LOGIN" と書かれたボタンをクリックすると Auth0 の認証ダイアログが表示され、ここで認証できます。今回はログインしませんが、ログインに成功すると元の画面の "LOGIN" ボタンが "LOGOUT" に変わって、ログアウトできるようになります(ここはサンプルのまま)。今回はそのままブラウザの「戻る」機能で1つ前のページに戻ります:
2022050402


元の画面に戻りました。では改めて公開されている情報をブラウザから確認してみます。画面を右クリックして「ページのソースを表示」を選択します:
2022050403


React によってビルドされた index.html の内容が表示されます。1行で表示されているため少し見にくいのですが、"<script" を検索すると中程にこのような JavaScript をロードしている箇所が見つかります。この JavaScript 部分をクリックして内容を確認します:
2022050404


React と Auth0 SDK によってビルドされた JavaScript ファイルの内容が表示されます。難読化処理が施されているためこれも見にくくなっていますが、この画面で "clientId" という文字列を検索してみます:
2022050405


"clientId" は数か所見つかりますが、その最後の1つの周辺が以下のようになっています:
2022050406

※これ、本当に公開しちゃっていいのか、少し怖いんですけど、後述のように規格としては機密情報ではないらしいので、それを信じて表示しちゃってます。


上図のように "clientId" と "domain" の情報が平文で格納されていることがわかります(わざと clientId が全て見えない位置でスクリーンショットを撮っていますが、実際に試すと全部表示されるはずです)。繰り返しますが、この画面は管理者や開発者だけが見ることのできる画面ではなく、利用者が誰でも見ることのできる画面です。A や B とは異なり、C のケースではこれらの情報をバックエンド側に安全に隠すことができないためです。

ここでは Auth0 を使っている場合の結果を紹介していますが、AWS Cognito や IBM AppID を使った場合でも同様の手順で JavaScript ファイルから設定に使った情報が見えてしまうことを確認しています。


【フロントエンドフレームワークを使った場合に漏れた情報で別のアプリケーションを稼働させる】
さて、上述したようにフロントエンドフレームワークではビルドされた JavaScript から一部の設定内容が見えてしまうことがわかりました。問題はこの状況は安全なのか? です。

ベンダー側の説明をまとめると「これはこのような仕様であり、表示されている情報は機密情報ではない」との認識でした。OAuth 2.0 を定義する RFC6749 でもクライアントを識別する ClientID 自体は単独では機密情報ではない、と明文化されており、これが画面を通じて表示されていることが即イコール危険にさらされている、という認識にはならないと判断しているようでした:
(RFC6749 2.2 より抜粋)The authorization server issues the registered client a client identifier -- a unique string representing the registration information provided by the client. The client identifier is not a secret; it is exposed to the resource owner and MUST NOT be used alone for client authentication

まあ確かにアクセストークンが奪取されるわけではないし、これがわかってもユーザー権限でログインできるわけではない(後述のような環境を作ることができるため、誰にも知られずに好きなだけログインを試すことができる、というだけ)のでギリギリセーフ、といった認識なのだと思います。

とはいえ、それでも全く安心してよいとは思えない点もあります。例えば Auth0 の場合、このフロントエンドフレームワークを使ってアプリケーションを作るために必要な情報は ClientID と Domain 情報、そしてコールバック URL の3つです。前者2つは上述のようにブラウザから確認することができ、また SPA であるためコールバック URL はブラウザのアドレス欄に表示されている文字列(今回の例だと https://dotnsf-auth0.herokuapp.com/)と一致しているはずです。つまり同じ認証機能を使うアプリケーションを作るために必要な3つの情報が全て開示されてしまっていることになります。後は SSL のオレオレ証明書を作って /etc/hosts ファイルを編集すれば、全く同じ Auth0 のサービスを使う、別の(悪意を持った)アプリケーションを作り上げてしまうこともできるわけです:
d_


本当にできるか、実際に試してみました。上述の方法で Auth0 の ClientID, Domain, CallbackURL(今回の場合は https://dotnsf-auth0.herokuapp.com/) の3つを取り出し、これら取り出した情報だけで別のサーバーに別途自分で作ったアプリケーションを適当なサーバーにデプロイして、自己証明書を作って /etc/hosts ファイルも編集した上でアクセスしてみました。自己証明書なので「信頼してアクセス」するための手順は必要ですが、自分でアクセスするだけなので問題なくできてしまいます(オリジナルのアプリケーションとの比較のため、背景を青から赤に変えています):
2022050601


全く同じ認証情報を使って作られているため、Auth0 への認証も(本来のものと同じ条件で)動きます。管理者の知らない所でログインを試すこともできますし、アタックプロテクションが有効になっていれば、アタックがされたことを検知させることもできてしまうはずです:
2022050602


繰り返しますが、上述のアプリケーションでは Auth0 を使ってアプリケーションをビルド&デプロイしていますが、同様のことが AWS Cognito や IBM AppID でも再現します。


【フロントエンドフレームワークを使った場合にこれらの情報が漏れても安全なのか?】
さて問題はこれができてしまうことがどれだけ危ないのか/危なくないのか という判断を行うことになると思います。これを危険だという根拠は以下のようになると思っています:
・本来の管理者の知り得ないところで、自分達が使っているのと同じ認証サービスが使われている
・ブルートフォースアタックが行われていても気付けない
・アタックプロテクションなどの機能が有効になっていると、管理側の知り得ない所からのアタックによってサービスが止まってしまう可能性がある


一方で、規格としてはこれは仕様どおりというか想定通りというか・・・であって、この時点で危険ではないと判断しているようです。以下のような理由であると思います:
・ユーザーのパスワードなどの情報が直接漏れるわけではない
・特定の攻撃者からのみ攻撃できる(一般ユーザーが知らないうちに攻撃に参加している、とか、一般ユーザーが知らないうちに偽サイトに誘導されている、ということは考えにくい)
・アタックプロテクション機能で攻撃を防げる、または攻撃元を特定できる


※ただし特定の条件下でこの C の構成になっている場合、認証サーバー側からも実際のアプリケーションサーバーからも攻撃元を特定できなくなる可能性があると思っています。その具体的な内容はここに書かない方がいいと思ったので控えます。


繰り返しますが、これは OAuth 2.0 の規格としては仕様どおり/想定どおりの挙動であって、(個人情報漏洩といった)直接の被害が起こる内容ではないと思っています。ベンダーとしては規格の上で機密情報とされていない以上は大丈夫、と判断することになるでしょうから、ここまでの情報を元に自分達が管理するサービスにおいて、この機能を使うか使わないか(許可してもよいかよくないか)、を判断すべきことだと思っています。「管理者の知らない所で同じサービスを都合よく使われる事自体が許されない」と判断するのであれば、上述のCのケースのようなフロントエンドフレームワークを使ってのアプリケーション構築を避けて、バックエンド併用の B のような設計にするべき、という意味です。

なんとなく、スッキリとした判断はならないのですが、規格やサービスを提供する側としては「ギリギリセーフ」と判断しているのだと思っています(事実、これが許されないとフロントエンドフレームワークを使ったアプリケーションからの認証サービス利用はできなくなってしまいます)。実際にフロントエンドフレームワークを使って IDaaS の認証サービスを利用するアプリケーションを開発・運用する(可能性がある)場合は、少なくともここで説明したような情報は外部に漏れる可能性がある、という点を理解しておくべきであると思いました。


Github API を使って簡易アプリケーションを作ってみました。その成果物を紹介すると同時に、Github API そのものについても少しずつ紹介する内容を書きたいと思ってブログエントリを書き始めています。アプリケーションの紹介までは長くなりそうなのでテーマを分割して、今回はとりあえず準備段階を含めた OAuth ログインを紹介します。


【もともとやりたかったこと】
もともとは「ファイルサーバー的なものを作りたい」と思っていました。ファイルサーバー自体はアプリケーション開発テーマとしては(わざわざ新たに作る必要があるものとも思わず、Box とかを使えばいいので)既に枯れたテーマだと思っていますが、ファイルのバージョン管理機能など細かな使い勝手を意識して実装しようとすると既存のものだけでは難しそうだと判断しました。その技術検討の中で(本来の目的とは違う使い方であることを理解した上で)Git のリポジトリをファイルサーバーとみなすとバージョン管理ははじめからついているし、個人ごとのフォルダも個人ごとにブランチを作ればいけそうだし、管理者は管理者用ブランチにマージすればまとめて見れるし、自分の希望に近いことを実現できるのではないか? と思いついたのでした。

要するにファイルサーバーのバックエンドとして Git を使い、そのバックエンド部分を API で読み書きするようなフロントエンドをアプリケーションとして実装すればよい、ということになります。実現するための Git の API として上述の Github API の存在を知ったことでなんかできそうな目処がたったので、実際に作って検証してみた、という経緯です。


【Github API を使う準備】
これから作るアプリケーションでは Github API を使うため、まずは Github に API を利用するアプリケーションを登録して、OAuth 認証用の各種 ID を取得したり、コールバック URL を設定しておく、という準備段階を済ませておく必要があります。以下、その手順を紹介します。

ウェブブラウザで Github にログインし、右上のアイコンメニューから Settings を選択します:
2021051601


Settings 画面の左で Developer settings を選択します:
2021051602


今回作るアプリケーションは Github に外部ログインするウェブアプリケーションです。この外部ログインを実現するため OAuth App として登録する必要があります。左メニューで OAuth Apps を選択して、"New OAuth App" ボタン(初めて登録する場合は "Register a new application" ボタン)をクリックします:
2021051603


アプリケーション名(適当)とアプリケーション URL (適当、http://localhost:8080/ など)を指定後、コールバック URL を指定します。この値は OAuth 認証後にリダイレクトする先の URL となり、アプリケーション側ではこの URL にアクセスされた際のパラメータを見て(API 実行時に必要な)アクセストークンを取得・保管する処理が必要になります。なので、アプリケーション毎にこの処理を行う URL を定義する必要がありますが、後述のサンプルアプリケーションでは "http://localhost:8080/api/callback" に GET リクエストがあった時にこの処理を行う想定で作っています。そのためアプリケーション URL には http://localhost:8080/api/callback と入力してください。他の値はオプションなので適当に入力し、最後に "Register Application" ボタンをクリックします:
2021051604


※注意点として、Github API ではこのコールバック URL は OAuth アプリケーションごとに1つだけしか登録できないようです。今回は localhost 環境で動かす想定でコールバック URL を http://localhost:8080/api/callback と登録しましたが、実際に公開するサービスとして利用する場合は、その本番サーバーのホスト名やポート番号を使って指定する必要があります。本番利用時にはこの値を書き換えるか、あるいは本番サーバー用の OAuth アプリケーションを新たに登録し、そこで取得した値(後述)を使ってアプリケーションを動かしてください。


アプリケーションの登録が完了した直後の画面に、外部アプリケーションからの認証時に指定する必要のある各種情報が表示されます(client_secret は "Generate a new client secret" ボタンをクリックすることで表示されます)。このうちの client_idclient_secret の値を(後で使うので)メモしておきます。なお、client_id の値は忘れてしまった場合でも改めて OAuth Apps 一覧から選択することで再び参照することができますが、client_secret の値はこの画面を閉じてしまうと2度と参照することができません(client_id ごとリセットして取得し直す必要があります)。間違えないように正しく記録を残しておくように気をつけてください:
2021051605


これで Github API を使うための、OAuth App を登録する手順が完了しました。次はこの OAuth App を実際に作って動かす段階となりますが、その前に Github API を使って操作する Github リポジトリを用意しておきます。


【Github API で操作するリポジトリを用意する】
後述のサンプルアプリケーションでは「指定した特定の Github リポジトリ内の main ブランチに属しているファイルの一覧を取得する」ことが可能な実装をしています。そのための「特定の Github リポジトリ」を用意します。

単にファイル一覧を読み取るだけなので(変更を加えるわけではないので)、既存の Github リポジトリがあればそれを使ってもいいし、新規に Github リポジトリを作成した上で指定しても構いません。今回は dotnsf/my_githubapi_test という動作確認用リポジトリを作り、この中の main ブランチのファイル一覧を取得するようなアプリケーションとして以下の説明を続けます(実際にはみなさんも独自のリポジトリを作って、README.md 他を main ブランチに入れておいてください):
2021051606


ただ一点注意が必要です。上述の client_id / client_secret を取得した時に使った Github ユーザー ID と同じ ID でログインする場合はこのままでいいのですが、別の Github ユーザー ID で使いたい場合や、友人など別の Github ユーザー ID からも同アプリケーションを使わせたい場合など、(client_id / client_secret を取得した時とは)別の Github ユーザーにもこのアプリケーションを使わせたい場合、そのユーザーが対象のリポジトリを読み取るための Collaborator 権限設定が必要になります。 その場合は同リポジトリのオーナーでブラウザログインし、対象リポジトリを開いてから Settings メニューを選択します:
2021051607


そして画面左から Manage Access メニューを選択し(パスワードを聞かれます)、画面右の Manage Access 内の "Invite a collaborator" ボタンをクリックします:
2021051608


そして後述のアプリケーションを使わせたい Github ユーザーを指定して "Add **** to this repository" ボタンをクリックします:
2021051609


すると指定されたユーザーに招待メールが送信され、メール内の "View invitations" リンクから遷移して accept することで該当リポジトリに対する Collaborator 権限が付与され、リポジトリのオーナー以外のユーザーでも操作できるようになります:
2021051601



ここまでの作業で外部アプリケーションからログインするための設定と、操作対象リポジトリの準備ができました。ではサンプルのアプリケーションを使って、実際に Github API が動作する様子を体験してみます。


【Github の OAuth を使って外部アプリケーションから Github にログインする】
Node.js を使って Github API を実際に動かすサンプルアプリケーションを用意しました。Node.js 導入環境を使って、こちらから git clone するかダウンロード&展開してください:
https://github.com/dotnsf/github_oauth_sample


ソースコード内の settings.js ファイルを編集して、上述の準備段階で集めた情報を指定します。git clone 後かダウンロード後、ローカルシステムにある同ファイルをテキストエディタで開き、以下のように値を入力します(青字はコメント):
//. settings.js
exports.client_id = 'xxxxxxx'; OAuth App 作成時に取得した client_id の値
exports.client_secret = 'xxxxxxx'; OAuth App 作成時に取得した client_secret の値
exports.callback_url = 'http://localhost:8080/api/callback'; OAuth App 作成時に指定したコールバック URL の値
exports.repo_name = 'dotnsf/my_githubapi_test';  操作対象リポジトリ名

この状態で npm install を指定して依存ライブラリを導入してから node app でアプリケーションを起動します:
$ npm install

$ node app

起動に成功すると、このサンプルアプリケーションは 8080 番ポートで HTTP リクエストを待ち受けます。ウェブブラウザで http://localhost:8080/ にアクセスします:
2021051601


最初はログイン前なので "login" ボタンが表示されています。このボタンをクリックすると Github API を使った OAuth ログインの処理がスタートします。一度もログインしたことがない場合は以下のような同意画面が表示されるので、ユーザー名を確認後に "Authorize ***" をクリックしてください:
E1ewH_vVkAAwHHE


すると再度 http://localhost:8080/ に転送されますが、今度はログイン後なのでユーザー情報を取得することができ、ログインした Github ユーザーのアバターアイコンやユーザー ID が画面右上に表示されます。このアイコンが自分の Github ユーザーアイコンであることを確認してください。このアイコンをクリックしてログアウトすることもでき、ログアウトすると再度 login ボタンが表示されます:
2021051602


とりあえず、Github API を使ったログイン処理を実装することができました。以下、アプリケーションのログイン部分の仕組みを解説します。


【Github の OAuth ログインの仕組み】
詳しくはサンプルアプリケーションのソースコード内 api/github.js を見ていただきたいのですが、正確には「ログイン認証の仕組み」というよりは「ログイン認証してアクセストークンを取得する仕組み」です。

まずログイン認証の仕組みは OAuth を使っています。アプリケーションで "login" ボタンをクリックすると、https://github.com/login/oauth/authorize にリダイレクトしています。その際に URL パラメータに付与する形で settings.js 内に記載した client_id の値と callback_url の値を指定しています:
router.get( '/login', function( req, res ){
  //. GitHub API V3
  //. https://docs.github.com/en/developers/apps/authorizing-oauth-apps
  res.redirect( 'https://github.com/login/oauth/authorize?client_id=' + settings.client_id + '&redirect_uri=' + settings.callback_url + '&scope=repo' );
});


このリダイレクト先で上述の Github の認証を行います:
E1ewH_vVkAAwHHE


ここで Authorize するとコールバック URL に転送されます。その際に一時的なアクセストークンが code パラメータに指定される形で送付されてくるので、これを取り出した上で client_id や client_secrent などの値と一緒に https://github.com/login/oauth/access_token に POST リクエストを発行します。その実行結果(form encoded形式)から access_token 変数としてアクセストークンを取り出すことができるので、(本サンプルアプリケーションではセッション内に記録する形で)保存して、この後の Github API 実行時に指定できるようにしています。その上で改めて GetMyInfo() 関数を実行してログイン情報を取得し(詳しくは次回に)トップページにリダイレクトしています:
router.get( '/callback', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var code = req.query.code;
  var option = {
    url: 'https://github.com/login/oauth/access_token',
    form: { client_id: settings.client_id, client_secret: settings.client_secret, code: code, redirect_uri: settings.callback_url },
    method: 'POST'
  };
  request( option, async function( err, res0, body ){
    if( err ){
      console.log( { err } );
    }else{
      //. body = 'access_token=XXXXX&scope=YYYY&token_type=ZZZZ';
      var tmp1 = body.split( '&' );
      for( var i = 0; i < tmp1.length; i ++ ){
        var tmp2 = tmp1[i].split( '=' );
        if( tmp2.length == 2 && tmp2[0] == 'access_token' ){
          var access_token = tmp2[1];

          req.session.oauth = {};
          req.session.oauth.token = access_token;

          var r = await GetMyInfo( access_token );
          if( r ){
            req.session.oauth.id = r.id;
            req.session.oauth.login = r.login;
            req.session.oauth.name = r.name;
            req.session.oauth.email = r.email;
            req.session.oauth.avatar_url = r.avatar_url;
          }
        }
      }
    }
    //console.log( 'redirecting...' );
    res.redirect( '/' );
  });
});

トップページでは画面ロード直後に GET /api/isLoggedIn という API が実行されます。この API はセッション内にアクセストークンがあるかどうかを調べ、含まれていた場合はその情報(ログイン時に GetMyInfo() 関数によって取り出したユーザー情報)をレスポンスの値として返します。つまりログインしていない場合は false 値が、ログインしている場合はユーザー情報を返すという関数です。これによって、未ログイン時のトップページでは login ボタンを、ログイン時のトップページでは logout ボタンをそれぞれ表示するようにしています:
router.get( '/isLoggedIn', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var status = false;
  if( req.session && req.session.oauth && req.session.oauth.token ){
    status = JSON.parse( JSON.stringify( ( req.session.oauth ) ) );
  }

  if( !status ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false }, null, 2 ) );
  }else{
    res.write( JSON.stringify( { status: true, user: status }, null, 2 ) );
  }
  res.end();
});

なお、logout ボタンが押された場合はセッションの中身を空にする、という処理が実行されます。これによって取得したアクセストークンは無効になり、(再ログインによって)再びアクセストークンを取得するまで Github API は実行できなくなります:
router.post( '/logout', function( req, res ){
  if( req.session.oauth ){
    req.session.oauth = {};
  }
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

と、Github API を使ったログイン時の処理内容を中心に紹介しました。次回は GetMyInfo() 関数でユーザー情報を取り出す仕組みと、ログイン後に main ブランチ内のファイル一覧を取り出す仕組みを紹介する予定です。


(2021/05/18 追記 続きはこちらこちら

このブログエントリの続きです:
Slack の OAuth API を使ってみる

↑ここで紹介した方法を使って実際にアプリケーション・サービスを作ってみました。作った内容はこれ↓の Slack 版です:
お絵かき LIFF アプリを作ってみた

2019052501


(実はもともとは Twitter 向けに作ったのですが)LINE のフロントエンドフレームワーク: LIFF を使って、その場で指でお絵描きした画像を LINE のトークルームに送信する、という連携アプリケーションを以前に作りました。その Slack への移植です。

なお、このアプリケーションは Barloon というエンジニア向けバーで企画されたハッカソン向けに作成しました。興味ある方は "Barloon" でググって調べてみてください。


【作った物】
こちらで MIT ライセンスで公開しています。よかったら使ってください:
https://github.com/dotnsf/slack-doodle

2019052502



以下でも紹介していますが、注意点として「ワークスペースごとにアプリケーション・サーバーが1台必要」です。その理由ですが、このアプリは Slack API を利用して作っているのですが、Slack API に登録アプリを申請する際にワークスペース(https://○○○○.slack.com/ の ○○○○部分)を指定して申請する必要があるからです。ここで申請したワークスペース向けにアプリケーションを作って動かす形になるため、複数のワークスペースで動くアプリケーションを1つの URL で動かすことができないのでした(このあたりが上記の LINE 向けアプリと異なります)。


【サーバーの動かし方】
このアプリケーションは Node.js 上で動くウェブアプリケーションです。一応レスポンシブ対応しているつもりなので、PC ブラウザからも、スマホのブラウザからも使えます(PC ブラウザの場合はマウスで、スマホブラウザの場合は指でお絵描きすることになります)。したがって Node.js が導入済みのアプリケーション・サーバーが必要です。

利用にあたっては、まず Slack API のアプリケーション登録が必要です。こちらの詳しい手順はこのリンク先を参照してください:
http://dotnsf.blog.jp/archives/1074688701.html

ただし1点だけ注意が必要です。上記ページではアプリケーションの scope に channels:read のみを指定していますが、このお絵描きアプリケーションでは更に加えて:
 chat:write:user
 files:write:user
の2つ(計3つ)の scope を指定する必要があります(描いた画像を API でアップロードするために必要な scope です)。この3つを scope に指定する必要がある、という点に注意してください。

2019052503

※必要に応じてアプリケーションのアイコンなども好きなものに変えておいてください。


その上で上記 github の URLからソースコード一式を Node.js アプリケーションサーバー上に git clone するか download & unzip して、ソースコードを展開します。

展開後、settings.js というファイルが存在しているので、このファイルをテキストエディタで開き、exports.slack_client_id の値と、exports.slack_client_secret の値をそれぞれ Slack API 登録アプリの client_id および client_secret の値に書き換えて保存します(このあたりの具体的な情報はこちらを参照してください)。

そしてアプリケーションサーバーを起動、これで準備完了です:
$ npm install
$ node app

【遊び方】
アプリケーション・サーバー(例えば https://slack-doodle.xxx.com/ )が動いている状態で、そのアプリケーションサーバーの URL に PC かスマホのブラウザにアクセスするだけなのですが、その前にやっておくことがあります。

上記でも触れたのですが、このアプリケーションサーバーは特定のワークスペース向けに作られています(そのワークスペースでしか使えません)。一方、ブラウザで Slack にアクセスすると、いろんなワークスペースに切り替えて使うことができます。ということはブラウザが目的のワークスペース以外のセッションなどを保持している可能性があり、その状態で使っても期待通りの挙動にならない可能性があるのです。

この状態をクリアするために『念の為』以下の手順を最初に行っておくことを推奨します。まず PC かスマホのブラウザを起動し、目的のワークスペース(例えば目的のワークスペースが "abc" であれば https://abc.slack.com/ )にアクセスして、認証してログインします。これでブラウザが目的のワークスペースのセッションを保持した状態が作れました。

その上で、そのままアプリケーションサーバー( https://slack-doodle.xxx.com/ )にアクセスします。以下はスマホでの画面例ですが、PCブラウザだともう少し横に大きな画面になると思います(一応、この未ログインの時点でお絵描きを試すことはできるのですが、送信することはできません)。ではログインするため左上の "Login" をタップします:
2019052504


Slack アプリケーションのページに転送され、目的のワークスペースに向けた OAuth の認証が行われます:
2019052505


内容を確認して、「許可する」を選択します:
2019052506


するとログインが完了し、元のアプリの画面に戻ります。この時、画面上部にワークスペース上で自分が利用することのできるチャネルが選択できるようになり、POST をクリックすると、ここで選択したチャネルに描いた画像がアップロードされます:
2019052507


実際に POST するとこんな感じで目的のワークスペースの目的のチャネルに画像をアップロードすることができます:
2019052508


この系統のアプリ、Twitter ではじめて、LINE に移植して、今回は Slack にも移植して・・・ 次は何にしよう?? ちなみに facebook は publish の API が昨年廃止になってしまったので技術的に作れないことがわかってます。



某アプリを Slack 対応する経緯で Slack API の中の、特に認証/認可を司る OAuth API を使う機会があったので自己まとめです。

もともとやりたかったのはウェブアプリに Slack アカウントでログインして、そのログインした人の権限でチャネル一覧を取得し(※)、ウェブアプリから指定したチャネルにメッセージを書き込む、ということでした。この中の※部分までを Node.js + Express + EJS で実現したコードを Github に公開しています(後述)。


実際に試してみるにはまず Slack に対象アプリケーションを登録する必要があります。 https://api.slack.com/apps を開いてログインし、"Create New App" ボタンをクリックしてウェブアプリを登録します:
2019052401


登録するアプリの名前と、対象ワークスペースを指定します(つまり同じアプリを複数のワークスペースで使いたい場合は、アプリを複数登録する必要があります)。以下では名前は "Slack OAuth Sample"、ワークスペースは "dotnsf" を指定しています(ワークスペースはログインしたユーザーが利用可能なワークスペース一覧から選択します)。最後に "Create App" ボタンをクリックして作成します:
2019052402


すると指定したアプリケーションの API 設定画面に切り替わります。画面左上に入力したアプリ名がデフォルトアイコンと一緒に表示されていて、"Basic Information" メニューが選択されていることを確認します:
2019052403


この画面を下スクロールすると App Credentials という項目があります。この中の Client IDClient Secret の値を後で使うので、どこかにコピーしておくか、いつでもこの画面を開ける状態にしておきましょう。なお Client ID の値は画面内に表示されていますが、Client Secret の値は初期状態では非表示になっています。"Show" ボタンをクリックして内容を表示し、その表示された値をあとで使うことに注意してください。またこれらの値は他の人には教えないように、自分で管理する必要があります:
2019052404


次に画面左のメニューから "OAuth & Permissions" を選び、少し下にスクロールすると Redirect URLs という項目があります。ここにウェブアプリケーションを動かす際のコールバック URL を登録しておく必要があります。"Add New Redirect URL" ボタンをクリックします:
2019052405


すると Redirect URL を追加する画面になるので、http(s)://サーバー名/slack/callback と入力します。この値は開発時には開発時用のサーバー名とポート番号、本番環境では本番環境用のサーバー名を指定する必要があります。下図では開発時向けに localhost の 6010 番ポートで動かす想定で http://localhost:6010/slack/callback と指定しています。ここの値は実際の環境に合わせて適宜変更してください。入力し終わったら "Add" ボタンをクリックして、その後 "Save URLs" ボタンをクリックします:
2019052406


画面上部に "Success!" というメッセージが表示されればリダイレクト URL の設定は完了です。正しい Redirect URLs が登録されたことを確認します:
2019052407


続けて、このアプリで利用する Slack 機能のスコープを指定します。実は OAuth 認証だけであればここの設定は不要なのですが、今回のデモアプリでは OAuth 認証後にログインユーザーが参照できるチャネルの一覧を取得して表示する、という機能が含まれています。また実際のアプリケーションではそのアプリケーションで実装する機能によって、ここでスコープを追加する必要があります:
2019052408


今回はログインユーザーが利用できるチャネル一覧を取得するため、"channels:read" スコープを追加します。また他に必要なスコープがあればここで追加します。最後に "Save Changes" ボタンをクリックして変更を反映します:
2019052409


これで Slack API 側の設定は完了しました。

では改めて Github からアプリケーションを取得します。Node.js がインストール済みの実行サーバー上で以下の URL を指定して git clone するか、ソースコードをダウンロード&展開してください:
 https://github.com/dotnsf/slack-oauth

2019052410


ソースコード内の settings.js をテキストエディタで開き、exports.slack_client_id の値と exports.slack_client_secret の値を上記で確認した client_id と client_secret の値に(コピー&ペーストなどで)変更して、保存してください。

なお、このサンプルアプリケーションでは以下のリクエスト API(?)が用意されていて、これらを明示的&内部的に使って動きます:
リクエスト API用途
GET /ユーザーがアクセスする唯一のページ。アクセス時に認証情報がセッションに含まれているとチャネル一覧が表示される。認証情報がセッションに含まれていない場合は認証前とみなして「ログイン」ボタンを表示する
GET /slack/loginユーザーページで「ログイン」ボタンをクリックした時に実行される。Slack の OAuth 認証ページにリダイレクトされる
GET /slack/callbackSlack の OAuth 認証ページで Authorize された時のリダイレクトページ。この URL が Slack OAuth 設定時に指定されている必要がある。アクセストークンを暗号化してセッションに保存し、GET / へリダイレクトされる
POST /slack/logoutログアウト(セッション情報を削除)する
GET /channelsチャネル一覧を取得する。認証後にユーザーページが表示されると内部的にこの REST API が AJAX 実行されて、画面にチャネル一覧が表示される。


では実際に起動してみます。起動サーバーにログインし、ソースコードのあるフォルダに移動した状態で、以下を実行します:
$ npm install
$ node app

そしてウェブブラウザで起動中のアプリケーションにアクセスします。以下の例では localhost:6010 でアプリケーションが起動されている想定になっているので、http://localhost:6010/ にアクセスします。上記の GET / が実行され、ログイン前のシンプルなページが表示されます。ここで "Login" を選択します:
2019052411


GET /slack/login が実行され、ブラウザは Slack API の OAuth 認証ページにリダイレクトされます。アプリケーションが利用する scope が表示され、このまま認証処理を許可するかどうかを聞かれます。許可する場合は "Authorize" を選択します:
2019052412


Authorize を選択すると認証と認可が完了し、そのアクセストークンが GET /slack/callback へ渡されます。そこでアクセストークンを暗号化してセッションに含めます。この状態であらためてトップページ(GET /)が表示され、ログイン処理が住んでいるのでログイン後の画面が表示されます。AJAX で GET /channels が実行され、ログインユーザーが参照することのできるチャネルの一覧が表示されれば成功です:
2019052413


以上、Slack API の OAuth を使ってウェブアプリケーションから Slack の認証を行い、認証ユーザーの権限で Slack API を外部から実行する、というアプリケーションのサンプルを作って実行するまでの手順紹介でした。


 

このページのトップヘ