【MS Azure AD B2C】カスタム属性を一つにまとめてみた

## はじめに
　今回は、Microsoft Azure AD B2Cにおいて使用されるカスタム属性数を一つにまとめる方法をご紹介します。実際に業務上でAzure AD B2Cのカスタムポリシーを作成・修正していると、カスタム属性の数を多く定義しすぎてしまったということがあります。私も先日いくつかのカスタム属性を一つの属性としてコンパクトにまとめることができたので、その実体験をもとに記事にまとめました。

（※本稿では、新規でのカスタムポリシー作成方法やアップロード方法などのご説明は割愛させていただきます。）

## カスタム属性数の上限
　まず、注意すべき点があります。それは使用できるカスタム属性数には限りがあるということです。。どの程度の制限なのでしょうか？　下記はMicrosoftの公式ドキュメントになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670585/fbc8f720-ff28-fa51-cbf7-cec2210cdcdb.png)
参照：【Microsoft】Azure Active Directory B2C のドキュメント→[メニュー]リファレンス→サービスの制限と制約
https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/service-limits?pivots=b2c-custom-policy 　


　上記の表、カテゴリの上から2番目に「ユーザーあたりのカスタム属性の数」と記載がありますが、その右横の制限数に100と記載があるのがわかるかと思います。つまり、1つのユーザーにおいて100個までしかカスタム属性を使用できないということになります。「カスタム属性数は100まで」ということを念頭に置き、作業を行いましょう。

## 前提条件
　カスタムポリシーを実際に統合していくにあたって、前提条件は以下の通りです。

- AzureサブスクリプションにリンクされたAzure AD B2Cテナントが作成済みであること
- Webアプリケーションが登録済みかつ、IDトークンの暗黙的な許可が有効になっていること


また、私が今回使用するカスタムポリシーは以下のルールに基づいて作成されています。
- カスタムポリシー＜UserJourneys＞内は必要最低限の記載
- 今回触れていく部分は、3番目のOrchestrationStepで使用される（下記参照）TechnicalProfile「SelfAsserted-ProfileUpdate」
- ＜OrchestrationStep＞内に記載された上記「SelfAsserted-ProfileUpdate」以外のTechnicalProfileは継承元のカスタムポリシーに定義済み（本稿での説明は省略します）

下記、今回実装するカスタムポリシーのUserJourneysになります。

```xml
<UserJourneys>
    <UserJourney Id="ProfileEdit">
      <OrchestrationSteps>
        <!--
            IdP選択画面を表示する
        -->
        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
          </ClaimsProviderSelections>
          <ClaimsExchanges>
            <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email"/>
          </ClaimsExchanges>
        </OrchestrationStep>
        <!--
            objectIdをキーにAzure Active Directoryから属性を取得する
        -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!--
            属性を入力・更新する
        -->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="B2CUserProfileUpdateExchange" TechnicalProfileReferenceId="SelfAsserted-ProfileUpdate" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!--
            id_tokenを発行する
        -->
        <OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>
  </UserJourneys>
```

統合版の属性を出力させることを目的としているため、かなり少ないOrchestrationStepとなっています。

カスタムポリシーの詳しい作成方法や概要を知りたい場合は以下のリンクをご参照ください。
作成方法：https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/tutorial-create-user-flows?pivots=b2c-custom-policy
概要：https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/custom-policy-overview


## 今回統合するカスタム属性
　今回は、電話番号のカスタム属性を例に挙げ、属性数を減らしていきたいと思います。
登録画面（Webブラウザー）上に出ている電話番号は下記のようにテキストボックスが3つある状態です（表示画面上の統合は行いません）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670585/a0a188ac-f494-804b-e74e-f091b99e92bd.png)


ポリシー上では下記のように定義しています（2, 3つ目の電話番号テキストボックスも同様）。
※＜ClaimType＞の細かな定義方法の説明は今回省略します。

```xml
      <!-- 電話番号1つ目のテキストボックス -->
      <ClaimType Id="extension_phonenumber1">
        <DisplayName>phone1</DisplayName>
        <DataType>string</DataType>
        <UserInputType>TextBox</UserInputType>
        <Restriction>
          <Pattern RegularExpression="^\d{0,4}$" />
        </Restriction>
      </ClaimType>
```


下記は、修正前（電話番号の統合前）のjwt.ms画面上の出力結果になります。
3つの電話番号情報がそれぞれ別の属性として出力されているのがわかるかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670585/151fa165-4e1e-19c5-c477-8c22bc831ee8.png)


## カスタム属性の節約術
　それではここから、どのようにしてカスタム属性の統合が可能であるかについて詳しく見ていきます。大きく手順は以下の通りです。

1. 新たな属性を定義する
2. 文字列の変換要求を定義する
3. 既存のTechnicalProfileに追記する or 新規のOrchestrationstep, TechnicalProfileを作成する
4. RelyingParty要素内に追記する（必要あれば）


### 手順①新たな属性を定義する
　最初の手順としては、電話番号属性を統合するための新たな属性名を定義する必要があります。属性の定義場所は＜BuildingBlocks＞要素内の＜ClaimsSchema＞要素内で最低限必要な情報を記入し、定義づけします。
```xml
<!-- 新たな属性の定義(電話番号統合版) -->
      <ClaimType Id="extension_integrated_phonenumber">
        <DisplayName></DisplayName>
        <DataType>string</DataType>
      </ClaimType>      
    </ClaimsSchema>
```


今回は、拡張属性としてextension_integrated_phonenumberという識別子を設定しました。

BuildingBlocks, ClaimsSchemaの構成について詳しく知りたい方は下記のリンクをご参照ください。
https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/claimsschema

### 手順②文字列の変換要求
　2番目の手順はClaimsTransformationsです。ClaimsTransformationsでは「大文字→小文字に変換」というように、ある要求を別の要求に変換する定義を記載します。＜ClaimsTransformations＞内に要求変換の関数リストを下記のように追記していきます。

```xml
<!-- 新たに定義づけした電話番号属性に変換要求 -->
<!-- 2つの電話番号入力項目の属性を統合 -->
      <ClaimsTransformation Id="CreateIntegratedPhonenumberFromPhonenumber1AndPhonenumber2" TransformationMethod="FormatStringMultipleClaims">
        <InputClaims>
　　　　<!-- 1つ目のの電話番号入力項目の属性 -->
          <InputClaim ClaimTypeReferenceId="extension_phonenumber1" TransformationClaimType="inputClaim1" />
　　　　<!-- 2つ目のの電話番号入力項目の属性 -->
          <InputClaim ClaimTypeReferenceId="extension_phonenumber2" TransformationClaimType="inputClaim2" />
        </InputClaims>
        <InputParameters>
          <InputParameter Id="stringFormat" DataType="string" Value="{0}-{1}" />
        </InputParameters>
        <OutputClaims>
　　　　<!-- 統合した電話番号属性を出力 -->
          <OutputClaim ClaimTypeReferenceId="extension_integrated_phonenumber" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>

<!-- 3つ目のの電話番号入力項目の属性を統合 -->
      <ClaimsTransformation Id="CreateIntegratedPhonenumberFromPhonenumber3" TransformationMethod="FormatStringMultipleClaims">
        <InputClaims>
　　　　<!-- 上記で既に統合した電話属性属性 -->
          <InputClaim ClaimTypeReferenceId="extension_integrated_phonenumber" TransformationClaimType="inputClaim1" />
　　　　<!-- 3つ目のの電話番号入力項目の属性 -->
          <InputClaim ClaimTypeReferenceId="extension_phonenumber3" TransformationClaimType="inputClaim2" />
        </InputClaims>
        <InputParameters>
          <InputParameter Id="stringFormat" DataType="string" Value="{0}-{1}" />
        </InputParameters>
        <OutputClaims>
　　　　<!-- 統合した電話番号属性を出力（同じ統合用識別子） -->
          <OutputClaim ClaimTypeReferenceId="extension_integrated_phonenumber" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>
    </ClaimsTransformations>
```

　まず、ClaimsTransformationのIdに関数の識別子を定義付けます。TransformationMethodはどのように文字列を要求変換していくか、その方法を定義付けます。今回は[FormatStringMultipleClaims]です。
　次に、＜InputClaims＞内に今回変換する属性の識別子を[ClaimsTypeReferenceId]に記述し、変換要求の種類を参照する識別子を[TransformationClaimType]に記述します。今回は[inputClaims1], [inputClaims2]と定義しておきます(MSのドキュメント通り)。
　そして、＜InputParameters＞内で、パラメーターの参照識別子[Id]、データ型[DataType]、要求変換に渡される値[Value]をそれぞれ定義します。[Value]で、↑の[TransformationClaimType]で定義づけした[inputClaims1], [inputClaims2]をそれぞれ{0}, {1}パラメーターとし、今回は電話番号を統合するので-(ハイフン)でつなげた値を渡そうと思います。
　最後に、出力される統合された属性を[extension_integrated_phonenumber]（ClaimsSchemaで定義した識別子）として、＜OutputClaims＞内に記述します。このClaimsTransformationは2つの属性までしか統合できないため、3つ以上の属性を一つにまとめたい場合は、統合した属性にさらに3つ目の属性を付与して上げる形で、再統合させます。このように、最終的に3つの属性から一つの属性への統合が実現します。

・参考サイト
【Microsoft】Azure Active Directory B2C のドキュメント→[メニュー]リファレンス→カスタムポリシースキーマ→BuildingBlocks→ClaimsTransformations→stringから[FormatStringMultipleClaims]を参照
https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/string-transformations

### 手順③既存のTechnicalProfileに追記するor新規のTechnicalProfileを作成する
　続いて、TechnicalProfileに属性をそれぞれ追記していきましょう。もちろん新たにTechnicalProfileを作成しても構いません。ここで注意なのが、Orchestrationstepで実際にそのTechnicalProfileが使用されているかどうか必ず確認しましょう。もし記載がなかった場合、せっかく統合用の属性を定義してもプロセスとして処理されないため、カスタムポリシーをportal上にアップロードする際、エラーメッセージが出てしまいます。
　電話番号統合の処理を加えるTechnicalProfileを決めたらまずは、下記のように＜Inputclaims＞内と＜Outputclaims＞内の2か所に統合用の電話番号属性の識別子を追記していきましょう。

```xml
<!--プロファイル編集の際に手動で登録を求める属性の定義-->
<ClaimsProvider>
      <DisplayName>Profile</DisplayName>
      <TechnicalProfiles>
        <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>
            <!-- UI定義 -->
            <Item Key="ContentDefinitionReferenceId">api.selfasserted.profileupdate</Item>
            <Item Key="setting.showCancelButton">false</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
            <InputClaims>
              <!-- 現在の値をデータベースから取得して表示する属性 -->
              <InputClaim ClaimTypeReferenceId="givenName" />
              <InputClaim ClaimTypeReferenceId="surname" />
              <InputClaim ClaimTypeReferenceId="extension_phonetic_given_name" />
              <InputClaim ClaimTypeReferenceId="extension_phonetic_family_name" />
              <InputClaim ClaimTypeReferenceId="extension_phonenumber1" />
              <InputClaim ClaimTypeReferenceId="extension_phonenumber2" />
              <InputClaim ClaimTypeReferenceId="extension_phonenumber3" /> 
　　　　　　　　<!-- 統合用の電話番号属性を追記 --> 
              <InputClaim ClaimTypeReferenceId="extension_integrated_phonenumber" />
            </InputClaims>
            <DisplayClaims>
              <DisplayClaim ClaimTypeReferenceId="surname" Required="true" />
              <DisplayClaim ClaimTypeReferenceId="givenName" Required="true" />
              <DisplayClaim ClaimTypeReferenceId="extension_phonetic_family_name" Required="true" />
              <DisplayClaim ClaimTypeReferenceId="extension_phonetic_given_name" Required="true" />
              <DisplayClaim ClaimTypeReferenceId="extension_phonenumber1" Required="true" />
              <DisplayClaim ClaimTypeReferenceId="extension_phonenumber2" Required="true" />
              <DisplayClaim ClaimTypeReferenceId="extension_phonenumber3" Required="true" />
            </DisplayClaims>
            <OutputClaims>
              <!-- ユーザが手動で登録/編集することが出来る属性 -->
              <OutputClaim ClaimTypeReferenceId="givenName" />
              <OutputClaim ClaimTypeReferenceId="surname" />
              <OutputClaim ClaimTypeReferenceId="extension_phonetic_given_name" />
              <OutputClaim ClaimTypeReferenceId="extension_phonetic_family_name" />
              <OutputClaim ClaimTypeReferenceId="extension_phonenumber1" />
              <OutputClaim ClaimTypeReferenceId="extension_phonenumber2" />
              <OutputClaim ClaimTypeReferenceId="extension_phonenumber3" />
　　　　　　　<!-- 統合用の電話番号属性を追記 --> 
              <OutputClaim ClaimTypeReferenceId="extension_integrated_phonenumber" /> 
            </OutputClaims>
            <ValidationTechnicalProfiles>
              <ValidationTechnicalProfile ReferenceId="AAD-UserWriteProfileUsingObjectId" />
            </ValidationTechnicalProfiles>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
```

そして、ここでは＜OutputClaims＞タグ後に＜ValidationTechnicalProfiles＞タグが記載してあります。こちらはTechnicalProfile上で収集されたデータを検証するためのファイルになります。この「AAD-UserWriteProfileUsingObjectId」という識別子のValidationTechnicalProfileで、要求変換の呼び出しやAzure上で保持するデータを記述します。ValidationTechnicalProfileは以下のように記述します。

```xml
<!--登録属性情報をAzure ADへ書き込む-->
    <ClaimsProvider>
      <DisplayName>AAD-UserWriteProfile</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="AAD-UserWriteProfileUsingObjectId">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</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>
　　　　　<!-- 要求変換の呼び出し -->
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="CreateIntegratedPhonenumberFromPhonenumber1AndPhonenumber2" />
            <InputClaimsTransformation ReferenceId="CreateIntegratedPhonenumberFromPhonenumber3" />
          </InputClaimsTransformations>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
             <!-- Required claims -->
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <!-- 永続させる属性を指定します -->
            <PersistedClaim ClaimTypeReferenceId="givenName" />
            <PersistedClaim ClaimTypeReferenceId="surname" />
            <PersistedClaim ClaimTypeReferenceId="extension_phonetic_given_name" />
            <PersistedClaim ClaimTypeReferenceId="extension_phonetic_family_name" />
            <PersistedClaim ClaimTypeReferenceId="extension_phonenumber1" />
            <PersistedClaim ClaimTypeReferenceId="extension_phonenumber2" />
            <PersistedClaim ClaimTypeReferenceId="extension_phonenumber3" />
　　　　　　 <!-- 統合用の電話番号属性を追記 --> 
            <PersistedClaim ClaimTypeReferenceId="extension_integrated_phonenumber" /> 
          </PersistedClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>
```

　まず、先ほど定義したClaimsTransformationsの識別子を呼び出す＜InputClaimsTransformations＞を追記し、電話番号の統合処理をここで行わせます。
　そして、統合処理後のデータを保持するために＜PersistedClaims＞内に統合用の電話番号属性を追記します。これで、TechnicalProfileへの追記はすべて完了です。

### 手順④RelyingPartyに追加
最後にidトークンを発行した際に出力される属性を定義するRelyingPartyに付け加えたいと思います（一番下のOutputClaimです）。ここは必要がなければ、読み飛ばしても構いません。

```xml
   <RelyingParty>
    <DefaultUserJourney ReferenceId="ProfileEdit" />
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
        <OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="family_name" />
        <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
        <OutputClaim ClaimTypeReferenceId="extension_phonetic_family_name" PartnerClaimType="family_name#ja-Kana-JP" />
        <OutputClaim ClaimTypeReferenceId="extension_phonetic_given_name" PartnerClaimType="given_name#ja-Kana-JP"/>
        <!-- 統合前の電話番号はコメントアウト -->
        <!-- <OutputClaim ClaimTypeReferenceId="extension_phonenumber1" PartnerClaimType="phonenumber1" />
        <OutputClaim ClaimTypeReferenceId="extension_phonenumber2" PartnerClaimType="phonenumber2" />
        <OutputClaim ClaimTypeReferenceId="extension_phonenumber3" PartnerClaimType="phonenumber3" /> -->

        <!-- 統合用の電話番号属性を追記 -->
        <OutputClaim ClaimTypeReferenceId="extension_integrated_phonenumber" PartnerClaimType="integrated_phonenumber"/>
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>
```

これですべての行うべきステップが完了しました！

## jwt.msとMicrosoft Graph Explorer上で確認

実際に、リダイレクト先をjwtに設定した後カスタムポリシーアップロードを行い、jwt.msの画面で確認してみましょう（RelyingPartyに統合した属性を出力情報として定義していない場合は表示されません）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670585/6af7ddda-4dc5-2583-e2a6-6ba9a4bc14ba.png)


Microsoft Graph Explorerも確認してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670585/bca523f4-c696-2455-eccb-e6263362c630.png)






上記のように表示されていれば、統合成功です！

## まとめ
 今回の記事では、カスタム属性の統合方法についてご紹介しました。主に「新たな属性を定義する → 文字列の変換要求を定義する → テクニカルプロファイルに追加する → RelyingPartyに追加する」の流れでできるはずです。特に修正箇所が多いと自分がどこまで作業していたか、次に何をすべきかわからなくなってしまう場合もあるので、事前にメモ帳のようなツールを用いてTo-do-Listを作成することをおススメします。1ステップ終わるごとに✓を入れていくと迷子になりにくいです。

## 参考にしたサイト


https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/string-transformations