简体   繁体   中英

How do I set up authentication & authorisation for ASP .NET Core 3.0 with PageModels?

I'm trying to set up an authentication and authorization section for a frontend web application I have. The application is set up as an ASP.NET Core Razor Page Application. (.NET Core 3.0); I also set it up with authentication pre-installed with the following command: dotnet new razor -au Individual .

With this, I'm trying to set up an OpenID Authentication External Login from an external service (Identity Server) I have. The problem lies with displaying the final user on the frontend (or if I'm doing this entirely wrong... - I've spent a questionable amount of time on this topic, and I couldn't find anything other than old questions, or content on how you could previously do it with MVC Apps) - I was also able to find the external user stored in the local database and can tell through code successfully signed in from the external Identity Server. (explained later)

This is my first project with a major authentication/authorization section - I apologize in advance If anything stupid jumps out. I'd love to find out what went wrong. Here goes!

Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseMySql(Configuration.GetConnectionString("DefaultConnection")
                ));

            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();


            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                //options.DefaultAuthenticateScheme = IdentityConstants.ExternalScheme;
                //options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
                options.DefaultChallengeScheme = "oidc"; 

            })
                    .AddCookie(options =>
                    {
                        options.LoginPath = "/Account/Login/";
                    })
                    .AddOpenIdConnect(ChallengeScheme, o =>
                    {
                        o.ClientId = "client_id";
                        o.ClientSecret = "*******************";
                        o.Authority = "http://endpointhere.com";
                        o.ResponseType = "code" ;
                        o.SaveTokens = true;
                        o.Scope.Add("IdentityServerApi");
                    });


            services.AddAuthorization();
            services.AddRazorPages();
        }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }


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

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }

ExternalLogin.cshtml.cs ; OnGetCallbackAsync

public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            if (remoteError != null)
            {
                ErrorMessage = $"Error from external provider: {remoteError}";
                return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
            }
            var info = await _signInManager.GetExternalLoginInfoAsync();

            if (info == null)
            {
                ErrorMessage = "Error loading external login information.";
                return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
            }

            // Sign in the user with this external login provider if the user already has a login.
            var result = await _signInManager.ExternalLoginSignInAsync("oidc", info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
            if (result.Succeeded)
            {
                // Store access token, token so it is included in cookie
                var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                //await _signInManager.SignInAsync(user, props, info.LoginProvider);
                // Update external authentication tokens with signInManager
                await _signInManager.UpdateExternalAuthenticationTokensAsync(info);


                _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }
            if (result.IsLockedOut)
            {
                return RedirectToPage("./Lockout");
            }
            else
            {
                // If the user does not have an account, then ask the user to create an account.
                ReturnUrl = returnUrl;
                LoginProvider = info.LoginProvider;
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
                {
                    Input = new InputModel
                    {
                        Email = info.Principal.FindFirstValue(ClaimTypes.Email)
                    };
                }
                return Page();
            }
        }

_LoginPartial.cshtml

@using Microsoft.AspNetCore.Identity

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
    <li class="nav-item">
        <a  class="nav-link text-light" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post" >
            <button  type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-light" asp-area="Identity" asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-light" asp-area="Identity" asp-page="/Account/Login">Login</a>
    </li>
}
</ul>

From the source code above: I was able to retrieve the access token I'd retrieved from the external identity server after the successful login - but I was unable to load any user information in the UI.

For instance, in the _LoginPartial , neither User.Identity.IsAuthenticated is ever true, or the default one from SignInManager.IsSignedIn(User) .They were both always false.

I looked at the code below, and...:

// Code from ExternalLogin.cshtml.cs
var info = await _signInManager.GetExternalLoginInfoAsync();

info.Principal.Identity.IsAuthenticated always returned true; I also read up these resources to try and figure out what was going on - and I've tried a lot of things, to no avail. PS: I also went to try the old MVCs way - but I just could not do any scaffolding on the mac I own, so I made an issue on GitHub : https://github.com/aspnet/Identity/issues/1452

Now, I also understand that from this issue I know that User.Identity.IsAuthenticated is for anything else external, and SignInManager is solely for asp.net's identity framework. I'm only wondering if this has anything to do with my issue?

but I was unable to load any user information in the UI.

Note that when some user is authenticated and then be redirected to the URL of

/Identity/Account/ExternalLogin?returnUrl=%2F&handler=Callback

this Identity/Account/ExternalLogin Page will sign in the user using the scheme of Identity.Application by sending a cookie in following way:

set-cookie: .AspNetCore.Identity.Application={the-cookie-here}

In other words, the signin scheme here is IdentityConstants.ApplicationScheme (ie Identity.Application )

However, you've set the default authentication scheme as Cookies :

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    ...
})

As a result, the User property within a RazorPage by default will always be authenticated with the scheme of CookieAuthenticationDefaults.AuthenticationScheme , which is different from the Identity.Application scheme. That's why your User.Identity.IsAuthenticated is always false.

To fix that issue, you could :

  1. Approach 1: Configure a forward scheme for Cookies by :
    \n.AddCookie(options =>{ \n    options.LoginPath = "/Account/Login/"; \n     \n}) \n
  2. Approach 2: use the IdentityConstants.ApplicationScheme as the default scheme
  3. Approach 3: Add [Authorize(AuthenticationSchemes="Identity.Application")] manually for page handler/page model:
    \n[Authorize(AuthenticationSchemes="Identity.Application")] \n

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM