Two days. That’s how long I spent chasing HTTP Referer headers, wiring up REST APIs, and digging through Azure AD B2C documentation — only to find that the answer was a single line of XML that Microsoft had documented all along. If you’re building a multi-tenant authentication system and need to capture where your users came from, let me save you those two days.
The Problem
You’re building a multi-tenant authentication system where different applications (web portals, mobile apps, SaaS platforms) need to authenticate through Azure AD B2C. Each application needs to know where the user came from so it can redirect them back after authentication and track which origin initiated the login.
Your requirement: Capture the originating application URL as a custom claim in the ID token.
Example: When a user starts at https://your-app.example.com/portal/dashboard, B2C should capture this URL and include it in the JWT token as "consumerAppReferrer": "https://your-app.example.com/portal/dashboard".
The Dead Ends
Approach 1: HTTP Referer Headers
Initial Idea: Use the HTTP Referer header that browsers send with requests.
Why it Failed:
- file:// protocol never sends a Referer header (critical for local testing)
- Browser privacy settings and security policies strip Referer headers
- Not reliable across all browsers and configurations
- Lesson: HTTP headers are too unreliable for mission-critical authentication data
Approach 2: Technical Profile InputClaims Mid-Journey
Idea: Add InputClaims and OutputClaims to technical profiles in the middle of the user journey to capture the query parameter.
<!-- In SelfAsserted-Signin-Email-With-Validation -->
<InputClaim ClaimTypeReferenceId="consumerAppReferrer" DefaultValue="unknown" />
<OutputClaim ClaimTypeReferenceId="consumerAppReferrer" DefaultValue="" />
Why it Failed:
- Technical profiles in the middle of the journey don’t have access to the original authorization URL query parameters
- Query parameters are only available at the relying party level where the authorization request is received
- Claims captured mid-journey don’t automatically populate from query parameters
- Lesson: B2C’s claim flow architecture requires understanding where parameters are accessible
Approach 3: PolicyProfile InputClaims with Standard Resolvers
Idea: Try capturing at the PolicyProfile level using various claim resolvers.
<InputClaims>
<InputClaim ClaimTypeReferenceId="consumerAppReferrer" PartnerClaimType="consumerAppReferrer" />
</InputClaims>
Why it Failed:
- Standard InputClaim mappings don’t read from query parameters
- They expect the claim to already exist in the claims bag
- Query parameters are not automatically parsed into claims
- Lesson: Need the right resolver syntax for OAuth/OIDC query parameters
TL;DR
To capture custom OAuth query parameters in Azure AD B2C ID/access tokens, use the {OAUTH-KV:parameterName} claim resolver with AlwaysUseDefaultValue="true" in your relying party policy's OutputClaims. HTTP Referer headers and REST APIs are unnecessary—B2C has native support for this.
The Solution: {OAUTH-KV:parameterName}
Discovery: Azure AD B2C has a native claim resolver specifically for OAuth/OIDC query parameters: {OAUTH-KV:parameterName}
The working implementation has three components:
1. Define the Claim in ClaimsSchema
<ClaimType Id="consumerAppReferrer">
<DisplayName>Consumer App Referrer</DisplayName>
<DataType>string</DataType>
</ClaimType>
2. Use the OAUTH-KV Resolver in OutputClaims
<OutputClaim
ClaimTypeReferenceId="consumerAppReferrer"
PartnerClaimType="consumerAppReferrer"
DefaultValue="{OAUTH-KV:consumerAppReferrer}"
AlwaysUseDefaultValue="true"
/>
Key attributes:
- DefaultValue="{OAUTH-KV:parameterName}" — This resolver reads the query parameter from the authorization request
- AlwaysUseDefaultValue="true" — Forces B2C to use the resolver value, even if the claim exists elsewhere
3. Pass the Query Parameter in Authorization URL
https://tenant.b2clogin.com/tenant.onmicrosoft.com/oauth2/v2.0/authorize?
p=B2C_1A_POLICY_NAME
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&response_type=id_token
&scope=openid
&consumerAppReferrer=https://your-origin-app.com/path
How It Works
1. Authorization Request Arrives
↓
URL: ?...&consumerAppReferrer=https://your-app.com/portal
2. B2C Receives and Processes Request
↓
PolicyProfile OutputClaim executes
3. OAUTH-KV Resolver Activates
↓
Reads "consumerAppReferrer" from query parameters
4. Claim Added to Token
↓
JWT payload: { "consumerAppReferrer": "https://your-app.com/portal" }
5. Client Receives Token
↓
Application can now read the originating URL
Testing the Implementation
Test URL Format
https://your-tenant.b2clogin.com/your-tenant.onmicrosoft.com/oauth2/v2.0/authorize?p=B2C_1A_YOUR_POLICY&client_id=YOUR_CLIENT_ID&nonce=defaultNonce&redirect_uri=https%3A%2F%2Fjwt.ms&scope=openid&response_type=id_token&prompt=login&consumerAppReferrer=https%3A%2F%2Fyour-app.com%2Fpath
Verification Steps
- Open the authorization URL (with encoded consumerAppReferrer parameter)
- Complete authentication (enter credentials, TOTP, MFA, etc.)
- Arrive at https://jwt.ms
- Decode the ID token and verify the claim:
{
"iss": "https://your-tenant.b2clogin.com/...",
"sub": "user-object-id-guid",
"aud": "your-client-id",
"email": "[email protected]",
"consumerAppReferrer": "https://your-app.com/path",
"iat": 1704888000,
"auth_time": 1704888000
}Key Learnings
1. B2C Has Native OAuth Support
Many developers jump to REST APIs or custom solutions, but B2C’s {OAUTH-KV:} resolver handles this natively. Check the documentation before building custom integrations.
2. Claim Resolvers Are Powerful
Beyond {OAUTH-KV:}, B2C supports various resolvers:
- {OAUTH-KV:parameterName} — Query parameters
- {Policy:PolicyId} — Policy metadata
- {Context:...} — Request context
- Understand where to use each one
3. AlwaysUseDefaultValue Matters
Without AlwaysUseDefaultValue="true", the resolver might not execute properly. This attribute forces B2C to evaluate the resolver value.
4. Policy Hierarchy and Scope
- ClaimsSchema: Must define claims in the same policy where they’re used
- Query parameters: Only accessible at the relying party (outermost policy) level
- Claims flow: Claims defined in base/extension policies may need re-definition in relying party
5. Testing is Critical
The difference between "consumerAppReferrer": "unknown" (using DefaultValue) and a missing claim or empty string can indicate:
- Policy not redeployed
- Claim definition missing
- Resolver syntax incorrect
- Clear browser cache before each test
Production Considerations
1. URL Encoding
Custom query parameters must be properly URL-encoded:
Original: https://app.com/portal/s?param=value
Encoded: https%3A%2F%2Fapp.com%2Fportal%2Fs%3Fparam%3Dvalue
2. Parameter Size Limits
OAuth/OIDC URL parameters have practical size limits (typically 2KB). Validate your URL values don’t exceed this.
3. Security
- Validate on the backend: Don’t trust the parameter comes from the expected origin
- Use HTTPS only: Always use HTTPS, never http://
- Log URLs: Consider logging which origins initiated authentication for audit trails
4. Multiple Relying Party Policies
Apply the same pattern to all relying party policies:
- SignUpOrSignIn_Policy1.xml ✓
- SignUpOrSignIn_Policy2.xml — Apply same changes
- SignUpOrSignIn_Policy3.xml — Apply same changes
- etc.
5. Refresh Token Flow
To preserve the claim across token refreshes:
<!-- In the RedeemRefreshToken technical profile -->
<OutputClaim
ClaimTypeReferenceId="consumerAppReferrer"
DefaultValue="{OAUTH-KV:consumerAppReferrer}"
/>
Implementation Checklist
- Define consumerAppReferrer ClaimType in policy ClaimsSchema
- Add OutputClaim with {OAUTH-KV:consumerAppReferrer} to PolicyProfile
- Set AlwaysUseDefaultValue="true" on OutputClaim
- Deploy policy to Azure
- Test with authorization URL including &consumerAppReferrer=YOUR_URL
- Verify claim appears in JWT at jwt.ms
- Add to all relying party policies that need it
- Configure refresh token handling if needed
- Document the feature in your API/SDK documentation
Common Pitfalls
ProblemCauseSolutionClaim missing from JWTPolicy not redeployedRedeploy to staging/productionClaim shows “unknown”Parameter not being readCheck {OAUTH-KV:} resolver syntaxClaim always emptyAlwaysUseDefaultValue="false"Change to "true"Claim definition errorMissing ClaimType in ClaimsSchemaAdd ClaimType definitionOld value in tokenBrowser cacheUse incognito/private mode
Conclusion
Capturing custom query parameters in Azure AD B2C doesn’t require complex REST APIs or workarounds. The {OAUTH-KV:parameterName} claim resolver provides native support for exactly this use case.
The winning formula:
- Define your claim type
- Use the OAUTH-KV resolver
- Set AlwaysUseDefaultValue=”true”
- Pass the parameter in the authorization URL
Simple, elegant, and B2C native.
Resources
- Azure AD B2C Claim Resolvers
- Azure AD B2C Custom Policies Overview
- Testing with jwt.ms
- OAuth 2.0 Query Parameter Reference
Have you used custom query parameters in B2C? Share your experience in the comments!
This article is based on real-world implementation in a multi-tenant authentication system with Azure AD B2C. The journey from HTTP Referer headers through REST APIs to the native OAUTH-KV solution demonstrates the importance of understanding platform capabilities before building custom solutions.
Capturing Custom Query Parameters in Azure AD B2C: A Journey of Discovery was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.