ラムダ引き金を使っている電子メールでのアマゾン認知MFA


アマゾンの認知は簡単に認証を開始するための素晴らしいサービスです.また、SMSまたはTOTP(時間ベースのワンタイムパスワード)デバイスのようなボックスを使用してボックスからマルチファクタ認証(MFA)をしています.私が持っていた要件は、電子メールを介してMFAを使用することでした.私は組み込みのAWSの認知MFAワークフローを使用することを好んだが、これはこのプロジェクトのためのハード要件であった.これはヘルスケア産業にありました、そして、彼らの電子メールは通常病院ネットワークにあることに制限されました.したがって、私はメールでMFAを許可しなければならなかった.
このポストは、カスタム認証フローとアマゾンSES(簡単な電子メールサービス)を使用することによって、Amazon Genoctoに電子メールでMFAを実装する方法を説明します.
まず、AWSコンソールに行き、SESを設定します.あなたが電子メールをアマゾンSEで確かめたことを確認してください.あなたのアカウントがアマゾンSESのためのサンドボックス・モードにあるならば、あなたは送付者と受取人電子メールアドレスを確認するようにしたいです.アカウントが生産モードにあるときは、あなたが送っているメールアドレスを確認する必要はありません.私はなぜこの問題のためにメールを送信していなかった理由を把握しようとして2時間過ごしたので、私はこれを指摘する.
始めるために、我々はビルトインAWS認知MFAワークフローからこの逸脱を取り扱うために、Lambdasを通してカスタム認証フローを使わなければなりません.Amazonの認知は、ラムダ関数を介して動作し、異なるフックが認証フローをカスタマイズできるようにします.

Amazon Cognito Lifecyle Triggers
  • はAuth Challengeを定義します:カスタム認証フローの次の挑戦を決定します
  • 作成Auth Challenge:カスタム認証フロー(MFAコードを送る)でチャレンジを作成します
  • 検証Auth Challenge :カスタム認証フローでレスポンスが正しいかどうかを確認します(作成したコードにマッチしたコードを確認する)
    私たちが使用することができます課題に組み込まれているカップルは、強制的にNorRailパスワードとPasswordCan検証器を含む.
    メールを介してMFAコードを使用することができますカスタムAuthフローを作成するには、これらのラムダライフサイクルフックを使用してそれを実装するつもりです.認証フローは次のようになります.
  • ユーザーは、アプリケーション上で自分のメールとパスワードを入力します.
  • 私たちのセキュアサーバ側(nodejs/express)では、UpdateUserAttributes CoincToメソッドを使用してユーザーの新しいMFAコードを生成し、カスタムプロパティとして属性に保存します.
  • 私たちの定義authチャレンジラムダ関数がヒットされます.ユーザーが一時的なパスワードを持っている場合、我々はクライアントにForceCount Newwordパスワードチャレンジを返します.
  • カスタム認証ワークフローのバグのため、ユーザーがパスワードを変更した後、再度ログインを強制します.バグは、ユーザーがパスワード
  • を変更している場合は、私たちのカスタムの課題になることはありませんので、そのようになります
  • alright、今彼らはログイン画面に戻って、彼らはメールと新しいパスワード
  • を入力します
  • PasswordSum Verifierチャレンジはユーザーのパスワードを検証します.その後、我々は電子メールチャレンジによって私たちのMFA -私たちはカスタマイズチャレンジを返すよ!
  • 私たちの作成Authチャレンジラムダ関数が実行されます.この関数では、MFAコードを送ります.我々はユーザー属性を持っています、そして、我々はSESをセキュアサーバー側で生成されるコードに電子メールで送ります.
  • チャレンジが作成され、メールが送信されると、MFAコードを要求するUIを更新する必要がある現在の認証フロー状態(CustomRange Challenge)を返します.
  • ユーザーが彼らの電子メールから受け取ったMFAコードを入力します.今、我々は確認Authチャレンジにその答えを送信します.これは、現在のMFAコードのユーザーのカスタム属性に対する答えを確認します.また、有効なコードの時間枠が通過しないことを確認します.

    第1部:認知型ライフサイクルフックのための無セルプロジェクトの作成


    私はServerlessなフレームワークを使うつもりです.なぜならそれは管理するのが簡単で、私はそれをよく知っているからです.ただし、単純なままにしたい場合は、単にラムダ関数を使用できます.
    まず、アマゾンの認知コンソールを介して新しいプールを作成します.また、雲の形成テンプレートを使用することができますし、私は例here提供している.
    次に、“authChallenge”というカスタム属性を追加します.これは、期限切れでないことを確認するために我々のMFAコードとタイムスタンプを保持します.

    Multi Factors Authentication(MFA)を「オフ」に設定してください.私たちがカスタムauthフローと認知的なライフサイクルトリガーを通してこれを実行するつもりですから.
    プールが作成されると、新しいServerlessプロジェクトを作成します
    npm install -g serverless     
    serverless create -n cognito-mfa-email-example
    

    ライフサイクル関数の定義


    Definition LogFeint LifeStyleイベントは、カスタムAuthフローの次の課題を決定します.これは流れです、我々はPasswordRule Verifierで始まっている1つの出来事からもう一つへ行きます.返されるいくつかの重要なプロパティがあります.パスワードが確認されたら、我々はカスタムチャレンジを実行します.PasswordSum Verifierは、ユーザーのパスワードをチェックする認知チャレンジに組み込まれています.
    module.exports.handler = async event => {
      // Kicks off with Secure Remote Password
      if (
        event.request.session.length === 1 &&
        event.request.session[0].challengeName === 'SRP_A' &&
        event.request.session[0].challengeResult === true
      ) {
        event.response.issueTokens = false
        event.response.failAuthentication = false
        event.response.challengeName = 'PASSWORD_VERIFIER'
        return event
      }
      if (event.request.userNotFound) {
        event.response.failAuthentication = true
        event.response.issueTokens = false
        return event
      }
    
      // Check result of last challenge
      if (
        event.request.session &&
        event.request.session.length > 2 &&
        event.request.session.slice(-1)[0].challengeResult === true
      ) {
        // The user provided the right answer - issue their tokens
        event.response.failAuthentication = false
        event.response.issueTokens = true
        return event
      }
    
      event.response.issueTokens = false
      event.response.failAuthentication = false
      event.response.challengeName = 'CUSTOM_CHALLENGE'
      return event
    }
    

    ライフサイクル関数


    Create関数は実行されます.AWS SESを使用してMFAコードをメールで送信します.このリクエストには、ユーザーからの属性が含まれており、AuthChallengeカスタム属性を使用して、サーバー側で作成されたコードを取得し、MFAを完了するために使用するユーザーにコードをメールします.We also set the publicChallengeParameters and privateChallengeParameters which we'll pass through the flow .
    const mailer = require('./mailer')
    module.exports.handler = async event => {
      const challenge = event.request.userAttributes['custom:authChallenge']
      const [authChallenge, timestamp] = (event.request.userAttributes['custom:authChallenge'] || '').split(',')
      // This is sent back to the client app
      event.response.publicChallengeParameters = {
        email: event.request.userAttributes.email
      }
    
      // add acceptable answer
      // so it can be verified by the "Verify Auth Challenge Response" trigger
      event.response.privateChallengeParameters = {
        challenge: challenge
      }
    
      // we want to check and make sure we haven't sent the code before in this login session before sending the code
      if (event.request.session.length < 3 && !event.request.session.find(s => s.challengeName === 'CUSTOM_CHALLENGE'))
        await mailer.send(
          'Your Access Code',
          event.request.userAttributes.email,
          'Please use the code below to login: <br /><br /> <b>' + authChallenge + '</b>'
        )
    
      return event
    }
    

    ライフサイクル関数の検証


    検証ライフサイクル関数では、ユーザーが入力したコードと、PrivateChalledParametersのいずれかを使用して、正しいコードがあるかどうかを比較します.Amazon Genoctoは、このAuthoutを起動して、カスタムAuth Challengeのエンドユーザーからの応答が有効かどうかを確認します.また、期限が切れないことを確認するタイムスタンプをチェックします.回答に応答して回答応答特性を設定します.すべての課題が答えられるまで、あなたはさらに多くの課題とカスタムチャレンジフローループを繰り返す可能性があります.
    const LINK_TIMEOUT = 30 * 60
    
    module.exports.handler = async event => {
      // Get challenge and timestamp
      const [authChallenge, timestamp] = (event.request.privateChallengeParameters.challenge || '').split(',')
    
      // Check if code is equal to what we expect...
      if (event.request.challengeAnswer === authChallenge) {
        // Check if the link hasn't timed out
        if (Number(timestamp) > new Date().valueOf() / 1000 - LINK_TIMEOUT) {
          event.response.answerCorrect = true
          return event
        }
      }
    
      event.response.answerCorrect = false
      return event
    }
    

    プロジェクトの配備
    私はフルsample serverless.yaml here含まれている.次のIAMロール機能と次の関数宣言を行います.私は、Create LifeCycle関数で使用されたEmailCurnアドレス変数を設定するためのAmazon Systems Parameter Storeを使用しています.
    無力.YAMLの例
    iamRoleStatements:
        - Effect: "Allow"
          Action:
            - cognito-idp:AdminGetUser
            - cognito-idp:AdminUpdateUserAttributes
          Resource:
            - {"Fn::GetAtt": [UserPool, Arn]}
        - Effect: "Allow"
          Action:
            - ses:SendEmail
          Resource:
            - "*"
    
    functions:
      define:
        handler: defineAuth.handler
        events:
          - cognitoUserPool:
              pool: ${self:custom.userPoolName}
              trigger: DefineAuthChallenge
              existing: true
      create:
        handler: createAuth.handler
        environment:
          EMAIL_ADDRESS: ${ssm:/${self:provider.stage}_EMAIL_ADDRESS}
        events:
          - cognitoUserPool:
              pool: ${self:custom.userPoolName}
              trigger: CreateAuthChallenge
              existing: true
      verify:
        handler: verify.handler
        events:
          - cognitoUserPool:
              pool: ${self:custom.userPoolName}
              trigger: VerifyAuthChallengeResponse
              existing: true
    

    パート2 :擬似バックエンドコード


    ここで、我々の認知ハンドラーを設定し、ユーザプールを持っているので、Authシーケンスを扱ういくつかの例のサーバー側コードを実行できます.我々は、認証の流れをキラキラしたSRPHANES A(安全なリモートパスワード)の課題から起動します.

    ログインルート:キックオフAuthフローとカスタムチャレンジを準備する
    router.post('/login', async (req, res, next) => {
      try {
        const SRP_A = CognitoHelper.calculateA()
        const userHash = generateHash(req.body.username, this.secretHash, this.clientId)
        const params = {
          UserPoolId: this.poolId,
          AuthFlow: 'CUSTOM_AUTH',
          ClientId: this.clientId,
          AuthParameters: {
            USERNAME: req.body.username,
            PASSWORD: req.body.password,
            SECRET_HASH: userHash,
            SRP_A: SRP_A,
            CHALLENGE_NAME: 'SRP_A'
          }
        }
    
        try {
          const data = await this.cognitoIdentity.adminInitiateAuth(params).promise()
          // SRP_A response
          const { ChallengeName, ChallengeParameters, Session } = data
          // Set up User for MFA
          const authChallenge = _.map([...Array(8).keys()], n => Math.floor(Math.random() * 10)).join('')
          await this.cognitoIdentity
            .adminUpdateUserAttributes({
              UserAttributes: [
                {
                  Name: 'custom:authChallenge',
                  Value: `${authChallenge},${Math.round(new Date().valueOf() / 1000)}`
                }
              ],
              UserPoolId: this.poolId,
              Username: req.body.username
            })
            .promise()
          const hkdf = CognitoHelper.getPasswordAuthenticationKey(
            ChallengeParameters.USER_ID_FOR_SRP,
            req.body.password,
            ChallengeParameters.SRP_B,
            ChallengeParameters.SALT,
            this.poolId
          )
          const dateNow = CognitoHelper.getNowString()
          const signatureString = CognitoHelper.calculateSignature(
            hkdf,
            this.poolIdOnly,
            ChallengeParameters.USER_ID_FOR_SRP,
            ChallengeParameters.SECRET_BLOCK,
            dateNow
          )
          const responseParams = {
            ChallengeName: ChallengeName,
            ClientId: this.clientId,
            ChallengeResponses: {
              PASSWORD_CLAIM_SIGNATURE: signatureString,
              PASSWORD_CLAIM_SECRET_BLOCK: ChallengeParameters.SECRET_BLOCK,
              TIMESTAMP: dateNow,
              USERNAME: ChallengeParameters.USER_ID_FOR_SRP,
              SECRET_HASH: generateHash(ChallengeParameters.USERNAME, this.secretHash, this.clientId)
            },
            Session: Session
          }
          // Password verifier response, should be set up for custom mfa challenge now
          const respData = await this.cognitoIdentity.respondToAuthChallenge(responseParams).promise()
          res.status(200).json(respData).end()
        }
      } catch (err) {
        console.log(err)
        if (err.code === 'UserNotFoundException') return res.status(400).send('Invalid Credentials')
        res.sendStatus(500)
      }
    })
    

    ルートを確認してください:これは我々のAudit LifeCycleハンドラーと呼ぶ認知による我々のAuth挑戦に応じて扱います.
    router.post('/verify', async (req, res, next) => {
      try {
        const { email, password, confirmPassword, user, code } = req.body
        const params = {
          ChallengeName: user.ChallengeName,
          ClientId: this.clientId,
          ChallengeResponses: {
            USERNAME: email,
            ANSWER: code,
            SECRET_HASH: generateHash(email, this.secretHash, this.clientId)
          },
          Session: user.Session
        }
        const data = await this.cognitoIdentity.respondToAuthChallenge(params).promise()
    
        // if we failed the custom challenge
        if (!data.AuthenticationResult && user.ChallengeName === 'CUSTOM_CHALLENGE' && code) {
          data.error = 'Invalid Code'
          return res.json({ success: false, data: data })
        } else {
          // add any user claims here
          data.AuthenticationResult.UserClaims = {}
          return res.json({ success: true, data: data })
        }
      } catch (err) {
        console.log(err)
        res.sendStatus(500)
      }
    })
    

    結論


    それだ!我々は今Amazonの認知を使用してメールでMFAを処理するためのカスタム認証のワークフローを構築しました.我々のサーバーコードはAuthフローを開始し、ユーザーが有効なMFAコードを持っていることを確認するまでチャレンジ応答を提供します.ハッピーコーディング!

    参考文献
    Source Code Repository
    Cognito Custom Auth Lambdas