4

I've successful set up authentication with Azure AD B2C in my ASP.NET Core Blazor application. I can open the website (https://localhost:5001) in multiple tabs without signing in again. However, if I keep the server running but close and reopen the browser and navigate to the website, it requires me to sign in again. My understanding was that it should have kept me signed in between browser sessions. I'm fairly new to all this, so I'm not even sure where to begin looking for the problem. Any ideas what this might be?

Here is my Startup.cs, if it helps.

public class Startup
{
    private IConfiguration Configuration { get; }
        
    public Startup(IConfiguration configuration) => Configuration = configuration;

    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C"));
            
        services.AddControllersWithViews().AddMicrosoftIdentityUI();

        services.AddAuthorization(options =>
        {
            // By default, all incoming requests will be authorized according to the default policy
            options.FallbackPolicy = options.DefaultPolicy;
        });
            
        services.AddRazorPages();
        services.AddServerSideBlazor().AddMicrosoftIdentityConsentHandler();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/_Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });
    }
4
  • When you look at the authentication cookie in the browser, does it have an expiration date or is it a "Session" cookie? Commented Dec 25, 2020 at 21:08
  • If it's a session cookie, try setting an expiration time by calling ConfigureApplicationCookie with an ExpireTimeSpan like demonstrated here Commented Dec 25, 2020 at 21:10
  • I only see one cookie (.AspNetCore.Cookies) and it's a Session cookie. I added the ConfigureApplicationCookie method with an ExpireTimeSpan of 60 minutes, but it didn't change anything. The cookie is still a Session cookie. Commented Dec 25, 2020 at 21:23
  • However, I have confirmed this is the problem. Manually setting the expire time for the cookie in DevTools "resolves" the problem. I've just haven't been able to configure the expire time successfully in Startup.cs. Commented Dec 25, 2020 at 22:36

3 Answers 3

4

I found the solution to my problem. Thanks to @huysentruitw for putting me on the right path.

TL;DR: Creating my own AccountController and setting AuthenticationProperties.IsPersistent to true on sign in fixed the expiration of the auth cookie and ultimately solved my problem.

The problem was basically that the auth token cookie was not being persisted across browser sessions. It would always create the cookie, but looking in DevTools, the expiration was always set to "Session". However, configuring the expire times via the options available in Startup.cs didn't resolve it either. I tried the following:

  • Setting it via ConfigureApplicationCookie with CookieAuthenticationOptions.Cookie.Expires.
  • Setting it via ConfigureApplicationCookie with CookieAuthenticationOptions.ExpireTimeSpan
  • Setting it via .AddMicrosoftIdentityWebApp using the overload to provide configureCookieAuthenticationOptions with a CookieBuilder.Expires property populated.
  • A few other obscure things that I didn't expect to work anyway, but I was looking for anything to work.

No matter what I did, the cookie came through as expiring after the "Session".

So, I stepped into the ASP.Net Core authentication code (the CookieAuthenticationHandler class in Microsoft.AspNetCore.Authentication.Cookies) to find out why the expiration value wasn't being used. I found that CookieOptions.Expires is overridden for some reason (in the BuildCookieOptions method) and that the expiry is only set if AuthenticationProperties.IsPersistent is true (you can see this happening in HandleSignInAsync method). So, my next step was to figure out how to set that to true. I found that the AuthenticationProperties are set by an AccountController that is added automatically by the call in Startup.cs to .AddMicrosoftIdentityUI. I copied this AccountController to my project as a starting point instead and got rid of the call to .AddMicrosoftIdentityUI. I then updated the AuthenticationProperties to set IsPersistent to true, which is ultimated what fixed the issue. The cookie now comes through with an expiration date/time.

I don't know if I missed something, but it certainly seems like a bug to me, or at least a really poor configuration experience. Hopefully someone will come along and potentially point out my blunder, but this is what worked for me. I think it makes more sense to pull the AccountController into my project anyway so that I can see what's going on and fine tune it as needed.

Sign up to request clarification or add additional context in comments.

4 Comments

The IsPersistent flag is usually linked to a "Remember Me" checkbox on login pages. But since this is AD B2C, this should at least be configurable somewhere. On the other hand, doing a roundtrip to Azure AD for each browser session is not that bad as it validates if the identity is still known in the AD. Can't you configure an expiration at AD level instead, so only a roundtrip is needed instead of a sign-in each time?
This is exactly what I would expect. I had actually seen that same document and ensured that my AD B2C user flow was configured correctly. I have it set to a Rolling session timeout, which it says should extend the session each time we perform the cookie-based authentication. But it didn't resolve the problem. It still required a sign in for every new session.
It shouldn’t require credential entry, but would require the user to click “sign in” in the app, and AAD B2C would then auto login the user using it’s persistent cookies. That is, until you get the app to have its own persistent session.
0

I had this problem and resolved it by IsPersistent = true in SignInAsync() method of login page. I use .net 8 blazor ssr Cookie Authentication.

public async Task Login(EditContext editContext)
{
    var user = await appDbContext.Users.AsNoTracking()
        .FirstOrDefaultAsync(u => u.Email == vm.Email.ToLower());
    if (user is null || passwordService.VerifyHashedPasswordV3(user.PasswordHash, vm.Password) == false)
    {
        ShowAlert = true;
        AlertText = "The email or password is incorrect.";
        return;
    }

    var claims = new List<Claim>
    {
        new Claim(Constants.UserIdClaim, user.Id.ToString()),
        new Claim(Constants.EmailClaim, vm.Email),
        //new Claim(Constants.RoleClaim, user.RoleId.ToString())
    };
    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    await HttpContextAccessor.HttpContext!.SignInAsync(claimsPrincipal, new AuthenticationProperties { IsPersistent = true });
    NavigationManager.NavigateTo(ReturnUrl == null ? "/Panel" : ReturnUrl);
}

Also need builder.Services.AddAuthentication() and app.UseAuthorization() in Program.cs :

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
 .AddCookie(options => { options.LoginPath = "/Login"; options.ExpireTimeSpan = TimeSpan.FromDays(30); });

Comments

0

I wanted to add another answer to explain how you can sync the persistence of your application cookie with the Azure AD B2C cookies stored on the Azure AD-hosted domain based on whether the KMSI checkbox is checked.

If you don't persist the application cookie, code in your application like Context.User.Identity.IsAuthenticated will return false after the browser is closed even if the KMSI checkbox is checked on the login screen. I see this as a security risk, because the website is reporting to the user that they're not signed in, however if they try to access any restricted resources (e.g. controllers marked with [Authorize]), Identity will log them in with no user interaction because the cookie on the Azure AD-hosted domain is persisted.


The following assumes you're using custom policies (now defunct with the move to Entra External ID, but I digress) as it requires you to send through custom claims to the application.

  1. Ensure the setting.enableRememberMe is set to True on the relevant technical profile so the KMSI checkbox is visible:

    <ClaimsProvider>
      <DisplayName>Local Account</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
          <Metadata>
            <!--This enables the KMSI checkbox--> 
            <Item Key="setting.enableRememberMe">True</Item> 
          </Metadata>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
    
  2. Add a claim to store the value of the KMSI checkbox:

    <ClaimsSchema>
      <!-- ... -->
    
      <ClaimType Id="isPersistent">
        <DataType>string</DataType>
      </ClaimType>
    <ClaimsSchema>
    
  3. Configure the relying party file by setting the KeepAliveInDays attribute and pass through the isPersistent claim to the application. The following code would make the Azure AD-hosted domain cookie persist for 30 days:

    <RelyingParty>
      <DefaultUserJourney ReferenceId="SignUpOrSignIn" />
      <UserJourneyBehaviors>
        <!-- The KeepAliveInDays determines the lifetime of the Azure AD-hosted domain cookie --->
        <SingleSignOn Scope="Tenant" KeepAliveInDays="30" />
        <SessionExpiryType>Absolute</SessionExpiryType>
        <SessionExpiryInSeconds>1200</SessionExpiryInSeconds>
      </UserJourneyBehaviors>
      <TechnicalProfile Id="PolicyProfile">
        <DisplayName>PolicyProfile</DisplayName>
        <Protocol Name="OpenIdConnect" />
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="displayName" />
          <OutputClaim ClaimTypeReferenceId="givenName" />
          <OutputClaim ClaimTypeReferenceId="surname" />
          <OutputClaim ClaimTypeReferenceId="email" />
          <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
          <OutputClaim ClaimTypeReferenceId="identityProvider" />
          <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
    
          <!-- Send through whether the KMSI checkbox was checked as a claim -->
          <OutputClaim ClaimTypeReferenceId="isPersistent" AlwaysUseDefaultValue="true" DefaultValue="{Context:KMSI}" />
        </OutputClaims>
        <SubjectNamingInfo ClaimType="sub" />
      </TechnicalProfile>
    </RelyingParty>
    
  4. The standard approach when integrating with Azure AD B2C is to use the AddMicrosoftIdentityWebApp extension. It is here where you can add the following code to read the isPersistent claim sent through above to the AuthenticationProperties.IsPersistent property that will cause the cookie to be persisted.

    services
        .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(
           identityOptions => configuration.GetSection("AzureADB2C").Bind(identityOptions),
           applicationCookieOptions =>
           {
              // ...
    
              // set this to the same as the KeepAliveInDays attribute above
              applicationCookieOptions.ExpireTimeSpan = TimeSpan.FromDays(30);
    
              applicationCookieOptions.Events.OnSigningIn = context =>
              {
                 var isPersistentClaimValue = context.Principal?.Claims.SingleOrDefault(c => c.Type == "isPersistent")?.Value;
                 if (bool.TryParse(isPersistentClaimValue, out var isPersistent))
                    context.Properties.IsPersistent = isPersistent; 
                 return Task.CompletedTask;
              };
           },
           OpenIdConnectDefaults.AuthenticationScheme,
           AuthenticationSchemes.ApplicationScheme);
    

Overriding the IsPersistent property here works because the SigningIn event fires just before the application cookie expiry is set.

Now when a user logs in with the KMSI checkbox checked there will be two persistent cookies created with an in-sync expiry date. This ensures that the website accurately reflects the signed-in status of the user.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.