diff --git a/Tsi1.Api/Tsi1.Api/Controllers/UserController.cs b/Tsi1.Api/Tsi1.Api/Controllers/UserController.cs new file mode 100644 index 0000000000000000000000000000000000000000..c952152dc9ddc1d0a5f5913c957fea1d84821af0 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Controllers/UserController.cs @@ -0,0 +1,61 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tsi1.Api.Infrastructure; +using Tsi1.Api.Models; +using Tsi1.BusinessLayer.Interfaces; + +namespace Tsi1.Api.Controllers +{ + [Authorize] + [Route("api/[controller]")] + [ApiController] + public class UserController : ControllerBase + { + private readonly IUserService _userService; + private readonly IJwtAuthManager _jwtAuthManager; + + public UserController(IUserService userService, IJwtAuthManager jwtAuthManager) + { + _userService = userService; + _jwtAuthManager = jwtAuthManager; + } + + [AllowAnonymous] + [HttpPost("Login")] + public async Task<IActionResult> Login(LoginRequest request) + { + var user = await _userService.Authenticate(request.UserName, request.Password); + + if (user == null) + { + return BadRequest(); + } + + var claims = new[] + { + new Claim(ClaimTypes.Name,user.Username), + new Claim(ClaimTypes.Role, user.UserType.Name) + }; + + var jwtResult = _jwtAuthManager.GenerateTokens(user.Username, claims, DateTime.Now); + + return Ok(new LoginResult + { + UserName = user.Username, + Role = user.UserType.Name, + AccessToken = jwtResult.AccessToken, + RefreshToken = jwtResult.RefreshToken.TokenString + }); + } + + [HttpGet("Register")] + public async Task<IActionResult> Register() + { + return Ok(); + } + + } +} diff --git a/Tsi1.Api/Tsi1.Api/Infrastructure/IJwtAuthManager.cs b/Tsi1.Api/Tsi1.Api/Infrastructure/IJwtAuthManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..e9342c6dbb7d95fbf866b95abdd4a4fe6142e254 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Infrastructure/IJwtAuthManager.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Immutable; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Tsi1.Api.Models; + +namespace Tsi1.Api.Infrastructure +{ + public interface IJwtAuthManager + { + IImmutableDictionary<string, RefreshToken> UsersRefreshTokensReadOnlyDictionary { get; } + JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now); + JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now); + void RemoveExpiredRefreshTokens(DateTime now); + void RemoveRefreshTokenByUserName(string userName); + (ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token); + } +} diff --git a/Tsi1.Api/Tsi1.Api/Infrastructure/JwtAuthManager.cs b/Tsi1.Api/Tsi1.Api/Infrastructure/JwtAuthManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..b8c4a7b47aa63f84cc285c80cb4beafd2cfb683d --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Infrastructure/JwtAuthManager.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using Tsi1.Api.Models; + +namespace Tsi1.Api.Infrastructure +{ + public class JwtAuthManager : IJwtAuthManager + { + public IImmutableDictionary<string, RefreshToken> UsersRefreshTokensReadOnlyDictionary => _usersRefreshTokens.ToImmutableDictionary(); + private readonly ConcurrentDictionary<string, RefreshToken> _usersRefreshTokens; // can store in a database or a distributed cache + private readonly JwtTokenConfig _jwtTokenConfig; + private readonly byte[] _secret; + + public JwtAuthManager(JwtTokenConfig jwtTokenConfig) + { + _jwtTokenConfig = jwtTokenConfig; + _usersRefreshTokens = new ConcurrentDictionary<string, RefreshToken>(); + _secret = Encoding.ASCII.GetBytes(jwtTokenConfig.Secret); + } + + // optional: clean up expired refresh tokens + public void RemoveExpiredRefreshTokens(DateTime now) + { + var expiredTokens = _usersRefreshTokens.Where(x => x.Value.ExpireAt < now).ToList(); + foreach (var expiredToken in expiredTokens) + { + _usersRefreshTokens.TryRemove(expiredToken.Key, out _); + } + } + + // can be more specific to ip, user agent, device name, etc. + public void RemoveRefreshTokenByUserName(string userName) + { + var refreshTokens = _usersRefreshTokens.Where(x => x.Value.UserName == userName).ToList(); + foreach (var refreshToken in refreshTokens) + { + _usersRefreshTokens.TryRemove(refreshToken.Key, out _); + } + } + + public JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now) + { + var shouldAddAudienceClaim = string.IsNullOrWhiteSpace(claims?.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Aud)?.Value); + var jwtToken = new JwtSecurityToken( + _jwtTokenConfig.Issuer, + shouldAddAudienceClaim ? _jwtTokenConfig.Audience : string.Empty, + claims, + expires: now.AddMinutes(_jwtTokenConfig.AccessTokenExpiration), + signingCredentials: new SigningCredentials(new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256Signature)); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtToken); + + var refreshToken = new RefreshToken + { + UserName = username, + TokenString = GenerateRefreshTokenString(), + ExpireAt = now.AddMinutes(_jwtTokenConfig.RefreshTokenExpiration) + }; + _usersRefreshTokens.AddOrUpdate(refreshToken.TokenString, refreshToken, (s, t) => refreshToken); + + return new JwtAuthResult + { + AccessToken = accessToken, + RefreshToken = refreshToken + }; + } + + public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now) + { + var (principal, jwtToken) = DecodeJwtToken(accessToken); + if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature)) + { + throw new SecurityTokenException("Invalid token"); + } + + var userName = principal.Identity.Name; + if (!_usersRefreshTokens.TryGetValue(refreshToken, out var existingRefreshToken)) + { + throw new SecurityTokenException("Invalid token"); + } + if (existingRefreshToken.UserName != userName || existingRefreshToken.ExpireAt < now) + { + throw new SecurityTokenException("Invalid token"); + } + + return GenerateTokens(userName, principal.Claims.ToArray(), now); // need to recover the original claims + } + + public (ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new SecurityTokenException("Invalid token"); + } + var principal = new JwtSecurityTokenHandler() + .ValidateToken(token, + new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = _jwtTokenConfig.Issuer, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(_secret), + ValidAudience = _jwtTokenConfig.Audience, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(1) + }, + out var validatedToken); + return (principal, validatedToken as JwtSecurityToken); + } + + private static string GenerateRefreshTokenString() + { + var randomNumber = new byte[32]; + using var randomNumberGenerator = RandomNumberGenerator.Create(); + randomNumberGenerator.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Infrastructure/JwtRefreshTokenCache.cs b/Tsi1.Api/Tsi1.Api/Infrastructure/JwtRefreshTokenCache.cs new file mode 100644 index 0000000000000000000000000000000000000000..fce4adac5962d21187c6675152a00dec4f39492d --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Infrastructure/JwtRefreshTokenCache.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Tsi1.Api.Infrastructure +{ + public class JwtRefreshTokenCache : IHostedService, IDisposable + { + private Timer _timer; + private readonly IJwtAuthManager _jwtAuthManager; + + public JwtRefreshTokenCache(IJwtAuthManager jwtAuthManager) + { + _jwtAuthManager = jwtAuthManager; + } + + public Task StartAsync(CancellationToken stoppingToken) + { + // remove expired refresh tokens from cache every minute + _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + return Task.CompletedTask; + } + + private void DoWork(object state) + { + _jwtAuthManager.RemoveExpiredRefreshTokens(DateTime.Now); + } + + public Task StopAsync(CancellationToken stoppingToken) + { + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Infrastructure/JwtTokenConfig.cs b/Tsi1.Api/Tsi1.Api/Infrastructure/JwtTokenConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..854d596cf716e1388a8c507ba3c9426b77031016 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Infrastructure/JwtTokenConfig.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Tsi1.Api.Infrastructure +{ + public class JwtTokenConfig + { + [JsonPropertyName("secret")] + public string Secret { get; set; } + + [JsonPropertyName("issuer")] + public string Issuer { get; set; } + + [JsonPropertyName("audience")] + public string Audience { get; set; } + + [JsonPropertyName("accessTokenExpiration")] + public int AccessTokenExpiration { get; set; } + + [JsonPropertyName("refreshTokenExpiration")] + public int RefreshTokenExpiration { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Models/JwtAuthResult.cs b/Tsi1.Api/Tsi1.Api/Models/JwtAuthResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..25cf13759ba65ccecbc6d518680f7d9e7af26d09 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Models/JwtAuthResult.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace Tsi1.Api.Models +{ + public class JwtAuthResult + { + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } + + [JsonPropertyName("refreshToken")] + public RefreshToken RefreshToken { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Models/LoginRequest.cs b/Tsi1.Api/Tsi1.Api/Models/LoginRequest.cs new file mode 100644 index 0000000000000000000000000000000000000000..96e87b18d03e7185430f760fb0a5c9caefe6611f --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Models/LoginRequest.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Tsi1.Api.Models +{ + public class LoginRequest + { + [Required] + [JsonPropertyName("username")] + public string UserName { get; set; } + + [Required] + [JsonPropertyName("password")] + public string Password { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Models/LoginResult.cs b/Tsi1.Api/Tsi1.Api/Models/LoginResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..3fd35ddba714f89b0b6bc43f705af662ffd3c6dc --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Models/LoginResult.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Tsi1.Api.Models +{ + public class LoginResult + { + [JsonPropertyName("username")] + public string UserName { get; set; } + + [JsonPropertyName("role")] + public string Role { get; set; } + + [JsonPropertyName("originalUserName")] + public string OriginalUserName { get; set; } + + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } + + [JsonPropertyName("refreshToken")] + public string RefreshToken { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Models/RefreshToken.cs b/Tsi1.Api/Tsi1.Api/Models/RefreshToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..fec06fc0f2656cdf181ebb4158fcb60befa8b46b --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Models/RefreshToken.cs @@ -0,0 +1,18 @@ +using System; +using System.Text.Json.Serialization; + +namespace Tsi1.Api.Models +{ + public class RefreshToken + { + [JsonPropertyName("username")] + public string UserName { get; set; } // can be used for usage tracking + // can optionally include other metadata, such as user agent, ip address, device name, and so on + + [JsonPropertyName("tokenString")] + public string TokenString { get; set; } + + [JsonPropertyName("expireAt")] + public DateTime ExpireAt { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Models/RefreshTokenRequest.cs b/Tsi1.Api/Tsi1.Api/Models/RefreshTokenRequest.cs new file mode 100644 index 0000000000000000000000000000000000000000..9535cd536762adf0b97507327174e506c7e9e8c6 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Models/RefreshTokenRequest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Tsi1.Api.Models +{ + public class RefreshTokenRequest + { + [JsonPropertyName("refreshToken")] + public string RefreshToken { get; set; } + } +} diff --git a/Tsi1.Api/Tsi1.Api/Properties/launchSettings.json b/Tsi1.Api/Tsi1.Api/Properties/launchSettings.json index 24cce69c764987715a6740867b6857e7b9dbd4df..c733fd6ce95de3866e074b5acfe589be558c7f72 100644 --- a/Tsi1.Api/Tsi1.Api/Properties/launchSettings.json +++ b/Tsi1.Api/Tsi1.Api/Properties/launchSettings.json @@ -12,7 +12,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "weatherforecast", + "launchUrl": "swagger/index.html", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -20,7 +20,7 @@ "Tsi1.Api": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "weatherforecast", + "launchUrl": "swagger/index.html", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Tsi1.Api/Tsi1.Api/Startup.cs b/Tsi1.Api/Tsi1.Api/Startup.cs index 6d3605800e6f7dccf3e2bd7f1d6ce5a882fec727..766b76c5b01dde462ff86d5cbbacbb965cb87b7e 100644 --- a/Tsi1.Api/Tsi1.Api/Startup.cs +++ b/Tsi1.Api/Tsi1.Api/Startup.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; @@ -11,6 +13,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Tsi1.Api.Infrastructure; +using Tsi1.BusinessLayer.Interfaces; +using Tsi1.BusinessLayer.Services; using Tsi1.DataLayer; namespace Tsi1.Api @@ -30,6 +37,57 @@ namespace Tsi1.Api services.AddControllers(); services.AddDbContext<Tsi1Context>(x => x.UseNpgsql(Configuration.GetConnectionString("PostgreSql"))); + services.AddScoped<IUserService, UserService>(); + + var jwtTokenConfig = Configuration.GetSection("jwtTokenConfig").Get<JwtTokenConfig>(); + services.AddSingleton(jwtTokenConfig); + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(x => + { + x.RequireHttpsMetadata = true; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtTokenConfig.Issuer, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtTokenConfig.Secret)), + ValidAudience = jwtTokenConfig.Audience, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(1) + }; + }); + services.AddSingleton<IJwtAuthManager, JwtAuthManager>(); + services.AddHostedService<JwtRefreshTokenCache>(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Tsi1 api", Version = "v1" }); + + var securityScheme = new OpenApiSecurityScheme + { + Name = "JWT Authentication", + Description = "Enter JWT Bearer token **_only_**", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", // must be lower case + BearerFormat = "JWT", + Reference = new OpenApiReference + { + Id = JwtBearerDefaults.AuthenticationScheme, + Type = ReferenceType.SecurityScheme + } + }; + c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + {securityScheme, new string[] { }} + }); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -42,8 +100,19 @@ namespace Tsi1.Api app.UseHttpsRedirection(); + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Tsi1 api V1"); + c.DocumentTitle = "Tsi1 api"; + //c.RoutePrefix = string.Empty; + }); + app.UseRouting(); + app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); + + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => diff --git a/Tsi1.Api/Tsi1.Api/Tsi1.Api.csproj b/Tsi1.Api/Tsi1.Api/Tsi1.Api.csproj index cf8609719d70846dff5b7571b899da73861f6312..1ed4b443527a84acaf750eab3427ac11002a946a 100644 --- a/Tsi1.Api/Tsi1.Api/Tsi1.Api.csproj +++ b/Tsi1.Api/Tsi1.Api/Tsi1.Api.csproj @@ -2,10 +2,12 @@ <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> + <UserSecretsId>3e0d10d1-b8c2-4975-b04b-a2b796a71d30</UserSecretsId> </PropertyGroup> <ItemGroup> - <Folder Include="Controllers\" /> + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.9" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> </ItemGroup> <ItemGroup> diff --git a/Tsi1.Api/Tsi1.Api/Tsi1.Api.csproj.user b/Tsi1.Api/Tsi1.Api/Tsi1.Api.csproj.user new file mode 100644 index 0000000000000000000000000000000000000000..85e159c74d1484be795adf84409475cb0f95c4c9 --- /dev/null +++ b/Tsi1.Api/Tsi1.Api/Tsi1.Api.csproj.user @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <Controller_SelectedScaffolderID>ApiControllerWithActionsScaffolder</Controller_SelectedScaffolderID> + <Controller_SelectedScaffolderCategoryPath>root/Controller</Controller_SelectedScaffolderCategoryPath> + <WebStackScaffolding_ControllerDialogWidth>600</WebStackScaffolding_ControllerDialogWidth> + </PropertyGroup> +</Project> \ No newline at end of file diff --git a/Tsi1.Api/Tsi1.Api/appsettings.json b/Tsi1.Api/Tsi1.Api/appsettings.json index c796235b460965778236fabc90cdff4ede6f8d33..e957e7ef0ebdae633178642d900f87a2f773aee3 100644 --- a/Tsi1.Api/Tsi1.Api/appsettings.json +++ b/Tsi1.Api/Tsi1.Api/appsettings.json @@ -2,6 +2,13 @@ "ConnectionStrings": { "PostgreSql": "Host=localhost;Database=tsi1;Username=postgres;Password=111111" }, + "jwtTokenConfig": { + "secret": "1234567890123456789", + "issuer": "https://localhost:44363", + "audience": "https://localhost:44363", + "accessTokenExpiration": 20, + "refreshTokenExpiration": 60 + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IUserService.cs b/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IUserService.cs new file mode 100644 index 0000000000000000000000000000000000000000..2c715b938f377846db00cc3d129c9aeaf7ac9e5f --- /dev/null +++ b/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IUserService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Tsi1.DataLayer.Entities; + +namespace Tsi1.BusinessLayer.Interfaces +{ + public interface IUserService + { + Task<User> Authenticate(string username, string password); + } +} diff --git a/Tsi1.Api/Tsi1.BusinessLayer/Services/UserService.cs b/Tsi1.Api/Tsi1.BusinessLayer/Services/UserService.cs new file mode 100644 index 0000000000000000000000000000000000000000..935bddd00e09eee75706f501b247e7c3602a4554 --- /dev/null +++ b/Tsi1.Api/Tsi1.BusinessLayer/Services/UserService.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Tsi1.BusinessLayer.Interfaces; +using Tsi1.DataLayer; +using Tsi1.DataLayer.Entities; + +namespace Tsi1.BusinessLayer.Services +{ + public class UserService : IUserService + { + private readonly Tsi1Context _context; + + public UserService(Tsi1Context context) + { + _context = context; + } + + public async Task<User> Authenticate(string username, string password) + { + var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username); + + user = new User() + { + Id = 1, + Username = "lucca", + UserType = new UserType() + { + Id = 1, + Name = "admin" + } + }; + + if (user == null) + { + // no existe el usuario + } + + if (user.Password != password) + { + // contraseña incorrecta + } + + return user; + } + } +}