“Hello World and Then Some” with Azure Active Directory B2C Custom Policies – Part 4: Hello! (But Only If You Have an Access Code)

This post continues a series that provides a walkthrough illustrating how to work with Azure Active Directory B2C custom policies by building one from the ground up. As the series progresses, the topics will serve to show how the different parts of the AAD B2C policy syntax and the underlying framework can be brought together to realize desired functionality. At the conclusion of the series, you should be ready to both understand existing AAD B2C policies and to create new ones for yourself.

The topic areas being covered in this series are:

Topic Focus Area
Introduction
Hello World! Returning a simple message with a custom policy
Hello [Your Name Here]! Custom policy elements and syntax, how to collect and work with user input
Hello! (But Only If You Have an Access Code) <– You are here How to perform input validation
Hello! (NOW WITH REST!) How to make REST calls to external services for validation or information exchange
Hello! Let’s Add a User How to use the backing Azure Active Directory database to store and retrieve user information
Hello! Please Either Sign In or Sign Up How to either sign in an existing user or allow a new user to sign up
Hello! It’s Time To Clean Up a Bit How to use multiple files and a policy file hierarchy to manage and reuse policy content


NOTE

This walkthrough is intended for developers who want to learn how to leverage AAD B2C Custom Policies. The content assumes you understand basic AAD B2C concepts such as how to make use of the built-in policies to implement basic user management and authentication tasks. Basic functional tutorials for AAD B2C can be found in the online documentation here.

Catching Up

In the previous posts in this series you were shown the basic building blocks involved in assembling an AAD B2C custom policy. You first created a custom policy that would respond with a JWT Token containing a fixed “Hello World!” greeting in one of its claims. Then you saw how you could use Technical Profiles, Claims Transformations, and other elements to show a user interface that allowed a user to input their name in order to personalize that same greeting.

In this post we will continue to add to our “Hello World” policy. There are two key challenges with user interaction that need to be addressed. First, we need to help users provide valid data. Second, we need to ensure that they have in fact provided valid data. Once we have valid data, we can then proceed to code our policy “program” logic to respond to that data, executing actions that are only valid for some of the possible values.

We will update our policy to collect additional information from the user, showing how we can restrict the data that they enter. However, sometimes just applying restrictions to what can be input is not enough to ensure valid data. So the next thing we will look at is how we can add some steps to inspect data that has been entered and to make sure it is acceptable to whatever our business needs may be. Finally, we will see how we can execute logic conditionally based on the values that we were provided.

We will continue working with the example policy that we have developed throughout the previous posts in this series. You can download a copy of that file here, being sure to update the TenantId, PolicyId, and PublicPolicyUri attributes to the correct values from your AAD B2C tenant, and (if you used different names) replacing your own cryptographic key names in the JWTIssuer TechnicalProfile element.

If you find you are struggling to follow along or run into a problem that you cannot quite solve, a completed version of the policy that is being developed in this post is also available for download. You can download a copy of that file here, being sure to update the TenantId, PolicyId, and PublicPolicyUri attributes to the correct values from your AAD B2C tenant, and (if you used different names) replacing your own cryptographic key names in the JWTIssuer TechnicalProfile element.

As former US President Andrew Shepherd used to say, “Trust, but verify”. Actually, that is a Russian proverb that Ronald Reagan frequently quoted. Andrew Shepherd is a fictional character from the 1995 film The American President. See what I did there?

Restricting User Entry

We already saw one form of user input restriction in our “Hello World” policy. You were able to indicate that certain entries were mandatory by setting the Required attribute on a DisplayClaim element, as the following code illustrates:

<DisplayClaim ClaimTypeReferenceId="givenName" Required="true"/>
<DisplayClaim ClaimTypeReferenceId="surname" Required="true"/>

While making necessary fields required elements does help, there is a significant gap between “you must enter something” and “you must enter a valid thing.” To help with that, the first concept that will be addressed here is applying restrictions to limit what can be done when the data is being entered.

Data restrictions in AAD B2C are actually defined when a claim is declared, as part of the ClaimType element. There are currently three ways that data can be restricted in AAD B2C custom policies:

  • Listing a finite set of values that the user can select from.
  • Defining a pattern that indicates the characteristics that the provided data must match.
  • Defining data characteristic rules and requiring the entered data to abide by one or more of these rules.

We will look at each of these techniques in turn:

Restricting Data Input with Enumerated Values

One of the most basic approaches to restricting user data input is to reduce their input to making a selection from a list of preexisting values. In this case, since you have defined the set of available inputs, presumably any choice that the user makes will result in valid input (this ignores the case where there are inter-dependencies between data values or external factors that affect the validity of a subset of items in the list, which we will learn how to handle later.)

You can define a set of limited values for a claim by defining a Restriction element on the claim and then populating that Restriction with a set of Enumeration elements. Each Enumeration item includes:

  • A Text attribute that defines how it will be displayed in the user interface.
  • A Value attribute that defines what value will be set on the claim when it is selected.
  • An optional SelectByDefault attribute that can be set to true or false to indicate if the item is initially selected.

Enumeration values are only applied to restrict a user’s input for a claim when the claim’s UserInputType element is set to DropdownSinglSelect, RadioSingleSelect, or CheckboxMultiSelect.

NOTE:
Besides providing a restricted list of values for Dropdown, Radio, and Checkbox user interface controls, there is one other use for Enumeration restrictions. The GetMappedValueFromLocalizedCollection Claims Transformation allows you to set the value of a claim based on the result of looking up the value of another claim within the collection of Enumeration values in the target claim’s Restriction element. This is useful for mapping error codes to display text, for example. You can learn more about using GetMappedValueFromLocalizedCollection in the online documentation.

Let’s see this approach to restricting input in action. We’re going to extend our Hello World policy to use a dropdown control to ask if the user is entering their information as an individual consumer, or perhaps as a member of a company that we happen to do business with.

Add the following Claim Type declaration in the Claims Schema section of your policy:

<ClaimType Id="accountType">
  <DisplayName>Account Type</DisplayName>
  <DataType>string</DataType>
  <UserHelpText>The type of account being registered.</UserHelpText>
  <UserInputType>DropdownSingleSelect</UserInputType>
  <Restriction>
    <Enumeration Text="Company Account" Value="company" SelectByDefault="false"/>
    <Enumeration Text="Individual Account" Value="individual" SelectByDefault="true"/>
  </Restriction>
</ClaimType>

This defines the accountType claim to collect a string (text) value using a DropdownSingleSelect control. The dropdown control will show the user two options – Company Account and Individual Account – which when selected will set the claim value to company or individual, respectively. The value that will be initially selected by default will be Individual Account.

Now locate the UserInformationCollector Technical Profile and add a new DisplayClaim element to collect:

<DisplayClaim ClaimTypeReferenceId="accountType" Required="true"/>

Also add the accountType claim as an output for the same Technical Profile:

<OutputClaim ClaimTypeReferenceId="accountType"/>

Finally, include the accountType claim in the token that is returned by the policy by adding it as an Output Claim to the policy’s Relying Party element:

<OutputClaim ClaimTypeReferenceId="accountType" />

Now upload the updated policy and run it. You should see the dropdown control has now been added to the user interface, and the value you select is returned in the token.

Restricting User Input with a List of Values

Restricting user input with a list of values

Restricting Data Input with Regular Expressions

We can’t always know in advance the entire set of possible values for a claim, or perhaps the set of available values may be so large that showing every possible combination would be impractical. Instead, we may need to allow the user to type in a value while limiting the text which they may provide. In this case, you can use a regular expression (AKA “regex”) to restrict the user input to text that matches the criteria defined in the expression.

You define a regex within a Restriction element by specifying a Pattern element instead of the Enumeration elements we discussed previously. You can then provide the Pattern element with a RegularExpression attribute that defines the regex to apply, as well as an optional HelpText attribute that defines the message that will be shown to the user if their entry does not match the expression.

Let’s use this kind of restriction to help us collect an email address in our policy. Add the following Claim Type declaration in the Claims Schema section of your policy:

<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>

This defines the email claim to collect a string (text) value using a TextBox control. The value entered by the user will be validated against the given regex, and if it fails to match, the control will display the message “Please enter a valid email address.

NOTE:
There are several tools available to help you understand the specific validations being performed by the given regex, or to help you build up a regex of your own. I happen to have been using RegexBuddy for that purpose for several years. Please feel free to use it or any other tool of your choosing if you’d like assistance decoding or creating your own expressions.

There is a lot of debate around the topic of using a regex for email validation, and how limited or permissive the expression should be. This particular regex comes from the email claim definition found in the AAD B2C Starter Pack base policy. You are welcome to substitute your own expression if you do not care for this one.

As you did in the previous section, locate the UserInformationCollector Technical Profile and add a new DisplayClaim element to collect:

<DisplayClaim ClaimTypeReferenceId="email" Required="true"/>

Also add the email claim as an output for the same Technical Profile:

<OutputClaim ClaimTypeReferenceId="email"/>

Finally, include the email claim in the token that is returned by the policy by adding it as an Output Claim to the policy’s Relying Party element:

<OutputClaim ClaimTypeReferenceId="email" />

Now upload the updated policy and run it. You should see an email address textbox has now been added to the user interface. Entering an invalid email address should display an error, and when you enter a valid address, the value you typed in is now returned in the token.

Restricting User Input with a Regular Expression

Restricting user input with a regular expression

Using Rules to Restrict User Input

While regular expressions provide a lot of power for restricting user input, they suffer from a couple of weaknesses. First, they can get quite complicated for non-trivial scenarios, making them hard to develop and troubleshoot. Second, when the user input is deemed invalid based on the regular expression, the error message that is displayed is scoped to the entire regex – the specific problem is not identified.

Predicate Validations are another option available to restrict policy claim entry that overcomes both of these issues. With Predicate Validations, you define a collection of rules (Predicates) that describe the restrictions you wish to apply. Within this collection of rules you can specify error messages that apply to different rules or rule groups so you can help communicate what part of the user’s data entry needs to change to satisfy all of the appropriate rules.

To get started, add the following Claim Type declaration in the Claims Schema section of your policy:

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

This defines a new string (text) claim called password which is displayed to the user with a Password (masked text entry) control.

Defining Predicates

There are several different kinds of Predicates that you can define within a policy. Each Predicate declaration must include an ID, a Method, a set of Parameters specific to each method type, and a UserHelpText element that provides the error to be displayed if a claim fails validation against a given Predicate.

Predicate Method Behavior Parameters
IsLengthRange Checks that the length of a string claim falls between a defined minimum and maximum range. Minimum, Maximum are used to specify lower and upper character limits, respectively.
MatchesRegex Checks that a string claim is valid according to a provided regular expression. RegularExpression provides the regular expression used to validate the text.
IncludesCharacters Indicates that at least one of the indicated characters must occur within the value of a string claim. CharacterSet uses regex character class syntax to define a set of characters to locate within the value of a string claim.
IsDateRange Makes sure a date claim occurs between the specified start and end dates. Minimum, Maximum

Add the following Predicates element to your policy, immediately after the closing tag for the ClaimsSchema element:

<Predicates>

  <Predicate Id="DisallowedWhitespace" Method="MatchesRegex">
    <UserHelpText>The password must not begin or end with a whitespace character.</UserHelpText>
    <Parameters>
      <Parameter Id="RegularExpression">(^S.*S$)|(^S+$)|(^$)</Parameter>
    </Parameters>
  </Predicate>

  <Predicate Id="AllowedAADCharacters" Method="MatchesRegex">
    <UserHelpText>An invalid character was provided.</UserHelpText>
    <Parameters>
      <Parameter Id="RegularExpression">(^([0-9A-Za-zd@#$%^&amp;*-_+=[]{}|:',?/`~"();! ]|(.(?!@)))+$)|(^$)</Parameter>
    </Parameters>
  </Predicate>

  <Predicate Id="IsLengthBetween8And64" Method="IsLengthRange">
    <UserHelpText>The password must be between 8 and 64 characters.</UserHelpText>
    <Parameters>
      <Parameter Id="Minimum">8</Parameter>
      <Parameter Id="Maximum">64</Parameter>
    </Parameters>
  </Predicate>

  <Predicate Id="Lowercase" Method="IncludesCharacters">
    <UserHelpText>a lowercase letter</UserHelpText>
    <Parameters>
      <Parameter Id="CharacterSet">a-z</Parameter>
    </Parameters>
  </Predicate>

  <Predicate Id="Uppercase" Method="IncludesCharacters">
    <UserHelpText>an uppercase letter</UserHelpText>
    <Parameters>
      <Parameter Id="CharacterSet">A-Z</Parameter>
    </Parameters>
  </Predicate>

  <Predicate Id="Number" Method="IncludesCharacters">
    <UserHelpText>a digit</UserHelpText>
    <Parameters>
      <Parameter Id="CharacterSet">0-9</Parameter>
    </Parameters>
  </Predicate>

  <Predicate Id="Symbol" Method="IncludesCharacters">
    <UserHelpText>a symbol</UserHelpText>
    <Parameters>
      <Parameter Id="CharacterSet">@#$%^&amp;*-_+=[]{}|:',.?/`~"();!</Parameter>
    </Parameters>
  </Predicate>

</Predicates>

The Predicates declared here define several rules that together help describe a valid password entry. There are several MatchesRegex Predicates which make sure all of the characters in the password are valid. An IsLengthRange Predicate ensures the password has the correct number of characters. Finally, several IncludesCharacters Predicates are defined which indicate character sets that must be used in a password.

Grouping Predicates

Predicate Validations are made up of a collection of PredicateGroup elements. Each Predicate Group contains a reference to one or more Predicate child elements. An optional MatchAtLeast attribute can be included which allows you to indicate that a subset of the defined Predicates must apply (this is typically used for passwords, where complexity rules sometimes require including characters from 3 out of 4 predefined sets or similar.) If the MatchAtLeast attribute is omitted, all of the indicated Predicates must be matched. You can also optionally define a UserHelpText element for the Predicate Group. This value will be displayed whenever a claim value fails to match the criteria established by the Predicate Group. If the User Help Text value is defined for a Predicate Group, individual User Help Text values for any failing Predicates within the group will also be displayed indented under this text.

Add the following PredicateValidations element to your policy, immediately after the closing tag for the Predicates element that you previously added:

<PredicateValidations>
  <PredicateValidation Id="StrongPassword">
    <PredicateGroups>
      <PredicateGroup Id="DisallowedWhitespaceGroup">
        <PredicateReferences>
          <PredicateReference Id="DisallowedWhitespace" />
        </PredicateReferences>
      </PredicateGroup>
      <PredicateGroup Id="AllowedAADCharactersGroup">
        <PredicateReferences>
          <PredicateReference Id="AllowedAADCharacters" />
        </PredicateReferences>
      </PredicateGroup>
      <PredicateGroup Id="LengthGroup">
        <PredicateReferences>
          <PredicateReference Id="IsLengthBetween8And64" />
        </PredicateReferences>
      </PredicateGroup>
      <PredicateGroup Id="CharacterClasses">
        <UserHelpText>The password must have at least 3 of the following:</UserHelpText>
        <PredicateReferences MatchAtLeast="3">
          <PredicateReference Id="Lowercase" />
          <PredicateReference Id="Uppercase" />
          <PredicateReference Id="Number" />
          <PredicateReference Id="Symbol" />
        </PredicateReferences>
      </PredicateGroup>
    </PredicateGroups>
  </PredicateValidation>
</PredicateValidations>

This content define a Predicate Validation with an ID of StrongPassword. In this case, the rules that govern a valid Strong Password include:

  • Not starting or ending with a whitespace character.
  • Consisting only of characters that are valid for Azure Active Directory.
  • Contains between 8 and 64 characters.
  • Includes characters from 3 of the following: lowercase, uppercase, numbers, and symbols.

Locate the password Claim Type that you declared earlier and add the following reference to the StrongPassword Predicate Validation, just after the UserInputType element declaration it contains:

<PredicateValidationReference Id="StrongPassword" />

As you have previously done, locate the UserInformationCollector Technical Profile and add a new DisplayClaim element to collect:

<DisplayClaim ClaimTypeReferenceId="password" Required="true"/>

Also add the password claim as an output for the same Technical Profile:

<OutputClaim ClaimTypeReferenceId="email"/>

NOTE:
You will not add the password claim to the claims being returned in the token. This is something you might do to help debugging, but in production you will want to be very careful with the users’ passwords, exposing them as little as possible. You do not want to be returning users’ passwords to calling applications within the tokens generated by your policies.

Now upload the updated policy and run it. You should see a new password textbox has now been added to the user interface. Entering an invalid password should display one or more errors until you provide a password that meets all of the criteria specified in the Predicate Validation Rules.

Restricting User Input With Rules

Restricting user input with rules

Validating User Input

So far we have looked at ways that you can limit the data that a user provides when interacting with your policy by defining rules and restrictions when the claim is declared. However, sometimes business rules that govern the validity of the user’s data are too complex to be described at this level. For example, this may be the case where the value of one claim affects how you may want to determine the validity of another.

To help with this kind of validation, Self-Asserted Technical Profiles allow you to configure one or more references to Validation Technical Profiles. A Validation Technical Profile is just another Technical Profile (it can be any type). For the most part, if its execution results in an error, the calling Self-Asserted Technical Profile displays an error message, and the user must correct their input before being allowed to proceed.

NOTE:
Validation Technical Profiles do not need to strictly perform data validation. In some circumstances, they can be used to perform post-processing operations on the data collected by the calling Self-Asserted Technical Profile.

To demonstrate the use of a Validation Technical Profile, we are going to add a new business requirement to our policy. When a user indicates that their account type is a Corporate Account, we are going to make sure that their email address is from one of several predefined valid domains. Otherwise, an error message will be displayed until they either correct their email address or switch to an Individual Account.

First, add the following Claim Type declarations in the Claims Schema section of your policy:

<ClaimType Id="domain">
  <DataType>string</DataType>
</ClaimType>

<ClaimType Id="domainStatus">
  <DataType>string</DataType>
</ClaimType>

This adds a pair of claims. The domain claim is used to contain the domain value that is extracted from the user’s email address. The domainStatus claim is used to indicate if the domain is one of the predetermined valid domains.

Now add the following Claims Transformations to your policy:

<ClaimsTransformation Id="GetDomainFromEmail" TransformationMethod="ParseDomain">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="emailAddress" />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="domain" TransformationClaimType="domain" />
  </OutputClaims>
</ClaimsTransformation>

<ClaimsTransformation Id="LookupDomain" TransformationMethod="LookupValue">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="domain" TransformationClaimType="inputParameterId" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="training.atmosera.com" DataType="string" Value="valid" />
    <InputParameter Id="microsoft.com" DataType="string" Value="valid" />
    <InputParameter Id="test.com" DataType="string" Value="valid" />
    <InputParameter Id="errorOnFailedLookup" DataType="boolean" Value="true" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="domainStatus" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>

The first Claims Transformation defined above – GetDomainFromEmail – applies the ParseDomain method to the value of the email claim. This will extract the domain name and set it as the value of the domain claim. The second Claims Transformation – LookupDomain – applies the LookupValue method to the the domain claim value. If the value matches one of the values defined as an ID of the Input Parameters, the domainStatus claim’s value is set to the word valid. Otherwise, the errorOnFailedLookup Input Parameter specifies that this Claims Transformation will throw an error.

Locate the UserInformationCollector Technical Profile and add the following ValidationTechnicalProfiles element immediately after the closing tag for the OutputClaims element:

<ValidationTechnicalProfiles>
  <ValidationTechnicalProfile ReferenceId="CheckCompanyDomain">
    <Preconditions>
      <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
        <Value>accountType</Value>
        <Value>company</Value>
        <Action>SkipThisValidationTechnicalProfile</Action>
      </Precondition>
    </Preconditions>
  </ValidationTechnicalProfile>
</ValidationTechnicalProfiles>

This is what tells the Self Asserted Technical Profile to run the additional validation step by executing the CheckCompanyDomain Technical Profile. Notice that the declaration includes a Preconditions element. This element is used to determine if the Validation Technical Profile should be run, based on either the presence of a given claim, whether a given claim matches a predetermined value, or both. In this case, the value of the accountType claim is compared to the value company. If the values DO NOT match (notice the ExecuteActionsIf attribute is set to false), then the action SkipThisValidationTechnicalProfile is performed. To be clear (because this “Skip” logic can be a bit upside-down sometimes) if the accountType value is not equal to company, the validation gets skipped. If the accountType value is equal to company, then the validation is executed.

Now add the following Technical Profile immediately after the UserInformationCollector Technical Profile:

<TechnicalProfile Id="CheckCompanyDomain">
  <DisplayName>Check Company Domain</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <InputClaimsTransformations>
    <InputClaimsTransformation ReferenceId="GetDomainFromEmail"/>
  </InputClaimsTransformations>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="domain" />
  </OutputClaims>
  <OutputClaimsTransformations>
    <OutputClaimsTransformation ReferenceId="LookupDomain"/>
  </OutputClaimsTransformations>

</TechnicalProfile>

This declares the CheckCompanyDomain Technical Profile that was referenced in the ValidationTechnicalProfile element in the Self-Asserted Technical Profile. This is a Claims Transformation Technical Profile, which was described in a previous post in this series. It performs the GetDomainFromEmail Claims Transformation that you declared earlier in order to obtain the domain based on the user’s email address, then it performs the LookupDomain Claims Transformation to check to see if the email address specifies a valid domain. If the domain is not found, the CheckCompanyDomain Technical Profile will throw an error.

Return to the UserInformationCollector Technical Profile and add the following entry to its Metadata element:

<Item Key="LookupNotFound">The provided email address was not from a valid company.</Item>

Certain Claims Transformation operations – such as the LookupValue operation with an errorOnFailedLookup value set to true – are capable of returning errors as a result of invalid data. When these Claims Transformations are used in a Validation Technical Profile, you may want to control the error message that is shown in the Self-Asserted Technical Profile that invoked the Validation. To do so, refer to the documentation for the Claims Transformation. These should indicate a value that you can set in the Self-Asserted Technical Profile’s Metadata section that will be used as the error message for the Validation operation. In the case of the LookupValue operation, the LookupNotFound Metadata value is used to define the error message that will be shown if the lookup is unsuccessful.

Finally, add the following Output Claim entries to the OutputClaims collections in both the UserInformationCollector Technical Profile as well as the similar collection in the Relying Party section. While including these two claims is not strictly necessary, it will help us see what is going on.

<OutputClaim ClaimTypeReferenceId="domain"/>
<OutputClaim ClaimTypeReferenceId="domainStatus"/>

Now upload the updated policy once more and run it. Specify the user attributes and select Company Account as the account type. Try to use an email address that is not part of the training.atmosera.com, microsoft.com, or test.com domains.

Performing Data Validation on User Input

Performing data validation on user input

Now run the policy with a valid email address. Inspect the resulting claims – you will see the domain and domainStatus claims have been included.

{
  ...
  "accountType": "company",
  "email": "jgarland@training.atmosera.com",
  "domain": "training.atmosera.com",
  "domainStatus": "valid",
  "name": "John Garland",
  "message": "Hello John Garland"
}
//(This content has been trimmed for brevity)

Repeat running the policy again with with an Individual Account. Regardless of the email address you provide (as long as it is a properly formatted email address), notice that the domain and domainStatus claims are missing. Since the validation is skipped for Individual Accounts, the claims are never computed.

{
  ...
  "accountType": "individual",
  "email": "jmg@avidgator.com",
  "name": "John Garland",
  "message": "Hello John Garland"
}
//(This content has been trimmed for brevity)

Reacting to Data Values

In addition to restricting or displaying errors based on user input, you can also have your policy implement different behavior based on the values of data in the policy. You saw one example of this previously when a Precondition element was used to decide whether or not the Validation Technical Profile should be run depending on the value of the accountType claim. In this section we will look at two other alternatives. First we will see how we can conditionally dictate within a Technical Profile if it should be enabled or disabled based on data values. Next we will see how we can again use Preconditions, this time in our User Journey’s Orchestration Steps.

Conditionally Enable or Disable Technical Profiles

You can conditionally enable or disable the execution of a Technical Profile by adding an EnabledForUserJourneys element to the Technical Profile declaration. Within the EnabledForUserJourneys element you specify one of five modes that determine if the profile will run or not.

The first available mode value is Always. This is effectively the default mode if the EnabledForUserJourneys is omitted, and implies that the profile will always be available for execution.

The second available mode value is Never. In this case, the profile will not be executed at all, unless the value is later overridden (policy hierarchies and overrides will be discussed in a later post in this series.)

It is also valid to use true and false in place of Always and Never.

The remaining three modes are OnClaimsExistence, OnItemExistenceInStringCollectionClaim, and OnItemAbsenceInStringCollectionClaim. These three modes are somewhat similar, in that they use a claim value to determine if the Technical Profile should be run. OnClaimsExistence checks whether a claim’s value matches a desired value. OnItemExistenceInStringCollectionClaim looks for a specific value within a String Collection claim, and OnItemAbsenceInStringCollectionClaim checks to be sure that a specific value is NOT present within a String Collection claim. The claim to examine and the value to look for are specified in the Technical Profile’s Metadata element; an item with the key ClaimTypeOnWhichToEnable indicates the claim to examine, and an item with the key ClaimValueOnWhichToEnable specifies the value.

To see this in action, we are going to update our policy with one more piece of business logic. When a user selects Individual Account as their Account Type, we will make them enter an access (or invitation) code. Users from companies we know can just sign up, other individual users need to be invited.

First, add the following Claim Type declarations in the Claims Schema section of your policy:

<ClaimType Id="accessCode">
  <DisplayName>Access Code</DisplayName>
  <DataType>string</DataType>
  <UserHelpText>Enter your invitation access code.</UserHelpText>
  <UserInputType>Password</UserInputType>
  <Restriction>
    <Pattern RegularExpression="[0-9][0-9][0-9][0-9][0-9]" HelpText="Please enter your invitation access code."/>
  </Restriction>
</ClaimType>

<ClaimType Id="isValidAccessCode">
  <DataType>boolean</DataType>
</ClaimType>

This defines a string (text) claim that allows the user to enter a four-digit access (invitation) code. It also defines a boolean claim that will indicate if the access code that has been entered is valid or not. Notice that the the accessCode claim employs the Pattern input restriction that we saw previously.

Now add the following Claims Transformations to your policy:

<ClaimsTransformation Id="CheckIfIsValidAccessCode" TransformationMethod="CompareClaimToValue">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="accessCode" TransformationClaimType="inputClaim1"/>
  </InputClaims>
  <InputParameters>
    <InputParameter Id="compareTo" DataType="string" Value="12345"/>
    <InputParameter Id="operator" DataType="string" Value="equal"/>
    <InputParameter Id="ignoreCase" DataType="string" Value="true"/>
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="isValidAccessCode" TransformationClaimType="outputClaim"/>
  </OutputClaims>
</ClaimsTransformation>

<ClaimsTransformation Id="ThrowIfIsNotValidAccessCode" TransformationMethod="AssertBooleanClaimIsEqualToValue">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="isValidAccessCode" TransformationClaimType="inputClaim"/>
  </InputClaims>
  <InputParameters>
    <InputParameter Id="valueToCompareTo" DataType="boolean" Value="true"/>
  </InputParameters>
</ClaimsTransformation>

The first Claims Transformation runs a CompareClaimToValue operation to examine the provided accessCode entry and compares it against a single hardcoded valid value – “12345”. Admittedly not a very secure code (I need to go change the combination on my luggage…) but it will do for our current example. The boolean claim isValidAccessCode will be set depending on whether the code value matches. The second transformation runs an AssertBooleanClaimIsEqualToValue operation which examines the isValidAccessCode value to ensure it is true. If not, an exception is thrown.

Add the following Technical Profiles to the policy:

<TechnicalProfile Id="AccessCodeInputCollector">
  <DisplayName>Collect Access Code Input Technical Profile</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <Item Key="ContentDefinitionReferenceId">SelfAssertedContentDefinition</Item>
    <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Invalid access code.</Item>
    
    <Item Key="ClaimTypeOnWhichToEnable">accountType</Item>
    <Item Key="ClaimValueOnWhichToEnable">individual</Item>
  </Metadata>
  <DisplayClaims>
    <DisplayClaim ClaimTypeReferenceId="accessCode" Required="true"/>
  </DisplayClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="accessCode"/>
  </OutputClaims>
  <ValidationTechnicalProfiles>
    <ValidationTechnicalProfile ReferenceId="CheckAccessCodeViaClaimsTransformationChecker"/>
  </ValidationTechnicalProfiles>
  
  <EnabledForUserJourneys>OnClaimsExistence</EnabledForUserJourneys>
</TechnicalProfile>

<TechnicalProfile Id="CheckAccessCodeViaClaimsTransformationChecker">
  <DisplayName>Check that the user has entered a valid access code by using Claims Transformations</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="isValidAccessCode"/>
  </OutputClaims>
  <OutputClaimsTransformations>
    <OutputClaimsTransformation ReferenceId="CheckIfIsValidAccessCode"/>
    <OutputClaimsTransformation ReferenceId="ThrowIfIsNotValidAccessCode"/>
  </OutputClaimsTransformations>
</TechnicalProfile>

The first Technical Profile has an ID of AccessCodeInputCollector is a Self-Asserted Technical Profile, which will present the user with a prompt to enter their access code. The second Technical Profile is configured as the Validation Technical Profile for the user’s entry, just as we had seen previously when we discussed validating user input. It is currently a Claims Transformation Technical Profile that is configured to run the Claims Transformations we previously configured.

In the Self-Asserted Technical Profile, notice the Metadata element also includes an item with a Key of UserMessageIfClaimsTransformationBooleanValueIsNotEqual. This value is used for AssertBooleanClaimIsEqualToValue Claims Transformations, and defines the error message that will be shown to users in the event that the Claims Transformation results in an exception.

Revisiting the AccessCodeInputCollector Technical Profile above, you will see that it includes an EnabledForUserJourneys declaration that is set to OnClaimsExistence, with Metadata values examining the accountType claim for a value of individual. So this profile will only be run if the user has chosen an Individual Account type.

Finally, update the User Journey’s Orchestration Steps to include the new step below:

<OrchestrationStep Order="3" Type="ClaimsExchange">
  <ClaimsExchanges>
    <ClaimsExchange Id="GetAccessCodeClaimsExchange" TechnicalProfileReferenceId="AccessCodeInputCollector" />
  </ClaimsExchanges>
</OrchestrationStep>

Be sure to update the Order attributes for the GetMesageClaimsExchange and SendClaims actions to “4” and “5” respectively in order to account for the new “Step 3”.

Upload the updated policy and run it. Enter the user information fields, select Individual Account as the Account Type, and click Continue. Notice that you now are now prompted to enter an access code. If you enter the valid code of 12345, you will receive a token. Run the policy again and select Company Account as the Account Type. Notice that this time the prompt to enter an access code has been skipped.

Requesting an Access Code Only for Individual Accounts

Requesting an access code only for individual accounts

NOTE:
Although this technique does work, it is somewhat uncommon to use the EnabledForUserJourneys element this way. The more common approach to flow-control in a policy is to use Preconditions within User Journey Orchestration Steps, which is discussed below. The EnabledForUserJourneys element is almost exclusively used for policies that work with multiple identity providers to help determine which providers will be presented to the users. You can find additional information about this use for the EnabledForUserJourneys element in the online documentation.

Using Preconditions in User Journey Orchestration Steps

You previously saw that you could use Precondition declarations to determine whether or not a Validation Technical Profile should be executed. You can similarly use a Preconditions element to determine if an Orchestration Step in the User Journey should be executed. There are two kinds of Preconditions, specified by the Type attribute in the Precondition declaration. A ClaimsExist precondition checks to see if a claim has a value. A ClaimEquals precondition compares the value of a claim against a predetermined value. In addition to the Type, an ExecuteActionsIf attribute is used to determine if the Precondition’s declared Action is executed if the comparison provided by the Type specification is true or false.

NOTE:
The only one possible action that a Precondition can perform is SkipThisOrchestrationStep, which as the name implies, skips the execution of the Orchestration Step. This logic can be somewhat convoluted. You are defining a condition (or conditions) which when met results in NOT running the Orchestration Step. Combine this with an ExecuteActionsIf value of false, and you can give yourself quite the headache trying to unravel the behavior. Just remember – you are defining conditions under which the Orchestration Step should NOT be executed.

We are going to modify our policy to use this approach to run the Access Code check step instead of using the EnabledForUserJourney element. Go back to the AccessCodeInputCollector Technical Profile and remove the EnabledForUserJourney element (you can also optionally remove the ClaimTypeOnWhichToEnable and ClaimValueOnWhichToEnable Metadata elements if you want to.) The Technical Profile declaration should now resemble the following:

<TechnicalProfile Id="AccessCodeInputCollector">
  <DisplayName>Collect Access Code Input Technical Profile</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <Item Key="ContentDefinitionReferenceId">SelfAssertedContentDefinition</Item>
    <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Invalid access code.</Item>
  </Metadata>
  <DisplayClaims>
    <DisplayClaim ClaimTypeReferenceId="accessCode" Required="true"/>
  </DisplayClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="accessCode"/>
  </OutputClaims>
  <ValidationTechnicalProfiles>
    <ValidationTechnicalProfile ReferenceId="CheckAccessCodeViaClaimsTransformationChecker"/>
  </ValidationTechnicalProfiles>
</TechnicalProfile>

Next, replace the GetAccessCodeClaimsExchange User Journey Orchestration Step (Step 3) with the following declaration:

<OrchestrationStep Order="3" Type="ClaimsExchange">
  <Preconditions>
    <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
      <Value>accountType</Value>
      <Value>company</Value>
      <Action>SkipThisOrchestrationStep</Action>
    </Precondition>
  </Preconditions>
  <ClaimsExchanges>
    <ClaimsExchange Id="GetAccessCodeClaimsExchange" TechnicalProfileReferenceId="AccessCodeInputCollector" />
  </ClaimsExchanges>
</OrchestrationStep>

Notice the new “Step 3” entry includes a Preconditions element. This precondition examines the accountType claim, and the Orchestration Step’s function of invoking the AccessCodeInputCollector Technical Profile is skipped if it has a value of company (it is run if the accountType has a value of individual.)

Finally, upload the updated policy and run it. Notice that you are still prompted to enter an access code for Individual Accounts, and you are not prompted for Company accounts.

NOTE:
In the end, I think the use of Preconditions to guide the flow of a User Journey makes the policy a little more readable than using the EnabledForUserJourneys element. This is because you can read through the User Journey Orchestration Steps and see which Technical Profiles will be run or not run without having to go into the Technical Profiles themselves to make that same determination. Also, although EnabledForUserJourneys is better suited for examining whether an item is contained in a list (or not), Preconditions allow you to specify multiple criteria that can be met – it is not uncommon to see Preconditions that first check for the existence of a claim, then check the claims value, then do the same for one or more additional claims.

Recap

In this post, you have seen several different techniques that you can use to restrict, verify, and validate user input. These included:

  • Marking Display Claims as required to compel user input
  • Using enumerations in Claim Type declarations to restrict data input
  • Using regular in Claim Type declarations expressions to restrict data input
  • Defining and referencing Predicate rules to restrict data input
  • Validating user input with Validation Technical Profiles
  • Altering User Journeys based on claim values with Preconditions

Once again, if you had any problems following along or if you ran into a problem that you could not quite solve, a completed version of the policy that was developed in this post is available for download. You can download a copy of that file here, being sure to update the TenantId, PolicyId, and PublicPolicyUri attributes to the correct values from your AAD B2C tenant, and (if you used different names) replacing your own cryptographic key names in the JWTIssuer TechnicalProfile element.

Up to this point we have seen several different kinds of Technical Profiles collaborate to provide our desired policy functionality. However, the Technical Profiles we have used have been limited mostly to presenting user interfaces and transforming claims values. In the next post in this series, we will introduce the REST Technical Profile and see how we can invoke external APIs to greatly extend the functional reach of our policies.

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X