Serverless Framework の API Gateway Authorizer 実装を解説します!😎

Serverless Framework の API Gateway Authorizer 実装を解説します!😎

こんにちは!

ServerlessFramework での AWS インフラ構築は、私たちにとってデファクトスタンダードです。

Serverless Framework 関連のプラグインを活用すれば AppSync 構築をはじめとした大部分の構築を簡略化できますし、Cloud Formation を自由に書けるので拡張性も十分です。

本記事では、Serverless Framwork で実装する API Gateway の Authorizer のについてソースコード交え解説します。

想定する読者

  • API Gateway の Authorizer を実装したしヒト
  • Serverless Framework で開発を行っているヒト
  • Cloud Formation でインフラ構築を自動化しているヒト

はじめに

Serverless Framework でのAPI Gateway 開発

functions の event プロパティに http イベントを設定することで、API Gateway が自動的に構築されます。

Cloud Formationの記述などは基本的に必要ありません。

# serverless.yml
functions:
  index:
    handler: handler.hello
    events:
      - http: GET hello

APIGatewayAuthorizer の構築コードの紹介

Cognito User Pool を認可方式に指定しますので、Coognito も紹介します。尚、Lambda のソースコードにはお好きな処理を記述してください。

serverless.yml

service: ApiGatewayWithAuthorization

provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage,"dev"}
  region: ap-northeast-1
  profile: ragate
  iamRoleStatements:
    - Effect: 'Allow'
      Action:
        - 'lambda:*'
        - 'cognito:*'
        - 'apigateway:*'
      Resource:
        - '*'

plugins:
  - serverless-webpack

custom:
  webpack:
    includeModules: true
    packager: 'npm'

functions: ${file(./resources/functions.yml)}

resources:
  - ${file(./resources/cognito.yml)}
  - ${file(./resources/apigateway-authorizer.yml)}

package:
  individually: true
  exclude:
    - node_modules/**
    - resources/**
    - package-lock.json
    - package.json
    - webpack.config.js
    - yarn.lock
    - README.md
    - .git/**
    - tmp/**
#  include:

Cognito の構築

Resources:
  ApiGatewayWithAuthorizationUserPool:
    Type: 'AWS::Cognito::UserPool'
    Properties:
      AccountRecoverySetting: # パスワード忘れの際の復帰方法
        RecoveryMechanisms:
          - Name: 'verified_email'
            Priority: 1
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false # 管理者権限を持つLambdaでのみユーザー作成を実行するので有効にする(ユーザー自身がSDKでサインアップする際はfalseにすること)
        InviteMessageTemplate:
          EmailMessage: 'Your username is {username} and temporary password is {####}.'
          EmailSubject: 'Your temporary password'
          SMSMessage: 'Your username is {username} and temporary password is {####}.'
      AliasAttributes:
        - email
        - preferred_username # 独自のユーザー名称でログインできる仕様なので入れておく
      AutoVerifiedAttributes:
        - email # emailは自動的に検証し、保有を検証するうようにしておく
      DeviceConfiguration: # デバイスはいったん記憶させない、今後MFA認証を実装する場合に有効にするのを検討
        ChallengeRequiredOnNewDevice: false
        DeviceOnlyRememberedOnUserPrompt: false
      EmailConfiguration:
        EmailSendingAccount: COGNITO_DEFAULT
      EmailVerificationMessage: 'Your verification code is {####}.'
      EmailVerificationSubject: 'Your verification code'
      MfaConfiguration: OFF
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: false
          RequireNumbers: false
          RequireSymbols: false
          RequireUppercase: false
          TemporaryPasswordValidityDays: 365 # 管理者によるパスワードリセットは実装しないので、最長にしておく(セキュリティホールになる様子であれば今後対応を検討)
      Schema:
        - AttributeDataType: String
          DeveloperOnlyAttribute: false
          Mutable: true # OIDC, SAML2などのIDプロバイダーを追加し属性マッピングする可能性があるので変更可能にしておく
          Name: email
          Required: true
      SmsAuthenticationMessage: 'Your verification code is {####}.'
      SmsVerificationMessage: 'Your verification code is {####}.'
      UsernameConfiguration:
        CaseSensitive: true # 大文字と小文字を区別する
      UserPoolAddOns:
        AdvancedSecurityMode: AUDIT # いったん監視のみし、CloudWatchにてログ確認のみにする、今後セキュリティ強化のニーズが高まったらENFORCEDへ設定したい
      UserPoolName: ${self:service}-${self:provider.stage}-user-pool
      UserPoolTags:
        Service: ${self:service}-${self:provider.stage}
      VerificationMessageTemplate:
        DefaultEmailOption: CONFIRM_WITH_CODE # 認証リンクの送信は行わない、コード送信のみ送信
        EmailMessage: 'Your verification code is {####}.'
        EmailSubject: 'Your verification code'
        SmsMessage: 'Your verification code is {####}.'

  ApiGatewayWithAuthorizationUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      CallbackURLs:
        - 'http://localhost:3000'
      ClientName: ${self:service}-${self:provider.stage}-user-pool-client
      DefaultRedirectURI: 'http://localhost:3000'
      ExplicitAuthFlows:
        - ALLOW_USER_PASSWORD_AUTH
        - ALLOW_ADMIN_USER_PASSWORD_AUTH
        - ALLOW_USER_SRP_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH
      # GenerateSecret: Boolean
      LogoutURLs:
        - 'http://localhost:3000'
      PreventUserExistenceErrors: ENABLED # Cognitoから返却するエラーは具体的にしておく(エラーによってUIを動的にしたい可能性があるので...)
      ReadAttributes:
        - email
        - preferred_username
      RefreshTokenValidity: 10 # リフレッシュトークンの生存日数
      SupportedIdentityProviders:
        - COGNITO # 今後拡張していくが、今はCognitoのみでOK
      UserPoolId:
        Ref: ApiGatewayWithAuthorizationUserPool
      WriteAttributes: # 外部のIDプロバイダー利用時に属性を書き込む可能性があるので使用している属性情報の書き込みが可能にしておく
        - email
        - preferred_username

Authorizer の構築

Resources:
  ApiGatewayWithAuthorizationAuthorizer:
    Type: AWS::ApiGateway::Authorizer
    DependsOn:
      - ApiGatewayRestApi # 暗黙的に適用されるが一応入れておく
    Properties:
      Name: ApiGatewayWithAuthorizationAuthorizer
      RestApiId:
        Ref: ApiGatewayRestApi
      IdentitySource: method.request.header.Authorization
      Type: COGNITO_USER_POOLS
      ProviderARNs:
        - { Fn::GetAtt: [ApiGatewayWithAuthorizationUserPool, Arn] }

関数の構築

同じスタックで構築するAPI Gatewayの Authorizer のID を指定し認証に指定します。逆に指定しない API は認証不要となります。

getWithAuth:
  handler: src/functions/example/get.handler
  events:
    - http:
        path: /example_with_auth
        method: get
        integration: lambda
        authorizer:
          type: COGNITO_USER_POOLS
          authorizerId: !Ref ApiGatewayWithAuthorizationAuthorizer

getWithoutAuth:
  handler: src/functions/example/get.handler
  events:
    - http:
        path: /example_without_auth/
        method: get

get.js

// src/functions/example/get.handler
export async function handler(event, context, callback) {
  callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      result: 'success'
    }),
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials': 'true'
    },
  })
}

上記のコードをデプロイしたら、AWS の管理コンソールへアクセスし、API Gateway のエンドポイントへリクエストしてみてください。

getWithAuth リソースは、Authorization ヘッダーに ユーザープールトークンを設定しないと 401 がレスポンスされるはずです。

まとめ

実運用の際にはユーザーグループを活用して、グループ単位で API アクセスに認可を設定するケースが多数です。今後ソースコードを交え紹介していきます。

API Gateway、AppSync に関する開発はお気軽に相談ください。