diff --git a/IdentityServer.Api/Controllers/IdentityController.cs b/IdentityServer.Api/Controllers/IdentityController.cs new file mode 100644 index 0000000..d333781 --- /dev/null +++ b/IdentityServer.Api/Controllers/IdentityController.cs @@ -0,0 +1,41 @@ +using IdentityServer.Application.Commands; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; + +namespace IdentityServer.Api.Controllers +{ + [ApiController] + [Route("identity")] + public class IdentityController : ControllerBase + { + private readonly IMediator _mediator; + + public IdentityController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpPost("authenticate/{userName}/{password}")] + public async Task AuthenticateUser([FromRoute] AuthenticateUser authenticateUser) + { + var result = await _mediator.Send(authenticateUser); + + if (result != null) + return Ok(result); + else + return BadRequest(); + } + + [HttpPost("authorize/{token}")] + public async Task AuthorizeToken([FromRoute] AuthorizeToken authorizeToken) + { + var result = await _mediator.Send(authorizeToken); + + if (result != null) + return Ok(result); + else + return BadRequest(); + } + } +} diff --git a/IdentityServer.Api/Controllers/WeatherForecastController.cs b/IdentityServer.Api/Controllers/WeatherForecastController.cs deleted file mode 100644 index 4c4f395..0000000 --- a/IdentityServer.Api/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace IdentityServer.Api.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/IdentityServer.Api/IdentityServer.Api.csproj b/IdentityServer.Api/IdentityServer.Api.csproj index d12c450..639c3e4 100644 --- a/IdentityServer.Api/IdentityServer.Api.csproj +++ b/IdentityServer.Api/IdentityServer.Api.csproj @@ -1,8 +1,27 @@ - + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + diff --git a/IdentityServer.Api/WeatherForecast.cs b/IdentityServer.Api/WeatherForecast.cs deleted file mode 100644 index f28828d..0000000 --- a/IdentityServer.Api/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace IdentityServer.Api -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } - } -} diff --git a/IdentityServer.Application/Class1.cs b/IdentityServer.Application/Class1.cs deleted file mode 100644 index aa42caa..0000000 --- a/IdentityServer.Application/Class1.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace IdentityServer.Application -{ - public class Class1 - { - } -} diff --git a/IdentityServer.Application/CommandHandlers/AuthenticateUserHandler.cs b/IdentityServer.Application/CommandHandlers/AuthenticateUserHandler.cs new file mode 100644 index 0000000..1faefd8 --- /dev/null +++ b/IdentityServer.Application/CommandHandlers/AuthenticateUserHandler.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using IdentityServer.Application.Commands; +using IdentityServer.Application.Services; +using IdentityServer.PublishedLanguage.Dto; +using MediatR; +using System.Threading; +using System.Threading.Tasks; + +namespace IdentityServer.Application.CommandHandlers +{ + public class AuthenticateUserHandler : IRequestHandler + { + private readonly IUserService _userService; + private readonly IMapper _mapper; + + public AuthenticateUserHandler(IUserService userService, IMapper mapper) + { + _userService = userService; + _mapper = mapper; + } + + public async Task Handle(AuthenticateUser command, CancellationToken cancellationToken) + { + var internalToken = await _userService.Authenticate(command.UserName, command.Password); + if (internalToken == null) + return null; + + var token = _mapper.Map(internalToken); + return token; + } + } +} diff --git a/IdentityServer.Application/CommandHandlers/AuthorizeTokenHandler.cs b/IdentityServer.Application/CommandHandlers/AuthorizeTokenHandler.cs new file mode 100644 index 0000000..1821469 --- /dev/null +++ b/IdentityServer.Application/CommandHandlers/AuthorizeTokenHandler.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using IdentityServer.Application.Commands; +using IdentityServer.Application.Services; +using IdentityServer.PublishedLanguage.Dto; +using MediatR; +using System.Threading; +using System.Threading.Tasks; + +namespace IdentityServer.Application.CommandHandlers +{ + public class AuthorizeTokenHandler : IRequestHandler + { + private readonly IUserService _userService; + private readonly IMapper _mapper; + + public AuthorizeTokenHandler(IUserService userService, IMapper mapper) + { + _userService = userService; + _mapper = mapper; + } + + public async Task Handle(AuthorizeToken command, CancellationToken cancellationToken) + { + var appUser = await _userService.Authorize(command.Token); + if (appUser == null) + return null; + + var user = _mapper.Map(appUser); + return user; + } + } +} diff --git a/IdentityServer.Application/Commands/AuthenticateUser.cs b/IdentityServer.Application/Commands/AuthenticateUser.cs new file mode 100644 index 0000000..07035f2 --- /dev/null +++ b/IdentityServer.Application/Commands/AuthenticateUser.cs @@ -0,0 +1,16 @@ +using IdentityServer.PublishedLanguage.Dto; + +namespace IdentityServer.Application.Commands +{ + public class AuthenticateUser : Command + { + public string UserName { get; set; } + public string Password { get; set; } + + public AuthenticateUser(string userName, string password) + { + UserName = userName; + Password = password; + } + } +} diff --git a/IdentityServer.Application/Commands/AuthorizeToken.cs b/IdentityServer.Application/Commands/AuthorizeToken.cs new file mode 100644 index 0000000..280d889 --- /dev/null +++ b/IdentityServer.Application/Commands/AuthorizeToken.cs @@ -0,0 +1,9 @@ +using IdentityServer.PublishedLanguage.Dto; + +namespace IdentityServer.Application.Commands +{ + public class AuthorizeToken : Command + { + public string Token { get; set; } + } +} diff --git a/IdentityServer.Application/Commands/Command.cs b/IdentityServer.Application/Commands/Command.cs new file mode 100644 index 0000000..dc28127 --- /dev/null +++ b/IdentityServer.Application/Commands/Command.cs @@ -0,0 +1,45 @@ +using MediatR; +using System; +using System.Collections.Generic; + +namespace IdentityServer.Application.Commands +{ + public abstract class Command : ICommand, IRequest + { + public Metadata Metadata { get; } + + protected Command() + { + Metadata = new Metadata() { CorrelationId = Guid.NewGuid() }; + } + + protected Command(Metadata metadata) + { + Metadata = metadata; + } + } + + public interface ICommand + { + } + + public class Metadata : Dictionary + { + public const string CorrelationIdKey = "CorrelationId"; + + public Guid CorrelationId + { + get + { + return Guid.Parse(this[CorrelationIdKey]); + } + set + { + if (ContainsKey(CorrelationIdKey)) + this[CorrelationIdKey] = value.ToString(); + else + Add(CorrelationIdKey, value.ToString()); + } + } + } +} diff --git a/IdentityServer.Application/DependencyInjectionExtensions.cs b/IdentityServer.Application/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..ac4cbe3 --- /dev/null +++ b/IdentityServer.Application/DependencyInjectionExtensions.cs @@ -0,0 +1,20 @@ +using IdentityServer.Application.Services; +using IdentityServer.Application.Stores; +using Microsoft.Extensions.DependencyInjection; + +namespace IdentityServer.Application +{ + public static class DependencyInjectionExtensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddStores(); + services.AddScoped(); + } + + private static void AddStores(this IServiceCollection services) + { + services.AddSingleton(); + } + } +} diff --git a/IdentityServer.Application/IdentityServer.Application.csproj b/IdentityServer.Application/IdentityServer.Application.csproj index 9f5c4f4..7fd2280 100644 --- a/IdentityServer.Application/IdentityServer.Application.csproj +++ b/IdentityServer.Application/IdentityServer.Application.csproj @@ -4,4 +4,18 @@ netstandard2.0 + + + + + + + + + + + + + + diff --git a/IdentityServer.Application/Mappings/MappingProfile.cs b/IdentityServer.Application/Mappings/MappingProfile.cs new file mode 100644 index 0000000..9e44121 --- /dev/null +++ b/IdentityServer.Application/Mappings/MappingProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using IdentityServer.Domain.Entities; +using dto = IdentityServer.PublishedLanguage.Dto; +using models = IdentityServer.Domain.Models; + +namespace IdentityServer.Application.Mappings +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + CreateMap(); + } + } +} diff --git a/IdentityServer.Application/Queries/Query.cs b/IdentityServer.Application/Queries/Query.cs new file mode 100644 index 0000000..476452a --- /dev/null +++ b/IdentityServer.Application/Queries/Query.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace IdentityServer.Application.Queries +{ + public abstract class Query : IRequest, IBaseRequest { } +} diff --git a/IdentityServer.Application/Services/IUserService.cs b/IdentityServer.Application/Services/IUserService.cs new file mode 100644 index 0000000..4bf7618 --- /dev/null +++ b/IdentityServer.Application/Services/IUserService.cs @@ -0,0 +1,12 @@ +using IdentityServer.Domain.Entities; +using IdentityServer.Domain.Models; +using System.Threading.Tasks; + +namespace IdentityServer.Application.Services +{ + public interface IUserService + { + Task Authenticate(string userName, string password); + Task Authorize(string token); + } +} \ No newline at end of file diff --git a/IdentityServer.Application/Services/UserService.cs b/IdentityServer.Application/Services/UserService.cs new file mode 100644 index 0000000..7225d2c --- /dev/null +++ b/IdentityServer.Application/Services/UserService.cs @@ -0,0 +1,46 @@ +using IdentityServer.Application.Stores; +using IdentityServer.Domain.Entities; +using IdentityServer.Domain.Models; +using IdentityServer.Domain.Repositories; +using System; +using System.Threading.Tasks; + +namespace IdentityServer.Application.Services +{ + public class UserService : IUserService + { + private readonly ISecurityStore _securityStore; + private readonly IIdentityRepository _identityRepository; + + public UserService(ISecurityStore securityStore, IIdentityRepository identityRepository) + { + _securityStore = securityStore; + _identityRepository = identityRepository; + } + + public async Task Authenticate(string userName, string password) + { + var user = await _identityRepository.GetAppUser(userName, password); + if (user == null) + return null; + + var tokenRaw = $"{Guid.NewGuid()}-{Guid.NewGuid()}-{user.UserId}"; + _securityStore.SetToken(tokenRaw, user.UserId); + + var token = new Token() { Raw = tokenRaw }; + return token; + } + + public async Task Authorize(string token) + { + var tokenValidation = _securityStore.ValidateToken(token); + if (tokenValidation.Success) + { + var user = await _identityRepository.GetAppUser(tokenValidation.UserId); + return user; + } + + return null; + } + } +} diff --git a/IdentityServer.Application/Stores/ISecurityStore.cs b/IdentityServer.Application/Stores/ISecurityStore.cs new file mode 100644 index 0000000..a9e8cc5 --- /dev/null +++ b/IdentityServer.Application/Stores/ISecurityStore.cs @@ -0,0 +1,10 @@ +using IdentityServer.Domain.Models; + +namespace IdentityServer.Application.Stores +{ + public interface ISecurityStore + { + void SetToken(string token, int userId); + TokenValidation ValidateToken(string token); + } +} diff --git a/IdentityServer.Application/Stores/SecurityStore.cs b/IdentityServer.Application/Stores/SecurityStore.cs new file mode 100644 index 0000000..1bf4d1c --- /dev/null +++ b/IdentityServer.Application/Stores/SecurityStore.cs @@ -0,0 +1,50 @@ +using IdentityServer.Domain.Models; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace IdentityServer.Application.Stores +{ + public class SecurityStore : ISecurityStore + { + private ConcurrentDictionary> Tokens { get; } + + public SecurityStore() + { + Tokens = new ConcurrentDictionary>(); + } + + public void SetToken(string token, int userId) + { + var registered = Tokens.TryGetValue(userId, out List list); + + if (registered) + list.Add(new Token() { Raw = token }); + else + Tokens.TryAdd(userId, new List() { new Token() { Raw = token } }); + } + + public TokenValidation ValidateToken(string token) + { + var lastIndexOfSeparator = token.LastIndexOf('-') + 1; + var userIdString = token.Substring(lastIndexOfSeparator, token.Length - lastIndexOfSeparator); + + if (!int.TryParse(userIdString, out int userId)) + return InvalidToken; + + var registered = Tokens.TryGetValue(userId, out List list); + + if (!registered) + return InvalidToken; + + var valid = list.FirstOrDefault(z => z.Raw == token); + if (valid != null) + return new TokenValidation() { Success = true, UserId = userId }; + + return InvalidToken; + } + + private TokenValidation InvalidToken => new TokenValidation() { Success = false }; + } +} diff --git a/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs b/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs index f847f3c..7a33807 100644 --- a/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs +++ b/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs @@ -15,6 +15,11 @@ namespace IdentityServer.Domain.Data.Repositories _dbContext = dbContext; } + public Task GetAppUser(int userId) + { + return _dbContext.AppUsers.FirstOrDefaultAsync(z => z.UserId == userId); + } + public Task GetAppUser(string userName, string password) { return _dbContext.AppUsers.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password); diff --git a/IdentityServer.Domain/Models/Token.cs b/IdentityServer.Domain/Models/Token.cs new file mode 100644 index 0000000..8222ba8 --- /dev/null +++ b/IdentityServer.Domain/Models/Token.cs @@ -0,0 +1,10 @@ +using System; + +namespace IdentityServer.Domain.Models +{ + public class Token + { + public string Raw { get; set; } + public DateTime ValidUntil { get; set; } + } +} diff --git a/IdentityServer.Domain/Models/TokenValidation.cs b/IdentityServer.Domain/Models/TokenValidation.cs new file mode 100644 index 0000000..91f0096 --- /dev/null +++ b/IdentityServer.Domain/Models/TokenValidation.cs @@ -0,0 +1,8 @@ +namespace IdentityServer.Domain.Models +{ + public class TokenValidation + { + public bool Success { get; set; } + public int UserId { get; set; } + } +} diff --git a/IdentityServer.Domain/Repositories/IIdentityRepository.cs b/IdentityServer.Domain/Repositories/IIdentityRepository.cs index 1c6f544..ceded78 100644 --- a/IdentityServer.Domain/Repositories/IIdentityRepository.cs +++ b/IdentityServer.Domain/Repositories/IIdentityRepository.cs @@ -1,10 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; +using IdentityServer.Domain.Entities; +using System.Threading.Tasks; namespace IdentityServer.Domain.Repositories { public interface IIdentityRepository { + Task GetAppUser(int userId); + Task GetAppUser(string userName, string password); } } diff --git a/IdentityServer.PublishedLanguage/Dto/Token.cs b/IdentityServer.PublishedLanguage/Dto/Token.cs new file mode 100644 index 0000000..81e6b78 --- /dev/null +++ b/IdentityServer.PublishedLanguage/Dto/Token.cs @@ -0,0 +1,10 @@ +using System; + +namespace IdentityServer.PublishedLanguage.Dto +{ + public class Token + { + public string Raw { get; set; } + public DateTime ValidUntil { get; set; } + } +} diff --git a/IdentityServer.PublishedLanguage/Dto/User.cs b/IdentityServer.PublishedLanguage/Dto/User.cs new file mode 100644 index 0000000..5c55b9c --- /dev/null +++ b/IdentityServer.PublishedLanguage/Dto/User.cs @@ -0,0 +1,8 @@ +namespace IdentityServer.PublishedLanguage.Dto +{ + public class User + { + public int UserId { get; set; } + public string UserName { get; set; } + } +} diff --git a/IdentityServer.PublishedLanguage/IdentityServer.PublishedLanguage.csproj b/IdentityServer.PublishedLanguage/IdentityServer.PublishedLanguage.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/IdentityServer.PublishedLanguage/IdentityServer.PublishedLanguage.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/IdentityServer.sln b/IdentityServer.sln index 3130fd5..80abf51 100644 --- a/IdentityServer.sln +++ b/IdentityServer.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityServer.Domain", "Id EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityServer.Domain.Data", "IdentityServer.Domain.Data\IdentityServer.Domain.Data.csproj", "{CE81A435-49AC-4544-A381-FAC91BEB3C49}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityServer.PublishedLanguage", "IdentityServer.PublishedLanguage\IdentityServer.PublishedLanguage.csproj", "{67B4D1FF-D02E-4DA6-9FB8-F71667360448}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {CE81A435-49AC-4544-A381-FAC91BEB3C49}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE81A435-49AC-4544-A381-FAC91BEB3C49}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE81A435-49AC-4544-A381-FAC91BEB3C49}.Release|Any CPU.Build.0 = Release|Any CPU + {67B4D1FF-D02E-4DA6-9FB8-F71667360448}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67B4D1FF-D02E-4DA6-9FB8-F71667360448}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67B4D1FF-D02E-4DA6-9FB8-F71667360448}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67B4D1FF-D02E-4DA6-9FB8-F71667360448}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -52,6 +58,7 @@ Global {6556D255-AF22-478E-A71A-BE37C16D5EE4} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C} {5890B079-3CB0-4AD6-8809-BB2E081590B1} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C} {CE81A435-49AC-4544-A381-FAC91BEB3C49} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C} + {67B4D1FF-D02E-4DA6-9FB8-F71667360448} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E93DC46D-9C55-4A05-B299-497CDD90747E} diff --git a/Notes.txt b/Notes.txt index 64d2e32..4d9f3a0 100644 --- a/Notes.txt +++ b/Notes.txt @@ -4,4 +4,8 @@ dotnet publish --configuration Release --runtime win7-x64 Create windows service: sc create NetworkResurrector.Api binPath= "" -####################################################################################################################################################### \ No newline at end of file +####################################################################################################################################################### + + +TO DO: +- Cache for users \ No newline at end of file