Compare commits
18 Commits
6553104f8d
...
d6315ce8a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6315ce8a5 | ||
|
|
7bab2657d4 | ||
|
|
79eaa06ed4 | ||
|
|
69efe28310 | ||
|
|
cd4428b8f0 | ||
|
|
a66570bebb | ||
|
|
0a3e1566eb | ||
|
|
de296d34f3 | ||
|
|
314bb08125 | ||
|
|
9c5c0dc61b | ||
|
|
3231aa3299 | ||
|
|
9d35881327 | ||
|
|
f898d8c4a4 | ||
|
|
82f23d447b | ||
|
|
b1bfc46a60 | ||
|
|
8e5180188e | ||
|
|
404ab74ce1 | ||
|
|
34b939f75a |
183
src/DigitalData.Auth.API/Controllers/AuthController.cs
Normal file
183
src/DigitalData.Auth.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using DigitalData.Auth.API.Config;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.UserManager.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using DigitalData.UserManager.Application.DTOs.Auth;
|
||||
using DigitalData.UserManager.Application.Contracts;
|
||||
using DigitalData.UserManager.Application.DTOs.User;
|
||||
using DigitalData.Core.Abstractions.Application;
|
||||
using System.Net;
|
||||
using DigitalData.Auth.API.Dto;
|
||||
using DigitalData.Auth.API.Services.Contracts;
|
||||
|
||||
namespace DigitalData.Auth.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IJwtSignatureHandler<UserReadDto> _userSignatureHandler;
|
||||
|
||||
private readonly IJwtSignatureHandler<ConsumerApi> _apiSignatureHandler;
|
||||
|
||||
private readonly AuthApiParams _apiParams;
|
||||
|
||||
private readonly ICryptoFactory _cryptoFactory;
|
||||
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
|
||||
private readonly IUserService _userService;
|
||||
|
||||
private readonly IDirectorySearchService _dirSearchService;
|
||||
|
||||
private readonly IConsumerApiService _consumerApiService;
|
||||
|
||||
public AuthController(IJwtSignatureHandler<UserReadDto> userSignatureHandler, IOptions<AuthApiParams> cookieParamsOptions, ICryptoFactory cryptoFactory, ILogger<AuthController> logger, IUserService userService, IDirectorySearchService dirSearchService, IConsumerApiService consumerApiService, IJwtSignatureHandler<ConsumerApi> apiSignatureHandler)
|
||||
{
|
||||
_apiParams = cookieParamsOptions.Value;
|
||||
_userSignatureHandler = userSignatureHandler;
|
||||
_cryptoFactory = cryptoFactory;
|
||||
_logger = logger;
|
||||
_userService = userService;
|
||||
_dirSearchService = dirSearchService;
|
||||
_consumerApiService = consumerApiService;
|
||||
_apiSignatureHandler = apiSignatureHandler;
|
||||
}
|
||||
|
||||
private async Task<IActionResult> CreateTokenAsync(LogInDto login, string consumerRoute, bool cookie = true)
|
||||
{
|
||||
bool isValid = _dirSearchService.ValidateCredentials(login.Username, login.Password);
|
||||
|
||||
if (!isValid)
|
||||
return Unauthorized();
|
||||
|
||||
//find the user
|
||||
var uRes = await _userService.ReadByUsernameAsync(login.Username);
|
||||
if (!uRes.IsSuccess || uRes.Data is null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
if (!_apiParams.Consumers.TryGetByRoute(consumerRoute, out var consumer))
|
||||
return Unauthorized();
|
||||
|
||||
if (!_cryptoFactory.TokenDescriptors.TryGet(_apiParams.Issuer, consumer.Audience, out var descriptor) || descriptor is null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
|
||||
var token = _userSignatureHandler.WriteToken(uRes.Data, descriptor);
|
||||
|
||||
//set cookie
|
||||
if (cookie)
|
||||
{
|
||||
Response.Cookies.Append(_apiParams.CookieName, token, consumer.CookieOptions.Create(lifetime: descriptor.Lifetime));
|
||||
return Ok();
|
||||
}
|
||||
else
|
||||
return Ok(token);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> CreateTokenAsync(ConsumerApiLogin login, bool cookie = true)
|
||||
{
|
||||
var api = await _consumerApiService.ReadByNameAsync(login.Name);
|
||||
|
||||
if (api is null || api.Password != login.Password)
|
||||
return Unauthorized();
|
||||
|
||||
if (!_cryptoFactory.TokenDescriptors.TryGet(_apiParams.Issuer, _apiParams.DefaultConsumer.Audience, out var descriptor) || descriptor is null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
|
||||
var token = _apiSignatureHandler!.WriteToken(api, descriptor);
|
||||
|
||||
//set cookie
|
||||
if (cookie)
|
||||
{
|
||||
Response.Cookies.Append(_apiParams.CookieName, token, _apiParams.DefaultConsumer.CookieOptions.Create(lifetime: descriptor.Lifetime));
|
||||
return Ok();
|
||||
}
|
||||
else
|
||||
return Ok(token);
|
||||
}
|
||||
|
||||
//TODO: Add role depends on group name
|
||||
[HttpPost("~/{consumerRoute}/login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromForm] LogInDto login, [FromRoute] string consumerRoute)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CreateTokenAsync(login, consumerRoute, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{Message}", ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("~/login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromForm] ConsumerApiLogin login)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CreateTokenAsync(login, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{Message}", ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
public IActionResult Logout()
|
||||
{
|
||||
try
|
||||
{
|
||||
Response.Cookies.Delete(_apiParams.CookieName);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{Message}", ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{consumerRoute}")]
|
||||
public async Task<IActionResult> CreateTokenViaBody([FromBody] LogInDto login, [FromRoute] string consumerRoute, [FromQuery] bool cookie = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CreateTokenAsync(login, consumerRoute, cookie);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{Message}", ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> CreateTokenViaBody([FromBody] ConsumerApiLogin login, [FromQuery] bool cookie = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CreateTokenAsync(login, cookie);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{Message}", ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("check")]
|
||||
[Authorize]
|
||||
public IActionResult Check() => Ok();
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,20 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyVersion>1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.1.0" />
|
||||
<PackageReference Include="DigitalData.Core.Security" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\" />
|
||||
<PackageReference Include="UserManager.Application" Version="3.1.0" />
|
||||
<PackageReference Include="UserManager.Domain" Version="3.0.0" />
|
||||
<PackageReference Include="UserManager.Infrastructure" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,20 +1,132 @@
|
||||
using DigitalData.Auth.API.Config;
|
||||
using DigitalData.Auth.API.Dto;
|
||||
using DigitalData.Auth.API.Services;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Application;
|
||||
using DigitalData.Core.Security;
|
||||
using DigitalData.UserManager.Application;
|
||||
using DigitalData.UserManager.Application.DTOs.User;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Security.Claims;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddJsonFile("consumer-api.json", true, true);
|
||||
|
||||
var config = builder.Configuration;
|
||||
|
||||
builder.Configuration.AddJsonFile("consumers.json", true, true);
|
||||
var apiParams = config.Get<AuthApiParams>() ?? throw new InvalidOperationException("AuthApiOptions is missing or invalid in appsettings.");
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.Configure<AuthApiParams>(config);
|
||||
builder.Services.AddConsumerApiServiceFromConfiguration(config);
|
||||
builder.Services.AddCryptoFactory(config.GetSection("CryptParams"));
|
||||
builder.Services.AddJwtSignatureHandler<ConsumerApi>(api => new Dictionary<string, object>
|
||||
{
|
||||
{ JwtRegisteredClaimNames.Sub, api.Name },
|
||||
{ JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
|
||||
});
|
||||
builder.Services.AddJwtSignatureHandler<UserReadDto>(user => new Dictionary<string, object>
|
||||
{
|
||||
{ JwtRegisteredClaimNames.Sub, user.Id },
|
||||
{ JwtRegisteredClaimNames.UniqueName, user.Id },
|
||||
{ JwtRegisteredClaimNames.Email, user.Email ?? string.Empty },
|
||||
{ JwtRegisteredClaimNames.GivenName, user.Prename ?? string.Empty },
|
||||
{ JwtRegisteredClaimNames.FamilyName, user.Name ?? string.Empty },
|
||||
{ JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
|
||||
});
|
||||
builder.Services.AddLocalization();
|
||||
builder.Services.Configure<DirectorySearchOptions>(config.GetSection("DirectorySearchOptions"));
|
||||
builder.Services.AddDirectorySearchService();
|
||||
|
||||
var cnn_str = builder.Configuration.GetConnectionString("Default") ?? throw new InvalidOperationException("Default connection string is not found.");
|
||||
|
||||
builder.Services.AddUserManager(cnn_str);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Enter 'Bearer' [space] and then your valid token."
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
},
|
||||
Scheme = "oauth2",
|
||||
Name = "Bearer",
|
||||
In = ParameterLocation.Header
|
||||
},
|
||||
new List<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add authentication
|
||||
Lazy<SecurityKey>? issuerSigningKeyInitiator = null;
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.RequireHttpsMetadata = apiParams!.RequireHttpsMetadata;
|
||||
options.ClaimsIssuer = apiParams!.Issuer;
|
||||
options.Audience = apiParams!.DefaultConsumer.Audience;
|
||||
options.TokenValidationParameters = new()
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = apiParams!.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = apiParams!.DefaultConsumer.Audience,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = issuerSigningKeyInitiator?.Value,
|
||||
NameClaimType = JwtRegisteredClaimNames.Name,
|
||||
RoleClaimType = ClaimTypes.Role
|
||||
};
|
||||
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
// if there is no token read related cookie
|
||||
if (context.Token is null // if there is no token
|
||||
&& context.Request.Cookies.TryGetValue(apiParams!.CookieName, out var token) // get token from cookies
|
||||
&& token is not null)
|
||||
context.Token = token;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
issuerSigningKeyInitiator = new Lazy<SecurityKey>(() =>
|
||||
{
|
||||
var factory = app.Services.GetRequiredService<ICryptoFactory>();
|
||||
var desc = factory.TokenDescriptors.Get(apiParams.Issuer, apiParams.DefaultConsumer.Audience);
|
||||
return desc.Validator.SecurityKey;
|
||||
});
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
var use_swagger = config.GetValue<bool>("UseSwagger");
|
||||
if (app.Environment.IsDevelopment() || use_swagger)
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
@@ -22,6 +134,8 @@ if (app.Environment.IsDevelopment())
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using DigitalData.Auth.API.Dto;
|
||||
using DigitalData.Auth.API.Services.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Auth.API.Services
|
||||
{
|
||||
public class ConfiguredConsumerApiService : IConsumerApiService
|
||||
{
|
||||
private readonly IEnumerable<ConsumerApi> _consumerAPIs;
|
||||
public ConfiguredConsumerApiService(IOptions<IEnumerable<ConsumerApi>> options)
|
||||
{
|
||||
_consumerAPIs = options.Value;
|
||||
}
|
||||
|
||||
public Task<ConsumerApi?> ReadByNameAsync(string name) => Task.Run(() => _consumerAPIs.FirstOrDefault(api => api.Name == name));
|
||||
|
||||
public async Task<bool> VerifyAsync(string name, string password) => (await ReadByNameAsync(name))?.Password == password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using DigitalData.Auth.API.Dto;
|
||||
|
||||
namespace DigitalData.Auth.API.Services.Contracts
|
||||
{
|
||||
public interface IConsumerApiService
|
||||
{
|
||||
public Task<ConsumerApi?> ReadByNameAsync(string name);
|
||||
|
||||
public Task<bool> VerifyAsync(string name, string password);
|
||||
}
|
||||
}
|
||||
17
src/DigitalData.Auth.API/Services/DIExtensions.cs
Normal file
17
src/DigitalData.Auth.API/Services/DIExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using DigitalData.Auth.API.Dto;
|
||||
using DigitalData.Auth.API.Services.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Auth.API.Services
|
||||
{
|
||||
public static class DIExtensions
|
||||
{
|
||||
public static IServiceCollection AddConsumerApiServiceFromConfiguration(this IServiceCollection services, IConfiguration configuration, string key = "ConsumerAPIs")
|
||||
{
|
||||
var consumerApis = configuration.GetSection("ConsumerAPIs").Get<IEnumerable<ConsumerApi>>() ?? throw new InvalidOperationException($"No Consumer list found in {key} in configuration.");
|
||||
services.AddSingleton(Options.Create(consumerApis));
|
||||
services.AddSingleton<IConsumerApiService, ConfiguredConsumerApiService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,60 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
"UseSwagger": true,
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||
},
|
||||
"DirectorySearchOptions": {
|
||||
"ServerName": "DD-VMP01-DC01",
|
||||
"Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works"
|
||||
},
|
||||
"Consumers": [
|
||||
{
|
||||
"Route": "api",
|
||||
"Audience": "api.digitaldata.works"
|
||||
},
|
||||
{
|
||||
"Route": "work-flow",
|
||||
"Audience": "work-flow.digitaldata.works"
|
||||
}
|
||||
],
|
||||
"Issuer": "auth.digitaldata.works",
|
||||
"CryptParams": {
|
||||
"KeySizeInBits": 4096,
|
||||
"Padding": "OaepSHA512",
|
||||
"PemDirectory": "Secrets",
|
||||
"Decryptors": [
|
||||
{
|
||||
"IsEncrypted": true
|
||||
}
|
||||
],
|
||||
"TokenDescriptors": [
|
||||
{
|
||||
"Id": "4062504f-f081-43d1-b4ed-78256a0879e1",
|
||||
"Issuer": "auth.digitaldata.works",
|
||||
"Audience": "api.digitaldata.works",
|
||||
"IsEncrypted": true,
|
||||
"Lifetime": "5:00:00"
|
||||
},
|
||||
{
|
||||
"Id": "61c07d26-baa8-4cbb-bb33-ac4ee1838c3a",
|
||||
"Issuer": "auth.digitaldata.works",
|
||||
"Audience": "work-flow.digitaldata.works",
|
||||
"IsEncrypted": true,
|
||||
"Lifetime": "02:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ConsumerAPIs": [
|
||||
{
|
||||
"Name": "WorkFlow.API",
|
||||
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'iL"
|
||||
},
|
||||
{
|
||||
"Name": "DigitalData.UserManager.API",
|
||||
"Password": "a098Hvu1-y29ep{KPQO]#>8TK+fk{O`_d"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"Consumers": [
|
||||
"ConsumerAPIs": [
|
||||
{
|
||||
"Name": "WorkFlow.API",
|
||||
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'i<>L"
|
||||
Reference in New Issue
Block a user