Skip to content

Commit

Permalink
Moved logic in the Authentication Controller to a dedicated service s…
Browse files Browse the repository at this point in the history
…o that it can be reused by the Blazor user interface. Moved validations for the Authentication processing into a dedicated class, for reuse also. Ported the authentication page to Blazor using more modern design patterns, relying on DI and declarations where possible. Some modifications to Dto's affecting the Authentication Controller in Jube.App (current version). Implemented resource localisation in the Blazor user interface, although its implementation in Validations is pending. Implemented a declarative policy authentication for pages, although intend to explore ways to reference the claim directly in the Authorize attribute (e.g. [Authorize Claim(Permission,1)]) rather than relying on Policy, which depends on claims anyway. Some typo changes and nullable field refactoring implemented.
  • Loading branch information
Richard Churchman committed Aug 4, 2024
1 parent 9f073be commit 398f332
Show file tree
Hide file tree
Showing 31 changed files with 1,098 additions and 215 deletions.
199 changes: 60 additions & 139 deletions Jube.App/Controllers/Authentication/AuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@
*
* This file is part of Jube™ software.
*
* Jube™ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License
* Jube™ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* Jube™ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* Jube™ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License along with Jube™. If not,
* You should have received a copy of the GNU Affero General Public License along with Jube™. If not,
* see <https://www.gnu.org/licenses/>.
*/

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using Jube.App.Code;
using Jube.App.Dto.Authentication;
using Jube.App.Validators.Authentication;
using Jube.Data.Context;
using Jube.Data.Poco;
using Jube.Data.Repository;
using Jube.Data.Security;
using Jube.Engine.Helpers;
using Jube.Service.Dto.Authentication;
using Jube.Service.Exceptions.Authentication;
using Jube.Validations.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -33,195 +31,118 @@ namespace Jube.App.Controllers.Authentication
[AllowAnonymous]
public class AuthenticationController : Controller
{
private readonly IHttpContextAccessor _contextAccessor;
private readonly IHttpContextAccessor contextAccessor;

private readonly DbContext _dbContext;
private readonly DynamicEnvironment.DynamicEnvironment _dynamicEnvironment;
private readonly DbContext dbContext;
private readonly DynamicEnvironment.DynamicEnvironment dynamicEnvironment;
private readonly Service.Authentication.Authentication service;

public AuthenticationController(DynamicEnvironment.DynamicEnvironment dynamicEnvironment,IHttpContextAccessor contextAccessor)
public AuthenticationController(DynamicEnvironment.DynamicEnvironment dynamicEnvironment,
IHttpContextAccessor contextAccessor)
{
_dynamicEnvironment = dynamicEnvironment;
_dbContext = DataConnectionDbContext.GetDbContextDataConnection(_dynamicEnvironment.AppSettings("ConnectionString"));
_contextAccessor = contextAccessor;
this.dynamicEnvironment = dynamicEnvironment;
dbContext =
DataConnectionDbContext.GetDbContextDataConnection(this.dynamicEnvironment.AppSettings("ConnectionString"));
this.contextAccessor = contextAccessor;
service = new Service.Authentication.Authentication(dbContext);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_dbContext.Close();
_dbContext.Dispose();
dbContext.Close();
dbContext.Dispose();
}

base.Dispose(disposing);
}

[HttpPost("ByUserNamePassword")]
[ProducesResponseType(typeof(AuthenticationResponseDto), (int) HttpStatusCode.OK)]
public ActionResult<AuthenticationResponseDto> ExhaustiveSearchInstance([FromBody] AuthenticationRequestDto model)
[ProducesResponseType(typeof(AuthenticationResponseDto), (int)HttpStatusCode.OK)]
public ActionResult<AuthenticationResponseDto> ExhaustiveSearchInstance(
[FromBody] AuthenticationRequestDto model)
{
var userRegistryRepository = new UserRegistryRepository(_dbContext);
var validator = new AuthenticationRequestDtoValidator();

var results = validator.Validate(model);

if (!results.IsValid) return BadRequest();

var userLogin = new UserLogin
{
RemoteIp = _contextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(),
LocalIp = _contextAccessor.HttpContext?.Connection.LocalIpAddress?.ToString()
};

var userRegistry = userRegistryRepository.GetByUserName(model.UserName);

if (userRegistry == null)
try
{
LogLoginFailed(userLogin,model.UserName,1);
return Unauthorized();
}
model.UserAgent = contextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
model.LocalIp = contextAccessor.HttpContext?.Connection.LocalIpAddress?.ToString();
model.UserAgent = Request.Headers.UserAgent.ToString();

if (userRegistry.Active != 1)
{
LogLoginFailed(userLogin,userRegistry.Name,2);
return Unauthorized();
service.AuthenticateByUserNamePassword(model, dynamicEnvironment.AppSettings("PasswordHashingKey"));
}

if (userRegistry.PasswordLocked == 1)
catch (PasswordExpiredException)
{
LogLoginFailed(userLogin,userRegistry.Name,3);
return Unauthorized();
return Forbid();
}

if (!userRegistry.PasswordExpiryDate.HasValue
|| string.IsNullOrEmpty(userRegistry.Password)
|| !userRegistry.PasswordCreatedDate.HasValue)
catch (PasswordNewMustChangeException)
{
LogLoginFailed(userLogin,userRegistry.Name,4);
return Unauthorized();
return Forbid();
}

if (!HashPassword.Verify(userRegistry.Password,
model.Password,
_dynamicEnvironment.AppSettings("PasswordHashingKey")))
catch (Exception)
{
userRegistryRepository.IncrementFailedPassword(userRegistry.Id);

if (userRegistry.FailedPasswordCount > 8)
{
userRegistryRepository.SetLocked(userRegistry.Id);
}

LogLoginFailed(userLogin,userRegistry.Name,5);

return Unauthorized();
}

if (!string.IsNullOrEmpty(model.NewPassword))
{
var hashedPassword = HashPassword.GenerateHash(model.NewPassword,
_dynamicEnvironment.AppSettings("PasswordHashingKey"));

userRegistryRepository.SetPassword(userRegistry.Id,hashedPassword,DateTime.Now.AddDays(90));
}
else
{
if (!(DateTime.Now <= userRegistry.PasswordExpiryDate.Value)) return Forbid();
}

var authenticationDto = SetAuthenticationCookie(model);
return Ok(authenticationDto);
}

private AuthenticationResponseDto SetAuthenticationCookie(AuthenticationRequestDto model)
{
var token = Jwt.CreateToken(model.UserName,
_dynamicEnvironment.AppSettings("JWTKey"),
_dynamicEnvironment.AppSettings("JWTValidIssuer"),
_dynamicEnvironment.AppSettings("JWTValidAudience")
dynamicEnvironment.AppSettings("JWTKey"),
dynamicEnvironment.AppSettings("JWTValidIssuer"),
dynamicEnvironment.AppSettings("JWTValidAudience")
);

var expiration = DateTime.Now.AddMinutes(15);

var authenticationDto = new AuthenticationResponseDto
{
Token = new JwtSecurityTokenHandler().WriteToken(token),
Expiration = expiration
};

var cookieOptions = new CookieOptions
{
Expires = expiration,
HttpOnly = true
};

Response.Cookies.Append("authentication",
authenticationDto.Token,cookieOptions);

LogLoginSuccess(userLogin,userRegistry.Name);

if (userRegistry.FailedPasswordCount > 0)
{
userRegistryRepository.ResetFailedPasswordCount(userRegistry.Id);
}

return Ok(authenticationDto);
Response.Cookies.Append("authentication",
authenticationDto.Token, cookieOptions);
return authenticationDto;
}

[Authorize]
[HttpPost("ChangePassword")]
[ProducesResponseType(typeof(AuthenticationResponseDto), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(AuthenticationResponseDto), (int)HttpStatusCode.OK)]
public ActionResult ChangePassword([FromBody] ChangePasswordRequestDto model)
{
if (User.Identity == null) return Ok();
var userRegistryRepository = new UserRegistryRepository(_dbContext,User.Identity.Name);
var validator = new ChangePasswordRequestDtoValidator();

var validator = new ChangePasswordRequestDtoValidator();

var results = validator.Validate(model);

if (!results.IsValid) return BadRequest();

var userRegistry = userRegistryRepository.GetByUserName(User.Identity.Name);

if (!HashPassword.Verify(userRegistry.Password,
model.Password,
_dynamicEnvironment.AppSettings("PasswordHashingKey")))

try
{
service.ChangePassword(User.Identity.Name, model,
dynamicEnvironment.AppSettings("PasswordHashingKey"));
}
catch (BadCredentialsException)
{
return Unauthorized();
}

var hashedPassword = HashPassword.GenerateHash(model.NewPassword,
_dynamicEnvironment.AppSettings("PasswordHashingKey"));

userRegistryRepository.SetPassword(userRegistry.Id,hashedPassword,DateTime.Now.AddDays(90));

return Ok();
}

private void LogLoginFailed(UserLogin userLogin,string createdUser,int failureTypeId)
{
var userLoginRepository = new UserLoginRepository(_dbContext,createdUser);
userLogin.Failed = 1;
userLogin.FailureTypeId = failureTypeId;

if (_contextAccessor.HttpContext?.Connection.RemoteIpAddress != null)
userLogin.LocalIp = _contextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();

if (_contextAccessor.HttpContext?.Connection.LocalIpAddress != null)
userLogin.LocalIp = _contextAccessor.HttpContext.Connection.LocalIpAddress.ToString();

userLogin.UserAgent = Request.Headers["User-Agent"].ToString();

userLoginRepository.Insert(userLogin);
}

private void LogLoginSuccess(UserLogin userLogin,string createdUser)
{
var userLoginRepository = new UserLoginRepository(_dbContext,createdUser);
userLogin.Failed = 0;

if (_contextAccessor.HttpContext?.Connection.RemoteIpAddress != null)
userLogin.LocalIp = _contextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();

if (_contextAccessor.HttpContext?.Connection.LocalIpAddress != null)
userLogin.LocalIp = _contextAccessor.HttpContext.Connection.LocalIpAddress.ToString();

userLogin.UserAgent = Request.Headers["User-Agent"].ToString();

userLoginRepository.Insert(userLogin);
return Ok();
}
}
}
2 changes: 2 additions & 0 deletions Jube.App/Jube.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
<ProjectReference Include="..\Jube.Engine\Jube.Engine.csproj"/>
<ProjectReference Include="..\Jube.Migrations\Jube.Migrations.csproj"/>
<ProjectReference Include="..\Jube.Parser\Jube.Parser.csproj"/>
<ProjectReference Include="..\Jube.Service\Jube.Service.csproj" />
<ProjectReference Include="..\Jube.Validations\Jube.Validations.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Jube.App/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ public void ConfigureServices(IServiceCollection services)

Console.WriteLine(@"Copyright (C) 2022-present Jube Holdings Limited.");
Console.WriteLine(@"");
Console.WriteLine(@"This software is Jube. Welcome.");
Console.WriteLine(@"This software is Jube. Welcome.");
Console.WriteLine(@"");
Console.Write(
@"Jube™ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.");
Expand Down
4 changes: 3 additions & 1 deletion Jube.App/wwwroot/js/Account/Authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ $( document ).ready(function() {
let data = {
userName: $("#UserName").val(),
password: $("#Password").val(),
newPassword: $("#NewPassword").val()
newPassword: $("#NewPassword").val(),
repeatNewPassword: $("#VerifyNewPassword").val(),
PasswordChangeState: isChange
};

$.ajax({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@
* see <https://www.gnu.org/licenses/>.
*/

namespace Jube.App.Dto.Authentication
using Jube.Data.Context;
using LinqToDB.Configuration;

namespace Jube.Blazor.Components.Code.Helpers;

public static class DataConnectionDbContext
{
public class AuthenticationRequestDto
public static DbContext GetDbContextDataConnection(string connectionString)
{
public string UserName { get; set; }
public string Password { get; set; }
public string NewPassword { get; set; }
var builder = new LinqToDbConnectionOptionsBuilder();
builder.UsePostgreSQL(connectionString);
var connection = builder.Build<DbContext>();
return new DbContext(connection);
}
}
15 changes: 14 additions & 1 deletion Jube.Blazor/Components/Code/NavigationManagerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
/* Copyright (C) 2022-present Jube Holdings Limited.
*
* This file is part of Jube™ software.
*
* Jube™ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* Jube™ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License along with Jube™. If not,
* see <https://www.gnu.org/licenses/>.
*/

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;

namespace Jube.Blazor.Components.Code;

public static class NavigationManagerExtensions
{
public static bool TryGetQueryString<T>(this NavigationManager navManager, string key, out T value)
public static bool TryGetQueryString<T>(this NavigationManager navManager, string key, out T? value)
{
var uri = navManager.ToAbsoluteUri(navManager.Uri);

Expand Down
2 changes: 2 additions & 0 deletions Jube.Blazor/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@inherits LayoutComponentBase
@inject IStringLocalizer<Resources> Localizer

<RadzenComponents @rendermode="InteractiveServer" />
<RadzenLayout>
<RadzenHeader>
Expand Down
Loading

0 comments on commit 398f332

Please sign in to comment.