Pull to refresh
91.52
2ГИС
Главные по городской навигации

Авторизация для ленивых. Наши грабли

Reading time 57 min
Views 28K


Всем привет! Недавно мы решали задачу авторизации пользователей мобильного приложения на нашем бекенде. Ну и что, спросите вы, задача-то уже тысячу раз решённая. В этой статье я не буду рассказывать историю успеха. Лучше расскажу про те грабли, которые мы собрали.


Немного про проект


Мы в 2ГИС делаем крутой и точный справочник компаний. Для обеспечения качества и актуальности данных в 2ГИС есть несколько внутренних систем. Одна из них называется YouLa — нет, не та, где публикуют объявления. Наша YouLa поддерживает процесс выверки данных на местности.


Часть системы — мобильное приложение, с которым ходят пешеходники. Пешеходники — специалисты, которые обходят весь город. Карта разбита на разные участки для проверки.


Посмотрите, как выглядит территориальное деление Московской области. Разные цвета на карте обозначают разные назначения территорий.



Пешеходники приносят нам данные об организациях, в которые невозможно позвонить или у которых нет сайта. Например, шашлычные, киоски с овощами. Кроме того, бывает так, что у организации изменился телефон и мы не можем туда дозвониться. Во всех упомянутых случаях в организацию приходит наш специалист и сверяет информацию на местности.


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


На нашем новом бекенде мы хотим знать, что за пользователь к нам пришёл.


К авторизации у нас несколько требований:


— надежность и безопасность,
— аутентификация по разным источникам,
— аутентификация нескольких типов клиентов Web, Mobile, API.


Выбор способа аутентификации


Для реализации аутентификации существует много разных подходов, у каждого есть свои плюсы и минусы. Учитывая, что у нас большое количество точек интеграции,
мы решили не изобретать велосипед и взять провайдера аутентификации и авторизации на базе OpenId Connect. Для авторизации на бекенде используем JWT.


Подробнее можно прочитать в статье «Аутентификация и авторизация в микросервисных приложениях».


Чем хорош JWT и стандарт OpenId Connect в Enterprise?


Сейчас даже в рамках одной компании системы разрабатываются на разных стеках технологий и зачастую их потом тяжело подружить. В рамках одного стека технологий тоже можно поймать много странных и неожиданных эффектов, что уж говорить про ситуацию, когда у вас несколько систем. Для JWT и OpenId Connect список поддерживаемых клиентов и платформ впечатляет.


Схема работы всех компонентов выглядит вот так:


В рамках протокола поддерживается динамическое подключение поставщиков аутентификации. Мы рассматривали два источника — Google+ и ADFS. Но в дальнейшем нам бы хотелось просто и быстро расширять аудиторию продукта, например, за счёт подключения к системе других компаний, которые могли бы решать в нашей системе свои задачи.


С помощью JWT можно легко организовать аутентификацию разношёрстных клиентов. Более того, многие облачные клиенты предлагают сразу целый набор библиотек, облегчающих интеграцию провайдера в ваше приложение.


Облачные решения


Первой платформой, которую мы решили попробовать, был Auth0. Платформа очень крутая и для разработчика, и для администратора. В ней есть подробная документация, красивый и понятный Web UI для настройки всех параметров. В наше Java/Kotlin-приложение и на бекенд аутентификация была прикручена за пару часов.


Основные плюсы, которые мы отметили при работе с платформой Auth0:


— подробная документация и бесконечное количество примеров кода на распространённых языках программирования;
— возможность использовать для аутентификации не веб, а нативную форму входа.


Для того, чтобы реализовать поддержку JWT аутентификации в бекенде, достаточно написать всего несколько строчек (этот код для разных платформ будет отличаться только параметрами Authority и Audience), в некоторых случаях потребуется ещё указать сертификаты для проверки подписи токенов:


Код бекенда на .NET Core
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            // 1. Add Authentication Services
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

            }).AddJwtBearer(options =>
            {
                options.Authority = "https://devday2gis.auth0.com/";
                options.Audience = "https://devday.api";
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // 2. Add Authentication
            app.UseAuthentication();

            app.UseMvc();
        }

Для того, чтобы прикрутить аутентификацию к мобилке — ещё несколько строчек:


Код логина на мобилке
 private void login() {
        token.setText("Not logged in");
        Auth0 auth0 = new Auth0(this);
        auth0.setOIDCConformant(true);
        WebAuthProvider.init(auth0)
                .withScheme("demo")
                .withAudience(String.format("https://%s/userinfo", getString(R.string.com_auth0_domain)))
                .withScope("openid email profile")
                .start(MainActivity.this, new AuthCallback() {

                    @Override
                    public void onSuccess(@NonNull final Credentials credentials) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                idToken = credentials.getIdToken();
                                accessToken = credentials.getAccessToken();
                                Log.d("id token", credentials.getIdToken());
                                Log.d("access token", credentials.getAccessToken());
                                token.setText("Logged in: " + credentials.getIdToken());
                            }
                        });
                    }

                    @Override
                    public void onFailure(@NonNull final Dialog dialog) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                dialog.show();
                            }
                        });
                    }

                    @Override
                    public void onFailure(final AuthenticationException exception) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(MainActivity.this, "Error: " + exception.getMessage(), Toast.LENGTH_SHORT).show();
                            }
                        });
                    }

                });
    }

Как видно из примера, после аутентификации к нам приходит два токена + ещё один (RefreshToken) не показан в коде.


Для чего они нужны?


IdToken — содержит учетные данные пользователя
AccessToken — для авторизации на API
RefreshToken — для обновления AccessToken


Вопрос на засыпку: зачем необходимы два токена Access и Refresh?


Ответ

Рассмотрим два случая кражи ключей:


  1. Негодяй украл только AccessToken. Тогда он будет валиден только до того момента, пока вы не воспользуетесь своим RefreshToken.
  2. Негодяй украл оба токена. Тогда, как только он воспользуется RefreshToken, ваши токены перестанут действовать и вас разлогинит из приложения. Если вы воспользуетесь своими учётными данными, то токены атакующего перестанут действовать.
    Использование двух токенов ограничивает время, на которое атакующий будет иметь доступ к вашим API.

Сам JWT-IdToken токен выглядит так:


Из этого токена мобильное приложение получает информацию об аутентифицированном пользователе. Соответственно, IdToken мы используем для отрисовки ФИО пользователя и его аватарки.


AccessToken мы прикрепляем к header запросов:


Вызов API из Android приложения
private void makeApiCall()
    {
        DevDayApi api = CreateApi("http://rnd-123.2gis.local/", accessToken);

        api.getUserProfile().enqueue(new Callback<ApiResponse>() {
            @Override
            public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
                runOnUiThread(() -> {

                    if(response.body() != null)
                        apiAnswer.setText(response.body().Answer);
                });
            }

            @Override
            public void onFailure(Call<ApiResponse> call, Throwable t) {
                apiAnswer.setText(t.getMessage());
            }
        });
    }

    private DevDayApi CreateApi(String baseUrl, String authToken)
    {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);

        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(chain -> {
                    Request newRequest = chain.request().newBuilder()
                            .addHeader("Authorization", "Bearer " + authToken)//tokenProvider.getAuthToken())
                            .build();

                    return chain.proceed(newRequest);
                })
                .addInterceptor(logging)
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        return retrofit.create(DevDayApi.class);
    }

Для аутентификации web-клиента также достаточно просто выполнить интерактивный вход через IdenitityProvider. Ниже пример из официальной документации, как это прикрутить к Angular4-приложению.


Аутентификация на бекенде в веб-приложении
import { Injectable } from '@angular/core';
import { AUTH_CONFIG } from './auth0-variables';
import { Router } from '@angular/router';
import * as auth0 from 'auth0-js';

@Injectable()
export class AuthService {

  auth0 = new auth0.WebAuth({
    clientID: AUTH_CONFIG.clientID,
    domain: AUTH_CONFIG.domain,
    responseType: 'token id_token',
    audience: `https://${AUTH_CONFIG.domain}/userinfo`,
    redirectUri: AUTH_CONFIG.callbackURL,
    scope: 'openid'
  });

  constructor(public router: Router) {}

  public login(): void {
    this.auth0.authorize();
  }

  public handleAuthentication(): void {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        this.setSession(authResult);
        this.router.navigate(['/home']);
      } else if (err) {
        this.router.navigate(['/home']);
        console.log(err);
        alert(`Error: ${err.error}. Check the console for further details.`);
      }
    });
  }
}

Как видно из примеров, никто не ушёл обиженным — реализация для клиентов получается простой и понятной.


Для полного счастья нам не хватало аутентификации пользователей через нашу локальную Active Directory.


Для настройки синхронизации между Auth0 и локальной Active Directory, Auth0 предоставляет powershell-скрипт.



Когда мы уже обрадовались, что всё отлично работает, и пошли к админам с просьбой настроить синхронизацию между нашим AD и Auth0, то получили отказ. Ребята сказали, что максимум, куда они готовы лить наши данные, — это Azure. Также на решение повлияло то, что у нас уже использовалась подписка Office 365 и часть учёток уже была залита в Azure.


Окей, сказали мы.


Azure Active Directory B2C


У Microsoft есть сервис, который называется Azure Active Directory B2C. С помощью админов удалось настроить синхронизацию нашей AD с инстансом Azure AD и настроить вход через наш Active Directory Federation Services (ADFS).


Настройка политик входа в Azure B2C


На момент написания статьи сервис находится в превью версии, поэтому через UI можно настроить только самые примитивные сценарии, вроде входа через Google+ или Facebook. Вход через Active Directory производится через загрузку xml-файлов через Identity Experience Framework. На отладку сценариев входа ушло около восьми часов + ещё день на рефакторинг входа мобилки и прикручивание провайдера аутентификации от Microsoft.


На бекенде потребовалось только указать новый IdentityProvider и Audience.


Для того, чтобы настроить вход, потребуется скачать репозиторий и пройти процедуру настройки, описанную в статье. Всего несколько часов вы программируете на xml — и вуаля! Ваш клиент аутентифицируется через серверы Azure.


Узреть и ужаснуться
<?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="yourtenant.onmicrosoft.com"
  PolicyId="B2C_1A_TrustFrameworkBase"
  PublicPolicyUri="http://yourtenant.onmicrosoft.com/B2C_1A_TrustFrameworkBase">

  <BuildingBlocks>
    <ClaimsSchema>
      <!-- The ClaimsSchema is divided into three sections:
           1. Section I lists the minimum claims that are required for the user journeys to work properly.
           2. Section II lists the claims required for query string parameters and other special parameters 
              to be passed to other claims providers, esp. login.microsoftonline.com for authentication. 
              Please do not modify these claims.
           3. Section III lists any additional (optional) claims that can be collected from the user, stored 
              in the directory and sent in tokens during sign in. Add new claims to be collected from the user 
              and/or sent in the token in Section III. -->

      <!-- NOTE: The claims schema contains restrictions on certain claims such as passwords and usernames. 
           The trust framework policy treats Azure AD as any other claims provider and all its restrictions 
           are modelled in the policy. A policy could be modified to add more restrictions, or use another 
           claims provider for credential storage which will have its own restrictions. -->

      <!-- SECTION I: Claims required for user journeys to work properly -->

      <ClaimType Id="socialIdpUserId">
        <DisplayName>Username</DisplayName>
        <DataType>string</DataType>
        <UserHelpText/>
        <UserInputType>TextBox</UserInputType>
        <Restriction>
          <Pattern RegularExpression="^[a-zA-Z0-9]+[a-zA-Z0-9_-]*$" HelpText="The username you provided is not valid. It must begin with an alphabet or number and can contain alphabets, numbers and the following symbols: _ -" />
        </Restriction>
      </ClaimType>

      <ClaimType Id="tenantId">
        <DisplayName>User's Object's Tenant ID</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="tid" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="tid" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.microsoft.com/identity/claims/tenantid" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Tenant identifier (ID) of the user object in Azure AD.</UserHelpText>
      </ClaimType>

      <ClaimType Id="objectId">
        <DisplayName>User's Object ID</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="oid" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="oid" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.microsoft.com/identity/claims/objectidentifier" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Object identifier (ID) of the user object in Azure AD.</UserHelpText>
      </ClaimType>

      <!-- Claims needed for local accounts. -->
      <ClaimType Id="signInName">
        <DisplayName>Sign in name</DisplayName>
        <DataType>string</DataType>
        <UserHelpText/>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>

      <ClaimType Id="signInNames.emailAddress">
        <DisplayName>Email Address</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Email address to use for signing in.</UserHelpText>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>

      <ClaimType Id="password">
        <DisplayName>Password</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Enter password</UserHelpText>
        <UserInputType>Password</UserInputType>
      </ClaimType>

      <!-- The claim types newPassword and reenterPassword are considered special, please do not change the names. 
           The UI validates the the user correctly re-entered their password during account creation based on these 
           claim types.   -->
      <ClaimType Id="newPassword">
        <DisplayName>New Password</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Enter new password</UserHelpText>
        <UserInputType>Password</UserInputType>
        <Restriction>
          <Pattern RegularExpression="^((?=.*[a-z])(?=.*[A-Z])(?=.*\d)|(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])|(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9])|(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]))([A-Za-z\d@#$%^&amp;*\-_+=[\]{}|\\:',?/`~&quot;();!]|\.(?!@)){8,16}$" HelpText="8-16 characters, containing 3 out of 4 of the following: Lowercase characters, uppercase characters, digits (0-9), and one or more of the following symbols: @ # $ % ^ &amp; * - _ + = [ ] { } | \ : ' , ? / ` ~ &quot; ( ) ; ." />
        </Restriction>
      </ClaimType>
      <!-- The password regular expression above is constructed for AAD passwords based on restrictions at https://msdn.microsoft.com/en-us/library/azure/jj943764.aspx

        ^( # one of the following four combinations must appear in the password
         (?=.*[a-z])(?=.*[A-Z])(?=.*\d) |            # matches lower case, upper case or digit
         (?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9]) |  # matches lower case, upper case or special character (i.e. non-alpha or digit)
         (?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]) |     # matches lower case, digit, or special character
         (?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9])       # matches upper case, digit, or special character
        )
        ( # The password must match the following restrictions
         [A-Za-z\d@#$%^&*\-_+=[\]{}|\\:',?/`~"();!] |   # The list of all acceptable characters (without .)
         \.(?!@)                                        # or . can appear as long as not followed by @
        ) {8,16}$                                       # the length must be between 8 and 16 chars inclusive

      -->

      <ClaimType Id="reenterPassword">
        <DisplayName>Confirm New Password</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Confirm new password</UserHelpText>
        <UserInputType>Password</UserInputType>
        <Restriction>
          <Pattern RegularExpression="^((?=.*[a-z])(?=.*[A-Z])(?=.*\d)|(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])|(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9])|(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]))([A-Za-z\d@#$%^&amp;*\-_+=[\]{}|\\:',?/`~&quot;();!]|\.(?!@)){8,16}$" HelpText=" " />
        </Restriction>
      </ClaimType>

      <ClaimType Id="passwordPolicies">
        <DisplayName>Password Policies</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Password policies used by Azure AD to determine password strength, expiry etc.</UserHelpText>
      </ClaimType>

      <ClaimType Id="client_id">
        <DisplayName>client_id</DisplayName>
        <DataType>string</DataType>
        <AdminHelpText>Special parameter passed to EvoSTS.</AdminHelpText>
        <UserHelpText>Special parameter passed to EvoSTS.</UserHelpText>
      </ClaimType>

      <ClaimType Id="resource_id">
        <DisplayName>resource_id</DisplayName>
        <DataType>string</DataType>
        <AdminHelpText>Special parameter passed to EvoSTS.</AdminHelpText>
        <UserHelpText>Special parameter passed to EvoSTS.</UserHelpText>
      </ClaimType>

      <ClaimType Id="sub">
        <DisplayName>Subject</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OpenIdConnect" PartnerClaimType="sub" />
        </DefaultPartnerClaimTypes>
        <UserHelpText/>
      </ClaimType>

      <ClaimType Id="alternativeSecurityId">
        <DisplayName>AlternativeSecurityId</DisplayName>
        <DataType>string</DataType>
        <UserHelpText/>
      </ClaimType>

      <ClaimType Id="mailNickName">
        <DisplayName>MailNickName</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Your mail nick name as stored in the Azure Active Directory.</UserHelpText>
      </ClaimType>

      <ClaimType Id="identityProvider">
        <DisplayName>Identity Provider</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="idp" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="idp" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.microsoft.com/identity/claims/identityprovider" />
        </DefaultPartnerClaimTypes>
        <UserHelpText/>
      </ClaimType>

      <ClaimType Id="displayName">
        <DisplayName>Display Name</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="unique_name" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="name" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Your display name.</UserHelpText>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>

      <ClaimType Id="strongAuthenticationPhoneNumber">
        <DisplayName>Phone Number</DisplayName>
        <DataType>string</DataType>
        <Mask Type="Simple">XXX-XXX-</Mask>
        <UserHelpText>Your telephone number</UserHelpText>
      </ClaimType>

      <ClaimType Id="Verified.strongAuthenticationPhoneNumber">
        <DisplayName>Verified Phone Number</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OpenIdConnect" PartnerClaimType="phone_number" />
        </DefaultPartnerClaimTypes>
        <Mask Type="Simple">XXX-XXX-</Mask>
        <UserHelpText>Your office phone number that has been verified</UserHelpText>
      </ClaimType>

      <ClaimType Id="newPhoneNumberEntered">
        <DisplayName>New Phone Number Entered</DisplayName>
        <DataType>boolean</DataType>
      </ClaimType>

      <ClaimType Id="userIdForMFA">
        <DisplayName>UserId for MFA</DisplayName>
        <DataType>string</DataType>
      </ClaimType>

      <ClaimType Id="email">
        <DisplayName>Email Address</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OpenIdConnect" PartnerClaimType="email" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Email address that can be used to contact you.</UserHelpText>
        <UserInputType>TextBox</UserInputType>
        <Restriction>
          <Pattern RegularExpression="^[a-zA-Z0-9.!#$%&amp;'^_`{}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" HelpText="Please enter a valid email address." />
        </Restriction>
      </ClaimType>

      <ClaimType Id="otherMails">
        <DisplayName>Alternate Email Addresses</DisplayName>
        <DataType>stringCollection</DataType>
        <UserHelpText>Email addresses that can be used to contact the user.</UserHelpText>
      </ClaimType>

      <ClaimType Id="userPrincipalName">
        <DisplayName>UserPrincipalName</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="upn" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="upn" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.microsoft.com/identity/claims/userprincipalname" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Your user name as stored in the Azure Active Directory.</UserHelpText>
      </ClaimType>

      <ClaimType Id="upnUserName">
        <DisplayName>UPN User Name</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>The user name for creating user principal name.</UserHelpText>
      </ClaimType>

      <ClaimType Id="newUser">
        <DisplayName>User is new</DisplayName>
        <DataType>boolean</DataType>
        <UserHelpText/>
      </ClaimType>

      <ClaimType Id="executed-SelfAsserted-Input">
        <DisplayName>Executed-SelfAsserted-Input</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>A claim that specifies whether attributes were collected from the user.</UserHelpText>
      </ClaimType>

      <ClaimType Id="authenticationSource">
        <DisplayName>AuthenticationSource</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Specifies whether the user was authenticated at Social IDP or local account.</UserHelpText>
      </ClaimType>

      <!-- SECTION II: Claims required to pass on special parameters (including some query string parameters) to other claims providers -->

      <ClaimType Id="nca">
        <DisplayName>nca</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Special parameter passed for local account authentication to login.microsoftonline.com.</UserHelpText>
      </ClaimType>

      <ClaimType Id="grant_type">
        <DisplayName>grant_type</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Special parameter passed for local account authentication to login.microsoftonline.com.</UserHelpText>
      </ClaimType>

      <ClaimType Id="scope">
        <DisplayName>scope</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Special parameter passed for local account authentication to login.microsoftonline.com.</UserHelpText>
      </ClaimType>

      <ClaimType Id="objectIdFromSession">
        <DisplayName>objectIdFromSession</DisplayName>
        <DataType>boolean</DataType>
        <UserHelpText>Parameter provided by the default session management provider to indicate that the object id has been retrieved from an SSO session.</UserHelpText>
      </ClaimType>

      <ClaimType Id="isActiveMFASession">
        <DisplayName>isActiveMFASession</DisplayName>
        <DataType>boolean</DataType>
        <UserHelpText>Parameter provided by the MFA session management to indicate that the user has an active MFA session.</UserHelpText>
      </ClaimType>

      <!-- SECTION III: Additional claims that can be collected from the users, stored in the directory, and sent in the token. Add additional claims here. -->

      <ClaimType Id="givenName">
        <DisplayName>Given Name</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="given_name" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="given_name" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Your given name (also known as first name).</UserHelpText>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>

      <ClaimType Id="surname">
        <DisplayName>Surname</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="family_name" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="family_name" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Your surname (also known as family name or last name).</UserHelpText>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>

    </ClaimsSchema>

    <ClaimsTransformations>
      <ClaimsTransformation Id="CreateOtherMailsFromEmail" TransformationMethod="AddItemToStringCollection">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="item" />
          <InputClaim ClaimTypeReferenceId="otherMails" TransformationClaimType="collection" />
        </InputClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="otherMails" TransformationClaimType="collection" />
        </OutputClaims>
      </ClaimsTransformation>

      <ClaimsTransformation Id="CreateRandomUPNUserName" TransformationMethod="CreateRandomString">
        <InputParameters>
          <InputParameter Id="randomGeneratorType" DataType="string" Value="GUID" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="upnUserName" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>

      <ClaimsTransformation Id="CreateUserPrincipalName" TransformationMethod="FormatStringClaim">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="upnUserName" TransformationClaimType="inputClaim" />
        </InputClaims>
        <InputParameters>
          <InputParameter Id="stringFormat" DataType="string" Value="cpim_{0}@{RelyingPartyTenantId}" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="userPrincipalName" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>

  <ClaimsTransformation Id="CreateAlternativeSecurityId" TransformationMethod="CreateAlternativeSecurityId">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="socialIdpUserId" TransformationClaimType="key" />
          <InputClaim ClaimTypeReferenceId="identityProvider" TransformationClaimType="identityProvider" />
        </InputClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="alternativeSecurityId" TransformationClaimType="alternativeSecurityId" />
        </OutputClaims>
      </ClaimsTransformation>

      <ClaimsTransformation Id="CreateUserIdForMFA" TransformationMethod="FormatStringClaim">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="objectId" TransformationClaimType="inputClaim" />
        </InputClaims>
        <InputParameters>
          <InputParameter Id="stringFormat" DataType="string" Value="{0}@{RelyingPartyTenantId}" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="userIdForMFA" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>

    <ClaimsTransformation Id="CreateSubjectClaimFromAlternativeSecurityId" TransformationMethod="CreateStringClaim">
        <InputParameters>
          <InputParameter Id="value" DataType="string" Value="Not supported currently. Use oid claim." />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="sub" TransformationClaimType="createdClaim" />
        </OutputClaims>
    </ClaimsTransformation>

    </ClaimsTransformations>

    <ClientDefinitions>
      <ClientDefinition Id="DefaultWeb">
        <ClientUIFilterFlags>LineMarkers, MetaRefresh</ClientUIFilterFlags>
      </ClientDefinition>
    </ClientDefinitions>

    <ContentDefinitions>

      <!-- This content definition is to render an error page that displays unhandled errors. -->
      <ContentDefinition Id="api.error">
        <LoadUri>~/tenant/default/exception.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:globalexception:1.1.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Error page</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.idpselections">
        <LoadUri>~/tenant/default/idpSelector.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:idpselection:1.0.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Idp selection page</Item>
          <Item Key="language.intro">Sign in</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.idpselections.signup">
        <LoadUri>~/tenant/default/idpSelector.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:idpselection:1.0.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Idp selection page</Item>
          <Item Key="language.intro">Sign up</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.signuporsignin">
        <LoadUri>~/tenant/default/unified.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:unifiedssp:1.0.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Signin and Signup</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.phonefactor">
        <LoadUri>~/tenant/default/multifactor-1.0.0.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:multifactor:1.1.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Multi-factor authentication page</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.selfasserted">
        <LoadUri>~/tenant/default/selfAsserted.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Collect information from user page</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.selfasserted.profileupdate">
        <LoadUri>~/tenant/default/updateProfile.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Collect information from user page</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.localaccountsignup">
        <LoadUri>~/tenant/default/selfAsserted.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Local account sign up page</Item>
        </Metadata>
      </ContentDefinition>

      <ContentDefinition Id="api.localaccountpasswordreset">
        <LoadUri>~/tenant/default/selfAsserted.cshtml</LoadUri>
        <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
        <DataUri>urn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0</DataUri>
        <Metadata>
          <Item Key="DisplayName">Local account change password page</Item>
        </Metadata>
      </ContentDefinition>

    </ContentDefinitions>
  </BuildingBlocks>

  <!--
        A list of all the claim providers that can be used in the technical policies. If a claims provider is not listed 
        in this section, then it cannot be used in a technical policy.
    -->
  <ClaimsProviders>

        <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="socialIdpUserId" 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" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" />
          </OutputClaims>
          <OutputClaimsTransformations>
            <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
            <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
            <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
          </OutputClaimsTransformations>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Local Account SignIn</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="login-NonInteractive">
          <DisplayName>Local Account SignIn</DisplayName>
          <Protocol Name="OpenIdConnect" />
          <Metadata>
            <Item Key="UserMessageIfClaimsPrincipalDoesNotExist">We can't seem to find your account</Item>
            <Item Key="UserMessageIfInvalidPassword">Your password is incorrect</Item>
            <Item Key="UserMessageIfOldPasswordUsed">Looks like you used an old password</Item>

            <Item Key="ProviderName">https://sts.windows.net/</Item>
            <Item Key="METADATA">https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration</Item>
            <Item Key="authorization_endpoint">https://login.microsoftonline.com/{tenant}/oauth2/token</Item>
            <Item Key="response_types">id_token</Item>
            <Item Key="response_mode">query</Item>
            <Item Key="scope">email openid</Item>

            <!-- Policy Engine Clients -->
            <Item Key="UsePolicyInRedirectUri">false</Item>
            <Item Key="HttpBinding">POST</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="username" Required="true" />
            <InputClaim ClaimTypeReferenceId="password" Required="true" />
            <InputClaim ClaimTypeReferenceId="grant_type" DefaultValue="password" />
            <InputClaim ClaimTypeReferenceId="scope" DefaultValue="openid" />
            <InputClaim ClaimTypeReferenceId="nca" PartnerClaimType="nca" DefaultValue="1" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="oid" />
            <OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid" />
            <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
            <OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
            <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" PartnerClaimType="upn" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
          </OutputClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>PhoneFactor</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="PhoneFactor-InputOrVerify">
          <DisplayName>PhoneFactor</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.PhoneFactorProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="ContentDefinitionReferenceId">api.phonefactor</Item>
            <Item Key="ManualPhoneNumberEntryAllowed">true</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="CreateUserIdForMFA" />
          </InputClaimsTransformations>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="userIdForMFA" PartnerClaimType="UserId" />
            <InputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="Verified.OfficePhone" />
            <OutputClaim ClaimTypeReferenceId="newPhoneNumberEntered" PartnerClaimType="newPhoneNumberEntered" />
          </OutputClaims>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-MFA" />
        </TechnicalProfile>

      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Azure Active Directory</DisplayName>
      <TechnicalProfiles>

        <TechnicalProfile Id="AAD-Common">
          <DisplayName>Azure Active Directory</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.AzureActiveDirectoryProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>

          <!-- We need this here to suppress the SelfAsserted provider from invoking SSO on validation profiles. -->
          <IncludeInSso>false</IncludeInSso>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
        </TechnicalProfile>

        <!-- Technical profiles for social logins -->

        <TechnicalProfile Id="AAD-UserWriteUsingAlternativeSecurityId">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
            <Item Key="UserMessageIfClaimsPrincipalAlreadyExists">You are already registered, please press the back button and sign in instead.</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="CreateOtherMailsFromEmail" />
          </InputClaimsTransformations>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="AlternativeSecurityId" PartnerClaimType="alternativeSecurityId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <!-- Required claims -->
            <PersistedClaim ClaimTypeReferenceId="alternativeSecurityId" />
            <PersistedClaim ClaimTypeReferenceId="userPrincipalName" />
            <PersistedClaim ClaimTypeReferenceId="mailNickName" DefaultValue="unknown" />
            <PersistedClaim ClaimTypeReferenceId="displayName" DefaultValue="unknown" />

            <!-- Optional claims -->
            <PersistedClaim ClaimTypeReferenceId="otherMails" />
            <PersistedClaim ClaimTypeReferenceId="givenName" />
            <PersistedClaim ClaimTypeReferenceId="surname" />
          </PersistedClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="newUser" PartnerClaimType="newClaimsPrincipalCreated" />
            <!-- The following other mails claim is needed for the case when a user is created, we get otherMails from directory. Self-asserted provider also has an
                 OutputClaims, and if this is absent, Self-Asserted provider will prompt the user for otherMails. -->
            <OutputClaim ClaimTypeReferenceId="otherMails" />
          </OutputClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
        </TechnicalProfile>

        <TechnicalProfile Id="AAD-UserReadUsingAlternativeSecurityId">
          <Metadata>
            <Item Key="Operation">Read</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
            <Item Key="UserMessageIfClaimsPrincipalDoesNotExist">User does not exist. Please sign up before you can sign in.</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="AlternativeSecurityId" PartnerClaimType="alternativeSecurityId" Required="true" />
          </InputClaims>
          <OutputClaims>
            <!-- Required claims -->

            <OutputClaim ClaimTypeReferenceId="objectId" />

            <!-- Optional claims -->
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="displayName" />
            <OutputClaim ClaimTypeReferenceId="otherMails" />
            <OutputClaim ClaimTypeReferenceId="givenName" />
            <OutputClaim ClaimTypeReferenceId="surname" />
          </OutputClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

        <TechnicalProfile Id="AAD-UserReadUsingAlternativeSecurityId-NoError">
          <Metadata>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
          </Metadata>
          <IncludeTechnicalProfile ReferenceId="AAD-UserReadUsingAlternativeSecurityId" />
        </TechnicalProfile>

        <!-- Technical profiles for local accounts -->

        <TechnicalProfile Id="AAD-UserWriteUsingLogonEmail">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <!-- Required claims -->
            <PersistedClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" />
            <PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password"/>
            <PersistedClaim ClaimTypeReferenceId="displayName" DefaultValue="unknown" />
            <PersistedClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="DisablePasswordExpiration" />

            <PersistedClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="strongAuthenticationPhoneNumber" />

            <!-- Optional claims. -->
            <PersistedClaim ClaimTypeReferenceId="givenName" />
            <PersistedClaim ClaimTypeReferenceId="surname" />
          </PersistedClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="newUser" PartnerClaimType="newClaimsPrincipalCreated" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
          </OutputClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
        </TechnicalProfile>

        <TechnicalProfile Id="AAD-UserReadUsingEmailAddress">
          <Metadata>
            <Item Key="Operation">Read</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
            <Item Key="UserMessageIfClaimsPrincipalDoesNotExist">An account could not be found for the provided user ID.</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames" Required="true" />
          </InputClaims>
          <OutputClaims>
            <!-- Required claims -->
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />

            <OutputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />

            <!-- Optional claims -->
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="displayName" />
            <OutputClaim ClaimTypeReferenceId="otherMails" />
            <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
          </OutputClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

        <TechnicalProfile Id="AAD-UserWritePasswordUsingObjectId">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password"/>

            <!-- If the user stepped up during password reset, their phone number should be persisted for future authentication requests. -->
            <PersistedClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="strongAuthenticationPhoneNumber" />

          </PersistedClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

        <!-- Technical profiles for updating user record using objectId -->

        <TechnicalProfile Id="AAD-UserWriteProfileUsingObjectId">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <!-- Required claims -->
            <PersistedClaim ClaimTypeReferenceId="objectId" />

            <!-- If the user stepped up during password reset, their phone number should be persisted for future authentication requests. -->
            <PersistedClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="strongAuthenticationPhoneNumber" />

            <!-- Optional claims -->
            <PersistedClaim ClaimTypeReferenceId="givenName" />
            <PersistedClaim ClaimTypeReferenceId="surname" />
          </PersistedClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

        <!-- The following technical profile is used to read data after user authenticates. -->
        <TechnicalProfile Id="AAD-UserReadUsingObjectId">
          <Metadata>
            <Item Key="Operation">Read</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <OutputClaims>

            <!-- Required claims -->
            <OutputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />

            <!-- Optional claims -->
            <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
            <OutputClaim ClaimTypeReferenceId="displayName" />
            <OutputClaim ClaimTypeReferenceId="otherMails" />
            <OutputClaim ClaimTypeReferenceId="givenName" />
            <OutputClaim ClaimTypeReferenceId="surname" />
          </OutputClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

        <TechnicalProfile Id="AAD-UserWritePhoneNumberUsingObjectId">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <PersistedClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="strongAuthenticationPhoneNumber" />
          </PersistedClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Self Asserted</DisplayName>
      <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>
            <!-- 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). -->
            <InputClaim ClaimTypeReferenceId="displayName" />
            <InputClaim ClaimTypeReferenceId="givenName" />
            <InputClaim ClaimTypeReferenceId="surname" />
          </InputClaims>
          <OutputClaims>
            <!-- These claims are not shown to the user because their value is obtained through the "ValidationTechnicalProfiles"
                 referenced below, or a default value is assigned to the claim. A claim is only shown to the user to provide a 
                 value if its value cannot be obtained through any other means. -->
            <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="displayName" />
            <OutputClaim ClaimTypeReferenceId="givenName" />
            <OutputClaim ClaimTypeReferenceId="surname" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
          </ValidationTechnicalProfiles>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialSignup" />
        </TechnicalProfile>

        <TechnicalProfile Id="SelfAsserted-ProfileUpdate">
          <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.profileupdate</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>

            <InputClaim ClaimTypeReferenceId="alternativeSecurityId" />

            <InputClaim ClaimTypeReferenceId="userPrincipalName" />

            <!-- Optional claims. These claims are collected from the user and can be modified. Any claim added here should be updated in the
                 ValidationTechnicalProfile referenced below so it can be written to directory after being updateed by the user, i.e. AAD-UserWriteProfileUsingObjectId. -->
            <InputClaim ClaimTypeReferenceId="givenName" />
            <InputClaim ClaimTypeReferenceId="surname" />
          </InputClaims>
          <OutputClaims>
            <!-- Required claims -->
            <OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />

            <!-- Optional claims. These claims are collected from the user and can be modified. Any claim added here should be updated in the
                 ValidationTechnicalProfile referenced below so it can be written to directory after being updateed by the user, i.e. AAD-UserWriteProfileUsingObjectId. -->
            <OutputClaim ClaimTypeReferenceId="givenName" />
            <OutputClaim ClaimTypeReferenceId="surname" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserWriteProfileUsingObjectId" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Local Account</DisplayName>
      <TechnicalProfiles>

        <TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
          <DisplayName>Email signup</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="IpAddressClaimReferenceId">IpAddress</Item>
            <Item Key="ContentDefinitionReferenceId">api.localaccountsignup</Item>
            <Item Key="language.button_continue">Create</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
            <OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
            <OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
            <OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />
            <OutputClaim ClaimTypeReferenceId="newUser" />

            <!-- Optional claims, to be collected from the user -->
            <OutputClaim ClaimTypeReferenceId="displayName" />
            <OutputClaim ClaimTypeReferenceId="givenName" />
            <OutputClaim ClaimTypeReferenceId="surName" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
          </ValidationTechnicalProfiles>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
        </TechnicalProfile>

        <!-- This technical profile uses a validation technical profile to authenticate the user. -->
        <TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
          <DisplayName>Local Account Signin</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="SignUpTarget">SignUpWithLogonEmailExchange</Item>
            <Item Key="setting.operatingMode">Email</Item>
            <Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="signInName" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
            <OutputClaim ClaimTypeReferenceId="password" Required="true" />
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
          </ValidationTechnicalProfiles>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
        </TechnicalProfile>

        <!-- This technical profile forces the user to verify the email address that they provide on the UI. Only after email is verified, the user account is
        read from the directory. -->
        <TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
          <DisplayName>Reset password using email address</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="IpAddressClaimReferenceId">IpAddress</Item>
            <Item Key="ContentDefinitionReferenceId">api.localaccountpasswordreset</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <IncludeInSso>false</IncludeInSso>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />

            <OutputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />

          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>

        <TechnicalProfile Id="LocalAccountWritePasswordUsingObjectId">
          <DisplayName>Change password (username)</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.localaccountpasswordreset</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" />

            <InputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" />

          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
            <OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserWritePasswordUsingObjectId" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>

      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Session Management</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="SM-Noop">
          <DisplayName>Noop Session Management Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.NoopSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        </TechnicalProfile>

        <TechnicalProfile Id="SM-AAD">
          <DisplayName>Session Mananagement Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.DefaultSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <PersistedClaim ClaimTypeReferenceId="signInName" />
            <PersistedClaim ClaimTypeReferenceId="authenticationSource" />
            <PersistedClaim ClaimTypeReferenceId="identityProvider" />
            <PersistedClaim ClaimTypeReferenceId="newUser" />
            <PersistedClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" />
          </PersistedClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectIdFromSession" DefaultValue="true"/>
          </OutputClaims>
        </TechnicalProfile>

        <!-- Profile name is being used to disambiguate AAD session between sign up and sign in -->
        <TechnicalProfile Id="SM-SocialSignup">
          <IncludeTechnicalProfile ReferenceId="SM-AAD" />
        </TechnicalProfile>

        <TechnicalProfile Id="SM-SocialLogin">
          <DisplayName>Session Mananagement Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.ExternalLoginSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="AlwaysFetchClaimsFromProvider">true</Item>
          </Metadata>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="AlternativeSecurityId" />
          </PersistedClaims>
        </TechnicalProfile>

        <TechnicalProfile Id="SM-MFA">
          <DisplayName>Session Mananagement Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.DefaultSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />
          </PersistedClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="isActiveMFASession" DefaultValue="true"/>
          </OutputClaims>
        </TechnicalProfile>

      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Trustframework Policy Engine TechnicalProfiles</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="TpEngine_c3bd4fe2-1775-4013-b91d-35f16d377d13">
          <DisplayName>Trustframework Policy Engine Default Technical Profile</DisplayName>
          <Protocol Name="None" />
          <Metadata>
            <Item Key="url">{service:te}</Item>
          </Metadata>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Token Issuer</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="JwtIssuer">
          <DisplayName>JWT Issuer</DisplayName>
          <Protocol Name="None" />
          <OutputTokenFormat>JWT</OutputTokenFormat>
          <Metadata>
            <Item Key="client_id">{service:te}</Item>
            <Item Key="issuer_refresh_token_user_identity_claim_type">objectId</Item>
            <Item Key="SendTokenResponseBodyWithJsonNumbers">true</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
            <Key Id="issuer_refresh_token_key" StorageReferenceId="B2C_1A_TokenEncryptionKeyContainer" />
          </CryptographicKeys>
          <InputClaims />
          <OutputClaims />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>

  <UserJourneys>

    <UserJourney Id="SignUpOrSignIn">
      <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
          </ClaimsProviderSelections>
          <ClaimsExchanges>
            <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- Check if the user has selected to sign in using one of the social providers -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />
            <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- For social IDP authentication, attempt to find the user account in the directory. -->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>authenticationSource</Value>
              <Value>localAccountAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- Show self-asserted page only if the directory does not have the user account already (i.e. we do not have an objectId). 
          This can only happen when authentication happened using a social IDP. If local account was created or authentication done
          using ESTS in step 2, then an user account must exist in the directory by this time. -->
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SelfAsserted-Social" TechnicalProfileReferenceId="SelfAsserted-Social" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- This step reads any user attributes that we may not have received when authenticating using ESTS so they can be sent 
          in the token. -->
        <OrchestrationStep Order="5" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>authenticationSource</Value>
              <Value>socialIdpAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- The previous step (SelfAsserted-Social) could have been skipped if there were no attributes to collect 
             from the user. So, in that case, create the user in the directory if one does not already exist 
             (verified using objectId which would be set from the last step if account was created in the directory. -->
        <OrchestrationStep Order="6" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserWrite" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- Phone verification: If MFA is not required, the next three steps (#5-#7) should be removed.
             This step checks whether there's a phone number on record,  for the user. If found, then the user is challenged to verify it. -->
        <OrchestrationStep Order="7" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>isActiveMFASession</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="PhoneFactor-Verify" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- Save MFA phone number: The precondition verifies whether the user provided a new number in the 
             previous step. If so, then the phone number is stored in the directory for future authentication 
             requests. -->
        <OrchestrationStep Order="8" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>newPhoneNumberEntered</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserWriteWithObjectId" TechnicalProfileReferenceId="AAD-UserWritePhoneNumberUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="9" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />

      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

    <UserJourney Id="ProfileEdit">
      <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="ClaimsProviderSelection" ContentDefinitionReferenceId="api.idpselections">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
            <ClaimsProviderSelection TargetClaimsExchangeId="LocalAccountSigninEmailExchange" />
          </ClaimsProviderSelections>
        </OrchestrationStep>
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />
            <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>authenticationSource</Value>
              <Value>localAccountAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserRead" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>authenticationSource</Value>
              <Value>socialIdpAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- If the user ever stepped up to use 2FA, profile update must verify this because the user will be able to change
          their sign in email address or strong authentication email here. This guards against scenarios where a user's 
          password is stolen, the attacker can change the email addresses leaving no way for the user to recover their account.
          By requiring 2FA, stolen passwords cannot be used to take over the account completely. -->
        <OrchestrationStep Order="5" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="PhoneFactor" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="6" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="B2CUserProfileUpdateExchange" TechnicalProfileReferenceId="SelfAsserted-ProfileUpdate" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="7" 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="PhoneFactor-Verify" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

  </UserJourneys>
</TrustFrameworkPolicy>

Тут я привёл базовую политику входа. Для того, чтобы всё работало, требуется загрузить ещё три файла (они гораздо меньше, но вам всё равно придётся залезть в базовую политику).


Аутентификация Azure Active Directory B2C через ADFS выглядит следующим образом:



Код аутентификации для Active Directory
public class AuthImpl {

    private static final String TAG = AuthImpl.class.getSimpleName();

    private Activity activity;
    private PublicClientApplication authClient;
    private AuthTokenProvider authTokenProvider;
    private String[] scopes;

    @Inject
    AuthImpl(AppCompatActivity activity, PublicClientApplication authClient, AuthTokenProvider authTokenProvider)
    {
        this.activity = activity;
        this.authClient = authClient;
        this.authTokenProvider = authTokenProvider;
        this.scopes = Constants.SCOPES.split("\\s+");
    }

    public boolean isUserAuthenticated() {
        return AndroidHelpers.isEmulator() || getCurrentUser() != null;
    }

    private User getCurrentUser() {
        try {
            return Helpers.getUserByPolicy(authClient.getUsers(), Constants.SISU_POLICY);

        } catch (MsalClientException e) {
            /* No token in cache, proceed with normal unauthenticated app experience */
            Log.d(TAG, "MSAL Exception Generated while getting users: " + e.toString());

        } catch (IndexOutOfBoundsException e) {
            Log.d(TAG, "User at this position does not exist: " + e.toString());
        }

        return  null;
    }

    public Single<AuthResultDto> login()
    {
        /* Attempt to get a user and acquireTokenSilently
         * If this fails we will do an interactive request
         */

        return Single.create(emitter -> {

            try {
                User currentUser = Helpers.getUserByPolicy(authClient.getUsers(), Constants.SISU_POLICY);

                if (currentUser != null) {
            /* We have 1 user */

                    authClient.acquireTokenSilentAsync(
                            scopes,
                            currentUser,
                            String.format(Constants.AUTHORITY, Constants.TENANT, Constants.SISU_POLICY),
                            false,
                            getAuthSilentCallback(emitter));
                } else {
            /* We have no user */
                    authClient.acquireToken(activity, scopes, getAuthInteractiveCallback(emitter));
                }
            } catch (MsalClientException e) {
        /* No token in cache, proceed with normal unauthenticated app experience */
                Log.d(TAG, "MSAL Exception Generated while getting users: " + e.toString());

            } catch (IndexOutOfBoundsException e) {
                Log.d(TAG, "User at this position does not exist: " + e.toString());
            }
        });

    }

    //
    // App callbacks for MSAL
    // ======================
    // getActivity() - returns activity so we can acquireToken within a callback
    // getAuthSilentCallback() - callback defined to handle acquireTokenSilent() case
    // getAuthInteractiveCallback() - callback defined to handle acquireToken() case
    //

    /* Callback used in for silent acquireToken calls.
     * Looks if tokens are in the cache (refreshes if necessary and if we don't forceRefresh)
     * else errors that we need to do an interactive request.
     */
    private AuthenticationCallback getAuthSilentCallback(final SingleEmitter<AuthResultDto> emitter) {
        return new AuthenticationCallback() {
            @Override
            public void onSuccess(AuthenticationResult authenticationResult) {
                /* Successfully got a token, call api now */
                Log.d(TAG, "Successfully authenticated");

                emitter.onSuccess(new AuthResultDto(authenticationResult.getIdToken()));
            }

            @Override
            public void onError(MsalException exception) {
                /* Failed to acquireToken */
                Log.d(TAG, "Authentication failed: " + exception.toString());

                if (exception instanceof MsalClientException) {
                    /* Exception inside MSAL, more info inside MsalError.java */
                    emitter.onError(exception);
                    assert true;

                } else if (exception instanceof MsalServiceException) {
                    /* Exception when communicating with the STS, likely config issue */
                    emitter.onError(exception);
                    assert true;

                } else if (exception instanceof MsalUiRequiredException) {
                    /* Tokens expired or no session, retry with interactive */
                    authClient.acquireToken(activity, scopes, getAuthInteractiveCallback(emitter));
                }
            }

            @Override
            public void onCancel() {
                /* User canceled the authentication */
                Log.d(TAG, "User cancelled login.");
                emitter.onSuccess(new AuthResultDto(true));
            }
        };
    }

    /* Callback used for interactive request.  If succeeds we use the access
     * token to call the api. Does not check cache.
     */
    private AuthenticationCallback getAuthInteractiveCallback(final SingleEmitter<AuthResultDto> emitter) {
        return new AuthenticationCallback() {
            @Override
            public void onSuccess(AuthenticationResult authenticationResult) {
                /* Successfully got a token, call api now */
                Log.d(TAG, "Successfully authenticated");
                Log.d(TAG, "ID Token: " + authenticationResult.getIdToken());

                authTokenProvider.setAuthToken(authenticationResult.getIdToken());
                emitter.onSuccess(new AuthResultDto(authenticationResult.getIdToken()));
            }

            @Override
            public void onError(MsalException exception) {
                /* Failed to acquireToken */
                Log.d(TAG, "Authentication failed: " + exception.toString());

                if (exception instanceof MsalClientException) {
                    /* Exception inside MSAL, more info inside MsalError.java */
                    emitter.onError(exception);
                    assert true;
                } else if (exception instanceof MsalServiceException) {
                    /* Exception when communicating with the STS, likely config issue */
                    emitter.onError(exception);
                    assert true;
                }
            }

            @Override
            public void onCancel() {
                /* User canceled the authentication */
                Log.d(TAG, "User cancelled login.");
                emitter.onSuccess(new AuthResultDto(true));
            }
        };
    }

    static final int BROWSER_FLOW = 1001;
    boolean handleInteractiveRequestRedirect(int requestCode, int resultCode, final Intent data){

        if(requestCode == BROWSER_FLOW)
        {
            authClient.handleInteractiveRequestRedirect(requestCode, resultCode, data);
            return true;
        }

        return false;
    }
}

Всё было отлично. Мы запустили тестовую эксплуатацию приложения на наших пользователей, но столкнулись с несколькими блокирующими проблемами:


  1. Компонента аутентификации полностью не работал, если на устройстве не был установлен Google Chrome.
  2. Авторизация в Azure AD для новых пользователей перестала работать.
  3. После удачного логина через ADFS было невозможно разлогиниться. Для разлогина требовалось очищать кэш браузера Chrome на устройстве.

Переезд на Google Firebase Auth


Вопрос с синхронизацией пользователей долго оставался подвешенным и, так как не было понятно, сколько времени потребует решение этой проблемы, мы экстренно переехали на другого провайдера. В этот раз мы попробовали Google Firebase Auth, так как он обладал нативным интерфейсом для входа и хорошо работал на всех наших тестовых устройствах.


Так как на клиенте мы абстрагировались от конкретного провайдера, то переехали на Google Firebase Auth за час.


Из доработок на бекенде потребовалось добавить код для скачивания сертификатов Google, которыми Firebase подписывает токены авторизации.


Код аутентификации Firebase
 private final FirebaseAuth authInstance;
  private CompositeDisposable disposables;
  private EventSubscriber eventSubscriber;

  public Single<AuthResultDto> login(boolean forceRefreshToken)
    {
        if (disposables != null) {
            disposables.dispose();
        }
        disposables = new CompositeDisposable();

        return Single.create(emitter -> {
            FirebaseUser user = authInstance.getCurrentUser();
            if (user == null || !user.isEmailVerified()) {
                List<AuthUI.IdpConfig> providers = Arrays.asList(
                        new AuthUI.IdpConfig.EmailBuilder().setRequireName(true).build(),
                        new AuthUI.IdpConfig.GoogleBuilder().build());

                activity.startActivityForResult(
                        AuthUI.getInstance()
                                .createSignInIntentBuilder()
                                .setAvailableProviders(providers)
                                .build(),
                        MainActivity.REQUES_LOGIN);
            } else {
                if (forceRefreshToken) {
                    refreshToken();
                } else {
                    emitter.onSuccess(new AuthResultDto(false));
                }
            }

            disposables.add(this.eventSubscriber.getEvent(new AuthResultDto(true))
                    .subscribe(e ->  {
                        emitter.onSuccess(e);
                        disposables.dispose();
                    }));
        });

    }
    private void refreshToken() {
        FirebaseUser user = authInstance.getCurrentUser();
        user.getIdToken(true)
                .addOnCompleteListener(task -> {
                    if (task.isSuccessful()) {
                        GetTokenResult tokenResult = task.getResult();
                        String idToken = tokenResult.getToken();

                        // Сохраняем idToken

                        eventSubscriber.Publish(new AuthResultDto( false));
                        // ...
                    } else {
                        AuthResultDto result = new AuthResultDto( false);
                        result.Error = new Exception(task.getException());
                        eventSubscriber.Publish(new AuthResultDto(false));
                    }
        });
    }

Сейчас, когда мы запустились в продакшн на Firebase, проблему с синхронизацией данных в Azure AD удалось решить. Поэтому в перспективе мы возможно вернёмся на Azure.


Обо всём ещё разок


Мы попробовали поработать с тремя разными провайдерами аутентификации в реальном проекте и, исходя из нашего опыта, я могу порекомендовать следующее.


Auth0 — отличный провайдер, очень дружественно настроенный к разработчикам. Приятное API, нет проблем с интеграцией. Если у вас нет каких-то административных барьеров, то рекомендую к использованию :)


Azure Active Directory B2C — хорош для Enterprise. Скорее всего, удастся договориться с админами и юристами. Пока ещё довольно сырой, поэтому приходится писать конфиги в xml. Ещё одна особенность платформы — для администрирования инстанса B2C в России требуется учётка того, кто привязал карту к аккаунту. Это неудобно, пока идёт отладка и тестирование.


Firebase Auth — самый лайтовый вариант. Подойдёт, если не требуется сложных сценариев входа и достаточно просто иметь аутентификацию. Из минусов — довольно аскетичная админ-панель и небольшой перечень дефолтных источников аутентификации.


Что ещё можно добавить?


Использование стандартов, в частности OpenId Connect, позволило нам:


— быстро и дёшево адаптировать наше решение на клиенте,
— сэкономить ресурсы на разработке серверной части,
— не тратить время на дебаг и отладку кастомного кода аутентификации.


Всем безопасного бекенда и лёгкой аутентификации на фронте :)

Tags:
Hubs:
+38
Comments 15
Comments Comments 15

Articles

Information

Website
2gis.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия