This commit is contained in:
2024-01-23 15:41:59 +01:00
Unverified
parent 5d5a69ccf7
commit 3b2b4c9b7e
76 changed files with 4100 additions and 888 deletions

View File

@@ -36,7 +36,7 @@ namespace SecureBank.API.Authentication
#region METHODS
public string GenerateToken(Guid tokenId, int accountId, bool oneTimeToken = false)
public string GenerateToken(Guid tokenId, Account account, bool oneTimeToken = false)
{
DateTime expirationTime = DateTime.UtcNow.AddMinutes(_configuration.TokenLifetime);
@@ -44,11 +44,13 @@ namespace SecureBank.API.Authentication
{
Subject = new ClaimsIdentity(new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, tokenId.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, accountId.ToString()),
new Claim(JwtRegisteredClaimNames.Exp, expirationTime.ToString()),
new Claim("jti", tokenId.ToString()),
new Claim("uid", account.Id.ToString()),
new Claim("first_name", account.FirstName),
new Claim("last_name", account.LastName),
new Claim("exp", expirationTime.ToString()),
new Claim("one_time_token", oneTimeToken.ToString()),
new Claim("admin", "false"), //TODO: w zależności od użytkownika
new Claim("admin", account.IsAdmin.ToString()),
}),
Expires = expirationTime,
Issuer = _configuration.TokenIssuer,

View File

@@ -1,12 +1,18 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Identity.Client;
using SecureBank.API.Authentication;
using SecureBank.API.Services;
using SecureBank.Authentication;
using SecureBank.Common;
using SecureBank.Common.Accounts;
using SecureBank.Helpers.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
@@ -40,18 +46,17 @@ namespace SecureBank.API.Controllers
[HttpPost]
[Route("create-account")]
[AllowAnonymous]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[RequiresClaim("admin", "True")]
public async Task<ActionResult<APIResponse<int>>> CreateAccount([FromBody] CreateAccountRequest data)
{
APIResponse<int> response = await _accountsService.CreateAccount(data);
if (response.Success)
return response.Status switch
{
return Ok(response);
}
else
{
return BadRequest(response);
}
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpGet]
@@ -60,35 +65,102 @@ namespace SecureBank.API.Controllers
public async Task<ActionResult<APIResponse<GetPasswordVariantResponse>>> GetPasswordVariant([FromRoute(Name = "account_id")] int accountId)
{
APIResponse<GetPasswordVariantResponse> response = await _accountsService.GetPasswordVariant(accountId);
if (response.Success)
return response.Status switch
{
return Ok(response);
}
else
{
return BadRequest(response);
}
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpPost]
[Route("{account_id}/authentication")]
[Route("authentication")]
[AllowAnonymous]
/*
* Action codes:
* 1 - Go back to client code input
* 2 - Failed login count increment
* 2 - Change password required
*/
public async Task<ActionResult<APIResponse<string>>> Authentication([FromRoute(Name = "account_id")] int accountId, [FromBody] AuthenticationRequest data)
public async Task<ActionResult<APIResponse<string>>> Authentication([FromBody] AuthenticationRequest data)
{
APIResponse<string> response = await _accountsService.Authentication(accountId, data);
if (response.Success)
APIResponse<string> response = await _accountsService.Authentication(data);
return response.Status switch
{
return Ok(response);
}
else
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpPost]
[Route("authentication-refresh")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse<string>>> AuthenticationRefresh()
{
APIResponse<string> response = await _accountsService.AuthenticationRefresh(new Claims(User.Claims));
return response.Status switch
{
return BadRequest(response);
}
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpPatch]
[Route("change-password")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse>> ChangePassword([FromBody] ChangePasswordRequest data)
{
APIResponse response = await _accountsService.ChangePassword(new Claims(User.Claims), data);
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse<IEnumerable<AccountResponse>>>> GetAccounts([FromQuery]int? id, [FromQuery] string? iban)
{
APIResponse<IEnumerable<AccountResponse>> response = await _accountsService.GetAccounts(iban, id, new Claims(User.Claims));
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpPatch]
[Route("{account_id}/reset-password")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[RequiresClaim("admin", "True")]
public async Task<ActionResult<APIResponse>> ResetPassword([FromRoute(Name = "account_id")] int accountId)
{
APIResponse response = await _accountsService.ResetPassword(accountId);
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpPatch]
[Route("{account_id}/unlock")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[RequiresClaim("admin", "True")]
public async Task<ActionResult<APIResponse>> UnlockAccount([FromRoute(Name = "account_id")] int accountId)
{
APIResponse response = await _accountsService.UnlockAccount(accountId);
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
#endregion

View File

@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SecureBank.API.Services;
using SecureBank.Authentication;
using SecureBank.Common;
using SecureBank.Helpers.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Controllers
{
[ApiController]
[Route("api/balance")]
public class BalanceController : ControllerBase
{
#region SERVICES
private IBalanceService _balanceService;
#endregion
#region CONSTRUCTORS
public BalanceController(IBalanceService balanceService)
{
_balanceService = balanceService;
}
#endregion
#region METHODS
[HttpGet]
[Route("{account_id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[RequiresClaim("admin", "True")]
public async Task<ActionResult<APIResponse<decimal>>> GetAccountBalance([FromRoute(Name = "account_id")]int accountId)
{
APIResponse<decimal> response = await _balanceService.GetAccountBalance(accountId);
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse<decimal>>> GetBalance()
{
APIResponse<decimal> response = await _balanceService.GetBalance(new Claims(User.Claims));
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
#endregion
}
}

View File

@@ -7,12 +7,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Authentication\SecureBank.Authentication.csproj" />
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\SecureBank.API.Services\SecureBank.API.Services.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,103 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SecureBank.API.Services;
using SecureBank.Authentication;
using SecureBank.Common;
using SecureBank.Common.Transfers;
using SecureBank.Helpers.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace SecureBank.API.Controllers
{
[ApiController]
[Route("api/transfers")]
public class TransfersController : ControllerBase
{
#region SERVICES
private ITransfersService _transfersService;
#endregion
#region CONSTRUCTORS
public TransfersController(ITransfersService transfersService)
{
_transfersService = transfersService;
}
#endregion
#region METHODS
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse<IEnumerable<TransferResponse>>>> GetTransfers()
{
APIResponse<IEnumerable<TransferResponse>> response = await _transfersService.GetTransfers(new Claims(User.Claims));
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpGet]
[Route("{account_id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[RequiresClaim("admin", "True")]
public async Task<ActionResult<APIResponse<IEnumerable<TransferResponse>>>> GetUserTransfers([FromRoute(Name = "account_id")]int accountId)
{
APIResponse<IEnumerable<TransferResponse>> response = await _transfersService.GetUserTransfers(accountId);
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpPost]
[Route("admin-transfer")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[RequiresClaim("admin", "True")]
public async Task<ActionResult<APIResponse>> CreateAdminTransfer([FromBody]CreateAdminTransferRequest data)
{
APIResponse response = await _transfersService.CreateAdminTransfer(data);
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
[HttpPost]
[Route("user-transfer")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult<APIResponse>> CreateUserTransfer([FromBody] CreateUserTransferRequest data)
{
APIResponse response = await _transfersService.CreateUserTransfer(data, new Claims(User.Claims));
return response.Status switch
{
ResponseStatus.Ok => Ok(response),
ResponseStatus.BadRequest => BadRequest(response),
ResponseStatus.Unauthorized => Unauthorized(response),
};
}
#endregion
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Encryption
{
public class EncryptionConfiguration
{
#region PROPERTIES
public byte[] Key { get; private set; }
public byte[] IV { get; private set; }
#endregion
#region CONSTRUCTORS
public EncryptionConfiguration(IConfiguration configuration)
{
Key = Encoding.UTF8.GetBytes(configuration.GetSection("Encryption")["Key"]);
IV = Encoding.UTF8.GetBytes(configuration.GetSection("Encryption")["IV"]);
}
#endregion
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace SecureBank.API.Encryption
{
public class EncryptionHelper
{
#region SERVICES
private EncryptionConfiguration _configuration;
#endregion
#region FIELDS
private Aes _aes;
#endregion
#region CONSTRUCTORS
public EncryptionHelper(EncryptionConfiguration configuration)
{
_configuration = configuration;
_aes = Aes.Create();
_aes.Key = _configuration.Key;
_aes.IV = _configuration.IV;
}
#endregion
#region PUBLIC METHODS
public byte[] Encrypt(string data)
{
ICryptoTransform encryptor = _aes.CreateEncryptor(_aes.Key, _aes.IV);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
{
streamWriter.Write(data);
}
return memoryStream.ToArray();
}
}
public string Decrypt(byte[] data)
{
ICryptoTransform decryptor = _aes.CreateDecryptor(_configuration.Key, _configuration.IV);
using (MemoryStream memoryStream = new MemoryStream(data))
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
byte[] outputBytes = new byte[data.Length];
int decryptedByteCount = cryptoStream.Read(outputBytes, 0, outputBytes.Length);
return Encoding.UTF8.GetString(outputBytes.Take(decryptedByteCount).ToArray());
}
}
#endregion
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.Helpers.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequiresClaimAttribute : Attribute, IAuthorizationFilter
{
#region FIELDS
private readonly string _claimName;
private readonly string _claimValue;
#endregion
#region CONSTRUCTORS
public RequiresClaimAttribute(string claimName, string claimValue)
{
_claimName = claimName;
_claimValue = claimValue;
}
#endregion
#region PUBLIC METHODS
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.User.HasClaim(_claimName, _claimValue))
{
context.Result = new ForbidResult();
}
}
#endregion
}
}

View File

@@ -6,8 +6,17 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Enums\" />
</ItemGroup>
</Project>

View File

@@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using SecureBank.API.Helpers;
using SecureBank.API.Authentication;
using SecureBank.Authentication;
using SecureBank.Common;
using SecureBank.Common.Accounts;
using SecureBank.Database;
@@ -15,6 +16,10 @@ using System.Runtime.Intrinsics.Arm;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using SecureBank.API.Encryption;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.Identity.Client;
namespace SecureBank.API.Services
{
@@ -22,7 +27,12 @@ namespace SecureBank.API.Services
{
Task<APIResponse<int>> CreateAccount(CreateAccountRequest data);
Task<APIResponse<GetPasswordVariantResponse>> GetPasswordVariant(int accountId);
Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data);
Task<APIResponse<string>> Authentication(AuthenticationRequest data);
Task<APIResponse<string>> AuthenticationRefresh(Claims claims);
Task<APIResponse> ChangePassword(Claims claims, ChangePasswordRequest data);
Task<APIResponse<IEnumerable<AccountResponse>>> GetAccounts(string? iban, int? id, Claims claims);
Task<APIResponse> ResetPassword(int accountId);
Task<APIResponse> UnlockAccount(int accountId);
}
@@ -33,6 +43,8 @@ namespace SecureBank.API.Services
private AuthenticationHelper _authenticationHelper;
private EncryptionHelper _encryptionHelper;
private DatabaseContext _database;
private ILogger<AccountsService> _logger;
@@ -43,12 +55,11 @@ namespace SecureBank.API.Services
#region CONSTRUCTORS
public AccountsService(AuthenticationHelper authenticationHelper, DatabaseContext database, ILogger<AccountsService> logger)
public AccountsService(AuthenticationHelper authenticationHelper, EncryptionHelper encryptionHelper, DatabaseContext database, ILogger<AccountsService> logger)
{
_authenticationHelper = authenticationHelper;
_encryptionHelper = encryptionHelper;
_database = database;
_logger = logger;
}
@@ -103,6 +114,31 @@ namespace SecureBank.API.Services
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.PhoneNumber)),
Message = "Phone number cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.Address)),
Message = "Address cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.PESEL)),
Message = "PESEL cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => x.PESEL.Length != 11),
Message = "PESEL must be 11 charaters long"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => string.IsNullOrWhiteSpace(x.IdCardNumber)),
Message = "Id card number cannot be empty"
},
new Check<CreateAccountRequest>
{
CheckAction = new Predicate<CreateAccountRequest>((x) => x.IdCardNumber.Length != 9),
Message = "Id card number must be 9 characters long"
},
};
foreach (Check<CreateAccountRequest> check in checks)
@@ -112,21 +148,44 @@ namespace SecureBank.API.Services
return new APIResponse<int>
{
Message = check.Message,
Success = false
Status = ResponseStatus.BadRequest,
};
}
}
byte[] pesel = _encryptionHelper.Encrypt(data.PESEL);
byte[] idCardNumber = _encryptionHelper.Encrypt(data.IdCardNumber);
byte[] cardCVV = _encryptionHelper.Encrypt(StringExtensions.CreateRandom(3, "1234567890"));
byte[] cardExpirationDate = _encryptionHelper.Encrypt(DateTime.Now.AddYears(5).ToString("MM/yy"));
Account account = new Account
{
FirstName = data.FirstName,
LastName = data.LastName,
Email = data.Email,
PhoneNumber = data.PhoneNumber.Replace(" ", string.Empty),
Address = data.Address,
PESEL = pesel,
IdCardNumber = idCardNumber,
IBAN = string.Empty,
CardNumber = new byte[0],
CardCVV = cardCVV,
CardExpirationDate = cardExpirationDate
};
await _database.Accounts.AddAsync(account);
await _database.SaveChangesAsync();
string ibanGen = $"549745{StringExtensions.CreateRandom(12, "1234567890")}{account.Id:00000000}";
string cardNumberGen = $"49{StringExtensions.CreateRandom(6, "1234567890")}{account.Id:00000000}";
byte[] cardNumber = _encryptionHelper.Encrypt(cardNumberGen);
account.IBAN = ibanGen;
account.CardNumber = cardNumber;
await _database.SaveChangesAsync();
string password = GeneratePassword();
await GeneratePasswordVariants(password, account.Id);
@@ -134,10 +193,9 @@ namespace SecureBank.API.Services
//Send client code and temporary password to client by mail
_logger.LogInformation($"INFO DIRECTLY TO CLIENT: Your client code is {account.Id:00000000}. Your temporary password is {password}. You will be prompted to change it at first login");
return new APIResponse<int>
return new APIResponse<int>
{
Data = account.Id,
Success = true
Data = account.Id
};
}
@@ -148,7 +206,7 @@ namespace SecureBank.API.Services
{
return new APIResponse<GetPasswordVariantResponse>
{
Success = false,
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists"
};
}
@@ -157,7 +215,7 @@ namespace SecureBank.API.Services
{
return new APIResponse<GetPasswordVariantResponse>
{
Success = false,
Status = ResponseStatus.BadRequest,
Message = $"The number of failed login attempts for this account has exceeded 3. Contact your bank to confirm your identity and unlock your account."
};
}
@@ -166,7 +224,7 @@ namespace SecureBank.API.Services
{
return new APIResponse<GetPasswordVariantResponse>
{
Success = false,
Status = ResponseStatus.BadRequest,
Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account."
};
}
@@ -188,7 +246,6 @@ namespace SecureBank.API.Services
return new APIResponse<GetPasswordVariantResponse>
{
Success = true,
Data = new GetPasswordVariantResponse
{
LoginRequestId = loginRequest.Id,
@@ -198,26 +255,15 @@ namespace SecureBank.API.Services
};
}
public async Task<APIResponse<string>> Authentication(int accountId, AuthenticationRequest data)
public async Task<APIResponse<string>> Authentication(AuthenticationRequest data)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<string>
{
Success = false,
Message = $"Account does not exists"
};
}
AccountLoginRequest? loginRequest = await _database.AccountLoginRequests.FirstOrDefaultAsync(x => x.Id == data.LoginRequestId);
if (loginRequest is null)
{
return new APIResponse<string>
{
Success = false,
Status = ResponseStatus.BadRequest,
Message = $"Login request does not exist"
};
}
@@ -226,33 +272,17 @@ namespace SecureBank.API.Services
Account loginRequestAccount = password.Account;
if (loginRequestAccount.Id != account.Id)
APIResponse<string>? accountCheck = CheckAccount(loginRequestAccount);
if (accountCheck is not null)
{
account.LockReason = "Suspicious login attempt. The account provided does not match the account to which the login request is assigned.";
loginRequestAccount.LockReason = "Suspicious login attempt. The account provided does not match the account to which the login request is assigned.";
await _database.SaveChangesAsync();
return new APIResponse<string>
{
Success = false,
Message = $"Suspicious activity was detected during login. The account provided does not match the account to which the login request is assigned. Both accounts have been blocked. Contact your bank to confirm your identity and unlock your account."
};
}
if (account.LockReason is not null)
{
return new APIResponse<string>
{
Success = false,
Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account."
};
return accountCheck;
}
if (loginRequest.ValidTo < DateTime.Now)
{
return new APIResponse<string>
{
Success = false,
Status = ResponseStatus.BadRequest,
ActionCode = 1,
Message = $"Login request has expired. Go back and try again."
};
@@ -261,25 +291,199 @@ namespace SecureBank.API.Services
byte[] passwordDb = password.Password;
byte[] passwordProvided = HashPassword(data.Password, password.LeftSalt, password.RightSalt);
if (Enumerable.SequenceEqual(passwordDb, passwordProvided))
if (!Enumerable.SequenceEqual(passwordDb, passwordProvided))
{
account.LoginFailedCount++;
loginRequestAccount.LoginFailedCount++;
await _database.SaveChangesAsync();
return new APIResponse<string>
{
Success = false,
ActionCode = 2,
Status = ResponseStatus.BadRequest,
Message = $"Incorrect password"
};
}
string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), account.Id, account.TemporaryPassword);
loginRequestAccount.LoginFailedCount = 0;
await _database.SaveChangesAsync();
string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), loginRequestAccount, loginRequestAccount.TemporaryPassword);
return new APIResponse<string>
{
ActionCode = loginRequestAccount.TemporaryPassword ? 2 : 0,
Data = token
};
}
public async Task<APIResponse<string>> AuthenticationRefresh(Claims claims)
{
if (claims.IsOneTimeToken)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"One time token cannot be refreshed."
};
}
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == claims.AccountId);
APIResponse<string>? accountCheck = CheckAccount(account);
if (accountCheck is not null)
{
return accountCheck;
}
string token = _authenticationHelper.GenerateToken(Guid.NewGuid(), account, false);
return new APIResponse<string>
{
Data = token,
Success = true,
Data = token
};
}
public async Task<APIResponse> ChangePassword(Claims claims, ChangePasswordRequest data)
{
string password = data.Password;
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == claims.AccountId);
if (account is null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists"
};
}
IEnumerable<string> passwordChecks = CheckPassword(password);
if (passwordChecks.Any())
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("Provided password does not meet the security requirements:");
foreach (string check in passwordChecks)
{
sb.AppendLine(check);
}
return new APIResponse
{
Status = ResponseStatus.BadRequest,
Message = sb.ToString()
};
}
IEnumerable<AccountPasswordIndex> indexes = await _database.AccountPasswordIndexes.Where(x => x.AccountPassword.AccountId == claims.AccountId).ToListAsync();
_database.AccountPasswordIndexes.AttachRange(indexes);
_database.AccountPasswordIndexes.RemoveRange(indexes);
await _database.SaveChangesAsync();
IEnumerable<AccountPassword> variants = await _database.AccountPasswords.Where(x => x.AccountId == claims.AccountId).ToListAsync();
_database.AccountPasswords.AttachRange(variants);
_database.AccountPasswords.RemoveRange(variants);
await _database.SaveChangesAsync();
await GeneratePasswordVariants(password, claims.AccountId);
account.TemporaryPassword = false;
await _database.SaveChangesAsync();
return new APIResponse();
}
public async Task<APIResponse<IEnumerable<AccountResponse>>> GetAccounts(string? iban, int? id, Claims claims)
{
IEnumerable<Account> accounts = await _database.Accounts.ToListAsync();
if (id is not null)
{
accounts = accounts.Where(x => x.Id == id);
}
if (iban is not null)
{
accounts = accounts.Where(x => x.IBAN == iban);
}
if (accounts.Any(x => x.Id != claims.AccountId) && !claims.IsAdmin)
{
return new APIResponse<IEnumerable<AccountResponse>>
{
Status = ResponseStatus.Unauthorized,
Message = $"You don't have permission to get information about accounts that aren't yours"
};
}
List<AccountResponse> data = new List<AccountResponse>();
foreach (Account account in accounts)
{
data.Add(new AccountResponse
{
Id = account.Id,
FirstName = account.FirstName,
LastName = account.LastName,
Email = account.Email,
PhoneNumber = account.PhoneNumber,
Address = account.Address,
PESEL = _encryptionHelper.Decrypt(account.PESEL),
IdCardNumber = _encryptionHelper.Decrypt(account.IdCardNumber),
IBAN = account.IBAN,
CardNumber = _encryptionHelper.Decrypt(account.CardNumber),
CardExpirationDate = _encryptionHelper.Decrypt(account.CardExpirationDate),
CardCVV = _encryptionHelper.Decrypt(account.CardCVV),
IsAdmin = account.IsAdmin,
LoginFailedCount = account.LoginFailedCount,
TemporaryPassword = account.TemporaryPassword,
LockReason = account.LockReason,
});
}
return new APIResponse<IEnumerable<AccountResponse>>
{
Data = data
};
}
public async Task<APIResponse> ResetPassword(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists"
};
}
await PasswordReset(account);
return new APIResponse<int>
{
Data = account.Id
};
}
public async Task<APIResponse> UnlockAccount(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists"
};
}
await PasswordReset(account);
account.LockReason = null;
account.LoginFailedCount = 0;
await _database.SaveChangesAsync();
return new APIResponse<int>
{
Data = account.Id
};
}
@@ -289,6 +493,60 @@ namespace SecureBank.API.Services
#region PRIVATE METHODS
protected async Task PasswordReset(Account account)
{
IEnumerable<AccountPasswordIndex> indexes = await _database.AccountPasswordIndexes.Where(x => x.AccountPassword.AccountId == account.Id).ToListAsync();
_database.AccountPasswordIndexes.AttachRange(indexes);
_database.AccountPasswordIndexes.RemoveRange(indexes);
await _database.SaveChangesAsync();
IEnumerable<AccountPassword> variants = await _database.AccountPasswords.Where(x => x.AccountId == account.Id).ToListAsync();
_database.AccountPasswords.AttachRange(variants);
_database.AccountPasswords.RemoveRange(variants);
await _database.SaveChangesAsync();
string password = GeneratePassword();
await GeneratePasswordVariants(password, account.Id);
account.TemporaryPassword = true;
await _database.SaveChangesAsync();
_logger.LogInformation($"INFO DIRECTLY TO CLIENT: Your new temporary password is {password}. You will be prompted to change it at first login");
}
protected APIResponse<string>? CheckAccount(Account? account)
{
if (account is null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account does not exists."
};
}
if (account.LockReason is not null)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"Account is locked. Contact your bank to confirm your identity and unlock your account."
};
}
if (account.LoginFailedCount >= 3)
{
return new APIResponse<string>
{
Status = ResponseStatus.BadRequest,
Message = $"The number of failed login attempts for this account has exceeded 3. Contact your bank to confirm your identity and unlock your account."
};
}
return null;
}
protected byte[] HashPassword(string password, string leftSalt, string rightSalt)
{
SHA512 sha = SHA512.Create();
@@ -355,11 +613,16 @@ namespace SecureBank.API.Services
protected IEnumerable<string> CheckPassword(string password)
{
int minLength = 8;
uint maxLength = 20;
if (password.Length < minLength)
{
yield return $"Password must be at least {minLength} characters long";
}
if (password.Length > maxLength)
{
yield return $"Password cannot be longer than {maxLength} characters";
}
if (!password.Any(x => Char.IsUpper(x)))
{
yield return $"Password must contain at least one uppercase character";
@@ -372,7 +635,7 @@ namespace SecureBank.API.Services
{
yield return $"Password must contain at least one digit";
}
if (!password.Any(x => Char.IsSymbol(x)))
if (!password.Any(x => !Char.IsDigit(x) && !Char.IsUpper(x) && !Char.IsLower(x)))
{
yield return $"Password must contain at least one special character";
}

View File

@@ -0,0 +1,81 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.EntityFrameworkCore;
using SecureBank.Authentication;
using SecureBank.Common;
using SecureBank.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Services
{
public interface IBalanceService
{
Task<APIResponse<decimal>> GetAccountBalance(int accountId);
Task<APIResponse<decimal>> GetBalance(Claims claims);
}
public class BalanceService : IBalanceService
{
#region SERVICES
private DatabaseContext _database;
#endregion
#region CONSTRUCTORS
public BalanceService(DatabaseContext database)
{
_database = database;
}
#endregion
#region PUBLIC METHODS
public async Task<APIResponse<decimal>> GetBalance(Claims claims) => await GetAccountBalance(claims.AccountId);
public async Task<APIResponse<decimal>> GetAccountBalance(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<decimal>
{
Status = ResponseStatus.BadRequest,
Message = "Account does not exists"
};
}
string iban = account.IBAN;
Transfer[] transfersIncoming = await _database.Transfers.Where(x => x.ReceiverAccountNumber == iban).ToArrayAsync();
Transfer[] transfersOutcoming = await _database.Transfers.Where(x => x.SenderAccountNumber == iban).ToArrayAsync();
return new APIResponse<decimal>
{
Data = 0 + transfersIncoming.Sum(x => x.Amount) - transfersOutcoming.Sum(x => x.Amount),
};
}
#endregion
#region PRIVATE METHODS
#endregion
}
}

View File

@@ -7,10 +7,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SecureBank.Authentication\SecureBank.Authentication.csproj" />
<ProjectReference Include="..\..\SecureBank.Common\SecureBank.Common.csproj" />
<ProjectReference Include="..\..\SecureBank.Database\SecureBank.Database.csproj" />
<ProjectReference Include="..\..\SecureBank.Extensions\SecureBank.Extensions.csproj" />
<ProjectReference Include="..\SecureBank.API.Authentication\SecureBank.API.Authentication.csproj" />
<ProjectReference Include="..\SecureBank.API.Encryption\SecureBank.API.Encryption.csproj" />
<ProjectReference Include="..\SecureBank.API.Helpers\SecureBank.API.Helpers.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,250 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Client;
using SecureBank.API.Helpers;
using SecureBank.Authentication;
using SecureBank.Common;
using SecureBank.Common.Accounts;
using SecureBank.Common.Transfers;
using SecureBank.Database;
using SecureBank.Helpers.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
namespace SecureBank.API.Services
{
public interface ITransfersService
{
Task<APIResponse<IEnumerable<TransferResponse>>> GetTransfers(Claims claims);
Task<APIResponse<IEnumerable<TransferResponse>>> GetUserTransfers(int accountId);
Task<APIResponse> CreateAdminTransfer(CreateAdminTransferRequest data);
Task<APIResponse> CreateUserTransfer(CreateUserTransferRequest data, Claims claims);
}
public class TransfersService : ITransfersService
{
#region SERVICES
private DatabaseContext _database;
#endregion
#region CONSTRUCTORS
public TransfersService(DatabaseContext database)
{
_database = database;
}
#endregion
#region PUBLIC METHODS
public async Task<APIResponse<IEnumerable<TransferResponse>>> GetTransfers(Claims claims) => await GetUserTransfers(claims.AccountId);
public async Task<APIResponse<IEnumerable<TransferResponse>>> GetUserTransfers(int accountId)
{
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == accountId);
if (account is null)
{
return new APIResponse<IEnumerable<TransferResponse>>
{
Status = ResponseStatus.BadRequest,
Message = "Account does not exists"
};
}
string iban = account.IBAN;
List<TransferResponse> list = new List<TransferResponse>();
await foreach (Transfer transfer in _database.Transfers.Where(x => x.SenderAccountNumber == iban || x.ReceiverAccountNumber == iban).AsAsyncEnumerable())
{
list.Add(new TransferResponse
{
Id = transfer.Id,
SenderAccountNumber = transfer.SenderAccountNumber,
SenderAddress = transfer.SenderAddress,
SenderName = transfer.SenderName,
ReceiverAccountNumber = transfer.ReceiverAccountNumber,
ReceiverAddress = transfer.ReceiverAddress,
ReceiverName = transfer.ReceiverName,
Amount = transfer.Amount,
Title = transfer.Title,
Date = transfer.Date,
});
}
return new APIResponse<IEnumerable<TransferResponse>>
{
Data = list
};
}
public async Task<APIResponse> CreateAdminTransfer(CreateAdminTransferRequest data)
{
Check<CreateAdminTransferRequest>[] checks = new Check<CreateAdminTransferRequest>[]
{
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x is null),
Message = "Body cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.SenderAccountNumber is null),
Message = "Sender account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => !x.SenderAccountNumber.All(y => char.IsDigit(y))),
Message = "Wrong sender account number format. Account number consists only of digits"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.SenderAccountNumber.Length != 26),
Message = "Sender account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.ReceiverAccountNumber is null),
Message = "Receiver account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => !x.ReceiverAccountNumber.All(y => char.IsDigit(y))),
Message = "Wrong receiver account number format. Account number consists only of digits"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.ReceiverAccountNumber.Length != 26),
Message = "Receiver account number cannot be empty"
},
new Check<CreateAdminTransferRequest>
{
CheckAction = new Predicate<CreateAdminTransferRequest>((x) => x.Amount <= 0),
Message = "Receiver account number cannot be empty"
},
};
foreach (Check<CreateAdminTransferRequest> check in checks)
{
if (check.CheckAction.Invoke(data))
{
return new APIResponse
{
Message = check.Message,
Status = ResponseStatus.BadRequest,
};
}
}
data.Amount = Math.Round(data.Amount, 2, MidpointRounding.ToEven);
Transfer transfer = new Transfer
{
SenderAccountNumber = data.SenderAccountNumber,
SenderAddress = data.SenderAddress,
SenderName = data.SenderName,
ReceiverAccountNumber = data.ReceiverAccountNumber,
ReceiverAddress = data.ReceiverAddress,
ReceiverName = data.ReceiverName,
Amount = data.Amount,
Title = data.Title,
Date = DateTime.Now,
};
await _database.Transfers.AddAsync(transfer);
await _database.SaveChangesAsync();
return new APIResponse();
}
public async Task<APIResponse> CreateUserTransfer(CreateUserTransferRequest data, Claims claims)
{
Check<CreateUserTransferRequest>[] checks = new Check<CreateUserTransferRequest>[]
{
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x is null),
Message = "Body cannot be empty"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x.ReceiverAccountNumber is null),
Message = "Receiver account number cannot be empty"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => !x.ReceiverAccountNumber.All(y => char.IsDigit(y))),
Message = "Wrong receiver account number format. Account number consists only of digits"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x.ReceiverAccountNumber.Length != 26),
Message = "Receiver account number cannot be empty"
},
new Check<CreateUserTransferRequest>
{
CheckAction = new Predicate<CreateUserTransferRequest>((x) => x.Amount <= 0),
Message = "Receiver account number cannot be empty"
},
};
foreach (Check<CreateUserTransferRequest> check in checks)
{
if (check.CheckAction.Invoke(data))
{
return new APIResponse
{
Message = check.Message,
Status = ResponseStatus.BadRequest,
};
}
}
Account? account = await _database.Accounts.FirstOrDefaultAsync(x => x.Id == claims.AccountId);
if (account is null)
{
return new APIResponse<IEnumerable<TransferResponse>>
{
Status = ResponseStatus.BadRequest,
Message = "Account does not exists"
};
}
data.Amount = Math.Round(data.Amount, 2, MidpointRounding.ToEven);
Transfer transfer = new Transfer
{
SenderAccountNumber = account.IBAN,
SenderAddress = account.Address,
SenderName = $"{account.FirstName} {account.LastName}",
ReceiverAccountNumber = data.ReceiverAccountNumber,
ReceiverAddress = data.ReceiverAddress,
ReceiverName = data.ReceiverName,
Amount = data.Amount,
Title = data.Title,
Date = DateTime.Now,
};
await _database.Transfers.AddAsync(transfer);
await _database.SaveChangesAsync();
return new APIResponse();
}
#endregion
}
}