Azure B2C Policy for Azure AD IdP and Local Logon Problem - azure

I have what should seem like a very common scenario yet no solution in the policy starter pack or in any of the public repo's and custom policy examples.
I have an application which is used by both internal staff and external customers. I am using B2C for this and our own Azure AD as a 'social' IdP, and local logon for external users.
The built-in functionality through the Azure Portal does not meet the requirements for numerous reasons. The external user accounts are created manually in the B2C directory and signup is prohibited. Thus, SignUpSignSignIn is unviable. The experience I am trying to achieve is:
If LocalLogon Then
Authenticate with Azure B2C Directory
Redirect to Application
Else
If AADSocialIdP Selected
Authenticate with Azure AD
If User Exists in B2C Then
Redirect to Application
Else
Create User in B2C using claims received (do not prompt for email verification)
Redirect to Application
I have resorted to using custom policies, using SocialAndLocalAccounts from the starter pack as a baseline, and have significantly modified the UserJourney so that single sign-on with AAD is achieved, the user is not prompted for their name, surname, email address, and then to verify their email address (as is the case with the built-in functionality). And, the user is properly redirected to the application. However, by creating this AAD TechnicalProfile and integrating it with the SignUpSignIn journey - though I disabled Signup through various changes in the policy pack.
However, once this is integrated, the local logon is broken. I have used the vanilla LocalAccounts policy pack and confirmed that it works and redirects to the application with the claims as expected, but once I add my AAD TechnicalProfile and ClaimsExchange in then when using local login all I get is Username or password is incorrect.
I believe this is an issue with the UserJourney I've written but at the moment I'm lost as to invoke a different journey for a local logon to a social one. I believe that my TechnicalProfile is overwriting claims during the journey which is causing this error.
My AAD TechnicalProfile is:
<TechnicalProfile Id="AAD-GB-OpenIdConnect">
<DisplayName>XXXXXXXXXXXXX</DisplayName>
<Description>XXXXXXXXXXXXX</Description>
<Protocol Name="OpenIdConnect"/>
<OutputTokenFormat>JWT</OutputTokenFormat>
<Metadata>
<Item Key="METADATA">https://login.microsoftonline.com/XXXXXXXXXXXXX/v2.0/.well-known/openid-configuration</Item>
<Item Key="client_id">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</Item>
<Item Key="response_types">code</Item>
<Item Key="scope">openid profile</Item>
<Item Key="response_mode">form_post</Item>
<Item Key="HttpBinding">POST</Item>
<Item Key="UsePolicyInRedirectUri">false</Item>
<Item Key="Prompt">true</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_PortalAADSecret"/>
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid"/>
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
<OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
<OutputClaim ClaimTypeReferenceId="identityProviderAccessToken" PartnerClaimType="{oauth2:access_token}" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
<OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
<OutputClaimsTransformation ReferenceId="CreateOtherMailsFromEmail"/>
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin"/>
</TechnicalProfile>
It is worth mentioning here that I have created a CreateOtherMailsFromEmail OutputClaimsTransformation which basically creates an emails output claim, since the application is designed to take the first element of an array of emails, as opposed to a single email address.
My UserJourney is as follows:
<UserJourney Id="CustomSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="AzureADXXXXXXXXExchange" />
<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="AzureADXXXXXXXXExchange" TechnicalProfileReferenceId="AAD-GB-OpenIdConnect" />
<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>
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
I have enabled debugging with App Insights and am examining the logs through VSCode but didn't find anything helpful.
How can this journey be adapted to support both login methods?

This issue is the result of specifying output claims with the "required" attribute set in the unified SignInSignUp relying party section. All were available in the bag through the Social IdP flow but not through the local account sign-in option.
When specifying "required" output claims you must ensure that each possible user journey will follow a technical profile chain which retrieves or adds these claims. In my specific case, the specification of claims required by the application developer included some which were not made available through the baseline technical profiles in the starter pack.
To resolve, I had to modify several technical profiles, create a unique claims transformation and apply it as an output claims transformation to the AAD-UserReadUsingObjectId baseline technical profile.
aka.ms/iefsetup
This is a very useful tool which will help to create a Social IdP and Local Login custom B2C configuration (credit #Jas Suri - MSFT). It provides a fully automated means to customise and deploy all the necessary configuration to configure a B2C tenant to use the Identity Experience Framework.
In my case this was not the entire solution for me but it helped me take a fresh look at exactly how the framework operates and eventually achieve the desired solution.

Related

Azure B2C Prevent user entries

I published an application on my Azure B2C tenant. There are two auth methods, one federated SAML provider and the regular Azure AD tenant. I want to avoid the users from the external SAML idp being kept in my Azure B2C tenant. For every user that successfully logs in, an user entry is created in my Azure B2C tenant. How can I prevent the user entries in my b2c tenant? This is my user journey:
<UserJourneys>
<UserJourney Id="UDIMASignUpOrSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<!-- <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" /> -->
<ClaimsProviderSelection TargetClaimsExchangeId="CEF" />
<ClaimsProviderSelection TargetClaimsExchangeId="UDIMA" />
</ClaimsProviderSelections>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<!-- <ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" /> -->
<ClaimsExchange Id="CEF" TechnicalProfileReferenceId="AADCEF-OpenIdConnect" />
<ClaimsExchange Id="UDIMA" TechnicalProfileReferenceId="Saml2AssertionIssuer" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- For social IDP authentication, attempt to find the user account in the directory. -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- For social IDP authentication, attempt to find the user account in the directory. -->
<!-- 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="4" 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>
<!-- Show self-asserted page only if the directory does not have the user account already (i.e. we do not have an objectId). -->
<OrchestrationStep Order="5" 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>
<!-- <OrchestrationStep Order="6" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="AADCEF-OpenIdConnect" /> -->
<OrchestrationStep Order="6" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
```
Please check if the provided references can be worked around.
By default, for external identity, Azure AD B2C will create a user object (objectID) in its own directory .So one maybe able to store claims that are asserted by the external IdP and also end users claims or your own application.Object id that is created by this external identity can be used to be passed to next step to process and used further.
The ObjectID attribute from Azure AD is generally saved under alternateSecurityID with identityProvider.
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid"/>
<OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
</OutputClaimsTransformations>
Reference: AAD B2C querying for federated identities with the MS Graph API - Stack Overflow
Please check if the user is checked with its oid i.e; objectId and issuer (identity provider) to unlink or prevent to sign in or remove that user identiy.
<ClaimsTransformation Id="RemoveUserIdentityFromCollectionByIssuer" TransformationMethod="RemoveUserIdentityFromCollectionByIssuer">
<InputClaims>
<InputClaim ClaimTypeReferenceId="issuerToUnlink" TransformationClaimType="issuer" />
<InputClaim ClaimTypeReferenceId="userIdentities" TransformationClaimType="collection" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="userIdentities" TransformationClaimType="collection" />
</OutputClaims>
</ClaimsTransformation>
Please check active-directory-b2c-advanced-policies/TRUSTFRAMEWORKBASE.xml · GitHub for details.
Also Azure B2c how to prevent user to login till admin approve - Stack Overflow if required.
Reference:
active-directory-b2c-advanced-policies/account-linking at master · Azure-Samples/active-directory-b2c-advanced-policies · GitHub

MFA prompt after a certain time elapses

We are using B2C Custom Policies in production and have a requirement that after a certain time elapses (20 minutes + depending on the application) the user is prompted to login with MFA again irrespective of which service the user is logging into. One of our developers stated that we could use the max_age query string parameter to achieve this and I thought to post here to see if anyone has experience in using this with Azure B2C Custom Policies or could recommend another solution? I found this link but not much else https://github.com/MicrosoftDocs/azure-docs/issues/51307
We are currently using the following MFA method in our policies, slightly modified to remove email verification as we don’t require this : https://github.com/azure-ad-b2c/samples/tree/master/policies/mfa-email-or-phone
Edit
Hi #Jas I've had time to look into the solution but had an issue that I hope you could answer.
We've been able to store the last time the user did MFA in the session rather than in an extension attribute. At first we couldn't get the OutputClaimsTransformation "CompareTimetoLastMFATime" to run after the first login however we found removing in the technical profile "MFAReadStoredMFATime" shown in the code below. Could you please let us know why including SM-MFA blocks the claimstransformation from running on subsequent logins? We see that step 16 is run in the logs however no claimstransformation and no CompareTimetoLastMFATime is output therefore the user always skips MFA.
<OrchestrationStep Order="16" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="MFAReadStoredMFATime" TechnicalProfileReferenceId="MFAReadStoredMFATime" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="17" Type="ClaimsExchange">
<Preconditions>
<!--Sample: If the preferred MFA method is not 'phone' skip this orchestration step-->
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>extension_mfaByPhoneOrEmail</Value>
<Value>phone</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>mfa_required</Value>
<Value>true</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>MFADoneByFedIdp</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>isLastMFATimeGreaterThanWindow</Value>
<Value>False</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="PhoneFactor-Verify" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
</ClaimsExchanges>
</OrchestrationStep>
<TechnicalProfile Id="MFAReadStoredMFATime">
<DisplayName>Fixt the session username issue</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<InputClaims>
<InputClaim ClaimTypeReferenceId="LastMFATime" DefaultValue="2018-10-01T15:00:00.0000000Z" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="isLastMFATimeGreaterThanWindow" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CompareTimetoLastMFATime" />
</OutputClaimsTransformations>
</TechnicalProfile>
<ClaimsTransformation Id="CompareTimetoLastMFATime" TransformationMethod="DateTimeComparison">
<InputClaims>
<InputClaim ClaimTypeReferenceId="LastMFATime" TransformationClaimType="firstDateTime" />
<InputClaim ClaimTypeReferenceId="systemDateTime" TransformationClaimType="secondDateTime" />
</InputClaims>
<InputParameters>
<InputParameter Id="operator" DataType="string" Value="earlier than" />
<InputParameter Id="timeSpanInSeconds" DataType="int" Value="100" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="isLastMFATimeGreaterThanWindow" TransformationClaimType="result" />
</OutputClaims>
</ClaimsTransformation>
Something similar. I assume you mean 20-minute gap between logins?
Look in the samples - there are some examples of password reset that show you how to handle the date/time. In particular, there's one for a reset after 90 days so you could do something similar for MFA.
You can use the resolvers to get the query string value.
The main problem we had is that the query string is a string and there is no method to convert to date/time so we had to do that in an API.
Also, see this.
There is a sample here to force mfa after a certian time has surpassed.
https://github.com/azure-ad-b2c/samples/tree/master/policies/mfa-absolute-timeout-and-ip-change-trigger

Azure B2C Custom Policy - from email decide claims exchange

I want the below flow for authentication with the Azure B2C custom policy
user should see field, where user enters his email id
based on the email id (domain name), we decide the claims exchange to authenticate the user with.
available claims provider - local account, sign in with Azure B2B AD Tenant.
For point 2, we can parse the domain using Parse Domain Claims Transformation.
For point 3, I have already setup the necessary Claims Provider and verified it works, using the default Signing with Local and Social starter pack.
You can create a user journey such as this.
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-HRD" TechnicalProfileReferenceId="SelfAsserted-HRD" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>domainName</Value>
<Value>contoso.com</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AAD-OIDC" TechnicalProfileReferenceId="AAD-OIDC" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>domainName</Value>
<Value>contoso.com</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-LocalAccountSignin-Email" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
...
</OrchestrationSteps>
The first orchestration steps executes the SelfAsserted-HRD technical profile, at which an e-mail address can be entered, and then invokes the SetDomainName claims transformation that parses the e-mail domain.
<ClaimsProvider>
<DisplayName>Self-Asserted</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-HRD">
<DisplayName>Self-Asserted HRD</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>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="SetDomainName" />
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
If the e-mail domain is equal to the federated domain, then the second orchestration step executes the AAD-OIDC technical profile that redirects the external identity provider.
If the e-mail domain is not equal to the federated domain, then the third orchestration step executes the SelfAsserted-LocalAccountSignin-Email technical profile that shows the local account sign-up or sign-in page.

Azure AD B2C Validation Profile for non-SelfAsserted Technical profiles

From testing, it appears Validation Technical Profiles are only used when added to SelfAssserted Technical Profiles
E.g the following:
<TechnicalProfile Id="ExternalIDP">
<DisplayName>Some External IdP</DisplayName>
<Protocol Name="OpenIdConnect" />
<Metadata>
<!-- ... -->
</Metadata>
<OutputClaims>
<!-- ... -->
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="FETCH-MORE-CLAIMS" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
does not appear to call the FETCH-MORE-CLAIMS profile after authenticating to the external identity provider.
Is this correct, and if so, is there another way to always force a second technical profile to be called whenever a particular technical profile is called?
One possible way would be to set an output claim that indicates that was done, and then have an orchestration step after that with a condition on that claim, which then runs your TP as a claims exchange.
So an output claim like:
<OutputClaim ClaimTypeReferenceId="idp" DefaultValue="ThisIdp" AlwaysUseDefaultValue="true" />
You'd need to define that claim if it isn't already defined, or you can use another one you already have.
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>idp</Value>
<Value>ThisIdp</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="FetchMoreClaimsExchange" TechnicalProfileReferenceId="FETCH-MORE-CLAIMS" />
</ClaimsExchanges>
</OrchestrationStep>
This orchestration step is skipped if idp != ThisIdp, so it would only run if your external idp was used.

Skip self-asserted orchestration step if claims exist not working

This is in reference to custom policy signupsignin user journey.
I have a self-asserted orchestration step that collects users email address before the combined signupsignin orchestration step.
I'm wanting to skip this step if the users claims already exist
This is necessary when collecting refresh tokens.
Here's how I have the orchestration step coded.
<OrchestrationStep Order="1" Type="ClaimsExchange" ContentDefinitionReferenceId="api.selfasserted.email">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAssertedEmailExchange" TechnicalProfileReferenceId="SelfAsserted-CollectEmailAddress-NoError" />
</ClaimsExchanges>
</OrchestrationStep>
I believe this might be happening because the SelfAsserted-CollectEmailAddress-NoError technical profile isn't "participating" in the SSO session; therefore it prompts the same user in the same session for the e-mail address again.
For the SelfAsserted-CollectEmailAddress-NoError technical profile to participate in the SSO session, ensure this technical profile contains the <UseTechnicalProfileForSessionManagement /> element as follows:
<TechnicalProfile Id="SelfAsserted-CollectEmailAddress-NoError">
...
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress-NoError" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>

Resources