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

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

タグ:oauth

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 を外部から実行する、というアプリケーションのサンプルを作って実行するまでの手順紹介でした。


 

IBM Bluemix の利用料金は Bluemix ログイン後にアカウントメニューの「使用状況ダッシュボード」から確認することができます:
2016120801


組織とデータセンターの地域を指定して課金対象を絞りこむことができます。以下は月毎の推移グラフ:
2016120802


下にスクロールすると現在の月の、現時点での消費額を確認することもできます:
2016120803


さて、これらは利用料金を Bluemix のウェブコンソールにログインした上で確認する、という手順です。この内容を Bluemix のウェブコンソールを使わずに API で取得する方法は・・・ ありました!以下に Billing API を使って外部から利用料金を確認する手順を紹介します。

Billing API を実行する上で、以下の情報を指定する必要があります:
(1) 対象組織の GUID
(2) 対象地域(データセンター)
(3) 対象年月(YYYY-MM)
(4) OAuth トークン

(2) と (3) は指定するだけですが、(1) の組織 GUID と (4) の OAuth トークンはあらかじめ調べておく必要があります。仮に (2) はアメリカ南部("us-south")、(3) は 2016-11 を指定するものと仮定しておきます。


(1) の GUID は cf ツールを使って調べます。まずは利用料金を調べたい地域の API サーバーに cf ツールでログインします(下の例では US データセンターを指定しています):
$ cf login -a https://api.ng.bluemix.net/ -u (ユーザーID)

対象とする組織の名称(ID)がわかっている場合は不要ですが、組織 ID を確認する場合は "cf orgs" コマンドを実行します。ログインした ID で利用中の組織 ID の一覧が表示されます:
$ cf orgs

組織 ID が確認できている場合は、以下のコマンドでその組織の GUID を取得できます:
$ cf org (組織ID) --guid


次に (4) の OAuth トークンを取得します。これは同様に cf ツールで "cf oauth-token" コマンドを実行します。すると "bearer " から始まるトークン文字の羅列が戻ってきます。これが OAuth トークンです:
$ cf oauth-token
bearer XXXXXXXXXX....(文字の羅列)....XXXXXXXXXX

これで (1), (2), (3), (4) 全ての情報が揃いました。これらの情報を使って目的月(今回は 2016-11)の利用料金をコマンドラインから調べてみます。実行コマンドと、その実行結果(の一部)は以下のようになります:
$ curl -v -X GET -H "Authorization: bearer (OAuth トークン文字列)" "https://rated-usage.ng.bluemix.net/v2/metering/organizations/us-south:(組織 GUID)/usage/2016-11" | python -m json.tool

{
  "organizations": [
    {
      "billable_usage": {
        "spaces": []
      },
      "country_code": "JPN",
      "currency_code": "JPY",
      "id": "********-****-****-****-************",
      "name": "dotnsf@jp.ibm.com",
      "non_billable_usage": {
        "spaces": [
          {
            "applications": [],
            "containers": [],
            "id": "********-****-****-****-************",
"name": "********-****-****-****-************",
"services": [ { "id": "********-****-****-****-************",
"instances": [ { "id": "********-****-****-****-************",
"name": "IBM Insights for Twitter-5b", "plan_id": "********-****-****-****-************",
"usage": [ { "applicationId": "********-****-****-****-************",
"cost": 0, "name": "dotnsf-php-20161026", "quantity": 22066, "unit": "TWEET", "unitId": "TWEETS_PER_MONTH" } ] } ], "name": "twitterinsights" } ] }, { "applications": [ { "id": "********-****-****-****-************",
"name": "dotnsf-cloudant", "usage": [ { "buildpack": "********-****-****-****-************",
"cost": 2557.8000000000002, "quantity": 348, "runtime": { "id": "********-****-****-****-************",
"name": "liberty-for-java_v3_2-20160822-2200" }, "unit": "GB-HOURS", "unitId": "GB_HOURS_PER_MONTH" } ] }, : :

青字部分が実行コマンドで、その下が実行結果です。実行結果のところどころに "cost": ***** という形で各ランタイムやサービスごとの料金が表示されていることがわかります。

というわけで、この API を使うことで Bluemix の利用料金をコマンドラインから参照する、ことが実現できそうです。


(参考)
http://theblasfrompas.blogspot.jp/2016/02/invoking-billing-api-for-bluemix-public.html

このページのトップヘ