RailsでAuthorizationヘッダを用いた認証を実装する

背景

仕事でAPIの実装を行うことになりました。ログイン後に発行するtokenでユーザー認証が必要ということで、どういった方法があるかを調べてみました。Authorizationヘッダを用いることが多いのは知っていたので調べてみることにしました。

Authorizationヘッダについて

Authorizationの仕様についてはしっかりと理解していなかったので改めて調べてみました。

Authorization - HTTP | MDN

このサイトに記載されているように、
Authorization: <type> <credentials>
が構文のようです。以前、 Authorization: <credentials>という実装をしていましたが、こちらは仕様に準拠していないことになります。

typeについて

Typeにはいろんな種類があるようです。個人的によく見るのは Bearerです。OAuth認証で利用したりしました。
先ほどのMDNの中にtypeの一覧へのリンクがありました。
Hypertext Transfer Protocol (HTTP) Authentication Scheme Registry

TypeがBasicだった場合、credentialsをBase64でデコードする必要があったりして、少し手間がかかりそうな印象です。

Bearerを試してみる

まずは簡単にできそうなBearerを試してみます。
API側では、authenticate_or_request_with_http_tokenメソッドを用います。

1
2
3
4
5
6
def authenticate
authenticate_or_request_with_http_token do |token, _options|
@user = User.find_by_token token
head status: 401 if @user.blank?
end
end

クライアント側はcurlを使います

1
2
$ curl -H 'Authorization: Bearer xxx_credentials' https://api_endpoint
HTTP Token: Access denied.

認証が失敗してしまいました…
ブロック内でbinding.pryを呼び出してもスルーされることから、ブロックに到達する前に認証に失敗しているようです。

いろいろ調べてみると、以下のサイトが見つかりました。

週刊Railsウォッチ(20191118前編)ActiveJob引数のログ抑制、RailsガイドProプランお試し、ファイルアップロードのレジュームgemほか|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

BearerだけではなくTokenでも良いようなので、Tokenを試してみます

1
$ curl -H 'Authorization: Token xxx_credentials' https://api_endpoint

これだとよくわからないので、レスポンスヘッダを表示してみます

1
2
3
4
5
6
7
8
9
10
11
$ curl -D - -H 'Authorization: Token xxx_credentials' https://api_endpoint
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"d396de9e73b218ac6b8f0de7967e82bc"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 0f90b919-e9f6-42ee-a76a-96930fbfdff2
X-Runtime: 0.064172
Transfer-Encoding: chunked

認証成功しました!

ということはTokenではよくてBearerはダメということですね。原因を調査するためにauthenticate_or_request_with_http_tokenを調べてみます。
該当のバージョンでgithubを探すと該当箇所が見つかりました。

rails/http_authentication.rb at 4-2-stable · rails/rails · GitHub

なんと、Bearerでは引っかからないようになっています…
Tokenだと認証可能ですが、Tokenはの定義にはありません…

Basicを試す

こうなったら一般的なBasicを試してみます。credentialsの部分はユーザー名:パスワードになりますが、今回ユーザー名がないので:パスワードとなり、パスワードの部分にcredentialsを指定します。

API側ではauthenticate_or_request_with_http_basicを利用します

1
2
3
4
5
6
def authenticate
authenticate_or_request_with_http_basic do |_user, password|
@user = User.find_by_token password
head status: 401 if @user.blank?
end
end

クライアント側はBase64.encode64(‘:xxx_credentials’)でBase64エンコードした文字列を準備します。(先頭の:を忘れない)

1
2
$ curl -H 'Authorization: Basic base64_encode_credentials' https://api_endpoint 
HTTP/1.1 200 OK

うまく認証されました!

まとめ

認証はセキュリティ的に重要な箇所になるので、慎重に実装しましょう。
AuthorizationヘッダなどHTTPの仕様をしっかり理解してからどの方法を使うかを検討しましょう。

おまけ

今回クライアント側としてcurlを使いました。有用なオプションがいっぱいありますね。以下にまとめたいと思います。

  • -D (–dump-header)

    HTTPレスポンスヘッダを表示します。標準出力に出力するには-を指定します。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ curl -D - https://www.google.com
    HTTP/2 200
    date: Wed, 16 Sep 2020 17:16:16 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    (省略)
  • -o

    レスポンスボディの出力先を指定します。不要な場合に/dev/nullを指定したりします。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    $ curl -D - -o /dev/null https://www.google.com
    % Total % Received % Xferd Average Speed Time Time Time Current
    Dload Upload Total Spent Left Speed
    0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0HTTP/2 200
    date: Wed, 16 Sep 2020 17:19:47 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    x-frame-options: SAMEORIGIN
    set-cookie: 1P_JAR=2020-09-16-17; expires=Fri, 16-Oct-2020 17:19:47 GMT; path=/; domain=.google.com; Secure
    set-cookie: NID=204=rwcy-xyJUKkXMHIec1I-ETPuB_hzV-mbxnl5ot0DhU2q2B-I-aRIVHKkHFyKvsqESZqjRXQFxn7YjzjmvTkflbrfzePsc87d7F7GFELqNbXvPvATIo2v6EaeaHzIXrsoAL4LRKd20uniGfFR6dN8pq3gB4O4SPDzjYd7EJvWLaA; expires=Thu, 18-Mar-2021 17:19:47 GMT; path=/; domain=.google.com; HttpOnly
    alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
    accept-ranges: none
    vary: Accept-Encoding

    100 13141 0 13141 0 0 62279 0 --:--:-- --:--:-- --:--:-- 62279
  • -s

    進捗を表示しないようにします。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ curl -D - -o /dev/null -s https://www.google.com
    HTTP/2 200
    date: Wed, 16 Sep 2020 17:21:07 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    x-frame-options: SAMEORIGIN
    set-cookie: 1P_JAR=2020-09-16-17; expires=Fri, 16-Oct-2020 17:21:07 GMT; path=/; domain=.google.com; Secure
    set-cookie: NID=204=Mdp8loY0lxxV_Kf-YoGRq3db-HvCsX81ffRzePmxfFeT6gb2A72Ijy8AoG5PHsjGj962rqODoZ0VXn9ZMen5MPFGnUww7qnmuexWFLC0JGlyR07K1iIO3iAub_CFUzdjZogChUYvm1Qo9yxyzlm2pb0A2i44va50JOIBi-8qrDE; expires=Thu, 18-Mar-2021 17:21:07 GMT; path=/; domain=.google.com; HttpOnly
    alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
    accept-ranges: none
    vary: Accept-Encoding

    ヘッダだけ欲しい場合はこれで良いですね。

  • -X

    HTTPメソッドを指定します。指定しない場合はGETになります。

    例:

    1
    2
    3
    4
    5
    6
    7
    $ curl -X POST https://www.google.com
    <!DOCTYPE html>
    <html lang=en>
    <meta charset=utf-8>
    <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
    <title>Error 405 (Method Not Allowed)!!1</title>
    (省略)

    405が返ってきてますね…POSTには対応してないようです

  • -F(–form)

    フォームの値を指定します。key=valueという形式で指定します

    例:

    1
    $ curl -X POST -F 'username=user' -F 'password=password' https://somewhere_login_url

他にもいろいろあるようですが、また何かわかったら追記します。