防止用户自行向联合身份提供商 (FIP) 注册,但允许使用 FIP 登录(如果由管理员添加)

问题描述 投票:0回答:3

我已在 Amazon Cognito 中为我的 Web 应用程序设置了一个用户池。该应用程序并不打算公开,并且仅允许特定用户登录。Amazon 控制台中该用户池的策略仅允许管理员创建新用户。

我已经通过 Facebook 和 Google 实现了登录。 Cognito 确实允许用户使用这些联合身份提供商登录应用程序,这很棒。不过,似乎任何拥有 Facebook 或 Google 帐户的人现在都可以自行注册。

因此,一方面,人们无法使用常规 Cognito 凭据创建自己的用户,但另一方面,如果他们使用联合身份提供商,则可以在 Cognito 中创建新用户。

有没有办法限制只有用户池中已存在的用户才能使用 Facebook 或 Google 登录我的应用程序?这样,管理员仍然能够控制谁可以访问该应用程序。我想使用联合身份提供商共享的电子邮件来检查他们是否被允许登录。

应用程序是使用 CloudFront 设置的。我编写了一个 Lambda,它拦截源请求以检查 cookie 中的令牌并根据访问令牌的有效性授权访问。

我希望避免编写额外的代码来阻止用户在 Facebook 或 Google 上注册,但如果没有其他方法,我将更新 Lambda。

amazon-web-services authentication oauth-2.0 amazon-cognito federated-identity
3个回答
8
投票

所以,这是我最终编写的预注册 Lambda 触发器。我花时间使用

async
/
await
而不是 Promises。它工作得很好,只是有一个已记录的错误,即 Cognito 强制首次使用外部身份提供商的用户注册,然后再次登录(因此他们会看到身份验证页面两次),然后才能访问应用程序。我有一个关于如何解决这个问题的想法,但与此同时,下面的 Lambda 做了我想要的事情。另外,事实证明,来自 Login With Amazon 的 ID 没有使用正确的大小写,因此我不得不手动重新格式化该 ID,这很不幸。让我觉得 Cognito 触发器的实现有点错误。

const PROVIDER_MAP = new Map([
    ['facebook', 'Facebook'],
    ['google', 'Google'],
    ['loginwithamazon', 'LoginWithAmazon'],
    ['signinwithapple', 'SignInWithApple']
]);

async function getFirstCognitoUserWithSameEmail(event) {
    const { region, userPoolId, request } = event;

    const AWS = require('aws-sdk');
    const cognito = new AWS.CognitoIdentityServiceProvider({
        region
    });

    const parameters = {
        UserPoolId: userPoolId,
        AttributesToGet: ['sub', 'email'], // We don't really need these attributes
        Filter: `email = "${request.userAttributes.email}"` // Unfortunately, only one filter can be applied at once
    };

    const listUserQuery = await cognito.listUsers(parameters).promise();

    if (!listUserQuery || !listUserQuery.Users) {
        return { error: 'Could not get list of users.' };
    }

    const { Users: users } = listUserQuery;

    const cognitoUsers = users.filter(
        user => user.UserStatus !== 'EXTERNAL_PROVIDER' && user.Enabled
    );

    if (cognitoUsers.length === 0) {
        console.log('No existing enabled Cognito user with same email address found.');
        return {
            error: 'User is not allowed to sign up.'
        };
    }

    if (cognitoUsers.length > 1) {
        cognitoUsers.sort((a, b) =>
            a.UserCreateDate > b.UserCreateDate ? 1 : -1
        );
    }

    console.log(
        `Found ${cognitoUsers.length} enabled Cognito user(s) with same email address.`
    );

    return { user: cognitoUsers[0], error: null };
}

// Only external users get linked with Cognito users by design
async function linkExternalUserToCognitoUser(event, existingUsername) {
    const { userName, region, userPoolId } = event;

    const [
        externalIdentityProviderName,
        externalIdentityUserId
    ] = userName.split('_');

    if (!externalIdentityProviderName || !externalIdentityUserId) {
        console.error(
            'Invalid identity provider name or external user ID. Should look like facebook_123456789.'
        );
        return { error: 'Invalid external user data.' };
    }

    const providerName = PROVIDER_MAP.get(externalIdentityProviderName);

    let userId = externalIdentityUserId;
    if (providerName === PROVIDER_MAP.get('loginwithamazon')) {
        // Amazon IDs look like amzn1.account.ABC123DEF456
        const [part1, part2, amazonId] = userId.split('.');
        const upperCaseAmazonId = amazonId.toUpperCase();
        userId = `${part1}.${part2}.${upperCaseAmazonId}`;
    }

    const AWS = require('aws-sdk');
    const cognito = new AWS.CognitoIdentityServiceProvider({
        region
    });

    console.log(`Linking ${userName} (ID: ${userId}).`);

    const parameters = {
        // Existing user in the user pool to be linked to the external identity provider user account.
        DestinationUser: {
            ProviderAttributeValue: existingUsername,
            ProviderName: 'Cognito'
        },
        // An external identity provider account for a user who does not currently exist yet in the user pool.
        SourceUser: {
            ProviderAttributeName: 'Cognito_Subject',
            ProviderAttributeValue: userId,
            ProviderName: providerName // Facebook, Google, Login with Amazon, Sign in with Apple
        },
        UserPoolId: userPoolId
    };

    // See https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html
    await cognito.adminLinkProviderForUser(parameters).promise();

    console.log('Successfully linked external identity to user.');

    // TODO: Update the user created for the external identity and update the "email verified" flag to true. This should take care of the bug where users have to sign in twice when they sign up with an identity provider for the first time to access the website.
    // Bug is documented here: https://forums.aws.amazon.com/thread.jspa?threadID=267154&start=25&tstart=0

    return { error: null };
}

module.exports = async (event, context, callback) => {
    // See event structure at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
    const { triggerSource } = event;

    switch (triggerSource) {
        default: {
            return callback(null, event);
        }
        case 'PreSignUp_ExternalProvider': {
            try {
                const {
                    user,
                    error: getUserError
                } = await getFirstCognitoUserWithSameEmail(event);

                if (getUserError) {
                    console.error(getUserError);
                    return callback(getUserError, null);
                }

                const {
                    error: linkUserError
                } = await linkExternalUserToCognitoUser(event, user.Username);

                if (linkUserError) {
                    console.error(linkUserError);
                    return callback(linkUserError, null);
                }

                return callback(null, event);
            } catch (error) {
                const errorMessage =
                    'An error occurred while signing up user from an external identity provider.';
                console.error(errorMessage, error);

                return callback(errorMessage, null);
            }
        }
    }
};


4
投票

有一种方法可以做到这一点,但您需要编写一些代码 - 没有现成的解决方案。

您需要编写一个 lambda 并将其连接到 Cognito 预注册触发器。 https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html 触发器具有三种不同的事件源;

PreSignUp_SignUp
PreSignUp_AdminCreateUser
PreSignUp_ExternalProvider

您的 lambda 应该检查您是否有

PreSignUp_ExternalProvider
事件。对于这些事件,请使用 Cognito SDK 在现有池中查找用户。如果用户存在,则返回该事件。如果用户不存在,则返回字符串(错误信息)。

我将在此处粘贴我自己的预注册触发器。它不做你需要它做的事情,但你需要的所有主要组件都在那里。您基本上可以将其破解为您需要的功能。

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);
      });

  }

};

0
投票

非常感谢OP在这里发布的答案。

此后,AWS SDK v3 已发布,并且更新了以下代码以解决此问题(此外还进行了一些其他调整)。

// https://stackoverflow.com/a/66485066

const {
  AdminLinkProviderForUserCommand,
  CognitoIdentityProviderClient,
  ListUsersCommand,
} = require("@aws-sdk/client-cognito-identity-provider");


const LOGIN_WITH_AMAZON = 'LoginWithAmazon';
const PROVIDER_MAP = new Map([
  ['facebook', 'Facebook'],
  ['google', 'Google'],
  ['loginwithamazon', LOGIN_WITH_AMAZON],
  ['signinwithapple', 'SignInWithApple'],
]);


const getFirstCognitoUserWithSameEmail = async (event) => {
  const { region, userPoolId, request } = event;
  const email = request.userAttributes.email;

  const cognito = new CognitoIdentityProviderClient({
    region
  });

  const parameters = {
    UserPoolId: userPoolId,
    AttributesToGet: ['sub', 'email'], // We don't really need these attributes
    Filter: `email = "${email}"` // Unfortunately, only one filter can be applied at once
  };

  // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity-provider/command/ListUsersCommand/
  const command = new ListUsersCommand(parameters);
  const listUserQuery = await cognito.send(command);

  if (!listUserQuery || !listUserQuery.Users) {
    return { error: 'Could not get list of users.' };
  }

  const { Users: users } = listUserQuery;

  const cognitoUsers = users.filter(
    user => user.UserStatus !== 'EXTERNAL_PROVIDER' && user.Enabled
  );

  if (cognitoUsers.length === 0) {
    console.log('No existing enabled Cognito user with same email address found.');
    return {
      error: 'User is not allowed to sign up.'
    };
  }

  if (cognitoUsers.length > 1) {
    cognitoUsers.sort((a, b) =>
      a.UserCreateDate > b.UserCreateDate ? 1 : -1
    );
  }

  console.log(
    `Found ${cognitoUsers.length} enabled Cognito user(s) with email of '${email}'.`
  );

  return { user: cognitoUsers[0], error: null };
};


// Only external users get linked with Cognito users by design
const linkExternalUserToCognitoUser = async (event, existingUsername) => {
  const { userName, region, userPoolId } = event;

  // split on underscore
  const externalIdentityMatch = /^([^_]+)_([^_]+)$/.exec(userName.trim());
  if (!externalIdentityMatch) {
    console.error(
      'Invalid identity provider name or external user ID. Should look like facebook_123456789.'
    );
    return { error: 'Invalid external user data.' };
  }

  const [
    externalIdentityProviderName,
    externalIdentityUserId
  ] = externalIdentityMatch.slice(1);

  const providerName = PROVIDER_MAP.get(externalIdentityProviderName.toLowerCase());
  if (!providerName) {
    console.error(
      `Failed to map provider of '${externalIdentityProviderName}' for user of '${externalIdentityUserId}'.`
    );
    return { error: "Invalid external user's provider name." };
  }

  let userId = externalIdentityUserId;
  if (LOGIN_WITH_AMAZON === providerName) {
    // Amazon IDs look like amzn1.account.ABC123DEF456
    const amazonIdMatch = /^([^\.]+\.[^\.]+)\.([^\.]+)$/.exec(userId.trim());
    if (!amazonIdMatch) {
      console.error(`Failed to parse amazonId of '${userId}'`);
      return { error: "Invalid external user's amazonId." };
    }

    const [part1, amazonId] = amazonIdMatch.slice(1);
    userId = `${part1}.${amazonId.toUpperCase()}`;
  }

  console.log(`Linking '${existingUsername}' to '${userName}'.`);

  const cognito = new CognitoIdentityProviderClient({
    region
  });

  const parameters = {
    // Existing user in the user pool to be linked to the external identity provider user account.
    DestinationUser: {
      ProviderAttributeValue: existingUsername,
      ProviderName: 'Cognito'
    },
    // An external identity provider account for a user who does not currently exist yet in the user pool.
    SourceUser: {
      ProviderAttributeName: 'Cognito_Subject',
      ProviderAttributeValue: userId,
      ProviderName: providerName // Facebook, Google, Login with Amazon, Sign in with Apple
    },
    UserPoolId: userPoolId
  };

  // See https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity-provider/command/AdminLinkProviderForUserCommand/
  const command = new AdminLinkProviderForUserCommand(parameters);
  await cognito.send(command);

  console.log('Successfully linked external identity to user.');

  return { error: null };
};


exports.handler = async (event, context, callback) => {
  // See event structure at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
  const { triggerSource } = event;

  switch (triggerSource) {
    default: {
      return callback(null, event);
    }
    case 'PreSignUp_ExternalProvider': {
      try {
        const {
          user,
          error: getUserError
        } = await getFirstCognitoUserWithSameEmail(event);
        if (getUserError) {
          return callback(getUserError, null);
        }

        const {
          error: linkUserError
        } = await linkExternalUserToCognitoUser(event, user.Username);
        if (linkUserError) {
          return callback(linkUserError, null);
        }

        return callback(null, event);
      } catch (error) {
        const errorMessage =
          'An unhandled error occurred while signing up user from an external identity provider.';
        console.error(errorMessage, error);

        return callback(errorMessage, null);
      }
    }
  }
};

下面还包括用于部署此配置的 terraform 配置。

locals {
  user_pool_name          = "${var.name}-${var.environment}"
  preSignUp_zip_path      = "${path.module}/lambdas/preSignUp.js.zip"
  pre_sign_up_lambda_name = "cognito-${local.user_pool_name}-pre_sign_up"
}

data "archive_file" "pre_sign_up" {
  type        = "zip"
  source_file = "${path.module}/lambdas/preSignUp.js"
  output_path = local.preSignUp_zip_path
}

resource "aws_lambda_function" "pre_sign_up" {
  function_name    = local.pre_sign_up_lambda_name
  runtime          = "nodejs20.x"
  handler          = "preSignUp.handler"
  filename         = local.preSignUp_zip_path
  role             = aws_iam_role.pre_sign_up_lambda.arn
  source_code_hash = data.archive_file.pre_sign_up.output_base64sha256

  depends_on = [
    aws_iam_role_policy_attachment.pre_sign_up_lambda_cloudwatch,
    aws_cloudwatch_log_group.pre_sign_up_lambda,
  ]
}

resource "aws_lambda_permission" "pre_sign_up" {
  statement_id  = "AllowExecutionFromCognito"
  principal     = "cognito-idp.amazonaws.com"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.pre_sign_up.function_name
  source_arn    = aws_cognito_user_pool.this.arn
}

resource "aws_cloudwatch_log_group" "pre_sign_up_lambda" {
  name              = "/aws/lambda/${local.pre_sign_up_lambda_name}"
  retention_in_days = 7
}

resource "aws_cognito_user_pool" "this" {
  name                     = local.user_pool_name
  auto_verified_attributes = ["email"]
  deletion_protection      = "ACTIVE"

  admin_create_user_config {
    allow_admin_create_user_only = true
  }

  lambda_config {
    pre_sign_up = aws_lambda_function.pre_sign_up.arn
  }
}

data "aws_iam_policy_document" "pre_sign_up_lambda" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "pre_sign_up_lambda" {
  name               = "pre-sign-up-lambda-${var.name}-${var.environment}"
  assume_role_policy = data.aws_iam_policy_document.pre_sign_up_lambda.json
}

data "aws_iam_policy_document" "pre_sign_up_lambda_cloudwatch" {
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = ["arn:aws:logs:${var.aws_region}:*"]
  }
}

resource "aws_iam_policy" "pre_sign_up_lambda_cloudwatch" {
  name   = "pre-sign-up-lambda-cloudwatch-${var.name}-${var.environment}"
  policy = data.aws_iam_policy_document.pre_sign_up_lambda_cloudwatch.json
}

resource "aws_iam_role_policy_attachment" "pre_sign_up_lambda_cloudwatch" {
  role       = aws_iam_role.pre_sign_up_lambda.id
  policy_arn = aws_iam_policy.pre_sign_up_lambda_cloudwatch.arn
}

data "aws_iam_policy_document" "pre_sign_up_lambda_cognito" {
  statement {
    effect = "Allow"

    actions = [
      "cognito-idp:AdminLinkProviderForUser",
      "cognito-idp:ListUsers",
    ]

    resources = [aws_cognito_user_pool.this.arn]
  }
}

resource "aws_iam_policy" "pre_sign_up_lambda_cognito" {
  name   = "pre-sign-up-lambda-cognito-${var.name}-${var.environment}"
  policy = data.aws_iam_policy_document.pre_sign_up_lambda_cognito.json
}

resource "aws_iam_role_policy_attachment" "pre_sign_up_lambda_cognito" {
  role       = aws_iam_role.pre_sign_up_lambda.id
  policy_arn = aws_iam_policy.pre_sign_up_lambda_cognito.arn
}
© www.soinside.com 2019 - 2024. All rights reserved.