こんにちは、エンジニアの稲垣です。
はじめに
この記事はこちらの続きです。
今回は前回Auth0に設定した情報をもとに認証のGoのクライアントを実装してみます。
なお、OpenID Connectの仕様は深掘りせずに、ユーザ認証の雰囲気を掴むことを目的としています。
それでは、見てみましょう。
認証フローについておさらい
あらためて、認証フローについて確認します。
ユーザのログインボタンクリックなどをトリガーにアプリケーションサーバにリクエストが送信されます。
アプリケーションサーバはリクエストを受信すると、クライアント識別子(Client ID / Client Secretなど)を含めたリクエストを生成し、OPにリダイレクトします。
OPはリクエスト情報を検証し、認証・認可画面を表示します。
ユーザは認証に必要な情報を入力し、OPはその情報を検証します。OPは認可コードを付与し、アプリケーションサーバのコールバックURLにリダイレクトします。
アプリケーションサーバは認可コードを元に、OPにIDトークンと呼ばれる署名つきのJWTを要求します。
アプリケーションサーバはOPからIDトークンが返却されるとそれを解析し正当性を検証します。
太字の部分が今回実装するエンドポイントです。
- 認証要求を受け取り、OPへリダイレクトする処理を行うエンドポイントを
/auth
- OPでの認証完了後のコールバック用エンドポイントを
/callback
として実装します。
IDトークン
IDトークンとは単一の認証イベントに対し、一意に付与される署名付きのJWTのことです。
トークンの発行者や、発行時間、ユーザの一意識別子など認証済みのユーザの属性を含みます。
受け取り手はIDトークンを解析し、以下のような単純なチェックを加えることで攻撃から守ることができます。
- 有効なJWTであること
- トークンの署名が正しいこと(発見可能な公開鍵を使う)
- トークンの発行者が正しいこと
- 発行時間や有効期限が有効であること
準備
Auth0にログインし、設定編にて設定したアプリケーション画面を再度確認し、Client ID / Client Secret / Domain をメモしておきます。
また、Allowed Callback URLs
の欄に実装するコールバックURLのエンドポイントを設定します。
使用するライブラリ
今回は以下のライブラリを使用して実装しました。
- github.com/coreos/go-oidc"
- golang.org/x/oauth2"
/auth の実装
さて、それでは実装を見てみましょう。
まずはユーザからのログイン要求をトリガーに認証プロバイダへリダイレクトする処理を実装します。
http.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { provider, err := oidc.NewProvider(r.Context(), issuer) if err != nil { log.Fatal(err) } config := oauth2.Config{ ClientID: [clientID], ClientSecret: [clientSecret], Endpoint: provider.Endpoint(), RedirectURL: "http://localhost:3000/callback", Scopes: []string{oidc.ScopeOpenID}, } state := [ランダムな文字列] authURL:= config.AuthCodeURL(state) http.Redirect(w, r, authURL, http.StatusFound) }) // サーバ起動 http.ListenAndServe(":3000", nil)
この関数内のほとんどは、OPの情報を取得しリダイレクト先のURLを構築する、ということをしています。
provider, err := oidc.NewProvider(r.Context(), issuer) if err != nil { log.Fatal(err) } config := oauth2.Config{ ClientID: [clientID], ClientSecret: [clientSecret], Endpoint: provider.Endpoint(), RedirectURL: "http://localhost:3000/callback", Scopes: []string{oidc.ScopeOpenID}, }
ここに渡している情報がリダイレクトをするのに必要な項目です。
- ClientID / ClientSecretにはAuth0で設定したアプリケーションのClient ID / Client Secret をそれぞれ指定します。
- Endpoint には OPの認証エンドポイントとトークンエンドポイントを渡します。
このエンドポイントを取得するために、
oidc.NewProvider(r.Context(), issuer)
という関数を呼び出していますが、これは認証を開始するために必要となる情報を検出できる、OpenID Connectの仕様を利用しています。 RedirectURL
はOPでの認証完了後に呼び出されるURLです。あとで実装する/callback
を指定します。- Scopeには 少なくとも
openid
の指定が必須となっています。この値は保護されたリソースへのアクセス要求を示していて、エンドユーザのプロフィールを要求する場合はprofile
、メールアドレスを要求する場合にはemail
などの指定をすることができますが、単一の認証イベントを完了するだけであれば、openid
だけで大丈夫です。
これらの情報と state
という値をもとに config.AuthCodeURL(state)
を呼び出すことで、リダイレクトURLを得ることができます。
stateにはランダムな文字列を指定します。
後述の/callbackが呼ばれたときにここで生成した値と同じ値が渡ってくることをチェックすることでCSRF対策になります
動作確認
さてアプリケーションを実行し、動作を確認してみましょう。
http://localhost:3000/auth
にアクセスするとリダイレクトされ、以下のようなAuth0の認証画面が表示されるので、準備編で作成したユーザでログインします。
ログインが成功すると (まだ実装していないので404 エラーになっていますが)/callback
に返ってきているのがわかると思います。
/calbackには code
と state
というパラメータがついています。
code
は認証の結果得られる認可コードでこのコードをもとにIDトークンを要求します。
state
は先に指定したCSRF対策のための文字列と同じものが返ってきていると思います。
/callbackの実装
それでは、認証完了後にコールバックされるエンドポイントの実装を見てみます。
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // この部分は /auth のコードと同じ provider, err := oidc.NewProvider(ctx, issuer) if err != nil { log.Fatal(err) } config := oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: provider.Endpoint(), RedirectURL: "http://localhost:4000/callback", Scopes: []string{oidc.ScopeOpenID}, } // state := r.URL.Query().Get("state") // stateが返ってくるので認証画面へのリダイレクト時に渡したパラメータと矛盾がないか検証 // verifyState(state) // codeをもとにトークンエンドポイントから IDトークン を取得 code := r.URL.Query().Get("code") oauth2Token, err := config.Exchange(ctx, code) if err != nil { http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) return } // IDトークンを取り出す rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { http.Error(w, "missing token", http.StatusInternalServerError) return } oidcConfig := &oidc.Config{ ClientID: clientID, } verifier := provider.Verifier(oidcConfig) // IDトークンの正当性の検証 idToken, err := verifier.Verify(ctx, rawIDToken) if err != nil { http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) return } // アプリケーションのデータ構造におとすときは以下のように書く idTokenClaims := map[string]interface{}{} if err := idToken.Claims(&idTokenClaims); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Printf("%#v", idTokenClaims) fmt.Fprintf(w, "認証成功") })
このAPIは state
と code
という2つのパラメータを受け取ります。
state
は認証エンドポイントへのリダイレクト時に渡したランダムな文字列と同じものが入ります。
矛盾がないか検証し、悪意のあるリクエストでないことを確認します。
config.Exchange()の呼び出しによって code
とIDトークンと交換することができます。
provider.Verifier() 関数を呼び出すことで、IDトークンの検証を行います。
動作確認
さて動作を確認してみましょう。
http://localhost:3000/auth
にアクセスし、Auth0で認証すると、認証成功とメッセージが表示されるようになりました。
まとめ
いかがでしたでしょうか。
Auth0のようなIDaaSとGolangのライブラリを組み合わせることによって、簡単にOpenID Connectによる認証を実装できることがおわかりいただけたのではないかと思います。
OpenID Connectは今回書いた内容以外にも多くの仕様を持っています。
さらに理解を深めるためには、
OpenID Connectの公開資料 https://www.openid.or.jp/document/ や
OAuth徹底入門 セキュアな認可システムを適用するための原則と実践 https://www.amazon.co.jp/dp/4798159298
を読んでみるのがオススメです。