Microsoft oidc в AWS Cognito, позволяющий нескольким арендаторам

#amazon-web-services #single-sign-on #amazon-cognito #openid-connect #microsoft-account

#amazon-веб-сервисы #единый вход #amazon-cognito #OpenID-connect #microsoft-учетная запись

Вопрос:

Я пытаюсь реализовать социальный вход в систему с использованием учетной записи Microsoft в пулах пользователей AWS Cognito.

Я следил за документацией и решением, упомянутым в этой теме: https://forums.aws.amazon.com/thread.jspa?threadID=287376amp;tstart=0
Моя проблема заключается в настройке эмитента на разрешение нескольких арендаторов.

Этот эмитент работает только для личных учетных записей:
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0

Этот эмитент работает только для учетных записей в нашем каталоге (арендатор): https://login.microsoftonline.com/AZURE_ACTIVE_DIRECTORY/v2.0

Этот эмитент вообще не работает. Я получаю ошибку bad issuer или неверный запрос после входа в Microsoft: https://login.microsoftonline.com/common/v2.0

Мне нужен один поставщик oidc, который будет работать для любой учетной записи Microsoft (для всех арендаторов), возможно ли это вообще?

Если я установлю для арендатора-эмитента значение common в конфигурации AWS Cognito oidc, то это запустит правильный поток Microsoft, но я предполагаю, что проверка для эмитента в Cognito завершается неудачей, потому что Microsoft всегда возвращает конкретный идентификатор арендатора внутри токена jwt как часть эмитента.

Дополнительная информация из документации Microsoft, которую я проверил:
https://learn.microsoft.com/de-de/azure/active-directory/develop/v2-protocols-oidc
https://learn.microsoft.com/de-de/azure/active-directory/develop/id-tokens

Комментарии:

1. Привет @Nishant-MSFTIdentity, спасибо за ваш ответ, я все еще ищу ответ на этот вопрос, я попробовал упомянутого вами эмитента, и он работает только для личных учетных записей. Для моей рабочей учетной записи я получаю следующую ошибку «error_description = Bad id_token эмитент». Я могу успешно войти в систему в Microsoft, поэтому, вероятно, некоторые проверки для эмитента на стороне aws вызывают эту проблему

2. Я подумал, что можно было бы изменить утверждение «iss» в токене jwt azure ad, чтобы токен jwt всегда содержал: login.microsoftonline.com/common/v2.0 — Но, похоже, это невозможно, потому что «iss» принадлежит к ограниченному набору утверждений, см. Здесь: learn.microsoft.com/en-gb/azure/active-directory/develop /… и вот youtu.be /4wmKLAPvU6c? t = 225

3. на данный момент есть какое-либо решение?

Ответ №1:

Я коллега Драгана, и после долгих попыток мы нашли решение в нашей команде, которое действительно работает. Просто чтобы заметить, что у нас был доступ к премиум-сервисам AWS и службе поддержки Microsoft, но они не смогли нам помочь. Команда AWS Cognito знает об этой проблеме, но, похоже, она не имеет приоритетного значения — уже почти год не было никаких исправлений.

Блок-схема: настраиваемый поток авторизации Microsoft

Объяснение потока

Мы выполняем аутентификацию в Microsoft, используя их библиотеку JavaScript msal во внешнем интерфейсе (без участия Cognito). Мы получаем токен JWT и используем его для создания обычного пользователя Cognito в пуле пользователей. Электронная почта считывается из токена Microsoft, а пароль автоматически генерируется с помощью безопасного случайного (как можно дольше). Кроме того, мы отправляем токен Microsoft в качестве пользовательского атрибута пользователя. В PreSignUp Lambda мы автоматически активируем пользователя, если токен Microsoft действителен, поэтому пользователю не отправляется электронное письмо с подтверждением пароля.

Во внешнем интерфейсе мы используем расширенную пользовательскую проверку подлинности с помощью электронной почты, которую мы кэшировали во внешнем интерфейсе. Теперь мы проходим через DefineAuthChallenge, а затем создаем authchallenge. CreateAuthChallenge ничего не делает, поскольку токен Microsoft — это наша задача, и его не нужно создавать. В интерфейсе мы вызываем CustomChallenge, содержащий SessionKey и токен Microsoft. Сейчас мы находимся в VerifyChallenge Lambda, где мы проверяем сам токен Microsoft с помощью библиотек JWT с открытым исходным кодом. Поток возвращается через DefineAuthChallenge, где мы разрешаем только одну попытку. Наконец, пользователь получает токены Cognito от Cognito.

Следующие фрагменты представляют собой полные фрагменты кода для лямбд. Мне пришлось удалить некоторые конкретные вещи из нашего проекта, так что, надеюсь, при этом ничего не сломалось. Все файлы являются index.js исходными, и никакие дополнительные файлы не требуются для лямбд. Вы наверняка могли бы передать на аутсорсинг дублированный код, чего мы еще не сделали. Сюда также включены наиболее важные части кода FE.

Предварительная регистрация лямбда

 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 amp;amp; emailFromToken.toLowerCase() === email) {
                event.response.autoConfirmUser = true;
                event.response.autoVerifyEmail = true;
            }

        }

    return event;
};
  

DefineAuthChallenge Lambda

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

   if (event.request.session amp;amp;
       event.request.session.length > 0 amp;amp;
       event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE' amp;amp;
       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);
    }
};
  

Интерфейс (компонент 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);
    });
}
  

Вот несколько ссылок, которые мы использовали

PostScript

Если вы обнаружите в этом коде какую-либо проблему, связанную с безопасностью, пожалуйста, свяжитесь со мной в частном порядке, и наша компания выскажет некоторую признательность ($) в зависимости от серьезности.

Комментарии:

1. Спасибо за подход к решению. Я пытаюсь перестроить его прямо сейчас. Не уверен в лямбде предварительной регистрации, у вас есть код в custom.js и вернуть событие как обратный вызов? Или вы переопределили index.js ?

2. Все файлы представляют index.js файлы. Лямбда-код предварительной регистрации был написан с использованием синтаксиса async / await — см.: docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html . Извините за различный синтаксис в разных лямбда. Надеюсь, однажды я найду время для корректировки этого. Я также добавил полный код для предварительной регистрации lambda.

3. Было бы большим подспорьем, если бы был предоставлен некоторый фрагмент инициализации пользовательской аутентификации.

4. Я добавил код интерфейса.

5. привет, @flohall, мы столкнулись с той же проблемой здесь в 2023 году :). Существует ли какое-либо публичное отслеживание проблемы на стороне AWS? Есть ли способ повысить его? Спасибо, что поделились своим решением.

Ответ №2:

Основная причина проблемы:

Когда мы интегрируем вход в Microsoft через OIDC, у нас есть несколько вариантов, основанных на наших требованиях.

В случае, когда только пользователи с учетными записями для работы или учебы в 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 конечного пользователя id_token , утверждение, выданное Azure AD, будет иметь другое значение для утверждения issuer ( iss ).

Это означает iss , что требование динамически изменяется для каждого пользователя. Прямо сейчас это динамическое поведение не поддерживается Cognito. В Cognito в конфигурациях поставщика удостоверений OIDC мы должны указать эмитента вручную, и мы можем указать только одного. Таким образом, Cognito не может должным образом проверить id_token, выданный Azure AD. и он возвращает сообщение об ошибке с ошибочным эмитентом id_token.

Еще один обходной путь:

Существуют поставщики удостоверений, которые поддерживают такое динамическое iss поведение Azure AD с утверждениями. (Auth0, Azure AD B2C и т. Д.). Таким образом, мы можем выбрать одного из них и настроить его для взаимодействия с Microsoft (Azure AD) через OIDC. Затем добавьте этого IDP в качестве поставщика удостоверений OIDC в Cognito. По сути, мы размещаем этот IDP между Cognito и Microsoft (Azure AD).

Комментарии:

1. Я попытался использовать Azure AD B2C в качестве промежуточного звена, как предлагалось, но обнаружил, что войти в систему можно только с помощью личных учетных записей Microsoft вместе с индивидуально выбранными / локальными арендаторами AD. Это не похоже на общее решение, при котором вы можете войти в систему с помощью любой организации или личной учетной записи Microsoft, но, возможно, я что-то пропустил.

Ответ №3:

Я избежал этой проблемы (аренда / эмитент), избегая использования пользовательского пула и напрямую взаимодействуя с конечными точками Azure https://login.microsoftonline.com/common/oauth2/v2.0/authorize и т.д..

Мне все еще нужно использовать identitypool для сопоставления с ролью IAM.

Понятно, что это требует больше работы, чем использование пользовательского пула для обработки токенов, но это единственный способ, которым я нашел его для работы со всеми учетными записями Azure ad.

Комментарии:

1. Это может быть решением для некоторых вариантов использования, но не для нас, поскольку мы хотим управлять всеми пользователями в пуле пользователей независимо от того, используют ли пользователи ms, google, facebook или имя пользователя пароль. Кроме того, мы вообще не используем пулы удостоверений, поскольку нам не нужны разрешения IAM для пользователей.