diff --git a/Birdmap/Birdmap.csproj b/Birdmap/Birdmap.csproj
index 17e9dee..5496272 100644
--- a/Birdmap/Birdmap.csproj
+++ b/Birdmap/Birdmap.csproj
@@ -10,6 +10,7 @@
+
diff --git a/Birdmap/Controllers/AuthController.cs b/Birdmap/Controllers/AuthController.cs
new file mode 100644
index 0000000..fa70bc6
--- /dev/null
+++ b/Birdmap/Controllers/AuthController.cs
@@ -0,0 +1,60 @@
+using Birdmap.Models;
+using Birdmap.Services.Interfaces;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Birdmap.Controllers
+{
+ [Authorize]
+ [ApiController]
+ [Route("api/[controller]")]
+ public class AuthController : ControllerBase
+ {
+ private readonly IAuthService _service;
+ private readonly IConfiguration _configuration;
+
+ public AuthController(IAuthService service, IConfiguration configuration)
+ {
+ _service = service;
+ _configuration = configuration;
+ }
+
+ [AllowAnonymous]
+ [HttpPost("authenticate")]
+ [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
+ public async Task AuthenticateAsync([FromBody] AuthenticateRequest model)
+ {
+ var user = await _service.AuthenticateUserAsync(model.Username, model.Password);
+ var expires = DateTime.UtcNow.AddHours(2);
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var key = Encoding.ASCII.GetBytes(_configuration["BasicAuth:Secret"]);
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Subject = new ClaimsIdentity(new Claim[]
+ {
+ new Claim(ClaimTypes.Name, user.Name)
+ }),
+ Expires = expires,
+ SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
+ };
+ var token = tokenHandler.CreateToken(tokenDescriptor);
+ var tokenString = tokenHandler.WriteToken(token);
+
+ return Ok(
+ new
+ {
+ Name = user.Name,
+ Token = tokenString,
+ Expires = expires,
+ });
+ }
+ }
+}
diff --git a/Birdmap/Controllers/WeatherForecastController.cs b/Birdmap/Controllers/WeatherForecastController.cs
index cc49ec1..0572181 100644
--- a/Birdmap/Controllers/WeatherForecastController.cs
+++ b/Birdmap/Controllers/WeatherForecastController.cs
@@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Birdmap.Controllers
{
+ [Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
diff --git a/Birdmap/Exceptions/AuthenticationException.cs b/Birdmap/Exceptions/AuthenticationException.cs
new file mode 100644
index 0000000..a1a1a23
--- /dev/null
+++ b/Birdmap/Exceptions/AuthenticationException.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Birdmap.Exceptions
+{
+ public class AuthenticationException : Exception
+ {
+ public AuthenticationException()
+ : base("Username or password is incorrect.")
+ {
+ }
+ }
+}
diff --git a/Birdmap/Models/AuthenticateRequest.cs b/Birdmap/Models/AuthenticateRequest.cs
new file mode 100644
index 0000000..515c0df
--- /dev/null
+++ b/Birdmap/Models/AuthenticateRequest.cs
@@ -0,0 +1,13 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Birdmap.Models
+{
+ public class AuthenticateRequest
+ {
+ [Required(AllowEmptyStrings = false, ErrorMessage = "Username is required.")]
+ public string Username { get; set; }
+
+ [Required(AllowEmptyStrings = false, ErrorMessage = "Password is required.")]
+ public string Password { get; set; }
+ }
+}
diff --git a/Birdmap/Models/User.cs b/Birdmap/Models/User.cs
new file mode 100644
index 0000000..3ac4951
--- /dev/null
+++ b/Birdmap/Models/User.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Birdmap.Models
+{
+ public class User
+ {
+ public string Name { get; set; }
+ public byte[] PasswordHash { get; set; }
+ public byte[] PasswordSalt { get; set; }
+ }
+}
diff --git a/Birdmap/Services/AuthService.cs b/Birdmap/Services/AuthService.cs
new file mode 100644
index 0000000..82b77e5
--- /dev/null
+++ b/Birdmap/Services/AuthService.cs
@@ -0,0 +1,78 @@
+using Birdmap.Models;
+using Birdmap.Services.Interfaces;
+using Microsoft.Extensions.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Authentication;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Birdmap.Services
+{
+ public class AuthService : IAuthService
+ {
+ private readonly IConfiguration _configuration;
+
+ public AuthService(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public async Task AuthenticateUserAsync(string username, string password)
+ {
+ if (string.IsNullOrWhiteSpace(username) || string.IsNullOrEmpty(password))
+ throw new ArgumentException("Username or password cannot be null or empty.");
+
+ //var user = await _context.Users.SingleOrDefaultAsync(u => u.Name == username)
+ var user = await Temp_GetUserAsync(_configuration)
+ ?? throw new AuthenticationException();
+
+ if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
+ throw new AuthenticationException();
+
+ return user;
+ }
+
+ private Task Temp_GetUserAsync(IConfiguration configuration)
+ {
+ var name = configuration["BasicAuth:Username"];
+ var pass = configuration["BasicAuth:Password"];
+
+ CreatePasswordHash(pass, out var hash, out var salt);
+ return Task.FromResult(new User
+ {
+ Name = name,
+ PasswordHash = hash,
+ PasswordSalt = salt,
+ });
+ }
+
+ private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
+ {
+ if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
+
+ using var hmac = new System.Security.Cryptography.HMACSHA512();
+
+ passwordSalt = hmac.Key;
+ passwordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
+ }
+
+ private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
+ {
+ if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
+ if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
+ if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
+
+ using var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt);
+
+ var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
+ for (int i = 0; i < computedHash.Length; i++)
+ {
+ if (computedHash[i] != storedHash[i]) return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Birdmap/Services/Interfaces/IAuthService.cs b/Birdmap/Services/Interfaces/IAuthService.cs
new file mode 100644
index 0000000..11d4b6f
--- /dev/null
+++ b/Birdmap/Services/Interfaces/IAuthService.cs
@@ -0,0 +1,10 @@
+using Birdmap.Models;
+using System.Threading.Tasks;
+
+namespace Birdmap.Services.Interfaces
+{
+ public interface IAuthService
+ {
+ Task AuthenticateUserAsync(string username, string password);
+ }
+}
diff --git a/Birdmap/Startup.cs b/Birdmap/Startup.cs
index d6e1c5a..e95c385 100644
--- a/Birdmap/Startup.cs
+++ b/Birdmap/Startup.cs
@@ -1,3 +1,6 @@
+using Birdmap.Services;
+using Birdmap.Services.Interfaces;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
@@ -6,6 +9,10 @@ using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.Text;
+using System.Text.Json;
namespace Birdmap
{
@@ -21,7 +28,33 @@ namespace Birdmap
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddControllersWithViews();
+ services.AddControllersWithViews()
+ .AddJsonOptions(opt =>
+ {
+ //opt.JsonSerializerOptions.PropertyNamingPolicy = new JsonNamingPolicy()
+ });
+
+ services.AddTransient();
+
+ var key = Encoding.ASCII.GetBytes(Configuration["BasicAuth:Secret"]);
+ services.AddAuthentication(opt =>
+ {
+ opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ })
+ .AddJwtBearer(opt =>
+ {
+ // opt.RequireHttpsMetadata = false;
+ opt.SaveToken = true;
+ opt.IncludeErrorDetails = true;
+ opt.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = new SymmetricSecurityKey(key),
+ ValidateIssuer = false,
+ ValidateAudience = false
+ };
+ });
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
@@ -50,11 +83,12 @@ namespace Birdmap
app.UseRouting();
+ app.UseAuthorization();
+ app.UseAuthentication();
+
app.UseEndpoints(endpoints =>
{
- endpoints.MapControllerRoute(
- name: "default",
- pattern: "{controller}/{action=Index}/{id?}");
+ endpoints.MapControllers();
});
app.UseSpa(spa =>
diff --git a/Birdmap/appsettings.json b/Birdmap/appsettings.json
index d9d9a9b..c60ae0a 100644
--- a/Birdmap/appsettings.json
+++ b/Birdmap/appsettings.json
@@ -6,5 +6,10 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
- "AllowedHosts": "*"
+ "AllowedHosts": "*",
+ "BasicAuth": {
+ "Username": "user",
+ "Password": "pass",
+ "Secret": "7vj.3KW.hYE!}4u6"
+ }
}