Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

Basic認証についてざっくりまとめる

目次

概要

Basic認証とフォーム認証の違いをあまりよくわかっていなかったので、まとめます。

Basic認証とは

Basic認証とは、HTTPのプロトコルに標準で用意されている認証方法の1つです。Basic認証を導入している場合、Basic認証が必要なページをリクエストすると、ブラウザがフォームのポップアップを自動で表示してくれます。そこで正しいユーザー名とパスワードを入力して認証に成功すると、ページを表示できます。認証に失敗した場合、再度フォームのポップアップが表示されます。

HTTPがステートレスなプロトコルなので、Basic認証はステートレスな認証方式であると言えます。Basic認証では一度認証が成功すると、HTTPリクエストのたびにAuthorizationヘッダを用いて認証情報を送っています。そのため、HTTPのようなステートレスなプロトコルでもステートフルな通信ができます。Basic認証では、従来のフォーム認証のようにサーバー側でクライアントの認証状態を保持したりしません。ゆえに、Basic認証はステートレスな認証方式であることが分かります。

Basic認証では、認証情報がブラウザに保存されるそうです。 また、一度ブラウザを閉じてしまうと認証情報が破棄されるので、再度Basic認証が必要なページをアクセスする場合、また認証情報を入力する必要があります。

www.itra.co.jp

Basic認証とよくあるフォーム認証との違いとは

いくつかの観点で違いがあるので、まとめます。

ステートレス、ステートフルについて

Basic認証はHTTPリクエストのたびに認証情報を送っているので、ステートレスな認証方法です。 フォーム認証では、HTTPリクエストのたびにクッキーを送っていますが、サーバー側でクライアントの状態をセッション情報として保持しているので、ステートフルな認証方式です。

認証フォームについて

Basic認証の認証フォームは、ブラウザによって自動で表示されるものです。 フォーム認証のフォームは、HTMLのフォームです。そのため、自分でHTMLを用いてフォームを作る必要があります。

認証情報の保持について

Basic認証ではブラウザを閉じたら認証情報が消えます。 フォーム認証では、ブラウザを閉じても、クッキーにsession_idが存在して、サーバー側にセッション情報が存在していてれば、ブラウザを再度開いても認証状態を維持できます。

認証情報の有効範囲について

Basic認証の場合、http://example.com/basicBasic認証をした場合、/basic以下のリソースをリクエストする際に、ブラウザが自動的にAuthorizationヘッダを付与してくれます。 そのため、http://example.com/basicBasic認証をした場合、http://example.com/basic/sampleBasic認証を必要とするリソースだとしても、リクエスト時にブラウザが自動でAuthorizationヘッダを付与してくれるので、認証フォームに入力しなくてもhttp://example.com/basic/sampleのページを閲覧することができます。

フォーム認証の場合、ログインフォームで認証が成功すれば、あとは認証が必要なページは全て見ることができます。Basic認証のようにあるリソース配下のリソースしか見れないという制約はないです。

ユーザー情報について

Basic認証も、フォーム認証も事前にユーザー情報を登録しておく必要があります。登録フォームでユーザー情報を登録すれば良い話なので、ここはあまり差がないかなという印象です。

ログアウトについて

Basic認証にはログアウトという機能はありませんが、フォーム認証にはログアウトという機能があります。Basic認証でログアウトをしたい場合、一度ブラウザを閉じて認証情報が破棄する必要があります。

Basic認証とフォーム認証の認証フローを比較してみる

Basic認証

1.クライアント(ブラウザ)がBasic認証が必要なページをリクエストする。

GET / HTTP/1.1

2.サーバーは、Basic認証の必要性を表すHTTPレスポンスを返す。Basicの部分はauth-schemeと呼び、認証方式を指定する。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Private page"

3.それを受け取ったクライアントはフォームのポップアップを自動で表示する。

4.フォームのポップアップにusernameとpasswordを入力して送信するボタンを押す。この送信するボタンを押したタイミングで、クライアントはusernameとpasswordを読み取って、username:passwordBase64エンコードしたものをHTTPヘッダのAuthorizationヘッダに含めて、サーバーにHTTPリクエストする。

GET / HTTP/1.1
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW

5.サーバー側ではそのAuthorizationヘッダに指定してあるエンコードされているデータが、サーバー側で事前に登録してあるユーザー情報と一致するかをチェックして、もし一致するなら、認証後のページを返す。一致しないなら、再度フォームのポップアップをブラウザに表示させるようなHTTPレスポンスを返す。

以下は、Basic認証の認証フローの画像です。

画像引用(HTTP認証)

developer.mozilla.org

■フォーム認証

  1. HTMLのフォームに認証情報を入力する(メールアドレスとパスワード)
  2. フォームから送信された情報を元にサーバー側で認証をして、OKならサーバー側でセッションidとユーザー情報を紐づけてRedisサーバーに保存して、session_idという名前でvauleがセッションidであるようなクッキーを送信する。この時リダイレクトの指示も送る。
  3. クライアント(ブラウザ)側ではクッキーを受け取りつつ、リダクレイトの指示を受け取ったので、Locationヘッダに指定してあるパスにGETリクエストをする。この際、受け取ったクッキーも送信する(ブラウザが自動的に送信してくれる)。
  4. サーバー側ではクライアントから送信されたクッキーを元に、ユーザーのセッションが存在するかを確認する。もし存在するなら、サーバー側が認証後のページを送信する。存在しないなら、ログインページにリダイレクトさせる。

Basic認証の問題点

Basic認証ではフォームのポップアップから認証情報をサーバーに送る際に、username:passwordBase64エンコードしたものを送ります。このBase64は可逆なため、デコードしてユーザーのパスワードを取得することができます。Basic認証ではリクエストのたびにこれらの情報を送るので、盗まれるリスクも高いです。

www.aeyescan.jp

以上の理由から、セキュリティリスクの高い場面では、Basic認証は避けた方が良いことが分かります。

Basic認証を実装してみる

Basic認証をGoで実装してみます。

実際のコードに関しましては、以下のリポジトリにまとめます。 github.com

実装した部分を一部抜粋します。

↓ ルーティング

var Routing = []*pattern.URLPattern{
    pattern.NewURLPattern("/basic", middleware.CheckBasicAuthentication(controller.NewBasicAuthentication())),
    pattern.NewURLPattern("/basic/sample", middleware.CheckBasicAuthentication(controller.NewSample())),
}

ミドルウェア(ミドルウェアの中でBasic認証のメソッドを呼び出している)

package middleware

import (
    "github.com/yukiHaga/web_server/src/internal/app/controller"
    "github.com/yukiHaga/web_server/src/pkg/henagin/http"
)

type CheckBasicAuthenticationController struct {
    nextAction func(request *http.Request) *http.Response
}

func (c CheckBasicAuthenticationController) Action(request *http.Request) *http.Response {
    if request.CheckBasicAuthentication() {
        // 次に渡す
        return c.nextAction(request)
    } else {
        // Authorizationヘッダーがないか、あるけどvalueが間違っている場合
        // 再度Unauthorizedを返す
        response := http.NewResponse(
            http.VersionsFor11,
            http.StatusUnauthorizedCode,
            http.StatusReasonUnauthorized,
            request.TargetPath,
            []byte{},
        )

        // WWW-Authenticateヘッダーをセットする
        response.SetBasicAuthenticationHeader()
        return response
    }
}

// ミドルウェア
// / ダミーコントローラとダミーアクションを作って、ダミーアクションの中で元々のコントローラのアクションを呼び出して、最終的にレスポンスを返せばOK
// goではメソッドを書き換えるのはできなかった
func CheckBasicAuthentication(c controller.Controller) controller.Controller {
    return CheckBasicAuthenticationController{nextAction: c.Action}
}

ミドルウェアで実行していたビジネスロジック(Basic認証に成功するかを判定するメソッド。このメソッド内でユーザー情報を保持しているけど、ユーザー情報はDBに保持する方式にしてもOK)

func (request *Request) CheckBasicAuthentication() bool {
    users := []*model.BasicUser{
        model.NewBasicUser("yuki", "hogefuga"),
    }

    if authorizationHeader, isThere := request.Headers["Authorization"]; isThere {
        encodedData := strings.SplitN(authorizationHeader, " ", 2)[1]
        for _, user := range users {
            data := fmt.Sprintf("%s:%s", user.Name, user.Password)

            if base64.StdEncoding.EncodeToString([]byte(data)) == encodedData {
                return true
            }
        }
    }

    return false
}

Basic認証を満たさない場合に、WWW-Authenticateヘッダーをセットするためのメソッド

func (response *Response) SetBasicAuthenticationHeader() {
    response.SetHeader("WWW-Authenticate", "Basic realm=Secret Page")
}

↓ コントローラ

package controller

import (
    "github.com/yukiHaga/web_server/src/pkg/henagin/http"
    "github.com/yukiHaga/web_server/src/pkg/henagin/view"
)

type BasicAuthentication struct{}

func NewBasicAuthentication() *BasicAuthentication {
    return &BasicAuthentication{}
}

func (c *BasicAuthentication) Action(request *http.Request) *http.Response {
    // ミドルウェアにおけるbasic認証を通過したので、認証が必要なページを見れる
    body := view.Render("basic_authentication.html")
    return http.NewResponse(
        http.VersionsFor11,
        http.StatusSuccessCode,
        http.StatusReasonOk,
        request.TargetPath,
        body,
    )
}

↓ モデル(Basic認証で使うユーザーを表すモデル)

package model

type BasicUserId int64

// PasswordとConfirmationはRailsの家蔵属性として入っていたから、一応入れておいた
type BasicUser struct {
    Name     string
    Password string
}

func NewBasicUser(name, password string) *BasicUser {
    return &BasicUser{
        Name:     name,
        Password: password,
    }
}

↓ 認証後に見れるページ

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Basic認証を通過した人だけが見れるページ</h1>
  </body>
</html>

動作確認

basic認証が必要なページをリクエストすると、サーバーがUnauthorizedをレスポンスして、それを受け取ったブラウザがフォームを自動で出してくれます。

Image from Gyazo

不正な認証情報を入力してリクエストすると、サーバーは再度Unauthorizedをレスポンスして、それを受けとったブラウザがまたフォームを表示しています。

Image from Gyazo

正しいusernameとpasswordを入力して送信を押すと、ブラウザがusername:passwordBase64エンコードしたものをAuthorizationヘッダに含めてリクエストしてくれます。サーバー側で認証が成功すると、サーバーはリソースを送信します。

Image from Gyazo

リクエストヘッダーに注目すると、一回でもBasic認証を通過した場合、ブラウザはリクエスト時に自動的にAuthorizationヘッダーを付与していることが分かります。 Image from Gyazo

終わり

今回は自前でBasic認証を実装しましたが、ライブラリを使えばもっと簡単に早く実装できます。Basic認証は、社内ツールなどのセキュリティ要件が求められていないサービスだったり、ごくわずかな人にしか公開しないサービスには採用しても良いのかなと思いました。 (確かAWSのCognitoとかを使えば簡単に実装できた気がする)

次はBearer認証を実装してみます。

参考記事

Basic認証とBearer認証を作ってみる