【问题标题】:Microsoft oidc in AWS Cognito allowing multiple tenantsAWS Cognito 中的 Microsoft oidc 允许多个租户
【发布时间】:2021-01-27 13:19:54
【问题描述】:

我正在尝试使用 AWS Cognito 用户池中的 Microsoft 帐户实现社交登录。

我遵循了该线程中提到的文档和解决方案: https://forums.aws.amazon.com/thread.jspa?threadID=287376&tstart=0
我的问题是将发行者设置为允许多个租户。

此发行者仅适用于私人账户:
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0

此发行者仅适用于我们目录中的帐户(租户): https://login.microsoftonline.com/AZURE_ACTIVE_DIRECTORY/v2.0

这个发行者根本不工作。使用 Microsoft 登录后,我收到错误的颁发者错误或错误请求: https://login.microsoftonline.com/common/v2.0

我需要一个适用于任何 Microsoft 帐户(所有租户)的 oidc 提供商,这可能吗?

如果我在 AWS Cognito oidc 配置中将颁发者租户设置为 common,那么这将启动正确的 Microsoft 流程,但我假设 Cognito 中的颁发者检查失败,因为 Microsoft 总是返回 jwt 令牌中的特定租户 ID 作为发行人。

我检查过的 microsoft 文档中的其他信息:
https://docs.microsoft.com/de-de/azure/active-directory/develop/v2-protocols-oidc https://docs.microsoft.com/de-de/azure/active-directory/develop/id-tokens

【问题讨论】:

标签: amazon-web-services single-sign-on amazon-cognito openid-connect microsoft-account


【解决方案1】:

我是 Dragan 的同事,经过大量尝试,我们在团队中找到了一个切实可行的解决方案。只是注意到我们实际上可以访问高级 AWS 和 Microsoft 支持,但他们无法帮助我们。 AWS Cognito 团队意识到了这个问题,但似乎没有优先级 - 因为将近一年没有任何修复。

流程说明

我们在前端使用他们的 javascript 库 msal 对微软进行身份验证(不涉及 Cognito)。我们收到一个 JWT 令牌并使用这个令牌在用户池中创建一个普通的 Cognito 用户。电子邮件是从 microsoft 令牌中读取的,密码是使用安全随机(尽可能长)自动生成的。此外,我们将 microsoft 令牌作为自定义用户属性发送。在 PreSignUp Lambda 中,如果 microsoft 令牌有效,我们会自动激活用户,因此不会向用户发送密码验证电子邮件。回到前端,我们将放大自定义身份验证挑战登录与我们缓存在前端的电子邮件一起使用。现在我们通过 DefineAuthChallenge,然后是 CreateAuthChallenge。 CreateAuthChallenge 不做任何事情,因为 microsoft 令牌是我们的挑战,不需要创建。回到前端,我们调用包含 sessionKey 和 microsoft token 的 CustomChallenge。我们现在在 VerifyChallenge Lambda 中,我们使用开源 JWT 库验证 microsoft 令牌本身。流程通过我们只允许一次尝试的 DefineAuthChallenge 返回。最后,用户从 Cognito 收到 Cognito 令牌。

以下 sn-ps 是 Lambda 的完整代码 sn-ps。我不得不从我们的项目中删除一些特定的东西,所以希望这样做不会破坏任何东西。所有文件都是index.js,Lambda 不需要其他文件。您肯定可以外包一些我们尚未完成的重复代码。此处不包含 FE 代码。

PreSignUp Lambda

const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');

const client = jwksClient({
    jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});

const options = {
    algorithms: ['RS256']
};

function getKey(header, callback) {
    client.getSigningKey(header.kid, function (err, key) {
        const signingKey = key.publicKey || key.rsaPublicKey;
        callback(null, signingKey);
    });
}

const verifyMicrosoftToken = async (jwt, token, key) => {
    if (!token) return {};
    return new Promise((resolve, reject) =>
        jwt.verify(token, key, options, (err, decoded) => err ? reject({}) :
            resolve(decoded))
    );
};

exports.handler = async (event) => {

    const email = event.request.userAttributes.email.toLowerCase();

        //verify microsoft and auto enable user
        if (event.request.userAttributes['custom:msalIdtoken']) {
            const token = await verifyMicrosoftToken(
                jwt, event.request.userAttributes['custom:msalIdtoken'], getKey
            );
            const emailFromToken = token.email !== undefined ? token.email : token.preferred_username;
            if (token && emailFromToken.toLowerCase() === email) {
                event.response.autoConfirmUser = true;
                event.response.autoVerifyEmail = true;
            }

        }

    return event;
};

DefineAuthChallenge Lambda

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

   if (event.request.session &&
       event.request.session.length > 0 &&
       event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE' &&
       event.request.session.slice(-1)[0].challengeResult === true){
       console.log("Session: ", event.request.session);
       event.response.issueTokens = true;
       event.response.failAuthentication = false;

   } else {
       event.response.failAuthentication = false;
       event.response.issueTokens = false;
       event.response.challengeName = 'CUSTOM_CHALLENGE';
   }
    
   // Return to Amazon Cognito
   callback(null, event);
};

CreateChallenge Lambda

exports.handler = (event, context, callback) => {
   if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
       event.response.publicChallengeParameters = {};
       event.response.publicChallengeParameters.dummy = 'dummy';
       event.response.privateChallengeParameters = {};
       event.response.privateChallengeParameters.dummy = 'dummy';
       event.response.challengeMetadata = 'MICROSOFT_JWT_CHALLENGE';
   }
   callback(null, event);
};

VerifyAuthChallenge Lambda

const AWS = require('aws-sdk');
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
    jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});

const options = {
    algorithms: ['RS256']
};
function getKey(header, callback){
    client.getSigningKey(header.kid, function(err, key) {
        const signingKey = key.publicKey || key.rsaPublicKey;
        callback(null, signingKey);
    });
}

exports.handler = (event, context, callback) => {
    if(event.request.challengeAnswer){
        jwt.verify(event.request.challengeAnswer, getKey, options, function(err, decoded) {
            if(decoded){
                const email = decoded.email !== undefined ? decoded.email : decoded.preferred_username;
                if (email.toLowerCase() === event.request.userAttributes['email'].toLowerCase()) {
                    event.response.answerCorrect = true;
                    // it is necessary to add this group to user so in BE we can resolve microsoft provider
                    const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider();
                    var params = {
                        GroupName: "CUSTOM_MICROSOFT_AUTH",
                        UserPoolId: event.userPoolId,
                        Username: event.userName
                    };

                    cognitoIdentityServiceProvider.adminAddUserToGroup(params, function (err) {
                        if (err) {
                            console.log("Group cannot be added to the user: " + event.userName, err);
                        }
                        callback(null, event);
                    });
                }
            }
            if(err){
                console.log(err);
            }
        });
    }else{
        event.response.answerCorrect = false;
        callback(null, event);
    }
};

这是我们使用的一些链接

后记

如果您在此代码中发现任何与安全相关的问题,请私下与我联系,我们公司将根据严重程度给予一些赞赏($)。

【讨论】:

  • 感谢您的解决方法。我现在正在尝试重建它。不确定 PreSignUp Lambda,您有 custom.js 中的代码并将事件作为回调返回吗?还是你重写了 index.js?
  • 所有文件代表 index.js 文件。 PresignUp lambda 是使用 async/await 语法编写的 - 请参阅:docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html。很抱歉在不同的 lambda 中有不同的语法。希望有一天我能抽出时间来调整一下。我现在还添加了 PresignUp lambda 的完整代码。
【解决方案2】:

我通过避免使用用户池,并直接与 azure 端点 https://login.microsoftonline.com/common/oauth2/v2.0/authorize 等进行交互,从而避免了这个(租户/颁发者)问题。

我仍然必须使用身份池来映射到 IAM 角色。

可以理解,这比让用户池处理令牌内容要工作更多,但这是我发现它适用于所有 azure 广告帐户的唯一方法。

【讨论】:

  • 这可能是某些用例的解决方案,但不是我们的解决方案,因为我们希望管理用户池中的所有用户,无论用户使用 ms、google、facebook 还是用户名+密码。此外,我们根本不使用身份池,因为我们不需要用户的 IAM 权限。
【解决方案3】:

问题的根本原因:

当我们通过 OIDC 集成 Microsoft 登录时,根据我们的要求,我们有几个 options

如果只有 Azure AD 的工作或学校帐户的用户可以登录应用程序,我们必须参考https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration

此外,如果任何拥有 Microsoft 帐户(工作或学校 Azure AD 帐户,或个人 - Outlook、Live 等)的用户都可以登录应用程序,那么我们必须参考 https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

在这些元数据文件中,我们可以看到颁发者是https://login.microsoftonline.com/{tenantid}/v2.0
所以基本上,根据最终用户的 Azure AD 租户,Azure AD 发出的id_token 将具有不同的发出者 (iss) 声明的值。

这意味着iss 声明会针对每个用户动态变化。 目前,Cognito 不支持这种动态行为。 在 Cognito 中,在 OIDC 身份提供者配置下,我们必须手动指定颁发者,我们只能指定一个。 因此 Cognito 无法正确验证 Azure AD 颁发的 id_token。它返回一个错误,说 Bad id_token issuer。

另一种解决方法:

有些身份提供者支持 Azure AD 的这种动态 iss 声明行为。 (Auth0、Azure AD B2C 等)。因此,我们可以选择其中之一并将其配置为通过 OIDC 与 Microsoft (Azure AD) 进行通信。然后将该 IDP 添加为 Cognito 中的 OIDC 身份提供者。基本上,我们将该 IDP 置于 Cognito 和 Microsoft (Azure AD) 之间。

【讨论】:

    猜你喜欢
    • 2018-03-25
    • 2019-07-11
    • 1970-01-01
    • 2021-07-29
    • 2019-09-04
    • 2021-03-21
    • 2019-06-21
    • 2022-10-02
    • 2018-12-19
    相关资源
    最近更新 更多