Autentisering och behörighet i ASP.NET Core

I det här inlägget beskrivs hur du kan lägga till autentisering och behörigheter för din hemsida i ASP.NET Core. Vi använder ett mellanprogram för att skydda vissa delar av vår webbplats genom att kräva att användare måste vara inloggade med användarnamn och lösenord, åtkomsten till vissa metoder kommer också att begränsas till användare med vissa roller (behörighet).

Autentisering är en process för att bekräfta en användares identitet, ett inloggningsformulär används vanligen på webbplatser för att matcha en användare och en session. Auktorisation är en process för att ge tillgång till resurser beroende på användarrättigheter.

Mellanprogram

Vi använder ett mellanprogram för att hantera autentisering och auktorisation för olika delar av webbplatsen. Vi har ett api som kan nås av medlemmar, ett administratörsområde som kan nås av administratörer och ett medlemsområde som kan nås av medlemmar. Information om en användare sparas som ClaimsPrincipal claimsPrincipal i användarkontexten, detta innebär att det blir enkelt att ange behörighetsattribut för metoder och enkelt att hämta information om den inloggade användaren i metoder. Information om en inloggad användare läggs också till på delar av webbplatsen som är allmän, den här informationen kan vara användbar för att anpassa innehållet till olika användare.

public class AuthorizationMiddleware
{
    #region Variables

    private readonly RequestDelegate next;

    #endregion

    #region Constructors

    public AuthorizationMiddleware(RequestDelegate next)
    {
        // Set values for instance variables
        this.next = next;

    } // End of the constructor

    #endregion

    #region Methods

    public async Task Invoke(HttpContext context, IAdministratorRepository administrator_repository, IMemberRepository member_repository)
    {
        // Get the url path
        string path = context.Request.Path;

        // Respond to different paths
        if (path.StartsWith("/api") == true)
        {
            // Get the authorization header
            string authHeader = context.Request.Headers["Authorization"];

            // Make sure that there is a authorization header
            if (authHeader != null && authHeader.StartsWith("Basic"))
            {
                // Get tokens
                string authToken = authHeader.Substring("Basic ".Length).Trim();
                string decodedToken = Encoding.UTF8.GetString(Convert.FromBase64String(authToken));

                // Get the separator index
                Int32 seperatorIndex = decodedToken.IndexOf(":");

                // Get the username and password
                string email = decodedToken.Substring(0, seperatorIndex);
                string password = decodedToken.Substring(seperatorIndex + 1);

                // Get a member, username must be unique
                ModelItem<MemberDocument> api_user_model = await member_repository.GetApiUser(email, password);

                // Check for correct credentials
                if (api_user_model.item != null)
                {
                    // Create claims
                    ClaimsIdentity claimsIdentity = new ClaimsIdentity("ApiUser");
                    claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, api_user_model.item.id.ToString())); // Important for Antiforgery Ajax post
                    claimsIdentity.AddClaim(new Claim("member", JsonConvert.SerializeObject(api_user_model.item)));
                    //claimsIdentity.AddClaim(new Claim("etag", api_user_model.etag));
                    ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                    context.User = claimsPrincipal;
                }
                else
                {
                    // Unauthorized 401
                    byte[] unauthorized = Encoding.UTF8.GetBytes("Not authorized (401)");
                    context.Response.StatusCode = 401;
                    context.Response.Body.Write(unauthorized, 0, unauthorized.Length);
                    var statusCodePagesFeature = context.Features.Get<IStatusCodePagesFeature>();
                    if (statusCodePagesFeature != null)
                    {
                        statusCodePagesFeature.Enabled = false;
                    }
                    return;
                }
            }
            else
            {
                // Unauthorized 401
                byte[] unauthorized = Encoding.UTF8.GetBytes("Not authorized (401)");
                context.Response.StatusCode = 401;
                context.Response.Body.Write(unauthorized, 0, unauthorized.Length);
                var statusCodePagesFeature = context.Features.Get<IStatusCodePagesFeature>();
                if (statusCodePagesFeature != null)
                {
                    statusCodePagesFeature.Enabled = false;
                }
                return;
            }
        }
        else if (path.StartsWith("/member") == true)
        {
            // Get the signed in member
            ModelItem<MemberDocument> member_model = await member_repository.GetSignedInMember(context);

            if (member_model.item == null)
            {
                // Redirect the user to the log in page
                context.Response.Redirect("/auth/log_in");
                return;
            }
            else if(member_model.item.verified == 0)
            {
                // Redirect the user to the verify email page
                context.Response.Redirect("/auth/verify_email");
                return;
            }
            else
            {
                // Create claims
                ClaimsIdentity claimsIdentity = new ClaimsIdentity("User");
                claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, member_model.item.id.ToString())); // Important for Antiforgery Ajax post
                claimsIdentity.AddClaim(new Claim("member", JsonConvert.SerializeObject(member_model.item)));
                ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                context.User = claimsPrincipal;
            }
        }
        else if (path.StartsWith("/admin_") == true && path.StartsWith("/admin_login") == false)
        {
            // Get the signed in administrator
            ModelItem<AdministratorDocument> administrator_model = await administrator_repository.GetSignedInAdministrator(context);

            if (administrator_model.item == null)
            {
                // Redirect the user to the login page
                context.Response.Redirect("/admin_login");
                return;
            }
            else
            {
                // Create claims
                ClaimsIdentity claimsIdentity = new ClaimsIdentity("Administrator");
                claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, administrator_model.item.id.ToString())); // Important for Antiforgery Ajax post
                claimsIdentity.AddClaim(new Claim("administrator", JsonConvert.SerializeObject(administrator_model.item)));
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, administrator_model.item.admin_role));
                ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                context.User = claimsPrincipal;
            }
        }
        else
        {
            // Get the signed in member
            ModelItem<MemberDocument> member_model = await member_repository.GetSignedInMember(context);

            if(member_model.item != null)
            {
                // Create claims
                ClaimsIdentity claimsIdentity = new ClaimsIdentity("User");
                claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, member_model.item.id.ToString())); // Important for Antiforgery Ajax post
                claimsIdentity.AddClaim(new Claim("member", JsonConvert.SerializeObject(member_model.item)));
                ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                context.User = claimsPrincipal;
            }   
        }

        // Call the next delegate/middleware in the pipeline
        await next(context);

    } // End of the Invoke method

    #endregion

} // End of the class

Hämta inloggad medlem

Vi har en metod för att hämta en inloggad medlem som används i vårt mellanprogram, den här metoden returnerar den inloggade medlemmen och ökar cookiens utgångsdatum för att få en glidande utgångstid.

public async Task<ModelItem<MemberDocument>> GetSignedInMember(HttpContext context)
{
    // Create the model to return
    ModelItem<MemberDocument> model = new ModelItem<MemberDocument>();

    // Get the cookie
    string cookie = context.Request.Cookies["Member"];

    if (cookie != null)
    {
        model = await GetById(this.data_protector.Unprotect(cookie));

        // Renew the cookie
        CookieOptions options = new CookieOptions();
        options.Expires = DateTime.UtcNow.AddDays(1);
        options.HttpOnly = true;
        options.SameSite = SameSiteMode.Lax;
        context.Response.Cookies.Append("Member", cookie, options);
    }

    // Return the model
    return model;

} // End of the GetSignedInMember method

Tjänster

Vi måste säga till ASP.NET Core att använda vårt mellanprogram och vi lägger därför till följande innehåll i metoden Configure i klassen StartUp.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Use authorization middleware
    app.UseMiddleware<AuthorizationMiddleware>();

} // End of the Configure method

Inloggning och utloggning

Vi har ett formulär där medlemmar kan logga in och vi använder cookies för att lagra information om medlemmar som är inloggade. Inloggningsformuläret skickas till metoden log_in i auth-controllern. Vi validerar lösenordet som har angivits av personen som vill logga in, lösenordet är hashat i databasen.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> log_in(IFormCollection collection)
{
    // Get the current web domain
    WebDomain current_domain = await this.web_domain_repository.GetCurrentDomain(ControllerContext.HttpContext);

    // Get translated texts
    KeyStringList tt = await this.static_text_repository.GetFromCache(current_domain.front_end_language_code);

    // Get the email and the password
    string email = collection["txtEmail"].ToString();
    string password = collection["txtPassword"].ToString();

    // Get the member
    ModelItem<MemberDocument> member_model = await this.member_repository.GetByEmail(email);
    MemberDocument post = member_model.item;

    // Check if the member exists and if the password is correct
    if (post != null && await this.member_repository.ValidatePassword(post, password) == true)
    {
        // Create a member cookie
        CookieOptions options = new CookieOptions();
        options.Expires = DateTime.UtcNow.AddDays(1);
        options.HttpOnly = true;
        options.SameSite = SameSiteMode.Lax;
        ControllerContext.HttpContext.Response.Cookies.Append("Member", this.data_protector.Protect(post.id.ToString()), options);

        // Return success data
        return Json(data: new ResponseData(true, "/member", ""));
    }
    else
    {
        // Return error data
        return Json(data: new ResponseData(false, "", tt.Get("error_log_in")));
    }

} // End of the log_in method

För att logga ut en användare behöver vi bara ändra cookieens utgångsdatum till ett datum före dagens datum och tid.

[HttpGet]
public IActionResult log_out()
{
    // Delete the member cookie
    CookieOptions options = new CookieOptions();
    options.Expires = DateTime.UtcNow.AddDays(-1);
    options.HttpOnly = true;
    ControllerContext.HttpContext.Response.Cookies.Append("Member", "", options);

    // Redirect the user to the login page
    return Redirect("/auth/log_in");

} // End of the log_out method

Erhåll användarkontext

Vi kan enkelt få information om en inloggad användare i våra controllers och vyer eftersom vi sparade den här informationen i vårt mellanprogram.

// Get a member
Claim claim = ControllerContext.HttpContext.User.FindFirst("member");
MemberDocument member = JsonConvert.DeserializeObject<MemberDocument>(claim.Value);

Behörighetsattribut

Vi kan lägga till ett behörighetsattribut till controllers eller metoder i vårt projekt för att begränsa åtkomsten till användare med vissa roller. Vi lade till en roll för en administratör i mellanprogrammet och den här rollen matchas mot de roller som läggs till i attributet. En administratör behöver en av de roller som anges i attributet för att kunna komma åt en controller eller en metod. En användare behöver en roll som Administrator eller Editor för att kunna komma åt metoden nedan.

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Administrator,Editor")]
public async Task<IActionResult> index(IFormCollection collection)
{
    // Code ...

} // End of the index method

Lämna ett svar

E-postadressen publiceras inte. Obligatoriska fält är märkta *