We are building an IdP-initiated SSO flow using Azure AD B2C custom policies, where the journey must:
Read 3 querystring values:
enc_attrs_token,sp, andEntityIdPass them to a backend REST API via APIM
Use the backend response to construct the final SAML assertion
The incoming URL (masked) looks like:
https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1A_IdpSso/generic/login?EntityId=TP&enc_attrs_token=_TEST&sp=HD
What we tried
We call the backend using a REST technical profile as soon as the journey starts (Order="1").
We expected {QueryString:*} tokens to resolve before the REST body is serialized.
UserJourney
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="GetAttributes"
TechnicalProfileReferenceId="REST-GetAssertionAttributes-UAT" />
</ClaimsExchanges>
</OrchestrationStep>
REST Technical Profile
<TechnicalProfile Id="REST-GetAssertionAttributes-UAT">
<Metadata>
<Item Key="ServiceUrl">https://<masked-host>/idp/sso/assertion-attributes?subscription-key=<masked></Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
<Item Key="HttpBinding">POST</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="enc_attrs_token" PartnerClaimType="encryptedToken"
DefaultValue="{QueryString:enc_attrs_token}" AlwaysUseDefaultValue="true" />
<InputClaim ClaimTypeReferenceId="serviceProviderCode" PartnerClaimType="serviceProviderCode"
DefaultValue="{QueryString:sp}" AlwaysUseDefaultValue="true" />
<InputClaim ClaimTypeReferenceId="entityIdFromQuery" PartnerClaimType="entityIdFromQuery"
DefaultValue="{QueryString:EntityId}" AlwaysUseDefaultValue="true" />
</InputClaims>
</TechnicalProfile>
What the backend actually receives (logs)
Instead of resolved values, we receive literal tokens:
{
"encryptedToken": "{QueryString:enc_attrs_token}",
"serviceProviderCode": "{QueryString:sp}",
"entityIdFromQuery": "{QueryString:EntityId}"
}
What we expected
{
"encryptedToken": "_TEST",
"serviceProviderCode": "HD",
"entityIdFromQuery": "TP"
}
Issue
{QueryString:*} tokens are not getting resolved when the REST TP runs as the first orchestration step.
Even with:
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
Is there a limitation in Azure AD B2C that prevents {QueryString:*} claim resolution inside a REST Technical Profile when it is executed as the very first orchestration step?
And what is the minimal change required so that the querystring values resolve before being serialized into the REST request body?
here is complete policy for reference
<TrustFrameworkPolicy xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
PolicySchemaVersion="0.3.0.0"
TenantId="***.onmicrosoft.com"
PolicyId="B2C_1A_TrustFrameworkExtensions.IdpSso"
PublicPolicyUri="http://***.onmicrosoft.com/TrustFrameworkExtensions.IdpSso"
TenantObjectId="***-***-***-***-************">
<BasePolicy>
<TenantId>***.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TRUSTFRAMEWORKBASE</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="enc_attrs_token">
<DisplayName>Encrypted Attributes Token</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="serviceProviderCode">
<DisplayName>Service Provider Code</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="attribute_firstname">
<DisplayName>First name</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="attribute_lastname">
<DisplayName>Last name</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="attribute_birthdate">
<DisplayName>Birthdate</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="attribute_gender">
<DisplayName>Gender</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="attribute_memberid">
<DisplayName>Member ID</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="nameId">
<DisplayName>NameID</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="sp">
<DisplayName>Service Provider (short)</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="entityIdFromQuery">
<DisplayName>EntityId from Query</DisplayName>
<DataType>string</DataType>
</ClaimType>
</ClaimsSchema>
</BuildingBlocks>
<ClaimsProviders>
<ClaimsProvider>
<DisplayName>IdpSso - Querystring Reader</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-ReadQueryString">
<DisplayName>Read QueryString and map to claims (non-UI)</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.GenericProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="EnableUserIntervention">false</Item>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
<Item Key="PersistClaims">true</Item>
</Metadata>
<InputClaims />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="enc_attrs_token" DefaultValue="{QueryString:enc_attrs_token}" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="serviceProviderCode" DefaultValue="{QueryString:sp}" AlwaysUseDefaultValue="true" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>IdpSso - Backend Connector</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-GetAssertionAttributes-UAT">
<DisplayName>Call backend for assertion attributes</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine" />
<Metadata>
<Item Key="ServiceUrl">https://***.azurefd.net/idp/sso/assertion-attributes?subscription-key=***</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="HttpBinding">POST</Item>
<Item Key="Content-Type">application/json</Item>
<Item Key="HttpResponseTimeoutInSeconds">30</Item>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="enc_attrs_token" PartnerClaimType="encryptedToken" DefaultValue="{QueryString:enc_attrs_token}" AlwaysUseDefaultValue="true" />
<InputClaim ClaimTypeReferenceId="serviceProviderCode" PartnerClaimType="serviceProviderCode" DefaultValue="{QueryString:sp}" AlwaysUseDefaultValue="true" />
<InputClaim ClaimTypeReferenceId="entityIdFromQuery" PartnerClaimType="entityIdFromQuery" DefaultValue="{QueryString:EntityId}" AlwaysUseDefaultValue="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="attribute_firstname" PartnerClaimType="attribute_firstname" />
<OutputClaim ClaimTypeReferenceId="attribute_lastname" PartnerClaimType="attribute_lastname" />
<OutputClaim ClaimTypeReferenceId="attribute_birthdate" PartnerClaimType="attribute_birthdate" />
<OutputClaim ClaimTypeReferenceId="attribute_gender" PartnerClaimType="attribute_gender" />
<OutputClaim ClaimTypeReferenceId="attribute_memberid" PartnerClaimType="attribute_memberid" />
<OutputClaim ClaimTypeReferenceId="nameId" PartnerClaimType="nameId" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>IdP SAML Issuer</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="Saml2Assertion-TP">
<DisplayName>SAML2 Assertion</DisplayName>
<Protocol Name="SAML2" />
<Metadata>
<Item Key="IdpInitiatedProfileEnabled">true</Item>
<Item Key="Issuer">https://***.b2clogin.com/***/B2C_1A_IDPSSO</Item>
<Item Key="PartnerEntityId">***ENTITY***</Item>
<Item Key="PartnerEndpoint">https://***.example.com/saml/SSO</Item>
<Item Key="Audience">***ENTITY***</Item>
<Item Key="SignSamlMessage">true</Item>
<Item Key="SamlEncryptAssertion">true</Item>
<Item Key="EncryptClaims">true</Item>
<Item Key="NameIdFormat">urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</Item>
<Item Key="AssertionLifetime">240</Item>
</Metadata>
<CryptographicKeys>
<Key Id="SamlIssuerSigningKey" StorageReferenceId="B2C_1A_***" />
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_***" />
<Key Id="SamlAssertionSigning" StorageReferenceId="B2C_1A_***" />
<Key Id="SamlAssertionEncryptionKey" StorageReferenceId="B2C_1A_***" />
<Key Id="SamlAssertionEncryption" StorageReferenceId="B2C_1A_***" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="attribute_firstname" />
<InputClaim ClaimTypeReferenceId="attribute_lastname" />
<InputClaim ClaimTypeReferenceId="attribute_birthdate" />
<InputClaim ClaimTypeReferenceId="attribute_gender" />
<InputClaim ClaimTypeReferenceId="attribute_memberid" />
<InputClaim ClaimTypeReferenceId="nameId" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="attribute_firstname" PartnerClaimType="attribute_firstname" />
<OutputClaim ClaimTypeReferenceId="attribute_lastname" PartnerClaimType="attribute_lastname" />
<OutputClaim ClaimTypeReferenceId="attribute_birthdate" PartnerClaimType="attribute_birthdate" />
<OutputClaim ClaimTypeReferenceId="attribute_gender" PartnerClaimType="attribute_gender" />
<OutputClaim ClaimTypeReferenceId="attribute_memberid" PartnerClaimType="attribute_memberid" />
<OutputClaim ClaimTypeReferenceId="nameId" PartnerClaimType="NameID" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="SignSamlAssertionForTP">
<OrchestrationSteps>
<!-- STEP 1: read querystring into claims bag
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="ReadQueryString" TechnicalProfileReferenceId="SelfAsserted-ReadQueryString" />
</ClaimsExchanges>
</OrchestrationStep>
-->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="GetAttributes" TechnicalProfileReferenceId="REST-GetAssertionAttributes-UAT" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="Saml2Assertion-TP" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
</TrustFrameworkPolicy>