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

Dra*_*ski 6 amazon-web-services single-sign-on microsoft-account openid-connect amazon-cognito

我正在尝试在 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 作为颁发者的一部分。

我检查过的微软文档中的其他信息:
https : //docs.microsoft.com/de-de/azure/active-directory/develop/v2-protocols-oidc https://docs.microsoft.com/de-de/天蓝色/活动目录/开发/id-tokens

flo*_*all 15

我是 Dragan 的同事,经过多次尝试,我们在团队中找到了一个真正有效的解决方案。请注意,我们可以获得高级 AWS 和 Microsoft 支持,但他们无法帮助我们。AWS Cognito 团队已经意识到这个问题,但似乎没有优先考虑 - 近一年来还没有任何修复。

流程图:自定义 Microsoft 身份验证流程

流程说明

我们在前端使用他们的 JavaScript 库 msal 对 Microsoft 进行身份验证(不涉及 Cognito)。我们收到一个 JWT 令牌,并使用它在用户池中创建一个普通的 Cognito 用户。电子邮件是从 Microsoft 令牌中读取的,密码是使用安全随机数(尽可能长)自动生成的。此外,我们将 Microsoft 令牌作为自定义用户属性发送。在 PreSignUp Lambda 中,如果 Microsoft 令牌有效,我们会自动激活用户,因此不会向用户发送密码验证电子邮件。

回到前端,我们使用放大自定义身份验证挑战登录以及我们在前端缓存的电子邮件。现在我们依次执行 DefineAuthChallenge 和 CreateAuthChallenge。CreateAuthChallenge 不会执行任何操作,因为 microsoft 令牌是我们的挑战,不需要创建。回到前端,我们调用包含 sessionKey 和 Microsoft 令牌的 CustomChallenge。我们现在处于VerifyChallenge Lambda 中,我们使用开源JWT 库验证Microsoft 令牌本身。该流程返回到 DefineAuthChallenge,我们只允许一次尝试。最后,用户从 Cognito 收到 Cognito 令牌。

以下片段是 Lambda 的完整代码片段。我必须从我们的项目中删除一些特定的东西,所以希望这样做不会破坏任何东西。所有文件都是index.jsLambda,不需要任何其他文件。您当然可以外包一些重复的代码,但我们还没有这样做。FE 代码最重要的部分也包含在这里。

预注册 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;
};
Run Code Online (Sandbox Code Playgroud)

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);
};
Run Code Online (Sandbox Code Playgroud)

创建挑战 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);
};
Run Code Online (Sandbox Code Playgroud)

验证验证挑战 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);
    }
};
Run Code Online (Sandbox Code Playgroud)

前端(角度组件)

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);
    });
}
Run Code Online (Sandbox Code Playgroud)

这是我们使用的一些链接

后记

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


Sam*_*han 5

问题的根本原因:

\n

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

\n

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

\n

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

\n

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

\n

这意味着iss每个用户的声明都会动态更改。\n目前,Cognito 不支持该动态行为\xe2\x80\x99。\n在 Cognito 中,在 OIDC 身份提供程序配置下,我们必须手动指定颁发者,并且我们只能指定一个。\n因此 Cognito 无法正确验证 Azure AD 颁发的 id_token。它返回一个错误,指出 id_token 发行者错误。

\n

另一个解决方法:

\n

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

\n