infrastructure: swagger; authentication;user service; commands
parent
c889ea4ce7
commit
1b66c5c0ba
|
@ -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<AuthenticationSchemeOptions>
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<AuthenticationSchemeOptions, BasicAuthenticationHandler>("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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string>()
|
||||
}
|
||||
});
|
||||
|
||||
c.OperationFilter<PathParamsOperationFilter>();
|
||||
c.SchemaFilter<DtoSchemaFilter>();
|
||||
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<OpenApiServer>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,5 +10,9 @@
|
|||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"Credentials": {
|
||||
"UserName": "***REMOVED***",
|
||||
"Password": "***REMOVED***"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
using MediatR;
|
||||
|
||||
namespace NetworkResurrector.Application.Commands
|
||||
{
|
||||
public abstract class Command<TResponse> : ICommand, IRequest<TResponse>
|
||||
{
|
||||
public Metadata Metadata { get; }
|
||||
|
||||
protected Command(Metadata metadata)
|
||||
{
|
||||
Metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICommand
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NetworkResurrector.Application.Commands
|
||||
{
|
||||
public class Metadata : Dictionary<string, string>
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<IParamProvider, ParamProvider>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,4 +13,8 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NetworkResurrector.Domain\NetworkResurrector.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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<Credentials>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using NetworkResurrector.Domain.Entities;
|
||||
using NetworkResurrector.Domain.Services;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NetworkResurrector.Application.Services
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
Task<User> Authenticate(string username, string password);
|
||||
}
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly IParamProvider _paramProvider;
|
||||
|
||||
public UserService(IParamProvider paramProvider)
|
||||
{
|
||||
_paramProvider = paramProvider;
|
||||
}
|
||||
|
||||
public async Task<User> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace NetworkResurrector.Domain.Models.Settings
|
||||
{
|
||||
public class Credentials
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,9 @@
|
|||
using NetworkResurrector.Domain.Models.Settings;
|
||||
|
||||
namespace NetworkResurrector.Domain.Services
|
||||
{
|
||||
public interface IParamProvider
|
||||
{
|
||||
Credentials Credentials { get; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue