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
Related
I have an Orchestration Step which works on some precondition, even if one of the precondition is satisfied the orchestration step gets skipped but I want it to work the other way.
If any of the Precondition is not satisfied I want that Orchestration Step to be executed.
Below is my orchestration step where if remember me checkbox is not checked (kmsiValue) then I want to run the orchestration step irrespective of other preconditions but I am not able to achieve that.
<OrchestrationStep Order="7" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>isActiveMFASession</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>kmsiValue</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-MFATimeWindow" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
</ClaimsExchanges>
</OrchestrationStep>
Update :
Sorry but I am a bit confused now , I already have a TP for Remember me , the value changes accordingly on checkbox selection
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
<Metadata>
<Item Key="setting.enableRememberMe">True</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="kmsiValue" DefaultValue="{Context:KMSI}" AlwaysUseDefaultValue="true"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="kmsiValue" DefaultValue="{Context:KMSI}" AlwaysUseDefaultValue="true" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
The question I have is how can I split my orchestration steps now to make it work?
I would split this into two sub journeys. One subjourney for KMSI=True, and another subjourney for KMSI=False. Then in this subjourney design the orchestration steps. Where KMSI=true, the orchestration step contains your other preconditions. Where KMSI=false, your orchestration step contains no preconditions.
One way is to put all the logic into a REST API and return T or F depending on whether you want the step skipped.
The other way is to have a step that just checks KMSI. If that results in the TP being run, set a flag.
In the next step, check the flag and skip if the TP has been run. Otherwise, run the other steps.
Update
#Jas is a good idea but set a flag in the first step e.g.
<OutputClaim ClaimTypeReferenceId="kmsi" DefaultValue="true" AlwaysUseDefaultValue="true"/>
and use a precondition "claimsequal" in the second step.
"kmsi" should be boolean.
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.
In Azure AD B2C I am having multiple technical profiles.
In one technical profile, I may or may not have value for an output claim. But I can get it from another claim. Here for example let's take email.
So, it is possible that the user may not have an email claim, which is needed in the output. I can get it from the signInName as well.
What I want is to achieve is
if email claim has value, pass email claim in output
else use the claim signInName with PartnerClaimType="email"
How can I achieve this?
Use in AAD-OIDC:
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="email"/>
If email has a value at this stage, it will be output into the claimbag, else itll be null.
It will always try to map signInName claim to the email claim coming from AAD federation.
You cannot explicitly control what will get output based on another claim within the same technical profile.
What you can do is "if email exists, set signInName to null" after AAD-OIDC completes.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/string-transformations#nullclaim
<ClaimsTransformation Id="SetSignInNameToNull" TransformationMethod="NullClaim">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="claim_to_null" />
</OutputClaims>
</ClaimsTransformation>
<TechnicalProfile Id="CT-SetSignInNameToNull">
<DisplayName>Compare Email And Verify Email</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="SetSignInNameToNull" />
</OutputClaimsTransformations>
</TechnicalProfile>
<OrchestrationStep Order="X" Type="ClaimsExchange">
<Preconditions>
<!-- precondition here to only run this step if Email exists, and it was AAD Federated auth -->
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>authenticationSource</Value>
<Value>socialIdpAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetSignInNameToNull" TechnicalProfileReferenceId="CT-SetSignInNameToNull" />
</ClaimsExchanges>
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.
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.