.netasp.net-coreazure-ad-b2cazure-ad-b2c-custom-policy

Azure AD B2C Custom Policy Adds Duplicate p= Parameter or Results in p=null Error


Description:

We are implementing a custom policy (B2C_1A_CONTACTSIGNUP) for contact users in Azure AD B2C. Our application supports two separate authentication schemes: one for main account users (AzureAdB2C) and one for contact users (AzureAdB2CContact) using OpenIdConnect and custom policies.

In Program.cs, we configure MetadataAddress with the p= parameter:

Error Message:

The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

A email link is click and is using:

ContactController > Invite

return Challenge(props, "AzureAdB2CContact");

Program.cs

options.MetadataAddress = $"https://test.b2clogin.com/mylifestore.onmicrosoft.com/v2.0/.well-known/openid-configuration?p={policy}";

In OnRedirectToIdentityProvider, if we include:

context.ProtocolMessage.SetParameter("p", policy);

The redirect URL ends up with two p= parameters, e.g.:

...?p=B2C_1A_CONTACTSIGNUP&...&p=B2C_1A_CONTACTSIGNUP

When omitting that line, the redirect results in:

...?p=null

which triggers this error:

https://test.b2clogin.com/.../client/perftrace?tx=...&p=null

We also tried removing the p parameter before re-adding it:

context.ProtocolMessage.Parameters.Remove("p");
context.ProtocolMessage.SetParameter("p", policy);

But this still results in a duplicate or conflicting p= in the final redirect.

Full logic:

builder.Services.AddAuthentication()
 .AddOpenIdConnect("AzureAdB2CContact", options =>
 {
     var b2cConfig = builder.Configuration.GetSection("AzureAdB2CContact");
     var policy = b2cConfig["SignUpSignInPolicyId"];

     // Correct Authority and MetadataAddress for custom policy
     options.Authority = "https://test.b2clogin.com/test.onmicrosoft.com/v2.0/";
     options.MetadataAddress = $"https://test.b2clogin.com/test.onmicrosoft.com/v2.0/.well-known/openid-configuration?p={policy}";

     options.ClientId = b2cConfig["ClientId"];
     options.ClientSecret = b2cConfig["ClientSecret"];
     options.CallbackPath = b2cConfig["CallbackPath"];
     options.ResponseType = OpenIdConnectResponseType.IdToken;
     options.Scope.Clear();
     options.Scope.Add("openid");
     options.Scope.Add("offline_access");
     options.SaveTokens = true;

     options.TokenValidationParameters = new TokenValidationParameters
     {
         NameClaimType = "name",
         ValidateIssuer = true
     };

     options.Events = new OpenIdConnectEvents
     {
         OnRedirectToIdentityProvider = context =>
         {
             // Remove any existing 'p' parameter to avoid duplicates
             context.ProtocolMessage.Parameters.Remove("p");

             // Now set the correct policy
             context.ProtocolMessage.SetParameter("p", policy);

             var inviteToken = context.HttpContext.Request.Query["inviteToken"].FirstOrDefault();
             if (!string.IsNullOrEmpty(inviteToken))
             {
                 context.ProtocolMessage.SetParameter("extension_inviteToken", inviteToken);
             }

             return Task.CompletedTask;
         }
     };
 });

Using:

.NET 8.0

ASP.NET Core Web App

Custom B2C policy for contact sign-up

Please confirm how to reliably pass the p= parameter with a custom policy without causing duplication or null. Is there a recommended configuration or workaround for this dual-scheme setup?


Solution

  • The issue you're facing is because the p (policy) parameter is being added in both the MetadataAddress and manually in OnRedirectToIdentityProvider, which causes a duplicate or conflicting p= in the redirect URL, resulting in either p=null or a broken link.

    To resolve the issue,

    Set the policy only via Authority, and remove both MetadataAddress and any manual setting of p in event handlers.

    options.Authority = $"https://test.b2clogin.com/test.onmicrosoft.com/{policy}/v2.0/";
    

    Now remove,

    options.MetadataAddress = ...
    context.ProtocolMessage.SetParameter("p", policy);
    

    This ensures the middleware correctly includes the policy once, avoiding any duplication or p=null errors.

    Updated code:

    builder.Services.AddAuthentication()
     .AddOpenIdConnect("AzureAdB2CContact", options =>
     {
         var b2cConfig = builder.Configuration.GetSection("AzureAdB2CContact");
         var policy = b2cConfig["SignUpSignInPolicyId"];
    
              options.Authority = $"https://test.b2clogin.com/test.onmicrosoft.com/{policy}/v2.0/";
    
     
         options.ClientId = b2cConfig["ClientId"];
         options.ClientSecret = b2cConfig["ClientSecret"];
         options.CallbackPath = b2cConfig["CallbackPath"];
         options.ResponseType = OpenIdConnectResponseType.IdToken;
         options.Scope.Clear();
         options.Scope.Add("openid");
         options.Scope.Add("offline_access");
         options.SaveTokens = true;
    
         options.TokenValidationParameters = new TokenValidationParameters
         {
             NameClaimType = "name",
             ValidateIssuer = true
         };
    
         options.Events = new OpenIdConnectEvents
         {
             OnRedirectToIdentityProvider = context =>
             {
                            
                 var inviteToken = context.HttpContext.Request.Query["inviteToken"].FirstOrDefault();
                 if (!string.IsNullOrEmpty(inviteToken))
                 {
                     context.ProtocolMessage.SetParameter("extension_inviteToken", inviteToken);
                 }
    
                 return Task.CompletedTask;
             }
         };
     });
    

    Make sure the policy (e.g., B2C_1A_CONTACTSIGNUP) is correctly uploaded and available under Azure AD B2C -> Identity Experience Framework.

    By simplifying the config and letting the OpenID middleware handle the p parameter via Authority, you eliminate the risk of duplicate or missing policy values in your requests.