例えば次のようなケースをどうするか。
「既存のWebサイトがあり、既にユーザIDとパスワードによる認証によって、ブラウザでデータを提供している。 今回、この提供データをブラウザの画面ではなく、REST APIにて取得可能にしたい。 このデータはユーザ毎に取得可能な値が違うので、認証、または認可によって制限をかけたい。」
ユーザーがブラウザからIDとパスワード(以下ID/PW)を使ってログインする方式を、そのままWeb APIにも適用しても安全なのだろうか。 Web APIの先にはスマホアプリやシェルスクリプトなどから直接ログインするものなどが考えられるが、安全かつシンプルに実装するにはどうしたらいいのだろうか。
私はセキュリティの専門家ではないので間違った考え方をしている可能性もあるが、誰かの目に留まって助言いただけるかもしれないので、それを期待して今考えていることを率直に述べようと思う。
APIで安全な認証はそもそも作れるか
そもそも認証については原則ブラウザを使わないとダメなんじゃないかと私は考えている。
その理由は、ブラウザじゃないとサイトの安全性について確認することが困難だから。
ブラウザであれば、利用者はアドレスバーでドメイン名を確認できるし、https通信されていることやベリサインシールも確認できる。その逆にアドレスバーを隠してあるサイトは信用できないしパスワードを盗む偽サイトかもしれない。
世の中を見渡すと、ほぼ全ての認証の基本になっているのがブラウザからのログインケースであるように思う。
一方、API認証で、たとえID/PWで認証できるようなAPIを作ったとしても、ブラウザのようにサイトの信頼性を確認する手段がないため、利用者が安心して使えるものを作るのは難しいのではないかと思う。
例えば、コマンドラインのプロンプトやスマホアプリの画面でID/PWを入力するアプリを作ったとすると、どこで認証するかをユーザは正しく認識する必要があるが、送信先URLに不正がないことなどを確認するのは難しいと思う。(※1)
ただ、利用者に毎回ID/PWを入れさせなくてもアクセス制御さえきちんとできれば十分というケースもあるだろう。このようなケースであれば後述するOAuth2.0や認証キーを使う方法などがある。
API認証のパターン
認証(認可)の方法は、大きく分けて以下の3つがあると思う。
1.ID/PWを送信するBasic認証やDigest認証(含むWSSE)
2.(ブラウザの管理画面等で発行する)認証キーを使う方法
3.OAuth2.0 アクセストークンを使う方法
1.のBasic認証APIでID/PWをハードコードして使うケースはよくありがちで、このような安易なやり方はセキュリティ上まずいのはわかる。 APIはプログラムから利用されることを前提として作られているため,PWリスト攻撃/辞書攻撃を試みるクラッカーにとって攻撃に役立つ便利な機能になり格好なものになることは容易に想像できる。
2.の認証キーを使う方法では、まず管理者がログインして画面を開き、そこに表示されている認証キーを使ってAPIからアクセスする。MBaaSでもよく使われており、ID/PWを入力しないため安全な方法といえる。
3.のOAuth2.0では、アクセストークンをAPIの認証キーとして使う。アクセストークンを入手する際に最低一回は必ず、正規のWebのログイン画面で認証する必要がある。
DropboxUploader(https://github.com/andreafabrizi/Dropbox-Uploader)はbashでOAuth2.0を使っている。(しかし実際に導入してみるとわかるのだが設定がちょっと面倒くさい)
OAuth2.0は「認可」である。これを使うということは、つまり、認証はAPIでは行わないことと同意となる。認証キーを使う方法も、Authorizationヘッダに付与するOAuth2.0 Bearerも同様に「認可」の一部であると考えられる。認可はユーザによって正しく認証されていることが前提であり、この方式を使えばパスワードが罠サイトに送信されるような心配もない。
2.と3.は認証と認可を別々に分けることでリスクを減らすという考え方で、これらを採用できるのであれば特に問題はないし、また冒頭に挙げたケースもこれで解決できるかもしれない。 OAuthのアクセストークンを使えばAPIで認証やる意味はないという意見もアリだと思う。ということで、OAuth2.0などの認可の話はここで終わり。
問題は、1.のような「認証」をAPIでやってよいかどうかということである。
次にこの認証の論点に絞ってもう少し掘り下げてみる。
APIの認証をやらなくなった現状
Google Data APIには以前、Client Login方式 (https://developers.google.com/accounts/docs/AuthForInstalledApps)というのがあったが今は廃止されてOAuth2.0に移行している。
かつてのClientLogin方式はID/PW認証ができてとても便利だったのを覚えている。しかし、しばしばCAPTCHAロックが発生するため、アプリでID/PW固定で使うのは困難であった。結局、Googleは不正ログイン対策のためID/PW方式を諦めざるを得なかったのだと思う。(廃止理由:http://adwords-ja.blogspot.jp/2013/09/adwords-api-clientlogin.html)
GoogleのClientLoginの件もしかり、昔流行ったWSSEにしても、 https://www.google.co.jp/search?q=WSSE で検索してもわかるように、多くのサービスがOAuth2.0に移行している現状がある。これは、リピート攻撃に弱くて筋が悪いと言われているWSSEを廃止したいことや、Webの認証機能に一元化したいという背景があるように思う。また、前述したような、ユーザがどこで認証するかを正しく認識する必要があることや、認証と認可を分けて考えればそもそもAPIに認証が不要だったというケースも多いのかもしれない。
でも、よく考えてみると、WebのログインフォームからはID/PWが送信されているわけで、API認証を廃止したところでログインフォームの動作をAPIでエミュレートすることが可能である。むしろそれを禁止する方が難しく、例えば、ログインページをスクレイピングしてtokenを切り取るなどすれば大概はエミュレートできる。(ただ、ワンタイムトークンなど発行することで難しくすることはできる)
ブラウザでできることをプログラムから実行するのがなぜまずいのか。 今のところ、ログインフォームのようにID/PWをうまく保存する方法がないとか、不正ログイン対策のためのCAPTCHAをプログラムでハンドルするのが難しい、ぐらいしか説明が見つからない。
ログインフォームなどのセキュリティ強化策
ID/PWを送信するAPI認証がセキュリティ上まずいとはいったものの、そもそもログインフォームのエミュレートを防げないのであればAPI認証の非公開にかかわらず意味がないし、結局はログインフォーム自体のセキュリティを強化する必要があるように思う。
例えば、生PWを送信するリスクをどう考えるか。 ブラウザのログインフォームにしてもAPIにしても認証を行う際には必ずID/PWを必要とするが、生のPWを送信するときには細心の注意が必要である。(かつて、URLパラメータにPWを含めてしまうとRefererに機能によってリンク先に送出されることがあったように)
またSSLで暗号化していてもログに書かれてしまうことで漏洩の危険性が高まるため可能であれば生で送信はやめた方がよいと思う。
そこで、Basic認証は使わずにDigest認証やWSSEのようにパスワードをハッシュ化(※2)することでパスワードそのものの推定を難しくすることを考えてみる。これで万一漏洩した際にも同じパスワード使ってるサービスへの影響が少なくなるというメリットが生じる。
個人的には、(今さらかよ!と思うかもしれんが)WSSEをハッシュ化して送信する方法で十分だと考えている。ただし、サーバではリピート攻撃に耐えうるようにWSSEをワンタイム化する。 そのまま保存してしまうとハッシュ化パスが事実上のパスワードになってしまうため、PWをさらにハッシュ化(または暗号化)したものを保存する。 生パスワードを知らないので漏れることも悪用(生パスワードを保存しておいて、他のシステムの認証にも使ってしまうようなこと)される心配もないためユーザに対して安心感は増すと考えられる。 また、初回だけWSSEを送って認証に成功したらサービス側で有効なトークン(セッションID)を発行してクライアントは以後毎回それを送るというブラウザでのセッションクッキーと同等の仕組みにすると毎回パスワードを入力しなくてもよくなるのでユーザビリティが増す。
これを実装するのに、Javascriptでパスワードをハッシュ化してXHRで送信することになると思うが、CSRF対策も考える必要がある。
http://matome.naver.jp/odai/2133794394551702001
http://utf-8.jp/public/20131114/owasp.pptx (P24-25に対策が載っている)
不正ログイン対策まで考えるのであれば、CAPTCHAの他にリスクベース認証や二要素認証などの防御対策の他、不正アクセスがないかチェックするためのログイン履歴機能なども必要になってくるだろう。つまり、API認証だけのセキュリティを考えるのではなく、ログインフォームを含む認証全体の機能についてよく考えないといけないという話になる。
APIKey+WSSE
最後に、どうしてもAPIで認証したいという場合の簡易的な方法について思いついたことを紹介する。
これは正規のIdpからお墨付き(APIKey)をもらうことでログイン認証できる権限を得る方法である。
具体的には、WSSEのこれまでのPasswordDigest生成ロジックを改良したもので、管理画面から入手したapikeyを先頭にくっつけてsha256にしてbase64エンコードするだけ。(注:元のWSSEはsha1だった。WSSEはワンタイムである必要がある)
PasswordDigest = base64(sha256(APIKey.base64_decode(Nonce).Created.Password))ID/PW以外にsecretのAPIkeyを付けることで2要素認証的な意味でセキュリティを強化できる。 また、アプリごとにトークンを使い分けることや、(不正ログインなどの)問題が発生した場合にキーを無効にできたり、より柔軟な運用が可能になる。複数回ログイン失敗で検知できる仕組みは必要だが、CAPTCHAなどのロックの仕組みは必要ないだろう。
ただ、このようなオレオレ認証にはリスクが伴う。
snapchatの事件(http://www.thread-safe.com/2014/01/46m-usernames-phone-numbers-leaked-by.html) も起きたばかりなので慎重に考えていきたいところである。
(2016/1/6 追記) BaaS vte.cxでは結局、こんな感じで整理した。=> セキュリティについて
(※1) もしクローズドなHTTPクライアントだとしたらどうやってSSLチェックしてホスト証明したと示せるのかが問題になる。まずはクライアント自体が信頼できるものであることを証明できないといけない。詳しくはこちらの資料(http://www.slideshare.net/shunsuketaniguchi520/ssl-28267938)のP7-を参照。適切なサーバ証明書の設定により接続先のサーバの正当性は検証できるが、スマホアプリにはアドレスバーがないものが多く、利用者が能動的に接続先や接続プロトコル(https)を確かめるのが困難である。
(※2) Saltが無いハッシュでは生パスワードが推測可能になるという危険があります。sha256(APIkey+nonce+日時+パスワード)のnonceがソルトに相当します。さらにDBに保存するときにストレッティングして保存、または暗号化します。
@ritouさんからご意見いただいた。感謝です。
ブラウザ-Webアプリ間のミニマムな認証実装ってHTTPS使ってID/PW/CSRFトークンをPOSTで送り、レスポンスのCookieを引き回してるだけ。けっこうしょぼくね?WebAPIで認証したけりゃHTMLをJSONにすればいいのか?みたいな話はどこかであってもいいと思う。
— 秋田の猫 (@ritou) 2014, 1月 16
@stakezaki 同意です。 私はブラウザ認証と同等な機能をAPIでって話をするとき、OAuthのResource Owner Password Credentialsを出します。OAuthは認可だうんぬん来ますが、ブラウザがリクエストにCookieを付けるのは認可でしょと。
— 秋田の猫 (@ritou) 2014, 1月 16
@stakezaki そこからユーザー目線で気持ち悪さはないのか考えましょうと。 APIを使うのがブラウザと同じぐらい信用される公式アプリとかならID/PW預けていいと思うし、そうじゃないならPWの扱いはWebブラウザに任せるべき?アプリの動く環境によってはUX下がる困るねと。
— 秋田の猫 (@ritou) 2014, 1月 16
@stakezaki そんな感じでまずはPW扱うのかどうかを決め、OAuthで気になるとこがないか確認する流れが良いのではないでしょうか。強化したいとこがあったらそれ拡張仕様ですねと。オレオレプロトコルは大穴残すかいろいろ考えてOAuthっぽくなるかのどっちかだと思います。
— 秋田の猫 (@ritou) 2014, 1月 16
ごもっとも。ちょっと、Resource Owner Password Credentialsについて調べてみます。
https://github.com/applicake/doorkeeper/wiki/Using-Resource-Owner-Password-Credentials-flow によれば、
以下のリクエストをOAuth Providerに投げると、
{
"grant_type" : "password",
"username" : "user@example.com",
"password" : "sekret",
"client_id" : "the_client_id",
"client_secret" : "the_client_secret"
}
以下のように、access_tokenが返ってくるので、
{
"access_token": "1f0af717251950dbd4d73154fdf0a474a5c5119adad999683f5b450c460726aa",
"token_type": "bearer",
"expires_in": 7200
}
あとは、Authorizationヘッダに"Bearer " + access_token
をAPI実行時のヘッダに付ければいけます。
PS. しかし、これはPWが生で送られている。パスワードを送信してたらログに書くとかろくでもないことをする人が出てくるという経験則がある。
オレオレ認証作ったやつには税金かけろみたいな話( http://togetter.com/li/617280 )が聞こえてきて(;゜Д゜)ガクブル だしなあ。困った。
>APIを使うのがブラウザと同じぐらい信用される公式アプリとか
返信削除まずここのハードルが滅茶苦茶高いですよね…。
ブラウザの公式サイトからdlしたブラウザはまず間違いなく世界中に数多居るセキュリティ専門家によって「鍵マークが表示されてたら閲覧先とssl通信してる」事を検証されてるけど、
高々数十万ユーザーのマイナーアプリではそんな事期待できない。
一般ユーザーが技術的な策を講じずに自分で確認出来る事なんてせいぜい「正しいサーバーと接続してる」事ぐらいで、「通信には漏れなく毎回sslが使われる」事なんて確かめようがない。