Auth endpoints are now functional

This commit is contained in:
2023-08-13 00:54:23 -04:00
parent a636309b5a
commit fdf286e22f
26 changed files with 705 additions and 159 deletions

View File

@ -17,7 +17,12 @@ using System.ComponentModel.DataAnnotations;
using IO.Swagger.Attributes; using IO.Swagger.Attributes;
using IO.Swagger.Security; using IO.Swagger.Security;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using IO.Swagger.Models; using IO.Swagger.Models.dto;
using IO.Swagger.Repositories;
using System.Threading.Tasks;
using System.Linq;
using IO.Swagger.Services;
using System.Security.Claims;
namespace IO.Swagger.Controllers namespace IO.Swagger.Controllers
{ {
@ -27,6 +32,15 @@ namespace IO.Swagger.Controllers
[ApiController] [ApiController]
public class AuthApiController : ControllerBase public class AuthApiController : ControllerBase
{ {
private readonly IUserRepository repository;
private readonly JwtService jwt;
public AuthApiController(IUserRepository repository, JwtService jwt)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.jwt = jwt ?? throw new ArgumentNullException(nameof(jwt));
}
/// <summary> /// <summary>
/// Get user details /// Get user details
/// </summary> /// </summary>
@ -37,15 +51,13 @@ namespace IO.Swagger.Controllers
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)] [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("GetUserDetails")] [SwaggerOperation("GetUserDetails")]
public virtual IActionResult GetUserDetails() public virtual async Task<IActionResult> GetUserDetails()
{ {
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ... var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
// return StatusCode(200); if (!int.TryParse(userIdString, out int userId))
return Unauthorized();
//TODO: Uncomment the next line to return response 401 or use other options such as return this.NotFound(), return this.BadRequest(..), ... var user = await repository.RetrieveUser(userId);
// return StatusCode(401); return user == null ? NoContent() : Ok(user);
throw new NotImplementedException();
} }
/// <summary> /// <summary>
@ -58,40 +70,39 @@ namespace IO.Swagger.Controllers
[Route("/v1/api/auth/login")] [Route("/v1/api/auth/login")]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("LoginUser")] [SwaggerOperation("LoginUser")]
public virtual IActionResult LoginUser([FromBody]AuthLoginBody body) public virtual async Task<IActionResult> LoginUser([FromBody]AuthLoginBody body)
{ {
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ... if (!ModelState.IsValid)
// return StatusCode(200); {
var errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage));
//TODO: Uncomment the next line to return response 401 or use other options such as return this.NotFound(), return this.BadRequest(..), ... return BadRequest(errors);
// return StatusCode(401); }
var user = await repository.LoginUser(body);
throw new NotImplementedException(); return user == null ? Unauthorized() : Ok(new { token = jwt.GenerateJwt(user.Id) });
} }
/// <summary> /// <summary>
/// Register a new user /// Register a new user
/// </summary> /// </summary>
/// <param name="body"></param> /// <param name="body"></param>
/// <response code="201">User registered successfully</response> /// <response code="200">User registered successfully</response>
/// <response code="400">Bad Request</response> /// <response code="400">Bad Request</response>
/// <response code="409">Conflict (user with provided email already exists)</response> /// <response code="409">Conflict (user with provided email already exists)</response>
[HttpPost] [HttpPost]
[Route("/v1/api/auth/register")] [Route("/v1/api/auth/register")]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("RegisterUser")] [SwaggerOperation("RegisterUser")]
public virtual IActionResult RegisterUser([FromBody]AuthRegisterBody body) public async Task<IActionResult> RegisterUser([FromBody]AuthRegisterBody body)
{ {
//TODO: Uncomment the next line to return response 201 or use other options such as return this.NotFound(), return this.BadRequest(..), ... if (!ModelState.IsValid)
// return StatusCode(201); {
var errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage));
return BadRequest(errors);
}
//TODO: Uncomment the next line to return response 400 or use other options such as return this.NotFound(), return this.BadRequest(..), ... var user = await repository.RegisterUser(body);
// return StatusCode(400); return user == null ? StatusCode(409) : Ok(new { token = jwt.GenerateJwt(user.Id) });
//TODO: Uncomment the next line to return response 409 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(409);
throw new NotImplementedException();
} }
} }
} }

View File

@ -17,7 +17,7 @@ using System.ComponentModel.DataAnnotations;
using IO.Swagger.Attributes; using IO.Swagger.Attributes;
using IO.Swagger.Security; using IO.Swagger.Security;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using IO.Swagger.Models; using IO.Swagger.Models.dto;
namespace IO.Swagger.Controllers namespace IO.Swagger.Controllers
{ {

View File

@ -17,7 +17,7 @@ using System.ComponentModel.DataAnnotations;
using IO.Swagger.Attributes; using IO.Swagger.Attributes;
using IO.Swagger.Security; using IO.Swagger.Security;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using IO.Swagger.Models; using IO.Swagger.Models.dto;
namespace IO.Swagger.Controllers namespace IO.Swagger.Controllers
{ {

View File

@ -9,6 +9,12 @@
<PackageId>IO.Swagger</PackageId> <PackageId>IO.Swagger</PackageId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.5.1" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.5.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.5.1" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.5.1" />

View File

@ -0,0 +1,60 @@
// <auto-generated />
using IO.Swagger.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace IO.Swagger.Migrations
{
[DbContext(typeof(BankDbContext))]
[Migration("20230813022042_Initial creation")]
partial class Initialcreation
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("IO.Swagger.Models.db.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.HasColumnType("nvarchar(max)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("Salt")
.HasColumnType("nvarchar(max)");
b.Property<string>("Username")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IO.Swagger.Migrations
{
/// <inheritdoc />
public partial class Initialcreation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Email = table.Column<string>(type: "nvarchar(max)", nullable: true),
FirstName = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastName = table.Column<string>(type: "nvarchar(max)", nullable: true),
Username = table.Column<string>(type: "nvarchar(max)", nullable: true),
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
Salt = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@ -0,0 +1,59 @@
// <auto-generated />
using IO.Swagger.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace IO.Swagger.Migrations
{
[DbContext(typeof(BankDbContext))]
[Migration("20230813022424_Change user table")]
partial class Changeusertable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("IO.Swagger.Models.db.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<string>("FirstName")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("LastName")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("Salt")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,68 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IO.Swagger.Migrations
{
/// <inheritdoc />
public partial class Changeusertable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Username",
table: "Users");
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Users",
type: "nvarchar(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Users",
type: "nvarchar(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Users",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(32)",
oldMaxLength: 32,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "FirstName",
table: "Users",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(32)",
oldMaxLength: 32,
oldNullable: true);
migrationBuilder.AddColumn<string>(
name: "Username",
table: "Users",
type: "nvarchar(max)",
nullable: true);
}
}
}

View File

@ -0,0 +1,56 @@
// <auto-generated />
using IO.Swagger.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace IO.Swagger.Migrations
{
[DbContext(typeof(BankDbContext))]
partial class BankDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("IO.Swagger.Models.db.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<string>("FirstName")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("LastName")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("Salt")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace IO.Swagger.Models.db
{
public class User
{
public int Id { get; set; }
public string Email { get; set; }
[StringLength(32, MinimumLength = 3)]
public string FirstName { get; set; }
[StringLength(32, MinimumLength = 3)]
public string LastName { get; set; }
public string PasswordHash { get; set; }
public string Salt { get; set; }
}
}

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///
@ -32,6 +32,7 @@ namespace IO.Swagger.Models
[StringLength(32, MinimumLength = 3)] [StringLength(32, MinimumLength = 3)]
[DataMember(Name = "firstName")] [DataMember(Name = "firstName")]
[Required]
public string FirstName { get; set; } public string FirstName { get; set; }
/// <summary> /// <summary>
@ -40,6 +41,7 @@ namespace IO.Swagger.Models
[StringLength(32, MinimumLength = 3)] [StringLength(32, MinimumLength = 3)]
[DataMember(Name = "lastName")] [DataMember(Name = "lastName")]
[Required]
public string LastName { get; set; } public string LastName { get; set; }
/// <summary> /// <summary>
@ -47,6 +49,8 @@ namespace IO.Swagger.Models
/// </summary> /// </summary>
[DataMember(Name = "email")] [DataMember(Name = "email")]
[EmailAddress(ErrorMessage = "Invalid email format.")]
[Required]
public string Email { get; set; } public string Email { get; set; }
/// <summary> /// <summary>
@ -54,6 +58,8 @@ namespace IO.Swagger.Models
/// </summary> /// </summary>
[DataMember(Name = "password")] [DataMember(Name = "password")]
[StringLength(32, MinimumLength = 8)]
[Required]
public string Password { get; set; } public string Password { get; set; }
/// <summary> /// <summary>

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///
@ -78,11 +78,11 @@ namespace IO.Swagger.Models
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;
return return
(
CollectionName == other.CollectionName || CollectionName == other.CollectionName ||
CollectionName != null && CollectionName != null &&
CollectionName.Equals(other.CollectionName) CollectionName.Equals(other.CollectionName)
); ;
} }
/// <summary> /// <summary>

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///

View File

@ -18,7 +18,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IO.Swagger.Models namespace IO.Swagger.Models.dto
{ {
/// <summary> /// <summary>
/// ///

View File

@ -1,12 +1,4 @@
{ {
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50352/",
"sslPort": 0
}
},
"profiles": { "profiles": {
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",
@ -20,10 +12,12 @@
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "swagger", "launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development",
} "DATABASE_CONNECTION_STRING": "Server=localhost\\SQLEXPRESS;Database=BankDb;Trusted_Connection=True;Integrated Security=True;TrustServerCertificate=True",
"JWT_SECRET_KEY": "mysuperduperubereepykeepysecretkey"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}, },
"Docker": { "Docker": {
"commandName": "Docker", "commandName": "Docker",
@ -32,5 +26,13 @@
"publishAllPorts": true, "publishAllPorts": true,
"useSSL": true "useSSL": true
} }
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50352/",
"sslPort": 0
}
} }
} }

View File

@ -0,0 +1,13 @@
using IO.Swagger.Models.db;
using IO.Swagger.Models.dto;
using System.Threading.Tasks;
namespace IO.Swagger.Repositories
{
public interface IUserRepository
{
Task<User> LoginUser(AuthLoginBody request);
Task<User> RegisterUser(AuthRegisterBody request);
Task<User> RetrieveUser(int userId);
}
}

View File

@ -0,0 +1,77 @@
using IO.Swagger.Models.db;
using System.Security.Cryptography;
using System;
using IO.Swagger.Services;
using IO.Swagger.Models.dto;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace IO.Swagger.Repositories
{
public class UserRepository : IUserRepository
{
private readonly BankDbContext bankDbContext;
public UserRepository(BankDbContext bankDbContext)
{
this.bankDbContext = bankDbContext;
}
public async Task<User> RegisterUser(AuthRegisterBody request)
{
request.Email = request.Email.ToLower();
if (await bankDbContext.Users.CountAsync((User u) => u.Email == request.Email) > 0)
return null;
// Generate a random salt
byte[] saltBytes = new byte[16];
new RNGCryptoServiceProvider().GetBytes(saltBytes);
string salt = Convert.ToBase64String(saltBytes);
// Hash the password along with the salt
string password = request.Password;
string saltedPassword = password + salt;
byte[] passwordBytes = System.Text.Encoding.UTF8.GetBytes(saltedPassword);
byte[] hashedBytes = new SHA256Managed().ComputeHash(passwordBytes);
string hashedPassword = Convert.ToBase64String(hashedBytes);
// Create and insert the user
var newUser = new User
{
PasswordHash = hashedPassword,
Salt = salt,
Email = request.Email,
FirstName = request.FirstName,
LastName = request.LastName
};
await bankDbContext.Users.AddAsync(newUser);
await bankDbContext.SaveChangesAsync();
return newUser;
}
public async Task<User> LoginUser(AuthLoginBody request)
{
request.Email = request.Email.ToLower();
var user = await bankDbContext.Users.FirstOrDefaultAsync(u => u.Email.Equals(request.Email));
if (user == null)
return null;
// Hash the supplied password with the retrieved salt
string password = request.Password;
string saltedPassword = password + user.Salt;
byte[] passwordBytes = System.Text.Encoding.UTF8.GetBytes(saltedPassword);
byte[] hashedBytes = new SHA256Managed().ComputeHash(passwordBytes);
string hashedPassword = Convert.ToBase64String(hashedBytes);
if (hashedPassword != user.PasswordHash)
return null;
return user;
}
public async Task<User> RetrieveUser(int userId)
{
return await bankDbContext.Users.FirstOrDefaultAsync(u => u.Id == userId);
}
}
}

View File

@ -1,4 +1,6 @@
using System; using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
@ -7,6 +9,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
namespace IO.Swagger.Security namespace IO.Swagger.Security
{ {
@ -15,6 +19,9 @@ namespace IO.Swagger.Security
/// </summary> /// </summary>
public class BearerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> public class BearerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{ {
private readonly string secretKey;
private readonly byte[] secretBytes;
/// <summary> /// <summary>
/// scheme name for authentication handler. /// scheme name for authentication handler.
/// </summary> /// </summary>
@ -22,6 +29,8 @@ namespace IO.Swagger.Security
public BearerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) public BearerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{ {
secretKey = Environment.GetEnvironmentVariable("JWT_SECRET_KEY");
secretBytes = Encoding.UTF8.GetBytes(secretKey);
} }
/// <summary> /// <summary>
@ -37,22 +46,44 @@ namespace IO.Swagger.Security
{ {
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
/// TODO handle token. var tokenHandler = new JwtSecurityTokenHandler();
} var validationParameters = new TokenValidationParameters
catch
{ {
return AuthenticateResult.Fail("Invalid Authorization Header"); ValidateIssuerSigningKey = true,
} IssuerSigningKey = new SymmetricSecurityKey(secretBytes),
ValidateIssuer = false,
var claims = new[] { ValidateAudience = false
new Claim(ClaimTypes.NameIdentifier, "changeme"),
new Claim(ClaimTypes.Name, "changeme"),
}; };
var identity = new ClaimsIdentity(claims, Scheme.Name);
try
{
var claimsPrincipal = tokenHandler.ValidateToken(authHeader.Parameter, validationParameters, out _);
var userIdClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId))
{
var claims = new[]{ new Claim(ClaimTypes.NameIdentifier, userId.ToString()) };
var identity = new ClaimsIdentity(claims, SchemeName);
var principal = new ClaimsPrincipal(identity); var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name); var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket); return AuthenticateResult.Success(ticket);
} }
} }
catch (Exception)
{
return AuthenticateResult.Fail("Invalid Auth Token");
}
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
return AuthenticateResult.Fail("Missing Authorization Header");
}
}
} }

View File

@ -0,0 +1,17 @@
using IO.Swagger.Models.db;
using Microsoft.EntityFrameworkCore;
namespace IO.Swagger.Services
{
public class BankDbContext : DbContext
{
public BankDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<User> Users { get; set; }
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System;
namespace IO.Swagger.Services
{
public class JwtService
{
private readonly string secretKey;
private readonly byte[] secretBytes;
public JwtService()
{
secretKey = Environment.GetEnvironmentVariable("JWT_SECRET_KEY");
secretBytes = Encoding.UTF8.GetBytes(secretKey);
}
public string GenerateJwt(int userId)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString())
// You can add more claims as needed
};
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(1), // Token expiration time
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secretBytes), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public int GetUserIdFromJwt(string jwtToken)
{
return -1; // Return -1 if user ID extraction fails
}
}
}

View File

@ -23,6 +23,9 @@ using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using IO.Swagger.Filters; using IO.Swagger.Filters;
using IO.Swagger.Security; using IO.Swagger.Security;
using IO.Swagger.Repositories;
using IO.Swagger.Services;
using Microsoft.EntityFrameworkCore;
namespace IO.Swagger namespace IO.Swagger
{ {
@ -94,7 +97,40 @@ namespace IO.Swagger
// Include DataAnnotation attributes on Controller Action parameters as Swagger validation rules (e.g required, pattern, ..) // Include DataAnnotation attributes on Controller Action parameters as Swagger validation rules (e.g required, pattern, ..)
// Use [ValidateModelState] on Actions to actually validate it in C# as well! // Use [ValidateModelState] on Actions to actually validate it in C# as well!
c.OperationFilter<GeneratePathParamsValidationFilter>(); c.OperationFilter<GeneratePathParamsValidationFilter>();
c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme()
{
In = ParameterLocation.Header,
Description = "Please enter a valid token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "bearer"
}); });
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type=ReferenceType.SecurityScheme,
Id="bearerAuth"
}
},
new string[]{}
}
});
});
string connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING");
if (string.IsNullOrEmpty(connectionString))
{
throw new Exception("Database connection string not found in environment variable.");
}
services.AddScoped<IUserRepository, UserRepository>();
services.AddDbContext<BankDbContext>(x => x.UseSqlServer(connectionString: connectionString));
services.AddSingleton<JwtService>();
} }
/// <summary> /// <summary>
@ -103,8 +139,10 @@ namespace IO.Swagger
/// <param name="app"></param> /// <param name="app"></param>
/// <param name="env"></param> /// <param name="env"></param>
/// <param name="loggerFactory"></param> /// <param name="loggerFactory"></param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) /// <param name="context"></param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, BankDbContext context)
{ {
context.Database.Migrate();
app.UseRouting(); app.UseRouting();
//TODO: Uncomment this if you need wwwroot folder //TODO: Uncomment this if you need wwwroot folder

View File

@ -17,7 +17,7 @@
<environmentVariable name="DOTNET_MODIFIABLE_ASSEMBLIES" value="debug" /> <environmentVariable name="DOTNET_MODIFIABLE_ASSEMBLIES" value="debug" />
<environmentVariable name="ASPNETCORE_AUTO_RELOAD_WS_KEY" value="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvpSuAatIokMxzmPdM4egZimjWBPCukyvXO2w3GLeYU+aRSYH/KiZnBvy6785t/Fr2/Y4gF665nvaEMM1uveLOQvwRuICcL/R5mbYwKtKd/VJ4NYamzS5Zd4Fms/9eRf4IHGFu+SjX2Q/MTqVJa7a7w1hLAKoBDGbqJ2V0i7w87qj08XTcBdLRs5gL0je9EJ1QeggFzcY6SVCp+a9MFpybM57WI82h9uysW9rC9H9n0Nelc2D9+2HUbBExHQVTVXKlQ2KWHbxhBKsHzf4k4bBjy6V31e9G/dnWCciXNBQiIPidCp4Nt3AF+bpq080+aRFzIIh5wK1tupyG1BsffFHQIDAQAB" /> <environmentVariable name="ASPNETCORE_AUTO_RELOAD_WS_KEY" value="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvpSuAatIokMxzmPdM4egZimjWBPCukyvXO2w3GLeYU+aRSYH/KiZnBvy6785t/Fr2/Y4gF665nvaEMM1uveLOQvwRuICcL/R5mbYwKtKd/VJ4NYamzS5Zd4Fms/9eRf4IHGFu+SjX2Q/MTqVJa7a7w1hLAKoBDGbqJ2V0i7w87qj08XTcBdLRs5gL0je9EJ1QeggFzcY6SVCp+a9MFpybM57WI82h9uysW9rC9H9n0Nelc2D9+2HUbBExHQVTVXKlQ2KWHbxhBKsHzf4k4bBjy6V31e9G/dnWCciXNBQiIPidCp4Nt3AF+bpq080+aRFzIIh5wK1tupyG1BsffFHQIDAQAB" />
<environmentVariable name="ASPNETCORE_AUTO_RELOAD_VDIR" value="/" /> <environmentVariable name="ASPNETCORE_AUTO_RELOAD_VDIR" value="/" />
<environmentVariable name="DOTNET_HOTRELOAD_NAMEDPIPE_NAME" value="89813085-9fe3-4177-915d-b2a8e67e66aa" /> <environmentVariable name="DOTNET_HOTRELOAD_NAMEDPIPE_NAME" value="43e4d2da-7f6d-4426-a3ad-e156f95d80c9" />
</environmentVariables> </environmentVariables>
</aspNetCore> </aspNetCore>
</system.webServer> </system.webServer>