2

Using the .NET Core 3.1 framework, I'm trying to configure a web platform with the following setup:

  • A Razor Pages application, that acts as the landing page for the platform with features/pages such as advertising the platform, cookie consent, privacy policy, contacts, and the pages that come with Identity (e.g., login, register, manage account).
  • Authentication for the Razor Pages application is performed in the standard Identity way.
  • An Angular SPA, that is only accessible after the user is logged in.
  • OIDC configuration with Identity Server in order to add authentication and authorisation to the Angular SPA.

All of these three components (Razor Pages + Angular + Identity Server) are bundled into one single .NET Core web project. I have also scaffolded Identity so that I am able to customise the look and behaviour of the pages.

I was able to almost configure the application the way I want it, by basically mixing the code of the startup templates of the Razor Pages option (with user accounts stored locally) and the Angular template option (with user accounts stored locally) and with a bit of trial and error and investigation.

The current status of my application is:

  1. The user logs in in the Razor Pages application.
  2. The login is successful and the email is displayed on the navigation bar.
  3. When we navigate to the SPA, my Angular app tries to silently login and is successful:

localhost:5001/Dashboard (Angular SPA home route)

enter image description here

  1. If we navigate to a part of the Razor Pages application that does not have the /Identity route (which is only used for the pages that come with Identity) the cookies appear to no longer contain the right information and I have no session in those routes. This means that, for example, if I am using the SignInManager.IsSignedIn(User) to only display a navigation option to an Administration page that is protected with an options.Conventions.AuthorizePage($"/Administration"), if I am in a URL that has the Identity route, the navigation tab will be displayed, otherwise it will not be displayed:

localhost:5001/Identity/Account/Login

enter image description here

localhost:5001 (Razor Pages application home route)

enter image description here

  1. However, even though the Administration navigation tab is being displayed when I am on a URL that has the /Identity route, if I click on it I will get a 401 unauthorised error, because the Administration page is not preceded by the /Identity route:

localhost:5001/Administration

enter image description here

I have managed to trace the problem to the the AddIdentityServerJwt(). Without this, the login for the Razor Pages application works as intended, but I am obviously unable to use authentication and authorisation with the Angular application afterwards.

I went to check the source code for that method and it turns out that it creates a new IdentityServerJwtPolicySchemeForwardSelector that forwards the JWT policy scheme to the DefaultIdentityUIPathPrefix which, as you might have guessed it, contains only the value "/Identity".

I have configured my Startup class in the following way:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    services.Configure<CookiePolicyOptions>(options =>
    {            
        options.CheckConsentNeeded = context => true;            
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });
    services
        .AddDbContext<ApplicationDbContext>(optionsBuilder =>
        {
            DatabaseProviderFactory
                    .CreateDatabaseProvider(configuration, optionsBuilder);
        });
    services
        .AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services
        .AddIdentityServer()
        .AddApiAuthorization<IdentityUser, ApplicationDbContext>();
    services
        .AddAuthentication()
        .AddIdentityServerJwt();
    services
        .AddControllersWithViews();
    services
            .AddRazorPages()
            .AddRazorPagesOptions(options =>
            {
                options.Conventions.AuthorizePage($"/Administration");
            });       
    services
        .AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = "ClientApp/dist";
        });

    services.AddTransient<IEmailSender, EmailSenderService>();
    services.Configure<AuthMessageSenderOptions>(configuration);

    services.AddTransient<IProfileService, ProfileService>();
}

public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment)
{
    SeedData.SeedDatabase(applicationBuilder, configuration);

    if (webHostEnvironment.IsDevelopment())
    {
        applicationBuilder.UseDeveloperExceptionPage();
        applicationBuilder.UseDatabaseErrorPage();
    }
    else
    {
        applicationBuilder.UseExceptionHandler("/Error");           
        applicationBuilder.UseHsts();
    }

    applicationBuilder.UseHttpsRedirection();
    applicationBuilder.UseStaticFiles();
    applicationBuilder.UseCookiePolicy();

    if (!webHostEnvironment.IsDevelopment())
    {
        applicationBuilder.UseSpaStaticFiles();
    }

    applicationBuilder.UseRouting();

    applicationBuilder.UseAuthentication();
    applicationBuilder.UseIdentityServer();
    applicationBuilder.UseAuthorization();
    applicationBuilder.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });

    applicationBuilder.UseSpa(spa =>
    {              
        spa.Options.SourcePath = "ClientApp";

        if (webHostEnvironment.IsDevelopment())
        {
            if (bool.Parse(configuration["DevelopmentConfigurations:UseProxyToSpaDevelopmentServer"]))
            {
                spa.UseProxyToSpaDevelopmentServer(configuration["DevelopmentConfigurations:ProxyToSpaDevelopmentServerAddress"]);
            }
            else
            {
                spa.UseAngularCliServer(npmScript: configuration["DevelopmentConfigurations:AngularCliServerNpmScript"]);
            }
        }
    });
}

How can I configure my application so that the session is available across my entire application and not just on URLs that have the "/Identity" route while maintaining both authentication and authorisation for the Razor Pages application and the Angular application?

João Paiva
  • 1,937
  • 3
  • 19
  • 41

1 Answers1

8

I had the same problem and solved it by adding my own PolicyScheme that decides which type of authentication should be used based on the request path. All my razor pages have a path starting with "/Identity" or "/Server" and all other requests should use JWT.

I set this up in ConfigureServices using the collowing coding:

// Add authentication using JWT and add a policy scheme to decide which type of authentication should be used
services.AddAuthentication()
    .AddIdentityServerJwt()
    .AddPolicyScheme("ApplicationDefinedAuthentication", null, options =>
    {
        options.ForwardDefaultSelector = (context) =>
        {
            if (context.Request.Path.StartsWithSegments(new PathString("/Identity"), StringComparison.OrdinalIgnoreCase) ||
                context.Request.Path.StartsWithSegments(new PathString("/Server"), StringComparison.OrdinalIgnoreCase))
                return IdentityConstants.ApplicationScheme;
            else
                return IdentityServerJwtConstants.IdentityServerJwtBearerScheme;
        };
    });

// Use own policy scheme instead of default policy scheme that was set in method AddIdentityServerJwt 
services.Configure<AuthenticationOptions>(options => options.DefaultScheme = "ApplicationDefinedAuthentication");
Thomas Erdösi
  • 534
  • 3
  • 15
  • 1
    I was not counting on having any answers to this question since so much time has passed since I posted it, but your solution worked perfectly. Thank you so very much, Thomas. – João Paiva May 31 '20 at 18:41