Added auth/authenticate endpoint
This commit is contained in:
parent
33884cdd2f
commit
0fa2d3a579
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user