Skip to content

Commit

Permalink
Adding JWT Auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Webreaper committed Aug 29, 2022
1 parent ed7cc24 commit a66c3f4
Show file tree
Hide file tree
Showing 15 changed files with 513 additions and 15 deletions.
Binary file modified .DS_Store
Binary file not shown.
54 changes: 54 additions & 0 deletions Damselfly.Core.DbModels/Models/LoginModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;

namespace Damselfly.Core.Models;

public class LoginResult
{
public bool Successful { get; set; }
public string Error { get; set; }
public string Token { get; set; }
}


public class LoginModel
{
[Required]
public string Email { get; set; }

[Required]
public string Password { get; set; }

public bool RememberMe { get; set; }
}

public class RegisterModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }

[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }

[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}

public class RegisterResult
{
public bool Successful { get; set; }
public IEnumerable<string> Errors { get; set; }
}

public class UserModel
{
public string Email { get; set; }
public bool IsAuthenticated { get; set; }
}
13 changes: 11 additions & 2 deletions Damselfly.Core.ScopedServices/Client Services/RestClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -31,6 +32,14 @@ public static void SetJsonOptions(JsonSerializerOptions opts)
opts.PropertyNameCaseInsensitive = true;
}

public JsonSerializerOptions JsonOptions { get { return jsonOptions; } }

public AuthenticationHeaderValue AuthHeader
{
get { return _restClient.DefaultRequestHeaders.Authorization; }
set { _restClient.DefaultRequestHeaders.Authorization = value; }
}

private readonly JsonSerializerOptions jsonOptions;
private readonly HttpClient _restClient;
private readonly ILogger<RestClient> _logger;
Expand Down Expand Up @@ -60,9 +69,9 @@ public RestClient( HttpClient client, ILogger<RestClient> logger )
}
}

public async Task CustomPostAsJsonAsync<PostObj>(string? requestUri, PostObj obj)
public async Task<HttpResponseMessage> CustomPostAsJsonAsync<PostObj>(string? requestUri, PostObj obj)
{
await _restClient.PostAsJsonAsync<PostObj>(requestUri, obj, jsonOptions);
return await _restClient.PostAsJsonAsync<PostObj>(requestUri, obj, jsonOptions);
}

public async Task<RetObj?> CustomPostAsJsonAsync<PostObj, RetObj>(string? requestUri, PostObj obj)
Expand Down
97 changes: 97 additions & 0 deletions Damselfly.Web.Client/ApiAuthenticationStateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;

using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Damselfly.Core.ScopedServices.ClientServices;

namespace ChrisSaintyExample.Client;

public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly RestClient _httpClient;
private readonly ILocalStorageService _localStorage;

public ApiAuthenticationStateProvider(RestClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorage.GetItemAsync<string>("authToken");

if (string.IsNullOrWhiteSpace(savedToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}

_httpClient.AuthHeader = new AuthenticationHeaderValue("bearer", savedToken);

return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
}

public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}

public void MarkUserAsLoggedOut()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}

private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);

keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);

if (roles != null)
{
if (roles.ToString().Trim().StartsWith("["))
{
var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());

foreach (var parsedRole in parsedRoles)
{
claims.Add(new Claim(ClaimTypes.Role, parsedRole));
}
}
else
{
claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
}

keyValuePairs.Remove(ClaimTypes.Role);
}

claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));

return claims;
}

private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
2 changes: 2 additions & 0 deletions Damselfly.Web.Client/Damselfly.Web.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<PackageReference Include="Syncfusion.Blazor.Maps" Version="19.4.0.55" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.0-preview.7.22376.6" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -59,5 +60,6 @@
<None Remove="Syncfusion.Blazor.Maps" />
<None Remove="Microsoft.AspNetCore.SignalR.Client" />
<None Remove="Serilog" />
<None Remove="Blazored.LocalStorage" />
</ItemGroup>
</Project>
61 changes: 61 additions & 0 deletions Damselfly.Web.Client/Pages/Login.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@page "/login"
@inject IAuthService AuthService
@inject NavigationManager NavigationManager

@using ChrisSaintyExample.Client.Services;

<h1>Login</h1>

@if (ShowErrors)
{
<div class="alert alert-danger" role="alert">
<p>@Error</p>
</div>
}

<div class="card">
<div class="card-body">
<h5 class="card-title">Please enter your details</h5>
<EditForm Model="loginModel" OnValidSubmit="HandleLogin">
<DataAnnotationsValidator />
<ValidationSummary />

<div class="form-group">
<label for="email">Email address</label>
<InputText Id="email" Class="form-control" @bind-Value="loginModel.Email" />
<ValidationMessage For="@(() => loginModel.Email)" />
</div>
<div class="form-group">
<label for="password">Password</label>
<InputText Id="password" type="password" Class="form-control" @bind-Value="loginModel.Password" />
<ValidationMessage For="@(() => loginModel.Password)" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
</div>
</div>

@code {

private LoginModel loginModel = new LoginModel();
private bool ShowErrors;
private string Error = "";

private async Task HandleLogin()
{
ShowErrors = false;

var result = await AuthService.Login(loginModel);

if (result.Successful)
{
NavigationManager.NavigateTo("/");
}
else
{
Error = result.Error;
ShowErrors = true;
}
}

}
15 changes: 15 additions & 0 deletions Damselfly.Web.Client/Pages/Logout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@page "/logout"
@inject IAuthService AuthService
@inject NavigationManager NavigationManager
@using ChrisSaintyExample.Client.Services;


@code {

protected override async Task OnInitializedAsync()
{
await AuthService.Logout();
NavigationManager.NavigateTo("/");
}

}
68 changes: 68 additions & 0 deletions Damselfly.Web.Client/Pages/Register.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@page "/register"
@inject IAuthService AuthService
@inject NavigationManager NavigationManager
@using ChrisSaintyExample.Client.Services;

<h1>Register</h1>

@if (ShowErrors)
{
<div class="alert alert-danger" role="alert">
@foreach (var error in Errors)
{
<p>@error</p>
}
</div>
}

<div class="card">
<div class="card-body">
<h5 class="card-title">Please enter your details</h5>
<EditForm Model="RegisterModel" OnValidSubmit="HandleRegistration">
<DataAnnotationsValidator />
<ValidationSummary />

<div class="form-group">
<label for="email">Email address</label>
<InputText Id="email" class="form-control" @bind-Value="RegisterModel.Email" />
<ValidationMessage For="@(() => RegisterModel.Email)" />
</div>
<div class="form-group">
<label for="password">Password</label>
<InputText Id="password" type="password" class="form-control" @bind-Value="RegisterModel.Password" />
<ValidationMessage For="@(() => RegisterModel.Password)" />
</div>
<div class="form-group">
<label for="password">Confirm Password</label>
<InputText Id="password" type="password" class="form-control" @bind-Value="RegisterModel.ConfirmPassword" />
<ValidationMessage For="@(() => RegisterModel.ConfirmPassword)" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
</div>
</div>

@code {

private RegisterModel RegisterModel = new RegisterModel();
private bool ShowErrors;
private IEnumerable<string> Errors;

private async Task HandleRegistration()
{
ShowErrors = false;

var result = await AuthService.Register(RegisterModel);

if (result.Successful)
{
NavigationManager.NavigateTo("/login");
}
else
{
Errors = result.Errors;
ShowErrors = true;
}
}

}
9 changes: 8 additions & 1 deletion Damselfly.Web.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
using Syncfusion.Blazor;
using Microsoft.Extensions.Options;
using Syncfusion.Licensing;
using Blazored.LocalStorage;
using ChrisSaintyExample.Client;
using ChrisSaintyExample.Client.Services;
using Microsoft.AspNetCore.Components.Authorization;

namespace Damselfly.Web.Client;

Expand All @@ -35,15 +39,18 @@ public static async Task Main(string[] args)
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("DamselflyAPI"));
builder.Services.AddMemoryCache(x => x.SizeLimit = 500);

builder.Services.AddApiAuthorization();
builder.Services.AddAuthorizationCore(config => config.SetupPolicies(builder.Services));

builder.Services.AddMudServices();
builder.Services.AddSyncfusionBlazor(options => { options.IgnoreScriptIsolation = true; });
builder.Services.AddBlazoredLocalStorage();

builder.Services.AddScoped<ContextMenuService>();
builder.Services.AddSingleton<RestClient>();

builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>();
builder.Services.AddScoped<IAuthService, AuthService>();

builder.Services.AddDamselflyUIServices();

await builder.Build().RunAsync();
Expand Down
Loading

0 comments on commit a66c3f4

Please sign in to comment.