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;
+        }
+    }
+}