41

When signing in a user with the same email address through the Google and Facebook identity providers, AWS Cognito creates multiple entries in the user pool, one entry per identity provider used:

Screenshot of AWS Cognito user pool

I have used the example code provided in this tutorial to set up AWS Cognito: The Complete Guide to User Authentication with the Amplify Framework

  • How can I create just one user instead of multiple users?
  • Is it possible to have AWS Cognito automatically combine (federate) the entries from multiple providers into one entry or should AWS Lambda functions be used to accomplish this?
4

5 回答 5

40

是的。您可以使用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)
}
  1. 然后当用户使用带有用户池的 Facebook/Google 的 OAuth 时,池将返回此用户链接。

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

于 2020-01-08T08:39:21.183 回答
15

我一直在摆弄同样的问题。接受的答案类型的作品,但不涵盖所有场景。最主要的是,一旦用户使用外部登录名注册,他们将永远无法使用用户名和密码进行注册。目前,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(async 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
};


于 2021-02-01T03:20:46.910 回答
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);
      });

  }

};

于 2020-11-14T12:36:07.443 回答
2

如果您想允许用户继续使用电子邮件和密码登录(“选项 1:用户使用用户名注册并使用用户名或别名登录”)除了身份提供者(google、facebook 等)之外,那么接受的解决方案将不会还不够,因为 Cognito 只能验证一封电子邮件

我通过添加一个Post Confirmation 触发器来解决这个问题,该触发器会在需要时自动验证用户电子邮件:

const AWS = require('aws-sdk');
const cognitoIdp = new AWS.CognitoIdentityServiceProvider();

const markUserEmailAsVerified = async (username, userPoolId) => {
  console.log('marking email as verified for user with username: ' + username);
  const params = {
    UserAttributes: [
      {
        Name: 'email_verified',
        Value: 'true'
      }
      // other user attributes like phone_number or email themselves, etc
    ],
    UserPoolId: userPoolId,
    Username: username
  };

  const result = await new Promise((resolve, reject) => {
    cognitoIdp.adminUpdateUserAttributes(params, (err, data) => {
      if (err) {
        console.log(
          'Failed to mark user email as verified with error:\n' +
            err +
            '\n. Manual action is required to mark user email as verified otherwise he/she cannot login with email & password'
        );
        reject(err);
        return;
      }
      resolve(data);
    });
  });

  return result;
};

exports.handler = async (event, context, callback) => {
  console.log('event data:\n' + JSON.stringify(event));

  const isEmailVerified = event.request.userAttributes.email_verified;
  if (isEmailVerified === 'false') {
    await markUserEmailAsVerified(event.userName, event.userPoolId);
  }

  return callback(null, event);
};

注意:这似乎不是标准开发或通用要求,因此请采纳。

于 2021-02-11T12:17:12.873 回答
1

aws-sdk-js-v3我使用@subash方法。我发现当您进行错误回调时,不会创建额外的用户。只是您使用电子邮件创建的那个。

const {
  CognitoIdentityProviderClient,
  ListUsersCommand,
  AdminCreateUserCommand,
  AdminLinkProviderForUserCommand,
  AdminSetUserPasswordCommand,
} = require('@aws-sdk/client-cognito-identity-provider')
const client = new CognitoIdentityProviderClient({
  region: process.env.REGION,
})
const crypto = require("crypto")

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

  try {

    const {
      triggerSource,
      userPoolId,
      userName,
      request: {
        userAttributes: { email, name }
      }
    } = event

    if (triggerSource === 'PreSignUp_ExternalProvider') {

      const listParam = {
        UserPoolId: userPoolId,
        Filter: `email = "${email}"`,
      }
      const listData = await client.send(new ListUsersCommand(listParam))


      let [providerName, providerUserId] = userName.split('_')
      providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)

      let linkParam = {
        SourceUser: {
          ProviderAttributeName: 'Cognito_Subject',
          ProviderAttributeValue: providerUserId,
          ProviderName: providerName,
        },
        UserPoolId: userPoolId,
      }

      if (listData && listData.Users.length > 0) {

        linkParam['DestinationUser'] = {
          ProviderAttributeValue: listData.Users[0].Username,
          ProviderName: 'Cognito',
        }

      }
      else {

        const createParam = {
          UserPoolId: userPoolId,
          Username: email,
          MessageAction: 'SUPPRESS',
          UserAttributes: [{
            //optional name attribute. 
            Name: 'name', 
            Value: name,
          }, {
            Name: 'email',
            Value: email,
          }, {
            Name: 'email_verified',
            Value: 'true',
          }],
        }
        const createData = await client.send(new AdminCreateUserCommand(createParam))

        const pwParam = {
          UserPoolId: userPoolId,
          Username: createData.User.Username,
          Password: crypto.randomBytes(40).toString('hex'),
          Permanent: true,
        }
        await client.send(new AdminSetUserPasswordCommand(pwParam))

        linkParam['DestinationUser'] = {
          ProviderAttributeValue: createData.User.Username,
          ProviderName: 'Cognito',
        }

      }
      await client.send(new AdminLinkProviderForUserCommand(linkParam))
      //throw error to prevent additional user creation
      callback(Error('Social account was set, retry to sign in.'), null)
    }
    else {
      callback(null, event)
    }
  }
  catch (err) {
    console.error(err)
  }
}

然而,这是一个糟糕的用户体验,因为第一次使用联合身份登录只会创建用户,但不允许它进行身份验证。但是,随后使用联合身份登录将不会显示此类问题。让我知道,如果您对第一次登录有任何其他解决方案。

保留它也很有用email_verifiedtrue以便用户可以恢复他们的密码。如果您使用aws-amplify身份验证器,则尤其如此。这应该在您的身份验证后触发器中。

const {
  CognitoIdentityProviderClient,
  AdminUpdateUserAttributesCommand,
} = require('@aws-sdk/client-cognito-identity-provider')
const client = new CognitoIdentityProviderClient({
  region: process.env.REGION,
})

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

  try {

    const {
      userPoolId,
      userName,
      request: {
        userAttributes: { email_verified }
      }
    } = event

    if (!email_verified) {

      const param = {
        UserPoolId: userPoolId,
        Username: userName,
        UserAttributes: [{
          Name: 'email_verified',
          Value: 'true',
        }],
      }
      await client.send(new AdminUpdateUserAttributesCommand(param))

    }
    callback(null, event)
  }
  catch (err) {
    console.error(err)
  }
}
于 2021-11-15T21:29:44.863 回答