y-ohgi's blog

しがないSREなフリーランスの徒然

Twitter OAuthをServerless Framework + Cognito Federated Identityで行う

背景

  • Twitter OAuthをサーバーレスで作成したかった

やったこと

Cognitoの調査

AWSの認証基盤であるCognitoの調査を行った。
「Cognitoを使えば良い感じにAPI Gatewayの認証/認可が行けるのでは??」と甘く見積もるなどしたけど、結論しんどかった。

Cognito UserPool

API Gatewayがデフォルトで認証/認可に対応している、便利。
名前の通りUserPool機能も備えているので属性情報の格納もできる、凄い。
IDPはFB, Amazon, Google, SAMLのみ対応。
Google認証の実体はOpenID ConnectのようなのでLINEとかも行けるっぽい
Twitterに未対応だったため見送り。Twitter対応してる&Federated Identity同等のIDPの自由度があれば最高だった。。。

Cognit Federated Identity

所謂認可用トークンの発行しか行わない模様。
認証用IDの発行もするが、IAMと連携させないとうまい具合に認証ができないのが難点。正直認証の実装がまだ見えていない。(属性情報のベストプラクティスはやっぱりDynamoかな。ここだけUserPoolの機能借りたいしたい)
また、API GatewayはFederated Identityで発行するstsトークンでアクセスを行う。stsで認可を行うため結構癖が強く扱いづらい...
httpの署名はAPI GatewayサービスでSDKが生成されるが汎用性が低く使いづらい。野良のライブラリ探すか汎化させるのが吉。
IDPは複数対応しており、Twitterにも対応されている。なのでこちらを採用。

API Gatewayの調査

認証/認可の方法は3(+1)パターン

1. Cognit UserPool

Cognito UserPoolの場合、API Gatewayがデフォルトで認証/認可に対応している。
httpへの署名はAuthenticationヘッダーにjwtを仕込むだけなのでテストも楽。
今回はUserPoolを使用しないため見送り

2. API Gatewayの前段にLambdaを設定する

API Gatewayの前段に認可用の自前Lambdaを挟む事が可能。

3. IAMでの認可

パス単位でLambdaの認可を要求することが可能
Cognito Federated Identityのstsで行けるのでは?と思いこれを採用

4. 実行されたLambdaの中で認証/認可処理を行う

所謂自前実装。
最高の自由度が手に入る。

実装する

githubは↓
https://github.com/y-ohgi/serverless-practice/tree/master/twitter/api

Cognito Federated Identityの用意

Twitterからコンシューマートークンを取得し、Cognito Federated Identityに食わせる。以上。

serverless.ymlの記述

functions:
  auth_twitter:
    handler: src/functions/auth/twitter.auth
    environment:
      TWITTER_CONSUMER_KEY: ${self:custom.otherfile.environment.${self:provider.stage}.TWITTER_CONSUMER_KEY}
      TWITTER_CONSUMER_SECRET: ${self:custom.otherfile.environment.${self:provider.stage}.TWITTER_CONSUMER_SECRET}
    events:
      - http:
          path: auth
          method: get

  auth_callback:
    handler: src/functions/auth/twitter.callback
    environment:
      TWITTER_CONSUMER_KEY: ${self:custom.otherfile.environment.${self:provider.stage}.TWITTER_CONSUMER_KEY}
      TWITTER_CONSUMER_SECRET: ${self:custom.otherfile.environment.${self:provider.stage}.TWITTER_CONSUMER_SECRET}
      TWITTER_CLIENT_REDIRECT_URL: ${self:custom.otherfile.environment.${self:provider.stage}.TWITTER_CLIENT_REDIRECT_URL}
    events:
      - http:
          path: auth/callback
          method: get

主な登場人物は auth_twitterauth_callback の2つのfunction。
auth_twitter ではTwitterのログインURLの発行・リダイレクトを行う。
auth_callback ではTwitterからのリクエスト受け取り・アクセストークン発行・クライアントへのリダイレクトを行う。
リダイレクトはLambdaプロキシ統合を使用して行う。

  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
          authorizer: aws_iam

認可は authorizer プロパティに aws_iam を指定し、IAMでの認可させる。

Lambdaの実装

npmの ciaranj/node-oauth を用いて実行する。

auth_twitter

Twitterのログイン画面へ飛ばす。
リダイレクト処理は先述したLambdaプロキシ統合を選択していればヘッダーとHTTPステータスコードはコードで表現できる(プロキシ統合OFFだとヘッダーをいじるためにGUI上でポチポチが...)

const auth = (event, context, callback) => {
  const oa = new OAuth(
    'https://api.twitter.com/oauth/request_token',
    'https://api.twitter.com/oauth/access_token',
    ENV.TWITTER_CONSUMER_KEY,
    ENV.TWITTER_CONSUMER_SECRET,
    '1.0',
    'https://' + event.headers.Host + '/' + event.requestContext.stage + '/auth/callback',
    'HMAC-SHA1'
  )

  oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){
    if (error) {
      callback(error, null)
    } else {
      callback(null, {
        statusCode: 301,
        headers: {
          'Location': 'https://twitter.com/oauth/authenticate?oauth_token='+oauth_token,
          'Set-Cookie': 'oauth_token_secret=' + oauth_token_secret
        },
        body: JSON.stringify({ 'message': 'redirect...' })
      })
    }
  })
}

auth_callback

Twitterから帰ってきたリクエストを受け取り、アクセストークンを発行し、クライアントへリダイレクトさせる

const callback = (event, context, callback) => {
  const oa = new OAuth(
    'https://api.twitter.com/oauth/request_token',
    'https://api.twitter.com/oauth/access_token',
    ENV.TWITTER_CONSUMER_KEY,
    ENV.TWITTER_CONSUMER_SECRET,
    '1.0',
    null,
    'HMAC-SHA1'
  )
  
  oa.getOAuthAccessToken(
    event.queryStringParameters.oauth_token,
    Cookie.parse(event.headers.Cookie).oauth_token_secret,
    event.queryStringParameters.oauth_verifier,
    function(error, access_token, access_token_secret, results) {      
      if (error) {
        callback(error, null)
      } else {
        const clienturl = ENV.TWITTER_CLIENT_REDIRECT_URL + '?access_token=' + access_token + '&access_token_secret=' + access_token_secret
        
        callback(null, {
          statusCode: 301,
          headers: {
            'Location': clienturl
          },
          body: JSON.stringify({ 'message': 'redirect...' })
        })
      }
    }
  )
}

参考

serverlessでtwitter認証を作ってみた - Qiita

所感

マネージドサービス使っているはずが自前実装の箇所が多すぎて自前認証基盤作ったほうが後々幸せなんじゃないのだろうかと思う。
たぶん、UserPoolで対応されているIDPのみ or メアド&パスワードで要件が満たせる案件であれば(イマドキなかなか難しそうな気はするけど...)、UserPool使うのがAWSでサーバレススタック使う上で一番楽なのかなと。
今回はフロントがWebだったので、モバイルアプリで作るとまた違うのかもしれないー?