Added auth/authenticate endpoint
This commit is contained in:
		@@ -10,6 +10,7 @@
 | 
				
			|||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.9" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.9" />
 | 
					    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.9" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										60
									
								
								Birdmap/Controllers/AuthController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								Birdmap/Controllers/AuthController.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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<IActionResult> 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,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,11 +2,13 @@
 | 
				
			|||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Authorization;
 | 
				
			||||||
using Microsoft.AspNetCore.Mvc;
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Birdmap.Controllers
 | 
					namespace Birdmap.Controllers
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    [Authorize]
 | 
				
			||||||
    [ApiController]
 | 
					    [ApiController]
 | 
				
			||||||
    [Route("[controller]")]
 | 
					    [Route("[controller]")]
 | 
				
			||||||
    public class WeatherForecastController : ControllerBase
 | 
					    public class WeatherForecastController : ControllerBase
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								Birdmap/Exceptions/AuthenticationException.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Birdmap/Exceptions/AuthenticationException.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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.")
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								Birdmap/Models/AuthenticateRequest.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Birdmap/Models/AuthenticateRequest.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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; }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								Birdmap/Models/User.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Birdmap/Models/User.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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; }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										78
									
								
								Birdmap/Services/AuthService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								Birdmap/Services/AuthService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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<User> 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<User> 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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								Birdmap/Services/Interfaces/IAuthService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Birdmap/Services/Interfaces/IAuthService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					using Birdmap.Models;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Birdmap.Services.Interfaces
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public interface IAuthService
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Task<User> AuthenticateUserAsync(string username, string password);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,3 +1,6 @@
 | 
				
			|||||||
 | 
					using Birdmap.Services;
 | 
				
			||||||
 | 
					using Birdmap.Services.Interfaces;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Authentication.JwtBearer;
 | 
				
			||||||
using Microsoft.AspNetCore.Builder;
 | 
					using Microsoft.AspNetCore.Builder;
 | 
				
			||||||
using Microsoft.AspNetCore.Hosting;
 | 
					using Microsoft.AspNetCore.Hosting;
 | 
				
			||||||
using Microsoft.AspNetCore.HttpsPolicy;
 | 
					using Microsoft.AspNetCore.HttpsPolicy;
 | 
				
			||||||
@@ -6,6 +9,10 @@ using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
 | 
				
			|||||||
using Microsoft.Extensions.Configuration;
 | 
					using Microsoft.Extensions.Configuration;
 | 
				
			||||||
using Microsoft.Extensions.DependencyInjection;
 | 
					using Microsoft.Extensions.DependencyInjection;
 | 
				
			||||||
using Microsoft.Extensions.Hosting;
 | 
					using Microsoft.Extensions.Hosting;
 | 
				
			||||||
 | 
					using Microsoft.IdentityModel.Tokens;
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Text;
 | 
				
			||||||
 | 
					using System.Text.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Birdmap
 | 
					namespace Birdmap
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -21,7 +28,33 @@ namespace Birdmap
 | 
				
			|||||||
        // This method gets called by the runtime. Use this method to add services to the container.
 | 
					        // This method gets called by the runtime. Use this method to add services to the container.
 | 
				
			||||||
        public void ConfigureServices(IServiceCollection services)
 | 
					        public void ConfigureServices(IServiceCollection services)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            services.AddControllersWithViews();
 | 
					            services.AddControllersWithViews()
 | 
				
			||||||
 | 
					                .AddJsonOptions(opt =>
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    //opt.JsonSerializerOptions.PropertyNamingPolicy = new JsonNamingPolicy()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            services.AddTransient<IAuthService, AuthService>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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
 | 
					            // In production, the React files will be served from this directory
 | 
				
			||||||
            services.AddSpaStaticFiles(configuration =>
 | 
					            services.AddSpaStaticFiles(configuration =>
 | 
				
			||||||
@@ -50,11 +83,12 @@ namespace Birdmap
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            app.UseRouting();
 | 
					            app.UseRouting();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            app.UseAuthorization();
 | 
				
			||||||
 | 
					            app.UseAuthentication();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            app.UseEndpoints(endpoints =>
 | 
					            app.UseEndpoints(endpoints =>
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                endpoints.MapControllerRoute(
 | 
					                endpoints.MapControllers();
 | 
				
			||||||
                    name: "default",
 | 
					 | 
				
			||||||
                    pattern: "{controller}/{action=Index}/{id?}");
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            app.UseSpa(spa =>
 | 
					            app.UseSpa(spa =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,5 +6,10 @@
 | 
				
			|||||||
      "Microsoft.Hosting.Lifetime": "Information"
 | 
					      "Microsoft.Hosting.Lifetime": "Information"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "AllowedHosts": "*"
 | 
					  "AllowedHosts": "*",
 | 
				
			||||||
 | 
					  "BasicAuth": {
 | 
				
			||||||
 | 
					    "Username": "user",
 | 
				
			||||||
 | 
					    "Password": "pass",
 | 
				
			||||||
 | 
					    "Secret": "7vj.3KW.hYE!}4u6"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user