Предисловие
Продолжение цикла по работе с Azure B2C. В данной статье я расскажу о самом сложном и неочевидном моменте, а именно Identity Experience Framework.
Основная цель — собрать воедино картинку для тех кто вообще не в теме и помочь настроить какие-то основные фичи.
Ссылки на связанные посты
- Часть 1: Создание и настройка приложений на Azure AD B2C
- Часть 2: Работа с Identity Framework Experience
- Часть 3: Подключение приложения React
- Часть 4: Подключение приложения React Native
- Часть 5: Подключение и настройка бэкэнда на .NET Core 3
Базовая настройка
Перед тем как начать базовую настройку, хотел бы рассказать как происходит процесс загрузки новых правил:
- Заходим в Identity Experience Framework
- Нажимаем отправить пользовательскую политику
- Выбираем файл (не забываем нажать «Перезаписать настраиваемую политику, если она уже существует»)
- Отправляем
По сути, с прошлого раза ничего не изменилось, НО:
Если вы меняете файл TrustFrameworkExtension.xml или TrustFrameworkBase.xml — переодически загружайте файл, который на них ссылается.
Иногда, когда вы производите изменения в одном из этих файлов, тестируете, происходит так, что ваши изменения не появляются. Это происходит из за того, что в базовом файле
вы поменяли что-то так, что при проверке приведет к ошибке дочерний файл.
В прошлой статье мы остановились на том — что добавили следующие файлы:
a.TrustFrameworkBase.xml
b.TrustFrameworkExtensions.xml
c.SignUpOrSignin. XML
d.ProfileEdit. XML
e.PasswordReset. XML
Теперь мне бы хотелось рассказать подробно о каждом из них.
TrustFrameworkBase.xml
Данный файл содержит в себе базовую настройку. По сути он — основа основ, но в туториалах про него в основном говорят «Лучше не трогайте этот файл». Отчасти это правда, но есть несколько моментов о которых не говорят:
- Любой туториал, который говорит произвести изменения в TrustFrameworkExtensions.xml по сути своей перезаписывает правила из TrustFrameworkBase.xml
- Есть ситуации, когда удобнее поменять что-то в TrustFrameworkBase.xml.
- Если вы найдете в других файлах ссылку на объект, который отсутствует в этих файлах — то он 100% лежит в TrustFrameworkBase.xml и его можно открыть и посмотреть
Из моего опыта скажу — я поменял в этом файле всего две вещи (Локализацию и удалил одно поле).
TrustFrameworkExtension.xml
С данным файлом вы будете проводить много времени вместе. По сути, это основной файл для ваших настроек. О нем постоянно упоминается в туториалах.
SignUpOrSignin. XML, ProfileEdit. XML, PasswordReset. XML
Эти файлы — конечные страницы. Вероятно вы захотите добавить свои. В них будет происходить наименьшее количество изменений.
Теперь поговорим о структуре файлов. У всех файлов похожая структура, поэтому описывать я буду на основе файла TrustFrameworkExtension.xml.
Файл поделен на несколько основных блоков
<TrustFrameworkPolicy>
<BasePolicy> <!-- Ссылка на основной файл -->
<TenantId>customtenant.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkBase</PolicyId>
</BasePolicy>
<BuildingBlocks> <!-- Элементы из которых мы будем строить UI -->
</BuildingBlocks>
<ClaimsProviders> <!-- Собрание страниц воедино и добавление данных в JWT token) -->
</ClaimsProviders>
<UserJourneys> <!-- Переходы и передача данных между страницами -->
</UserJourneys>
</TrustFrameworkPolicy>
Теперь о каждом блоке отдельно.
BuildingBlocks
В этом блоке мы добавляем «инструменты», которые мы сможем использовать в дальнейшей работе.
ClaimsSchema
Элемент ClaimsSchema определяет типы утверждений, на которые можно ссылаться в рамках политики.
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="picture"><!-- Добавляем картинку как возможный элемент UI или данные для токена -->
<DisplayName>Picture</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="country"><!-- Пример добавления выпадающего списка -->
<DisplayName>Country</DisplayName>
<DataType>string</DataType>
<UserInputType>DropdownSingleSelect</UserInputType>
<Restriction>
<Enumeration Text="Russia" Value="russia" SelectByDefault="false" />
<Enumeration Text="Other" Value="other" SelectByDefault="false" />
</Restriction>
</ClaimType>
...
</ClaimsSchema>
Predicates
Предикаты и элементы предикат валидатионс позволяют выполнять проверку, чтобы убедиться, что в клиент Azure Active Directory B2C (Azure AD B2C) введены только правильно сформированные данные.
<Predicates> <!-- С помощью предикатов устанавливаются правила, в данном случае правила проверки пароля -->
<Predicate Id="LengthRange" Method="IsLengthRange">
<UserHelpText>The password must be between 6 and 64 characters.</UserHelpText>
<Parameters>
<Parameter Id="Minimum">6</Parameter>
<Parameter Id="Maximum">64</Parameter>
</Parameters>
</Predicate>
<Predicate Id="Lowercase" Method="IncludesCharacters">
<UserHelpText>a lowercase letter</UserHelpText>
<Parameters>
<Parameter Id="CharacterSet">a-z</Parameter>
</Parameters>
</Predicate>
...
</Predicates>
PredicateValidations
В то время как предикаты определяют проверку на соответствие типу утверждения, PredicateValidations группирует набор предикатов для формирования проверки ввода пользователя, соответствующей типу утверждения.
<PredicateValidations> <!-- Говорим по каким правилам проверять инпут -->
<PredicateValidation Id="CustomPassword">
<PredicateGroups>
<PredicateGroup Id="LengthGroup">
<PredicateReferences MatchAtLeast="1">
<PredicateReference Id="LengthRange" />
</PredicateReferences>
</PredicateGroup>
<PredicateGroup Id="CharacterClasses">
<UserHelpText>The password must have at least 1 of the following:</UserHelpText>
<PredicateReferences MatchAtLeast="2">
<PredicateReference Id="Lowercase" /> <!-- Ссылаемся на правила, которые добавили выше -->
<PredicateReference Id="Uppercase" />
...
</PredicateReferences>
</PredicateGroup>
</PredicateGroups>
</PredicateValidation>
</PredicateValidations>
ClaimsTransformations
Элемент ClaimsTransformations содержит список функций преобразования утверждений, которые могут использоваться в пути взаимодействия пользователя в качестве части настраиваемой политики.
<ClaimsTransformations> <!-- Превращаем одни данные в другие -->
<ClaimsTransformation Id="GenerateSendGridRequestBody" TransformationMethod="GenerateJson">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.to.0.email" />
<InputClaim ClaimTypeReferenceId="otp" TransformationClaimType="personalizations.0.dynamic_template_data.otp" />
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.dynamic_template_data.email" />
</InputClaims>
<InputParameters>
<InputParameter Id="template_id" DataType="string" Value="d-b0000000000000000000000000000000" /> <!-- Template ID SendGrid (это относится к костюмному письму) -->
<InputParameter Id="from.email" DataType="string" Value="custom@email.com" />
<InputParameter Id="personalizations.0.dynamic_template_data.subject" DataType="string" Value="Welcome to Habr!"/>
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="sendGridReqBody" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
...
</ClaimsTransformations>
ContentDefinitions
Позволяет задать шаблоны для каждой из ваших страниц
<ContentDefinitions> <!-- Тут указываем какой шаблон использовать для вашей страницы -->
<ContentDefinition Id="api.signuporsignin">
<LoadUri>https://azure.blob.core.windows.net/yourblobstorage/pagelayoutfile.html</LoadUri>
<RecoveryUri>~/common/default_page_error.html</RecoveryUri>
<DataUri>urn:com:microsoft:aad:b2c:elements:contract:unifiedssp:1.2.0</DataUri>
</ContentDefinition>
...
</ContentDefinitions>
DisplayControls
Элемент управления «Отображение» — это элемент пользовательского интерфейса, который имеет специальные функции и взаимодействует с серверной службой Azure Active Directory B2C (Azure AD B2C)
<DisplayControls> <!-- Создаем кнопку для превращения данных -->
<DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
<DisplayClaims> <!-- Отображаемое поле -->
<DisplayClaim ClaimTypeReferenceId="email" Required="true" />
<DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
</DisplayClaims>
<OutputClaims> <!-- Выходные данные (которые можно будет добавить в токен) -->
<OutputClaim ClaimTypeReferenceId="email" />
</OutputClaims>
<Actions>
<Action Id="SendCode"> <!-- Действия которые выполняются при нажатии на кнопку -->
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendGrid" />
</ValidationClaimsExchange>
</Action>
...
</Actions>
</DisplayControl>
...
</DisplayControls>
</BuildingBlocks>
ClaimsProviders
В этом блоке, мы будем создавать сами страницы, а точнее их наполнение. Тут мы будем указывать какие у страницы входные и выходные данные.
ClaimsProvider организует связь технических профилей с поставщиком утверждений.
<ClaimsProviders>
<ClaimsProvider> <!-- Для примера - этот провайдер есть в бэйз и мы его полностью перезаписываем-->
<DisplayName>Self Asserted</DisplayName>
Элемент TechnicalProfiles содержит набор технических профилей, поддерживаемых поставщиком утверждений.
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-Social">
<DisplayName>User ID signup</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item> <!-- Тип страницы. (Из моего опыта если хотите сделать полностью новую страницу, лучше использовать его)-->
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" /> <!-- Ключ из вашего контейнера с ключами -->
</CryptographicKeys>
<InputClaims> <!-- Входящие данные т.е. при переходе на эту страницу проверяем что поступили следующие данные-->
<InputClaim ClaimTypeReferenceId="givenName" />
<InputClaim ClaimTypeReferenceId="surname" />
</InputClaims>
<OutputClaims> <!-- Выходные данные. Некоторые получаются в результате работы технического профайла, другие в результате инпута юзера (ниже оставил оригинальные комментарии) -->
<!-- These claims ensure that any values retrieved in the previous steps (e.g. from an external IDP) are prefilled.
Note that some of these claims may not have any value, for example, if the external IDP did not provide any of
these values, or if the claim did not appear in the OutputClaims section of the IDP.
In addition, if a claim is not in the InputClaims section, but it is in the OutputClaims section, then its
value will not be prefilled, but the user will still be prompted for it (with an empty value). -->
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="newUser" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<!-- Optional claims. These claims are collected from the user and can be modified. If a claim is to be persisted in the directory after having been
collected from the user, it needs to be added as a PersistedClaim in the ValidationTechnicalProfile referenced below, i.e.
in AAD-UserWriteUsingAlternativeSecurityId. -->
<OutputClaim ClaimTypeReferenceId="givenName" Required="true"/>
<OutputClaim ClaimTypeReferenceId="surname" Required="true"/>
<OutputClaim ClaimTypeReferenceId="country" Required="true"/>
</OutputClaims>
</TechnicalProfile>
</ClaimsProvider>
Пример добавления поставщиков удостоверений Facebook
Пример частичной перезаписи правила
Полное правило из TrustFrameworkBase.xml
<ClaimsProvider> <!-- Маленький пример частичного наследования. Тут мы перезаписываем только некоторые параметры фэйсбука-->
<DisplayName>Facebook</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="Facebook-OAUTH">
<Metadata>
<Item Key="client_id">FACEBOOK_ID</Item>
<Item Key="scope">email public_profile</Item>
<Item Key="ClaimsEndpoint">https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="picture" PartnerClaimType="picture" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
Полное правило из TrustFrameworkBase.xml
<ClaimsProvider>
<!-- The following Domain element allows this profile to be used if the request comes with domain_hint
query string parameter, e.g. domain_hint=facebook.com -->
<Domain>facebook.com</Domain>
<DisplayName>Facebook</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="Facebook-OAUTH">
<!-- The text in the following DisplayName element is shown to the user on the claims provider
selection screen. -->
<DisplayName>Facebook</DisplayName>
<Protocol Name="OAuth2" />
<Metadata>
<Item Key="ProviderName">facebook</Item>
<Item Key="authorization_endpoint">https://www.facebook.com/dialog/oauth</Item>
<Item Key="AccessTokenEndpoint">https://graph.facebook.com/oauth/access_token</Item>
<Item Key="HttpBinding">GET</Item>
<Item Key="UsePolicyInRedirectUri">0</Item>
<!-- The Facebook required HTTP GET method, but the access token response is in JSON format from 3/27/2017 -->
<Item Key="AccessTokenResponseFormat">json</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_FacebookSecret" />
</CryptographicKeys>
<InputClaims />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="id" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="first_name" />
<OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="last_name" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="facebook.com" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
UserJourneys
В UserJourneys пользователя указываются явные пути, через которые политика позволяет приложению, основанному на утверждениях, предоставлять пользователю требуемые утверждения.
Ниже я добавил пару простых вещей, остальные найти легко в туториалах которые я добавлю ниже.
<UserJourneys>
<UserJourney Id="SignUp"> <!-- Объявляем ID UserJourney. На него мы будем ссылаться из файлов, которые были загружены после TrustFrameworkExtension.xml-->
<OrchestrationSteps><!-- Создаем шаги. Обязательно соблюдать нумерацию-->
<OrchestrationStep Order="1" Type="ClaimsExchange" ContentDefinitionReferenceId="api.localaccountsignup">
<ClaimsExchanges> <!-- Говорим какую страницу показать задав параметр TechnicalProfileReferenceId. Id нужен для того, что бы обмениваться данными из поставщика -->
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail-2" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" /> <!-- Отправляем данные на сервер -->
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
<UserJourney Id="PasswordReset">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
...
</UserJourneys>
Пример обмена ClaimsExchange
Ниже пример взаимодействия ClaimsExchanges с поставщиком.
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
<ClaimsProviderSelection TargetClaimsExchangeId="GoogleExchange" />
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
Типовые задачи
В результате прочитанного выше вам будет проще понимать туториалы ниже.
- Настройка регистрации и входа с учетной записью Facebook
- Настройка регистрации и входа с учетной записью Google
- Настраиваемая проверка электронной почты
- Настройка сложности пароля в настраиваемых политиках
- Настройка пользовательского интерфейса
- Настройка языка
SignUpOrSignin.XML, ProfileEdit.XML, PasswordReset.XML
Это конечные файлы, где можно перезаписать \ добавить BuildingBlocks и где мы указываем, какие данные добавить в токен.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="antekesd.onmicrosoft.com" PolicyId="B2C_1A_signup_signin" PublicPolicyUri="http://antekesd.onmicrosoft.com/B2C_1A_signup_signin">
<BasePolicy>
<TenantId>antekesd.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ContentDefinitions>
<ContentDefinition Id="api.signuporsignin"> <!-- Перезаписываем внешний вид именно для этой страницы -->
<LoadUri>https://some.blob.core.windows.net/some/some.html</LoadUri>
<RecoveryUri>~/common/default_page_error.html</RecoveryUri>
<DataUri>urn:com:microsoft:aad:b2c:elements:contract:unifiedssp:1.2.0</DataUri>
</ContentDefinition>
</ContentDefinitions>
</BuildingBlocks>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" /> <!-- Указываем путь, по которому должен пройти пользователь-->
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims> <!-- Добавляем данные в JWT Token. Эти данные нужно возвращать через технические профайлы -->
<OutputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="email"/>
<OutputClaim ClaimTypeReferenceId="givenName" Required="true"/>
<OutputClaim ClaimTypeReferenceId="surname" Required="true"/>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="identityProvider" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="picture" />
<OutputClaim ClaimTypeReferenceId="country" Required="true"/>
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
Тестирование
Для того что бы протестировать свежие изменения нужно:
- Перейти в Identity Experience Framework
- Выбрать Policy, который вы хотите протестировать
- Нажать «Запустить сейчас»
Заключение
В результате вы получите форму авторизации полностью (или почти) удовлетворяющую вашим \ заказчика требованиям.
Спасибо за внимание!