こんにちは、@corocn です。 この記事は Misoca+弥生 Advent Calendar 2018 3日目の記事です。 最近は認証認可やFIDO2.0周りを中心に遊んでます。
さてAWS re:Invent 2018では次の新機能が発表されました。
- [速報]AWS LambdaがRubyに対応。さらにカスタムランタイムであらゆるプログラミング言語にも対応へ。AWS re:Invent 2018 - Publickey
- アプリケーションロードバランサー(ALB)のターゲットにAWS Lambdaが選択可能になりました | Amazon Web Services ブログ
これを組み合わせると、ALBのエンドポイントを叩くことでRubyで記述されたLambdaを実行することができます。 既に沢山の方が試して、記事をあげていますね。
さらに半年ほど前に次のような機能の発表がありました。
ALBがOpenID Connectに対応したとの話です。ALBに簡単に認証機能を組み込むことができます。
これらを組み合わせて、セキュアなRubyの実行環境を作ろうというのが今回のネタです。
LambdaでRubyを動かす
まずはLambdaをRubyで記述します。ランタイム選択時にRuby 2.5を選択するだけです。 デフォルトのコードは次のようになっています。
require 'json' def lambda_handler(event:, context:) # TODO implement { statusCode: 200, body: JSON.generate('Hello from Lambda!') } end
ALBからアクセスする場合、この返り値では不十分なので修正します。
require 'json' def lambda_handler(event:, context:) { statusCode: 200, statusDescription: '200 OK', isBase64Encoded: false, headers: { 'Content-Type': 'text/html; charset=utf-8' }, body: 'Hello from Lambda!', } end
以下の記事を参考にしました。
- ALBのバックエンドにLambdaを選択してみた! #reinvent | DevelopersIO
Lambda functions as targets for Application Load Balancers | Networking & Content Delivery
ALBとLambdaを連携する
ALBとの連携はLambdaのトリガーの追加でも良いし、EC2のダッシュボードからALBを作成して、ターゲットに追加しても良いです。
最終的に認証機能を付与するので、ALBのエンドポイントはHTTPS対応しておく必要があります。ドメインや証明書の準備が必要です。
今回は https://api.corocn.me/ というAPIを作ってみました。(いつもはお名前.comでドメインを取得してるので、Route53で取得してACMで証明書も取得してみました。)
叩くとこんな感じで返ってきます。
Googleの OAuth2.0 クライアントを作成する
ALBにGoogle認証を追加します。事前にGCPでOAuth 2.0のクライアントを作成しておきます。
「OAuth同意画面」から アプリケーション名を入力するのと、承認済みドメインにALBに紐づけたドメイン名を追加しておきます。
認証情報を作成からOAuthクライアントIDを選択します。
リダイレクト先は次の値を設定します。
https://<YOUR_DOMAIN>/oauth2/idpresponse
ALBの認証リダイレクトはこのパスで固定です。
クライアントIDとシークレットは後ほど設定するので控えておきます。
ALBにOIDC認証を追加する
ALBのルール設定で認証ルールを追加します。
CognitoやOIDC(OpenID Connect)が使えますが、Google認証を使うのでOIDCです。
ポイント
- クライアントID・シークレットは控えておいた値を入力します
- 一度設定したら、設定を更新するたびにシークレットの入力が必要になります。注意です。
- スコープに「openid profile email」をすることで、名前やメールアドレスの情報が取得でき、Lambda側に渡ります。
各種エンドポイントは次のようになります。
- 発行者: https://accounts.google.com
- 認証エンドポイント: https://accounts.google.com/o/oauth2/v2/auth
- トークンエンドポイント: https://www.googleapis.com/oauth2/v4/token
- ユーザー情報エンドポイント: https://www.googleapis.com/oauth2/v3/userinfo
設定情報は、OpenID Connect | Google Identity Platform | Google Developers に書いてありました。
Lambda側で認証情報を使う
Lambda側にはヘッダの x-amzn-oidc-data にJSON Web Token(JWT)形式で格納されます。JWTの詳しい仕様は、 JSON Web Tokens - jwt.io あたりを参照すると良いです。
require 'json' require 'base64' def lambda_handler(event:, context:) jwt = event['headers']['x-amzn-oidc-data'] header, payload, signature = jwt.split('.') profile = JSON.parse(Base64.decode64(payload)) { statusCode: 200, statusDescription: '200 OK', isBase64Encoded: false, headers: { 'Content-Type': 'text/html; charset=utf-8' }, body: "Hello, #{profile['name']}!! Your email is #{profile['email']}" } end
JWTはBase64EncodeされたJSONがドット区切りでつながっているだけです。ペイロード部にprofile情報が含まれていますので、splitしてdecodeすれば必要な情報は取り出せます。
認証情報をLambdaに引き渡すことができました。
特定ユーザーのブロック
現状ですとGoogle認証を通過した全てのユーザーがAPIにアクセス可能です。 例えばG Suiteの自組織内のみ認証を通したいという場合が考えられます。
この場合、Lambdaの最初でemailの値を見て弾くのが一番簡単な処理ですが、そもそもLambdaまで到達させたくありません。 本来であれば認証の段階でユーザーをブロックしたいと思うので、その場合は Auth0 や Amazon Cognito を活用して処理を組み込むことになりそうです。
最後に
証明書の設定は面倒ですが、API Gatewayよりもクセなく使えて簡単でした。 OIDCのIdPとしてAuth0と連携させようと思ったのですが、時間が無かったのでまたどこかで試してみようと思います。