OAuth 2.0で認証してみた。

まずOAuth 2.0は認可フレームワークで認証は直接サポートされない。 だけどユーザー属性参照への認可(アクセストークン)で似たことができる。

実装が簡単なためOAuth 2.0で定義されているAuthorization Code Grantを利用して認証をする人が増えた。 これは野良ハックであって認証として合意は取られていない。 なので認証としての機能拡充や仕様として整理するためにOAuth 2.0上の認証機構OpenID Connect 1.0が定められた(と思われる)。

OpenID Connect 1.0のクライアントは利用するだけなら簡単との事だけど仕様を理解するのにだいぶ時間が掛かりそうだったので OAuth 2.0のハック版の認証を試してみた。

Authorization Code GrantにはセキュリティリスクがありRFC7636で対策が策定されている。 だけどgithubのドキュメントで対応を見つけられず今回は実装していない。

資料

まず概要をつかむための読み物として下を読んだ。

仕様

OAuth 2.0では第三者(Client)にリソースへのアクセスを認可するフローが定義されている。 ここではOAuth 2.0の要素とエンドポイントとフローを整理する。

仕様書

まず仕様書だがRFCで検索すると出て来る。 仕様は複数のドキュメントで構成されている。

今回ためしたAuthorization Code Grantにはセキュリティリスクがあり RFC7636で対策が策定されている。

仕様本体(トークン取得)

RFC6749 RFC6749(ja)

トークンの使い方

RFC6750, RFC6750(ja)

トークン操作(オプショナル)

取り消し(RFC7009), 情報取得(RFC7662)

セキュリティ

脅威モデル(RFC6819), 認可フロー対策(RFC7636)

認可フロー

5つの認可フローが定義されている。 これらの認可フローを実現するために認可サーバーは以下の2つのエンドポイントを提供する必要がある。

Authorization Endpoint
リソースオーナーが認証するエンドポイント
Token Endpoint
アクセストークンを取得するエンドポイント

Implicit GrantのみAuthorizationエンドポイントがアクセストークンを提供する。 それ以外のフローではtokenエンドポイントがアクセストークンを提供する。

5つのフローは以下である。RFC6749での各節へのリンクと特徴的なパラメタを整理しておく。

普段はAuthorization Code Grant 以外はあまり気にしなくていい。

認証済みリクエスト

認可を得られたら実際にリソースへアクセスする。 トークンの使い方はBearer Token Usage(RFC6750)で定義されている。 認可トークンを用いて実際にリソースにアクセスするにはリソースサーバーには認証済みリクエストを送ることが必要で、認証済みリクエストは以下の3種類が定義されていている。

  • Authorization Request Header Field
  • Form-Encoded Body Parameter
  • URI Query Parameter

Authorization Request Header Field

RFCのリクエストサンプルをそのまま下に書くと以下の様になる。

GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM

AuthorizationヘッダのスキーマをBearerにしてトークンを続けて送る。 クライアントはSHOULDでこの形式を利用する。リソースサーバーはMUSTでこの形式をサポートする。

Form-Encoded Body Parameter

RFCのリクエストサンプルをそのまま下に書くと以下の様になる。

POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

access_token=mF_9.B5f-4.1JqM

Content-Typeapplication/x-www-form-urlencoded と厳格に一致させる。また値はaccess_token に格納しW3C.REC-html401-19991224に従う。 request-bodyが使えるメソッドでリクエストする(特にGETはMUST NOT)。 など色々従う。参加しているAuthorizationヘッダへアクセス出来ない場合を除いてクライアントがこの形式を使うのはSHOULD NOTとされ推奨されない。 サーバー側がサポートするのは良い(MAY)。

URI Query Parameter

URIクエリパラメタを用いたアクセス形式もある。 サンプルリクエストは下でaccess_token パラメタを経由して渡される。

GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com
Cache-Control: no-store

このリクエストはセキュリティ的な欠点があるためできるだけ利用するべきではなく(SHOULD NOT)。 利用する場合はセキュリティの目的でCache-Controlについて強く推奨される設定がある。

クライアントはno-storeオプションを有効にして送信するべき(SHOULD)。 サーバはprivateオプションを有効にして送信するべき(SHOULD)。

リダイレクト

Authorization Endpointへリソースオーナーを導いたり、認証後に特定コンテンツに遷移したりはリダイレクトなどで実現する。 RFC5749では302を使う例で書かれているが実装の詳細に関わるのでリダイレクトできれば何でも良いって書かれていた。 聞いてみたけどUAの対応を考えて実装者が頑張れJavaScriptでもmetaタグでも何でも良いって感じらしい。 またネイティブアプリでリダイレクトだとブラウザに白い画面が残るらしい。

実装

oauth2ライブラリを利用してクライアントを認証機構を実装してみた。 設計不備がある。これくらいの観点は雑に書いても無意識のうちに満たしていたかったけど職人ではありませんでした。

  • 1つのリソースオーナーしか利用できない
  • サイト内のログインページがない
  • ログイン・ユーザーなどの状態管理をメモリでやっていて差し替えられない
  • ログイン・ユーザーなどのセッション情報用Cookieのパスやドメインをカスタムできない

とりあえず下の様に使う。

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func main() {
        var addr string
        flag.StringVar(&addr, "addr", ":8080", "addr")
        flag.Parse()

        conf := &oauth2.Config{
                RedirectURL: "http://localhost:8080/auth/github/callback",
                Endpoint: oauth2.Endpoint{
                        AuthURL:  "https://github.com/login/oauth/authorize",
                        TokenURL: "https://github.com/login/oauth/access_token",
                },
                Scopes: []string{"user", "public_repo", "repo"},
        }
        conf.ClientID = os.Getenv("GITHUB_CLIENT_ID")
        conf.ClientSecret = os.Getenv("GITHUB_CLIENT_SECRET")

        m := http.NewServeMux()
        safen := login.AddService(m, "/auth/github", login.NewService(conf))

        m.HandleFunc("/path-1", safen(func(w http.ResponseWriter, r *http.Request) {
                w.WriteHeader(http.StatusOK)
                w.Write([]byte("hello path-1"))
        }))
        m.HandleFunc("/path-2", func(w http.ResponseWriter, r *http.Request) {
                w.WriteHeader(http.StatusOK)
                w.Write([]byte("hello path-2"))
        })
        // サーバー設定
        s := &http.Server{
                Addr:         addr,
                Handler:      m,
                ReadTimeout:  1 * time.Second,
                WriteTimeout: 1 * time.Second,
        }
        fmt.Printf("%#v\n", s)
        if err := s.ListenAndServe(); err != nil {
                log.Fatalln(err)
        }
}

自分でワークフローを書いてみて勉強にはなったけど不完全に終わった。 どこかに柔軟で標準ライブラリと相性が良いミドルウェアみたいな実装が転がってないだろうか。 gothicをforkしたなんて方もいました。 まだまだGithub検索(oauth2), GolangLibs検索(oauth2)あたりで探すフェーズ。