こんにちは!広告技術部のUT@mocyutoです! 最近はスマブラでなんのキャラを使おうか迷っています
この記事はGunosy Advent Calender 19日目の記事です。
昨日の記事は@mathetakeのpeer-to-peerはGoogleの夢を見るかでした。
はじめに
みなさんユーザ認証はどうやって作っていますか? フレームワークを使って実装していますか? それともイチから自前で作っていますか? SSOのシステムとかはどうでしょう?
OIDCとAWSのALBを使えば簡単にユーザのログイン管理を作ることができます。 (少し煽り気味のタイトルですいません ;)
OIDC
OpenID Connect と言って、OAuth2.0の拡張であるOpenIDによる認証プロトコルのことです。 OIDCを提供しているものだと、例えばGoogle、Microsoft、OneLogin、Amazon, Auth0などがあります。
このOIDCを提供しているPlatformでログインすれば、アプリケーションにもログインできる状態を作れます。 例えば、Googleアカウントでログインすればそのままアプリケーションにもログインできている状態を作れます。
では何ができるかということをもう少し噛み砕いて説明しようと思います。
ALBの認証機能
一般的な認証機構
今までのログイン管理といえば、以下のアーキテクチャが一般的に使われているかと思います。
上記のアーキテクチャでは以下のような流れでログイン認証が行われます。
- クライアントからアクセス
- CookieのsessionIDをチェック
- なければ、DBを見て正しいユーザなら発行してキャッシュストレージに登録。
- あれば、キャッシュストレージからユーザ情報を引く
- EC2サーバからRDSなどを見てユーザの詳細を取得
- CookieにsessionIDを書いてレスポンスを返す
このアーキテクチャの場合、使ってるフレームワークにもよりますが、
- ユーザのログイン画面
- memcachedなどのセッションキャッシュストレージの構築
- クライアントサイド、サーバサイドでのセッションIDの管理
ALBを利用した認証
OIDC✕ALBでこの仕組みが全て実装不要になります。 (ユーザごとに権限を分ける場合は、同様にRDSなどの権限管理の構築は必要となります。) https://aws.amazon.com/jp/blogs/news/built-in-authentication-in-alb/
ではどうなるのでしょうか?
めちゃくちゃスッキリしましたね(?)
アーキテクチャ的にはキャッシュストレージが無くなっただけですが、セッション周りの実装が無くなっているので、実装はすごい減っています。
データフローの説明
- クライアントからアクセス
- 1のリクエストにALBのセッションキャッシュが載っていなければ、OIDCにアクセスして認証を行う。認証が失敗すれば、そのまま6に。
- 認証が成功すると、ALBはjwtをHeaderに付与して、バックエンドサーバに送る
- サーバでjwtをparseし、検証に成功すればそのユーザとして認識。(ユーザの権限対応などはここで対応)
- CookieへのSessionIDはALBが付与してくれるので、実装は単純にレスポンスを返すのみ
ALBは認証情報をjwtに格納していますが、jwtに関する詳しい情報は以下が詳しい解説になっています。
JSON Web Token(JWT)の紹介とYahoo! JAPANにおけるJWTの活用 - Yahoo! JAPAN Tech Blog
実際の導入
ではどうやって導入するのでしょうか? 実は基盤側の設定に関してはクラスメソッドさんのブログに全て書いてあります。
そこで、アプリケーション側はどうすればいいの?というところを例を交えて説明したいと思います。
弊社では以下の記事で書いた社内管理画面で利用しています。
APIでjwtから認証情報を取得する例
実際のコードでの例を記載してみました。 先程のデータフローの例では、4のサーバの部分の実装になります。
弊社ではOIDCとしてOneloginを利用しているので、claimのマッピングをOneloginにしていますが、別サービスであればstruct部分を変更していただければと思います。
import ( "encoding/json" "io/ioutil" "net/http" "github.com/dgrijalva/jwt-go" ) type OneLoginUser struct { Email string `json:"email"` PreferredUsername string `json:"preferred_username"` Name string `json:"name"` } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { id := req.Header.Get("x-amzn-oidc-data") user, err := parsedToken(id) if err != nil { return ErrUnauthorized(err) } // response 作成 return } func parsedToken(tokenStr string) (user *OneLoginUser, err error) { // 受け取ったjwtをパースして署名を検証 token, err := jwt.Parse(tokenStr, func(tk *jwt.Token) (interface{}, error) { // 正しい認証アルゴリズムかを判定 // ALBは現状ES256を使っているので以下を指定 if _, ok := tk.Method.(*jwt.SigningMethodECDSA); !ok { return nil, fmt.Errorf("unexpected signing method: %v", tk.Header["alg"]) } // 対象のkidに対する公開鍵を取得 kid := tk.Header["kid"] url := fmt.Sprintf("https://public-keys.auth.elb.ap-northeast-1.amazonaws.com/%s", kid) resp, err := http.Get(url) if err != nil { return nil, err } body, err := ioutil.ReadAll(resp.Body) // byte文字列をjwtライブラリがparseできるように変換 return jwt.ParseECPublicKeyFromPEM(body) }) if !token.Valid { return nil, err } // MapClaimsを使ってもいいのですが、structでjsonの値を定義しているので、json変換でstructに値をいれます。 tmp, err := json.Marshal(token.Claims) if err := json.Unmarshal(tmp, &user); err != nil { return nil, ErrUnauthorized("cannot create json. data: %v", tmp) } return }
上のコードの流れとしては、
- リクエストを受ける
- Headerからjwtを取得
- jwtをparseし、改ざんされていないjwtかを確認
- OKであれば、claim部分をstructにマッピング
という流れです。
署名の検証部分はやらなくてもclaimをbase64でデコードしてjson parseすれば値は取り出せますが、念の為やっておいたほうがよいでしょう。 基本的にはALBとEC2の間で何もプロキシなどを挟まないので、改ざんされる余地はないはずですが。
まとめ
ALBの認証機能がどれくらい楽でどうやって使うかを紹介しました。
OIDC部分はCognitoを使っても構築できるので、他にもいろいろな構築ができるかと思います。
ログイン認証部分をALBに任せて、コアのロジックに集中していきましょう!