AWS Cognito:处理从不同身份提供商(Google、Facebook)登录的同一用户(使用相同电子邮件地址)的最佳实践

Ale*_*exR 28 amazon-web-services google-login amazon-cognito aws-lambda amazon-cognito-facebook

当用户通过 Google 和 Facebook 身份提供商使用相同的电子邮件地址登录时,AWS Cognito 在用户池中创建多个条目,每个身份提供商使用一个条目:

AWS Cognito 用户池的屏幕截图

我使用本教程中提供的示例代码来设置 AWS Cognito:使用 Amplify 框架进行用户身份验证的完整指南

  • 如何只创建一个用户而不是多个用户?
  • 是否可以让 AWS Cognito 自动将来自多个提供商的条目合并(联合)到一个条目中,还是应该使用 AWS Lambda 函数来实现这一点?

Pha*_*iệt 31

是的。您可以使用AdminLinkProviderForUser https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html

这个想法是:

  1. 在 PreSignUp lambda 钩子中,如果用户已经注册,我们将提供者链接到用户。例如:
import CognitoIdentityServiceProvider from 'aws-sdk/clients/cognitoidentityserviceprovider'

const cognitoIdp = new CognitoIdentityServiceProvider()
const getUserByEmail = async (userPoolId, email) => {
 const params = {
   UserPoolId: userPoolId,
   Filter: `email = "${email}"`
 }
 return cognitoIdp.listUsers(params).promise()
}

const linkProviderToUser = async (username, userPoolId, providerName, providerUserId) => {
 const params = {
   DestinationUser: {
     ProviderAttributeValue: username,
     ProviderName: 'Cognito'
   },
   SourceUser: {
     ProviderAttributeName: 'Cognito_Subject',
     ProviderAttributeValue: providerUserId,
     ProviderName: providerName
   },
   UserPoolId: userPoolId
 }

 const result = await (new Promise((resolve, reject) => {
   cognitoIdp.adminLinkProviderForUser(params, (err, data) => {
     if (err) {
       reject(err)
       return
     }
     resolve(data)
   })
 }))

 return result
}

exports.handler = async (event, context, callback) => {
 if (event.triggerSource === 'PreSignUp_ExternalProvider') {
   const userRs = await getUserByEmail(event.userPoolId, event.request.userAttributes.email)
   if (userRs && userRs.Users.length > 0) {
     const [ providerName, providerUserId ] = event.userName.split('_') // event userName example: "Facebook_12324325436"
     await linkProviderToUser(userRs.Users[0].Username, event.userPoolId, providerName, providerUserId)
   } else {
     console.log('user not found, skip.')
   }

 }
 return callback(null, event)
}
Run Code Online (Sandbox Code Playgroud)
  1. 然后当用户将 OAuth 与 Facebook/Google 与用户池一起使用时,池将返回此用户链接。

注意:您可能会在用户池 UI 中看到 2 条记录,但是在访问用户记录详细信息时,它们已合并。

  • 首次尝试使用社交提供商登录会生成“已找到用户名条目”错误。以下是 AWS 支持论坛未解决的问题(自 2017 年以来):https://forums.aws.amazon.com/thread.jspa?threadID=267154&start=25&tstart=0 以及来自 SO /sf/ask/3347061301/ 的详细问题/cognito-auth-flow-fails-with-already-found-an-entry-for-username-facebook-10155 (6认同)
  • 我无法为 SAML 提供商提供此服务,出现用户已存在错误 (5认同)
  • 我将第一行替换为 const { CognitoIdentityServiceProvider } = require('aws-sdk'); (3认同)
  • 我收到错误“SourceProviderName 必须与为用户池配置的提供程序匹配”。问题:我的 google 用户有一个“google_x”的子目录,并且提供者名称需要“Google” 解决方案:在“linkProviderToUser”之前添加“const CapitalizedProviderName =providerName.charAt(0).toUpperCase() +providerName.slice(1);” ()` (2认同)

Sub*_*ash 7

我一直在摆弄同样的问题。接受的答案是有效的,但并不涵盖所有场景。主要的一点是,一旦用户使用外部登录进行注册,他们将永远无法使用用户名和密码进行注册。目前,Cognito 不允许将 Cognito 用户链接到外部用户。

我的场景如下:

场景

  1. 当用户使用用户名密码注册并注册外部提供商时,将它们链接起来。
  2. 当用户向外部提供商注册时,允许他们使用用户名和密码进行注册。
  3. username在所有链接的用户之间有一个共同点,将其用作其他服务中的唯一 id。

我建议的解决方案是始终首先创建 Cognito 用户并将所有外部用户链接到它。

建议的解决方案

  1. 用户首先使用用户名/密码注册,然后使用外部用户注册。没有戏剧性,只需将外部用户与 Cognito 用户联系起来。
  2. 用户首先与外部用户注册,然后想使用用户名/密码注册。在这种情况下,首先创建一个 Cognito 用户,然后将外部用户链接到这个新的 Cognito 用户。如果用户以后尝试使用用户名/密码进行注册,他们将收到user already exists错误消息。在这种情况下,他们可以使用forgot password流恢复然后登录。
const {
  CognitoIdentityServiceProvider
} = require('aws-sdk');


const handler = async event => {
  const userPoolId = event.userPoolId;
  const trigger = event.triggerSource;
  const email = event.request.userAttributes.email;
  const givenName = event.request.userAttributes.given_name;
  const familyName = event.request.userAttributes.family_name;
  const emailVerified = event.request.userAttributes.email_verified;
  const identity = event.userName;
  const client = new CognitoIdentityServiceProvider();

  if (trigger === 'PreSignUp_ExternalProvider') {

    await client.listUsers({
        UserPoolId: userPoolId,
        AttributesToGet: ['email', 'family_name', 'given_name'],
        Filter: `email = "${email}"`
      })
      .promise()
      .then(({
        Users
      }) => Users.sort((a, b) => (a.UserCreateDate > b.UserCreateDate ? 1 : -1)))
      .then(users => users.length > 0 ? users[0] : null)
      .then(user => {
        // user with username password already exists, do nothing
        if (user) {
          return user;
        }

        // user with username password does not exists, create one
        const newUser = await client.adminCreateUser({
            UserPoolId: userPoolId,
            Username: email,
            MessageAction: 'SUPPRESS', // dont send email to user
            UserAttributes: [{
                Name: 'given_name',
                Value: givenName
              },
              {
                Name: 'family_name',
                Value: familyName
              },
              {
                Name: 'email',
                Value: email
              },
              {
                Name: 'email_verified',
                Value: emailVerified
              }
            ]
          })
          .promise();
          // gotta set the password, else user wont be able to reset it
          await client.adminSetUserPassword({
              UserPoolId: userPoolId,
              Username: newUser.Username,                                                      
              Password: '<generate random password>',                                                       
              Permanent: true
          }).promise();
    
          return newUser.Username;
      }).then(username => {
        // link external user to cognito user
        const split = identity.split('_');
        const providerValue = split.length > 1 ? split[1] : null;
        const provider = ['Google', 'Facebook'].find(
          val => split[0].toUpperCase() === val.toUpperCase()
        );

        if (!provider || !providerValue) {
          return Promise.reject(new Error('Invalid external user'));
        }

        return client.adminLinkProviderForUser({
            UserPoolId: userPoolId,
            DestinationUser: {
              ProviderName: 'Cognito',
              ProviderAttributeValue: username
            },
            SourceUser: {
              ProviderName: provider,
              ProviderAttributeName: 'Cognito_Subject',
              ProviderAttributeValue: providerValue
            }
          })
          .promise()
      });
  }

  return event;
};

module.exports = {
  handler
};


Run Code Online (Sandbox Code Playgroud)

  • 我已经与这种流动作斗争好几天了。要让它发挥作用真是太痛苦了。每次我成功地完成此流程并尝试使用社交提供商登录/注册时,我都会收到 invalid_grant 错误。Cognito 是为了确保真正的原因,所以不可能找到真正的原因。太令人沮丧了! (2认同)

F_S*_*O_K 5

我认为,我创建的解决方案可以处理所有情况。它还解决了 Cognito 的一些常见问题。

  • 如果用户正在注册外部提供商,请将其链接到任何现有帐户,包括 Cognito(用户名/密码)或外部提供商帐户。
  • 链接到现有帐户时,仅链接到最旧的帐户。这很重要,因为您有 2 个以上的登录选项。
  • 如果用户使用 Cognito(用户名/密码)注册,如果外部提供商已存在,则拒绝注册并显示自定义错误消息(因为无法关联帐户)。

请注意,在关联帐户时,Cognito 预注册触发器会返回“已找到用户名条目”错误。您的客户端应处理此问题并重新尝试身份验证,或要求用户再次登录。更多信息在这里:

Cognito 身份验证流程失败,并显示“已找到用户名 Facebook_10155611263153532 的条目”

这是我的 lambda,在 Cognito 预注册触发器上执行

const AWS = require("aws-sdk");
const cognito = new AWS.CognitoIdentityServiceProvider();

exports.handler = (event, context, callback) => {

  function checkForExistingUsers(event, linkToExistingUser) {

    console.log("Executing checkForExistingUsers");

    var params = {
      UserPoolId: event.userPoolId,
      AttributesToGet: ['sub', 'email'],
      Filter: "email = \"" + event.request.userAttributes.email + "\""
    };

    return new Promise((resolve, reject) =>
      cognito.listUsers(params, (err, result) => {
        if (err) {
          reject(err);
          return;
        }
        if (result && result.Users && result.Users[0] && result.Users[0].Username && linkToExistingUser) {
          console.log("Found existing users: ", result.Users);
          if (result.Users.length > 1){
            result.Users.sort((a, b) => (a.UserCreateDate > b.UserCreateDate) ? 1 : -1);
            console.log("Found more than one existing users. Ordered by createdDate: ", result.Users);
          }
          linkUser(result.Users[0].Username, event).then(result => {
              resolve(result);
            })
            .catch(error => {
              reject(err);
              return;
            });
        } else {
          resolve(result);
        }

      })
    );

  }

  function linkUser(sub, event) {
    console.log("Linking user accounts with target sub: " + sub + "and event: ", event);

    //By default, assume the existing account is a Cognito username/password
    var destinationProvider = "Cognito";
    var destinationSub = sub;
    //If the existing user is in fact an external user (Xero etc), override the the provider
    if (sub.includes("_")) {
      destinationProvider = sub.split("_")[0];
      destinationSub = sub.split("_")[1];
    }
    var params = {
      DestinationUser: {
        ProviderAttributeValue: destinationSub,
        ProviderName: destinationProvider
      },
      SourceUser: {
        ProviderAttributeName: 'Cognito_Subject',
        ProviderAttributeValue: event.userName.split("_")[1],
        ProviderName: event.userName.split("_")[0]
      },
      UserPoolId: event.userPoolId
    };
    console.log("Parameters for adminLinkProviderForUser: ", params);
    return new Promise((resolve, reject) =>
      cognito.adminLinkProviderForUser(params, (err, result) => {
        if (err) {
          console.log("Error encountered whilst linking users: ", err);
          reject(err);
          return;
        }
        console.log("Successfully linked users.");
        resolve(result);
      })
    );
  }

  console.log(JSON.stringify(event));

  if (event.triggerSource == "PreSignUp_SignUp" || event.triggerSource == "PreSignUp_AdminCreateUser") {

    checkForExistingUsers(event, false).then(result => {
        if (result != null && result.Users != null && result.Users[0] != null) {
          console.log("Found at least one existing account with that email address: ", result);
          console.log("Rejecting sign-up");
          //prevent sign-up
          callback("An external provider account alreadys exists for that email address", null);
        } else {
          //proceed with sign-up
          callback(null, event);
        }
      })
      .catch(error => {
        console.log("Error checking for existing users: ", error);
        //proceed with sign-up
        callback(null, event);
      });

  }

  if (event.triggerSource == "PreSignUp_ExternalProvider") {

    checkForExistingUsers(event, true).then(result => {
        console.log("Completed looking up users and linking them: ", result);
        callback(null, event);
      })
      .catch(error => {
        console.log("Error checking for existing users: ", error);
        //proceed with sign-up
        callback(null, event);
      });

  }

};

Run Code Online (Sandbox Code Playgroud)