今日のつちや

田舎から元気に技術ネタと雑記を投稿します

ALB + Lambda + Ruby に認証機能を付けてみる

こんにちは、@corocn です。 この記事は Misoca+弥生 Advent Calendar 2018 3日目の記事です。 最近は認証認可やFIDO2.0周りを中心に遊んでます。

さてAWS re:Invent 2018では次の新機能が発表されました。

これを組み合わせると、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を連携する

ALBとの連携はLambdaのトリガーの追加でも良いし、EC2のダッシュボードからALBを作成して、ターゲットに追加しても良いです。

f:id:corocn:20181203183924p:plain

最終的に認証機能を付与するので、ALBのエンドポイントはHTTPS対応しておく必要があります。ドメインや証明書の準備が必要です。

今回は https://api.corocn.me/ というAPIを作ってみました。(いつもはお名前.comでドメインを取得してるので、Route53で取得してACMで証明書も取得してみました。)

f:id:corocn:20181203183445p:plain:w300

叩くとこんな感じで返ってきます。

Googleの OAuth2.0 クライアントを作成する

ALBにGoogle認証を追加します。事前にGCPでOAuth 2.0のクライアントを作成しておきます。

「OAuth同意画面」から アプリケーション名を入力するのと、承認済みドメインにALBに紐づけたドメイン名を追加しておきます。

f:id:corocn:20181203184546p:plain:w500

認証情報を作成からOAuthクライアントIDを選択します。

f:id:corocn:20181203184727p:plain:w500

リダイレクト先は次の値を設定します。

https://<YOUR_DOMAIN>/oauth2/idpresponse

ALBの認証リダイレクトはこのパスで固定です。

f:id:corocn:20181203185112p:plain:w500

クライアントIDとシークレットは後ほど設定するので控えておきます。

ALBにOIDC認証を追加する

ALBのルール設定で認証ルールを追加します。

CognitoやOIDC(OpenID Connect)が使えますが、Google認証を使うのでOIDCです。

f:id:corocn:20181203185421p:plain:w500

f:id:corocn:20181203185722p:plain:w500

ポイント

  • クライアントID・シークレットは控えておいた値を入力します
  • 一度設定したら、設定を更新するたびにシークレットの入力が必要になります。注意です。
  • スコープに「openid profile email」をすることで、名前やメールアドレスの情報が取得でき、Lambda側に渡ります。

各種エンドポイントは次のようになります。

設定情報は、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すれば必要な情報は取り出せます。

f:id:corocn:20181203191226p:plain:w400

認証情報をLambdaに引き渡すことができました。

特定ユーザーのブロック

現状ですとGoogle認証を通過した全てのユーザーがAPIにアクセス可能です。 例えばG Suiteの自組織内のみ認証を通したいという場合が考えられます。

この場合、Lambdaの最初でemailの値を見て弾くのが一番簡単な処理ですが、そもそもLambdaまで到達させたくありません。 本来であれば認証の段階でユーザーをブロックしたいと思うので、その場合は Auth0Amazon Cognito を活用して処理を組み込むことになりそうです。

最後に

証明書の設定は面倒ですが、API Gatewayよりもクセなく使えて簡単でした。 OIDCのIdPとしてAuth0と連携させようと思ったのですが、時間が無かったのでまたどこかで試してみようと思います。