diff --git a/NetworkResurrector.Api/Authentication/BasicAuthenticationHandler.cs b/NetworkResurrector.Api/Authentication/BasicAuthenticationHandler.cs new file mode 100644 index 0000000..ec1913a --- /dev/null +++ b/NetworkResurrector.Api/Authentication/BasicAuthenticationHandler.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NetworkResurrector.Application.Services; +using NetworkResurrector.Domain.Entities; +using System; +using System.Linq; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace NetworkResurrector.Api.Authentication +{ + public class BasicAuthenticationHandler : AuthenticationHandler + { + private readonly IUserService _userService; + + public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService) + : base(options, logger, encoder, clock) + { + _userService = userService; + } + + protected override async Task HandleAuthenticateAsync() + { + if (!Request.Headers.ContainsKey("Authorization")) + return AuthenticateResult.Fail("Missing Authorization Header"); + + User user; + try + { + var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); + var credentialBytes = Convert.FromBase64String(authHeader.Parameter); + var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':'); + var username = credentials.First(); + var password = credentials.Last(); + user = await _userService.Authenticate(username, password); + } + catch + { + return AuthenticateResult.Fail("Invalid Authorization Header"); + } + + if (user == null) + return AuthenticateResult.Fail("Invalid Username or Password"); + + var claims = new[] { + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.UserName), + }; + + var identity = new ClaimsIdentity(claims, Scheme.Name); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, Scheme.Name); + + return AuthenticateResult.Success(ticket); + } + } +} diff --git a/NetworkResurrector.Api/Startup.cs b/NetworkResurrector.Api/Startup.cs index 79c7b34..81e9148 100644 --- a/NetworkResurrector.Api/Startup.cs +++ b/NetworkResurrector.Api/Startup.cs @@ -1,14 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using AutoMapper; +using MediatR; +using MediatR.Pipeline; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using NetworkResurrector.Api.Authentication; +using NetworkResurrector.Api.Swagger; +using NetworkResurrector.Application; +using Newtonsoft.Json; +using System.Reflection; namespace NetworkResurrector.Api { @@ -24,25 +27,57 @@ namespace NetworkResurrector.Api // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddControllers(); + services.AddControllers() + .AddNewtonsoftJson(o => o.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc); + + // configure basic authentication + services.AddAuthentication("BasicAuthentication") + .AddScheme("BasicAuthentication", null); + + // MediatR + services.AddMediatR(GetMediatRAssemblies()); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); + + // AutoMapper + services.AddAutoMapper( + typeof(Application.Mappings.MappingProfile).Assembly); + + // Swagger + services.AddSwagger(); + + // Application + services.AddApplicationServices(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + // global cors policy + app.UseCors(x => x + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); - + app.UseAuthentication(); app.UseAuthorization(); - app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + app.ConfigureSwagger(); + } + + private Assembly[] GetMediatRAssemblies() + { + var assembly = typeof(Application.Queries.GetToken).Assembly; + return new Assembly[] { assembly }; } } } diff --git a/NetworkResurrector.Api/Swagger/DtoSchemaFilter.cs b/NetworkResurrector.Api/Swagger/DtoSchemaFilter.cs new file mode 100644 index 0000000..e833c90 --- /dev/null +++ b/NetworkResurrector.Api/Swagger/DtoSchemaFilter.cs @@ -0,0 +1,36 @@ +using Microsoft.OpenApi.Models; +using NetworkResurrector.Application.Commands; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Linq; + +namespace NetworkResurrector.Api.Swagger +{ + public class DtoSchemaFilter : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var targetType = context.Type; + while (targetType != null) + { + if (typeof(ICommand).IsAssignableFrom(targetType)) + { + foreach (var property in schema.Properties.ToList()) + { + property.Value.ReadOnly = false; + + switch (property.Key) + { + case "metadata": + schema.Properties.Remove(property.Key); + break; + default: + break; + } + } + } + + targetType = targetType.DeclaringType; + } + } + } +} diff --git a/NetworkResurrector.Api/Swagger/PathParamsOperationFilter.cs b/NetworkResurrector.Api/Swagger/PathParamsOperationFilter.cs new file mode 100644 index 0000000..ad82995 --- /dev/null +++ b/NetworkResurrector.Api/Swagger/PathParamsOperationFilter.cs @@ -0,0 +1,34 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace NetworkResurrector.Api.Swagger +{ + public class PathParamsOperationFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + const string paramCaptureGroup = "param"; + + var openApiPathParameters = operation.Parameters.Where(param => param.In == ParameterLocation.Path).ToList(); + var pathParamRegEx = $@"\{{(?<{paramCaptureGroup}>[^\}}]+)\}}"; + + if (openApiPathParameters.Any()) + { + var pathParameterMatches = Regex.Matches(context.ApiDescription.RelativePath, pathParamRegEx, RegexOptions.Compiled); + var pathParameters = pathParameterMatches.Select(x => x.Groups[paramCaptureGroup].Value); + + foreach (var openApiPathParameter in openApiPathParameters) + { + var correspondingPathParameter = pathParameters.FirstOrDefault(x => + string.Equals(x, openApiPathParameter.Name, StringComparison.InvariantCultureIgnoreCase)); + + if (correspondingPathParameter != null) + openApiPathParameter.Name = correspondingPathParameter; + } + } + } + } +} diff --git a/NetworkResurrector.Api/Swagger/SwaggerExtensions.cs b/NetworkResurrector.Api/Swagger/SwaggerExtensions.cs new file mode 100644 index 0000000..d40f687 --- /dev/null +++ b/NetworkResurrector.Api/Swagger/SwaggerExtensions.cs @@ -0,0 +1,114 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using System.Collections.Generic; +using System.Linq; + +namespace NetworkResurrector.Api.Swagger +{ + public static class SwaggerExtensions + { + public static IServiceCollection AddSwagger(this IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", + new OpenApiInfo + { + Title = "NetworkResurrector API", + Version = "v1" + }); + + c.AddSecurityDefinition("Basic", + new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = @"JWT Authorization header using the Basic scheme. Enter 'Basic' [space] and then your token in the text input below. Example: 'Basic 12345abcdef'", + Name = "Authorization", + Scheme = "Basic", + Type = SecuritySchemeType.ApiKey + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Basic" + }, + Scheme = "Basic", + Name = "Authorization", + In = ParameterLocation.Header + }, + new List() + } + }); + + c.OperationFilter(); + c.SchemaFilter(); + c.CustomSchemaIds(type => type.ToString()); + }); + + return services; + } + + public static IApplicationBuilder ConfigureSwagger(this IApplicationBuilder applicationBuilder) + { + applicationBuilder.UseSwagger(c => + { + c.PreSerializeFilters.Add((swagger, httpRequest) => + { + var (host, basePath, scheme) = GetUrlComponents(httpRequest); + + swagger.Servers = new List + { + new OpenApiServer {Url = $"{scheme}://{host}{basePath}"} + }; + }); + c.RouteTemplate = "swagger/{documentName}/swagger.json"; + }); + + applicationBuilder.UseSwaggerUI(c => + { + c.SwaggerEndpoint("v1/swagger.json", "Chatbot API"); + c.RoutePrefix = $"swagger"; + }); + + return applicationBuilder; + } + + private static (string host, string basePath, string scheme) GetUrlComponents(HttpRequest request) + { + var host = ExtractHost(request); + var basePath = ExtractBasePath(request); + var scheme = ExtractScheme(request); + + return (host, basePath, scheme); + } + + private static string ExtractHost(HttpRequest request) + { + if (request.Headers.ContainsKey("X-Forwarded-Host")) + return request.Headers["X-Forwarded-Host"].First(); + + return request.Host.Value; + } + + private static string ExtractBasePath(HttpRequest request) + { + if (request.Headers.ContainsKey("X-Forwarded-PathBase")) + return request.Headers["X-Forwarded-PathBase"].First(); + + return string.Empty; + } + + private static string ExtractScheme(HttpRequest request) + { + return request.Headers["X-Forwarded-Proto"].FirstOrDefault() ?? request.Scheme; + } + } +} diff --git a/NetworkResurrector.Api/appsettings.json b/NetworkResurrector.Api/appsettings.json index 5119161..dabcd02 100644 --- a/NetworkResurrector.Api/appsettings.json +++ b/NetworkResurrector.Api/appsettings.json @@ -10,5 +10,9 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Credentials": { + "UserName": "***REMOVED***", + "Password": "***REMOVED***" + } } diff --git a/NetworkResurrector.Application/Commands/Command.cs b/NetworkResurrector.Application/Commands/Command.cs new file mode 100644 index 0000000..f22f4b2 --- /dev/null +++ b/NetworkResurrector.Application/Commands/Command.cs @@ -0,0 +1,18 @@ +using MediatR; + +namespace NetworkResurrector.Application.Commands +{ + public abstract class Command : ICommand, IRequest + { + public Metadata Metadata { get; } + + protected Command(Metadata metadata) + { + Metadata = metadata; + } + } + + public interface ICommand + { + } +} diff --git a/NetworkResurrector.Application/Commands/Metadata.cs b/NetworkResurrector.Application/Commands/Metadata.cs new file mode 100644 index 0000000..421a5d6 --- /dev/null +++ b/NetworkResurrector.Application/Commands/Metadata.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace NetworkResurrector.Application.Commands +{ + 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/NetworkResurrector.Application/DependencyInjectionExtensions.cs b/NetworkResurrector.Application/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..0085e52 --- /dev/null +++ b/NetworkResurrector.Application/DependencyInjectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using NetworkResurrector.Application.Services; +using NetworkResurrector.Domain.Services; + +namespace NetworkResurrector.Application +{ + public static class DependencyInjectionExtensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + } + } +} diff --git a/NetworkResurrector.Application/NetworkResurrector.Application.csproj b/NetworkResurrector.Application/NetworkResurrector.Application.csproj index a5679d9..be77fc1 100644 --- a/NetworkResurrector.Application/NetworkResurrector.Application.csproj +++ b/NetworkResurrector.Application/NetworkResurrector.Application.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/NetworkResurrector.Application/Services/ParamProvider.cs b/NetworkResurrector.Application/Services/ParamProvider.cs new file mode 100644 index 0000000..2c45b9e --- /dev/null +++ b/NetworkResurrector.Application/Services/ParamProvider.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; +using NetworkResurrector.Domain.Models.Settings; +using NetworkResurrector.Domain.Services; + +namespace NetworkResurrector.Application.Services +{ + public class ParamProvider : IParamProvider + { + private readonly IConfiguration _configuration; + + public ParamProvider(IConfiguration configuration) + { + _configuration = configuration; + } + + public Credentials Credentials => _configuration.GetSection("Credentials").Get(); + } +} diff --git a/NetworkResurrector.Application/Services/UserService.cs b/NetworkResurrector.Application/Services/UserService.cs new file mode 100644 index 0000000..89a5127 --- /dev/null +++ b/NetworkResurrector.Application/Services/UserService.cs @@ -0,0 +1,34 @@ +using NetworkResurrector.Domain.Entities; +using NetworkResurrector.Domain.Services; +using System.Threading.Tasks; + +namespace NetworkResurrector.Application.Services +{ + public interface IUserService + { + Task Authenticate(string username, string password); + } + + public class UserService : IUserService + { + private readonly IParamProvider _paramProvider; + + public UserService(IParamProvider paramProvider) + { + _paramProvider = paramProvider; + } + + public async Task Authenticate(string username, string password) + { + return await Task.Run(() => CheckCredentials(username, password)); + } + + private User CheckCredentials(string username, string password) + { + if (_paramProvider.Credentials.UserName == username && _paramProvider.Credentials.Password == password) + return new User() { UserName = username, Id = 1 }; + else + return null; + } + } +} diff --git a/NetworkResurrector.Domain/Entities/User.cs b/NetworkResurrector.Domain/Entities/User.cs new file mode 100644 index 0000000..8ec1ee8 --- /dev/null +++ b/NetworkResurrector.Domain/Entities/User.cs @@ -0,0 +1,9 @@ +namespace NetworkResurrector.Domain.Entities +{ + public class User + { + public int Id { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + } +} diff --git a/NetworkResurrector.Domain/Models/Settings/Credentials.cs b/NetworkResurrector.Domain/Models/Settings/Credentials.cs new file mode 100644 index 0000000..a1d2b4e --- /dev/null +++ b/NetworkResurrector.Domain/Models/Settings/Credentials.cs @@ -0,0 +1,8 @@ +namespace NetworkResurrector.Domain.Models.Settings +{ + public class Credentials + { + public string UserName { get; set; } + public string Password { get; set; } + } +} diff --git a/NetworkResurrector.Domain/NetworkResurrector.Domain.csproj b/NetworkResurrector.Domain/NetworkResurrector.Domain.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/NetworkResurrector.Domain/NetworkResurrector.Domain.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/NetworkResurrector.Domain/Services/IParamProvider.cs b/NetworkResurrector.Domain/Services/IParamProvider.cs new file mode 100644 index 0000000..7fa2621 --- /dev/null +++ b/NetworkResurrector.Domain/Services/IParamProvider.cs @@ -0,0 +1,9 @@ +using NetworkResurrector.Domain.Models.Settings; + +namespace NetworkResurrector.Domain.Services +{ + public interface IParamProvider + { + Credentials Credentials { get; } + } +} diff --git a/NetworkResurrector.sln b/NetworkResurrector.sln index 7179bee..3a84ceb 100644 --- a/NetworkResurrector.sln +++ b/NetworkResurrector.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetworkResurrector.Application", "NetworkResurrector.Application\NetworkResurrector.Application.csproj", "{15D65D65-CC96-45DE-8590-AF9132889D98}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetworkResurrector.Domain", "NetworkResurrector.Domain\NetworkResurrector.Domain.csproj", "{EC78E88E-22DC-4FFD-881E-DEECF0D2494E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +31,10 @@ Global {15D65D65-CC96-45DE-8590-AF9132889D98}.Debug|Any CPU.Build.0 = Debug|Any CPU {15D65D65-CC96-45DE-8590-AF9132889D98}.Release|Any CPU.ActiveCfg = Release|Any CPU {15D65D65-CC96-45DE-8590-AF9132889D98}.Release|Any CPU.Build.0 = Release|Any CPU + {EC78E88E-22DC-4FFD-881E-DEECF0D2494E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC78E88E-22DC-4FFD-881E-DEECF0D2494E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC78E88E-22DC-4FFD-881E-DEECF0D2494E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC78E88E-22DC-4FFD-881E-DEECF0D2494E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE