認証とセキュリティ

Cookie と署名付き Cookie

set_cookie メソッドを使用して、ユーザーのブラウザに Cookie を設定できます。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_cookie("mycookie"):
            self.set_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

Cookie は安全ではなく、クライアントによって簡単に変更される可能性があります。たとえば、現在ログインしているユーザーを識別するために Cookie を設定する必要がある場合は、偽造を防ぐために Cookie に署名する必要があります。Tornado は、set_signed_cookieget_signed_cookie メソッドを使用して署名付き Cookie をサポートしています。これらのメソッドを使用するには、アプリケーションを作成するときに cookie_secret という名前の秘密鍵を指定する必要があります。アプリケーション設定はキーワード引数としてアプリケーションに渡すことができます。

application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

署名付き Cookie には、タイムスタンプと HMAC 署名に加えて、Cookie のエンコードされた値が含まれています。Cookie が古いか、署名が一致しない場合、get_signed_cookie は Cookie が設定されていない場合と同様に None を返します。上記の例的安全なバージョン

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_signed_cookie("mycookie"):
            self.set_signed_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

Tornado の署名付き Cookie は整合性を保証しますが、機密性は保証しません。つまり、Cookie は変更できませんが、その内容はユーザーによって見ることができます。cookie_secret は対称鍵であり、秘密にしておく必要があります。この鍵の値を取得した人は誰でも、独自の署名付き Cookie を作成できます。

デフォルトでは、Tornado の署名付き Cookie は 30 日後に期限切れになります。これを変更するには、set_signed_cookieexpires_days キーワード引数と、get_signed_cookiemax_age_days 引数を両方使用します。これら2つの値は別々に渡されるため、たとえば、ほとんどの目的で30日間有効なCookieを持ち、請求情報などの特定の機密操作に対しては、Cookieを読み取る際に小さいmax_age_daysを使用できます。

Tornado は、署名キーのローテーションを有効にするために、複数の署名キーもサポートしています。cookie_secret は、整数キーのバージョンをキーとし、対応するシークレットを値とする辞書である必要があります。現在使用されている署名キーは key_version アプリケーション設定として設定する必要がありますが、正しいキーバージョンがCookieに設定されている場合は、辞書内の他のすべてのキーがCookie署名検証に使用できます。Cookieの更新を実装するには、get_signed_cookie_key_version を介して現在の署名キーバージョンを照会できます。

ユーザー認証

現在認証されているユーザーは、すべてのリクエストハンドラーで self.current_user として、すべてのテンプレートで current_user として使用できます。デフォルトでは、current_userNone です。

アプリケーションでユーザー認証を実装するには、たとえば Cookie の値に基づいて現在のユーザーを決定するために、リクエストハンドラーの get_current_user() メソッドをオーバーライドする必要があります。これは、ユーザーがニックネームを指定するだけでアプリケーションにログインできる例です。ニックネームはCookieに保存されます。

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_signed_cookie("user")

class MainHandler(BaseHandler):
    def get(self):
        if not self.current_user:
            self.redirect("/login")
            return
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

class LoginHandler(BaseHandler):
    def get(self):
        self.write('<html><body><form action="/login" method="post">'
                   'Name: <input type="text" name="name">'
                   '<input type="submit" value="Sign in">'
                   '</form></body></html>')

    def post(self):
        self.set_signed_cookie("user", self.get_argument("name"))
        self.redirect("/")

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

Python デコレーター tornado.web.authenticated を使用して、ユーザーがログインしていることを要求できます。このデコレーターが付いたメソッドにリクエストが行われ、ユーザーがログインしていない場合、login_url(別のアプリケーション設定)にリダイレクトされます。上記の例は書き直すことができます。

class MainHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

settings = {
    "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
    "login_url": "/login",
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

post() メソッドを authenticated デコレーターでデコレートし、ユーザーがログインしていない場合、サーバーは 403 レスポンスを送信します。@authenticated デコレーターは単に if not self.current_user: self.redirect() の略記であり、ブラウザ以外のログインスキームには適していない可能性があります。

認証を使用する完全な例(およびユーザーデータを PostgreSQL データベースに保存する)については、Tornado ブログの例アプリケーション をご覧ください。

サードパーティ認証

tornado.auth モジュールは、Google/Gmail、Facebook、Twitter、FriendFeed など、Web 上で最も人気のあるサイトのいくつかについて、認証と承認のプロトコルを実装しています。このモジュールには、これらのサイトを介してユーザーをログインさせるメソッドと、該当する場合はサービスへのアクセスを承認するメソッド(ユーザーのアドレス帳をダウンロードしたり、代わりに Twitter メッセージを公開したりする場合など)が含まれています。

これは、認証に Google を使用し、後でアクセスするために Google の資格情報を Cookie に保存するハンドラーの例です。

class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
                               tornado.auth.GoogleOAuth2Mixin):
    async def get(self):
        if self.get_argument('code', False):
            user = await self.get_authenticated_user(
                redirect_uri='http://your.site.com/auth/google',
                code=self.get_argument('code'))
            # Save the user with e.g. set_signed_cookie
        else:
            await self.authorize_redirect(
                redirect_uri='http://your.site.com/auth/google',
                client_id=self.settings['google_oauth']['key'],
                scope=['profile', 'email'],
                response_type='code',
                extra_params={'approval_prompt': 'auto'})

詳細は、tornado.auth モジュールのドキュメントを参照してください。

クロスサイトリクエストフォージェリ保護

クロスサイトリクエストフォージェリ(XSRF)は、パーソナライズされた Web アプリケーションの一般的な問題です。

XSRF を防ぐために一般的に受け入れられている解決策は、予測不可能な値で各ユーザーに Cookie を設定し、その値をサイト上のすべてのフォーム送信に追加の引数として含めることです。Cookie とフォーム送信の値が一致しない場合、リクエストは偽造されている可能性が高いです。

Tornado には、組み込みの XSRF 保護が備わっています。サイトに含めるには、アプリケーション設定 xsrf_cookies を含めます。

settings = {
    "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
    "login_url": "/login",
    "xsrf_cookies": True,
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

xsrf_cookies が設定されている場合、Tornado Web アプリケーションはすべてのユーザーに対して _xsrf Cookie を設定し、正しい _xsrf 値を含まないすべての POSTPUT、および DELETE リクエストを拒否します。この設定をオンにした場合、POST で送信するすべてのフォームにこのフィールドを組み込む必要があります。これは、すべてのテンプレートで使用できる特別な UIModule xsrf_form_html() を使用して実行できます。

<form action="/new_message" method="post">
  {% module xsrf_form_html() %}
  <input type="text" name="message"/>
  <input type="submit" value="Post"/>
</form>

AJAX POST リクエストを送信する場合、各リクエストに _xsrf 値を含めるように JavaScript を調整する必要もあります。これは、FriendFeed で AJAX POST リクエストに使用している jQuery 関数であり、すべてのリクエストに _xsrf 値を自動的に追加します。

function getCookie(name) {
    var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
    return r ? r[1] : undefined;
}

jQuery.postJSON = function(url, args, callback) {
    args._xsrf = getCookie("_xsrf");
    $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
        success: function(response) {
        callback(eval("(" + response + ")"));
    }});
};

PUT および DELETE リクエスト(フォームエンコードされた引数を使用しない POST リクエストも同様)の場合、XSRF トークンは X-XSRFToken という名前の HTTP ヘッダーを介して渡すこともできます。XSRF Cookie は通常、xsrf_form_html が使用されるときに設定されますが、通常のフォームを使用しない純粋な JavaScript アプリケーションでは、self.xsrf_token に手動でアクセスする必要がある場合があります(プロパティを読み取るだけで、Cookie が副作用として設定されます)。

ハンドラーごとに XSRF の動作をカスタマイズする必要がある場合は、RequestHandler.check_xsrf_cookie() をオーバーライドできます。たとえば、認証に Cookie を使用しない API がある場合、check_xsrf_cookie() を何も実行しないようにして、XSRF 保護を無効にすることができます。ただし、Cookie ベースと Cookie ベース以外の認証の両方をサポートする場合は、現在のリクエストが Cookie で認証されているときは常に XSRF 保護を使用することが重要です。

DNS リバインディング

DNSリバインディングは、同一オリジンポリシーを回避し、外部サイトがプライベートネットワーク上のリソースにアクセスすることを可能にする攻撃です。この攻撃は、(短いTTLを持つ)DNS名を使用し、攻撃者によって制御されるIPアドレスと、被害者によって制御されるIPアドレス(多くの場合、127.0.0.1192.168.1.1などの推測可能なプライベートIPアドレス)との間で交互に切り替わります。

TLSを使用するアプリケーションは、この攻撃に対して脆弱ではありません(ブラウザは証明書ミスマッチの警告を表示し、ターゲットサイトへの自動アクセスをブロックするためです)。

TLSを使用できず、ネットワークレベルのアクセス制御に依存するアプリケーション(たとえば、127.0.0.1上のサーバーはローカルマシンからのみアクセスできると仮定する)は、Host HTTPヘッダーを検証することでDNSリバインディングから保護する必要があります。これは、HostMatchesルーターまたはApplication.add_handlersの最初の引数に、制限的なホスト名パターンを渡すことを意味します。

# BAD: uses a default host pattern of r'.*'
app = Application([('/foo', FooHandler)])

# GOOD: only matches localhost or its ip address.
app = Application()
app.add_handlers(r'(localhost|127\.0\.0\.1)',
                 [('/foo', FooHandler)])

# GOOD: same as previous example using tornado.routing.
app = Application([
    (HostMatches(r'(localhost|127\.0\.0\.1)'),
        [('/foo', FooHandler)]),
    ])

さらに、Applicationdefault_host引数とDefaultHostMatchesルーターは、DNSリバインディングに対して脆弱となる可能性のあるアプリケーションでは使用しないでください。これは、ワイルドカードホストパターンと同様の効果を持つためです。