WoD
WoD

Reputation: 13

Blazor-Server side authentication with Cookie

I am trying to implement on a Blazor-Server side application a simple login against LDAP server and use cookie to store user claims. I have the MainLayout set to Authorized, if the user is not authenticated it will be re-direct to Login page. I have already tested the LDAP connection and it works properly, the problem is no matter what I do the cookie doesn't get created in the browser. When I run the POST command I see the HttpStatusCode.OK but the cookie it's not created and the browser re-direct again to login page of course.

Can someone please tell me what am I doing wrong? My code:

Startup.cs

    public void ConfigureServices(IServiceCollection services)
    { 
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddControllersWithViews().AddRazorRuntimeCompilation();
      services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

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

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });
    }

AuthenticationController.cs

    [ApiController]
public class AuthenticationController : Controller
{
    [HttpPost]
    [Route("authentication/login")]
    public async Task<ActionResult> Login([FromBody]UserCredentials credentials)
    {
        string path = "LDAP://serveraddress.xxx";
        try
        {
            using DirectoryEntry entry = new(path, credentials.Username, credentials.Password);
            using DirectorySearcher searcher = new(entry);
            searcher.Filter = $"(&(objectclass=user)(objectcategory=person)(samaccountname={credentials.Username}))";
            var result = searcher.FindOne();
            if (result != null)
            {
                List<Claim> claims = new();                 
                claims.Add(new Claim(ClaimTypes.Name, credentials.Username));

                //Get Groups
                ResultPropertyCollection fields = result.Properties;
                foreach (var group in result.Properties["memberof"])
                {
                    var distinguishedName = new X500DistinguishedName(group.ToString());
                    var commonNameData = new AsnEncodedData("CN", distinguishedName.RawData);
                    var commonName = commonNameData.Format(false);

                    if (!string.IsNullOrEmpty(commonName))
                    {
                        claims.Add(new Claim(ClaimTypes.Role, commonName));
                    }
                }
                //Get Emails
                foreach (var email in result.Properties["mail"])
                {
                    claims.Add(new Claim(ClaimTypes.Email, email.ToString()));
                }

                ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme);

                AuthenticationProperties authProperties = new()
                {
                    AllowRefresh = true,
                    IssuedUtc = DateTime.Now,
                    ExpiresUtc = DateTimeOffset.Now.AddDays(1),
                    IsPersistent = true,
                    
                };

                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
                return Ok();
            }
            else
            {
                return NotFound("User Not Found!");
            }
        }
        catch (Exception)
        {
            return NotFound("Login credentials is incorrect!");
        }
    }

    [HttpPost]
    [Route("authentication/logout")]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return Ok();
    }
}

Login.razor

@page "/login"
@page "/login/{ErrorMessage}"
@layout CenteredBlockLayout
@attribute [AllowAnonymous]

<MudPaper Elevation="25" Class="pa-8" Width="100%" MaxWidth="500px">
    <MudItem><img src="/images/logo.svg" alt="Logo" style="width:400px; height:50px;" /></MudItem>
    <MudText Typo="Typo.h4" GutterBottom="true">Sign In</MudText>
    <MudTextField @bind-Value="@Username" T="string" Label="Username"/>
    <MudTextField @bind-Value="@Password" T="string" Label="Password"/>
    <MudButton OnClick="(() => PerformLoginAsync())">Sign In</MudButton>
</MudPaper>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
    <MudAlert Severity="Severity.Error">@ErrorMessage</MudAlert>
}

Login.razor.cs

public partial class Login
    {   
        public string Username { get; set; }    
        public string Password { get; set; }

        [Parameter]
        public string ErrorMessage { get; set; }

        [Inject]
        HttpClient Client { get; set; }

        [Inject]
        private NavigationManager NavMan { get; set; }
  
        private async Task PerformLoginAsync()
        {
            if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password))
            {
                UserCredentials cred = new UserCredentials
                {
                    Username = Username,
                    Password = Password
                };

                var serialized = JsonConvert.SerializeObject(cred);
                var stringContent = new StringContent(serialized, Encoding.UTF8, "application/json");

                using var result = await Client.PostAsync($"NavMan.BaseUri}authentication/login", stringContent);
                if (result.StatusCode == System.Net.HttpStatusCode.OK)
                {                       
                    NavMan.NavigateTo("/", true);
                }
                else
                {
                    ErrorMessage = await result.Content.ReadAsStringAsync();
                }                 
            }
        }
    }

Upvotes: 1

Views: 1218

Answers (1)

Dumas.DED
Dumas.DED

Reputation: 626

I believe you need to append the cookie to the response. I haven't tested this with your code but it should work something like this:

HttpContext.Response.Cookies.Append("my_cookie", claimsString, new CookieOptions()
{
    Domain = "mydomain.com",
    SameSite = SameSiteMode.Lax,
    Secure = true,
    Path = "/",
    Expires = DateTime.UtcNow.AddDays(1)
}

(These cookie options are just an example, of course. Tailor them to your specific needs.)

Keep in mind that you'll need to convert your claims to a string so that you can store it as the value in a cookie. In our case we store claims in a JWT, so that's what gets stored in the cookie. Here's how I do it:

public string CreateJWT(HttpContext httpContext, User user)
{
    var handler = new JwtSecurityTokenHandler();

    var descriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new Claim[] {
            new Claim(ClaimTypes.GivenName, user.FirstName),
            new Claim(ClaimTypes.Surname, user.LastName),
            new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
            new Claim(ClaimTypes.Email, user.Email),
        }),
        Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins),
        Issuer = Config.Issuer,
        Audience = Config.Audience,
        SigningCredentials = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256)
    };

    var token = handler.CreateJwtSecurityToken(descriptor);
    var accessToken = handler.WriteToken(token);

    httpContext.Response.Cookies.Append("my_cookie", accessToken, new CookieOptions()
    {
        Domain = Config.CookieDomain,
        SameSite = SameSiteMode.Lax,
        Secure = true,
        Path = "/",
        Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins)
    });

    return accessToken;
}

As for parsing the JWT, I'm sure there are a number of ways to go about it. The one that worked for me was this one.

Upvotes: 0

Related Questions