AWS Cognito 中的 Microsoft oidc 允许多个租户

Microsoft oidc in AWS Cognito allowing multiple tenants

我正在尝试使用 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 配置中将 issuer tenant 设置为 common,那么这将启动正确的 Microsoft 流程,但我假设在 Cognito 中检查 issuer 失败,因为 Microsoft 总是 returns 内部的特定租户 ID jwt 令牌作为发行者的一部分。

我检查过的来自 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

我通过避免使用用户池并直接与 azure 端点交互 https://login.microsoftonline.com/common/oauth2/v2.0/authorize 等来避免这个 (tenancy/issuer) 问题。

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

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

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

流程说明

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

以下片段是 Lambda 的完整代码片段。我不得不从我们的项目中删除一些特定的东西,所以希望在这样做时不会破坏任何东西。所有文件都是 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);
};

创建挑战 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);
    }
};

前端(Angular 组件)

ngOnInit() {
    // after microsoft successful sign in we need to continue to cognito authentication
    this.authMsalService.handleRedirectCallback((authError, response) => {
        if (authError) {
            this.showLoginError = true;
            return;
        }
        this.signUpOrSignInWithMicrosoftToken(response.idToken.rawIdToken);
    });
}

onSignInWithProvider(provider: string) {
    this.cognitoService.clearAuthData();
    if (provider === SINGLE_SIGN_ON_PROVIDER.MICROSOFT) {
        this.authMsalService.loginRedirect({
            scopes: ['user.read', 'email'],
        });
    } else {
        const options: FederatedSignInOptions = {provider: CognitoHostedUIIdentityProvider[GeneralUtils.capitalize(provider)]};
        this.socialSignIn(options);
    }
}

private socialSignIn(options: any): void {
    Auth.federatedSignIn(options).catch(() => {
        this.showLoginError = true;
        this.uiBlockerService.setIsUiBlocked(false);
    });
}

private signUpOrSignInWithMicrosoftToken(microsoftIdToken: string) {
    this.uiBlockerService.setIsUiBlocked(true);
    const attributes = {};
    const userName: string = this.authMsalService.getAccount().userName.toLowerCase();
    attributes['email'] = userName;
    attributes['custom:msalIdtoken'] = microsoftIdToken;
    if (this.authMsalService.getAccount().idToken['family_name']) {
        attributes['family_name'] = this.authMsalService.getAccount().idToken['family_name'];
    }
    if (this.authMsalService.getAccount().idToken['given_name']) {
        attributes['given_name'] = this.authMsalService.getAccount().idToken['given_name'];
    }
    Auth.signUp({
        username: userName,
        password: SSOUtils.getSecureRandomString(20),
        attributes: attributes
    }).then(user => {
        // register
        // after successfully signup we need to continue with authentication so user is signed in automatically
        this.authenticateWithMicrosoftToken(microsoftIdToken);
    }).catch(error => {
        // login
        // if user is already registered we continue with sign in
        if (error.code === 'UsernameExistsException') {
            this.authenticateWithMicrosoftToken(microsoftIdToken);
        }
        this.uiBlockerService.setIsUiBlocked(false);
    });

}

private authenticateWithMicrosoftToken(microsoftIdToken: string) {
    const userName: string = this.authMsalService.getAccount().userName.toLowerCase();
    Auth.signIn(userName).then(cognitoUser => {
        // after sign in is started we need to continue with authentication and we sent microsft token
        Auth.sendCustomChallengeAnswer(cognitoUser, microsoftIdToken);
    });
}

这是我们使用的一些链接

后记

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

问题的根本原因:

当我们通过 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。它 returns 一个错误,说 Bad id_token issuer.

另一种解决方法:

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