Добавил получение guid пользователя по фио через токен
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 3m54s
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 3m54s
This commit is contained in:
7
SfeduSchedule/Auth/ApiKeyAuthenticationDefaults.cs
Normal file
7
SfeduSchedule/Auth/ApiKeyAuthenticationDefaults.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SfeduSchedule.Auth;
|
||||
|
||||
public static class ApiKeyAuthenticationDefaults
|
||||
{
|
||||
public const string Scheme = "ApiKey";
|
||||
public const string HeaderName = "X-Api-Key";
|
||||
}
|
72
SfeduSchedule/Auth/ApiKeyAuthenticationHandler.cs
Normal file
72
SfeduSchedule/Auth/ApiKeyAuthenticationHandler.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace SfeduSchedule.Auth;
|
||||
|
||||
public class ApiKeyAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
IConfiguration configuration)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
{
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
// Ожидаем ключ из ENV
|
||||
var expectedKey = configuration["API_KEY"];
|
||||
if (string.IsNullOrEmpty(expectedKey))
|
||||
return Task.FromResult(AuthenticateResult.Fail("API key is not configured."));
|
||||
|
||||
// Ищем ключ в заголовке X-Api-Key (и опционально в query ?api_key=)
|
||||
string? providedKey = null;
|
||||
|
||||
if (Request.Headers.TryGetValue(ApiKeyAuthenticationDefaults.HeaderName, out var values))
|
||||
providedKey = values.FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(providedKey) &&
|
||||
Request.Query.TryGetValue("api_key", out var qv))
|
||||
providedKey = qv.FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(providedKey))
|
||||
// Нет ключа — позволь другим схемам (если есть) продолжить; при [Authorize] будет 401
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
|
||||
if (!ConstantTimeEquals(providedKey!, expectedKey))
|
||||
return Task.FromResult(AuthenticateResult.Fail("Invalid API key."));
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, "api-key"),
|
||||
new Claim(ClaimTypes.Name, "api-key-user"),
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, ApiKeyAuthenticationDefaults.Scheme);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationDefaults.Scheme);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.Headers["WWW-Authenticate"] =
|
||||
$"{ApiKeyAuthenticationDefaults.Scheme} realm=\"api\", header=\"{ApiKeyAuthenticationDefaults.HeaderName}\"";
|
||||
Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool ConstantTimeEquals(string a, string b)
|
||||
{
|
||||
var aBytes = Encoding.UTF8.GetBytes(a);
|
||||
var bBytes = Encoding.UTF8.GetBytes(b);
|
||||
|
||||
if (aBytes.Length != bBytes.Length)
|
||||
return false;
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(aBytes, bBytes);
|
||||
}
|
||||
}
|
33
SfeduSchedule/Auth/SwaggerAuthorizeOperationFilter.cs
Normal file
33
SfeduSchedule/Auth/SwaggerAuthorizeOperationFilter.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace SfeduSchedule.Auth
|
||||
{
|
||||
public class SwaggerAuthorizeOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var hasAuthorize = context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||
context.MethodInfo.DeclaringType?.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() == true;
|
||||
if (hasAuthorize)
|
||||
{
|
||||
operation.Security ??= new List<OpenApiSecurityRequirement>();
|
||||
operation.Security.Add(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = ApiKeyAuthenticationDefaults.Scheme
|
||||
}
|
||||
},
|
||||
new List<string>()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using SfeduSchedule.Services;
|
||||
@@ -41,5 +42,25 @@ namespace SfeduSchedule.Controllers
|
||||
var schedule = await modeusService.GetScheduleAsync(request);
|
||||
return Ok(schedule);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить GUID пользователя по полному имени. (требуется авторизация)
|
||||
/// </summary>
|
||||
/// <param name="fullname">Полное имя пользователя.</param>
|
||||
/// <returns>GUID пользователя.</returns>
|
||||
/// <response code="200">Возвращает GUID пользователя</response>
|
||||
/// <response code="404">Пользователь не найден</response>
|
||||
/// <response code="401">Неавторизованный</response>
|
||||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = "ApiKey")]
|
||||
[Route("GetGuid")]
|
||||
public async Task<IActionResult> GetGuid(string fullname)
|
||||
{
|
||||
var guid = await modeusService.GetGuidAsync(fullname);
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
return NotFound();
|
||||
|
||||
return Ok(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ namespace SfeduSchedule.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = "OpenIdConnect")]
|
||||
public class SfeduController : ControllerBase
|
||||
{
|
||||
private readonly ModeusService _modeusService;
|
||||
@@ -20,7 +20,8 @@ namespace SfeduSchedule.Controllers
|
||||
/// <returns>Строка GUID пользователя или редирект на указанный URI.</returns>
|
||||
/// <response code="200">Возвращает GUID пользователя</response>
|
||||
/// <response code="302">Редирект на указанный URI</response>
|
||||
/// <response code="500">Ошибка при получении имени пользователя или GUID</response>
|
||||
/// <response code="404">Пользователь не найден</response>
|
||||
/// <response code="401">Неавторизованный</response>
|
||||
[HttpGet]
|
||||
[Route("guid")]
|
||||
public async Task<IActionResult> Get([FromQuery] string? redirectUri)
|
||||
@@ -31,7 +32,7 @@ namespace SfeduSchedule.Controllers
|
||||
|
||||
var guid = await _modeusService.GetGuidAsync(name);
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
return NotFound();
|
||||
|
||||
if (!string.IsNullOrEmpty(redirectUri))
|
||||
{
|
||||
|
@@ -2,6 +2,8 @@ using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using SfeduSchedule.Plugin.Abstractions;
|
||||
|
||||
namespace SfeduSchedule;
|
||||
|
||||
public sealed record LoadedPlugin(IPlugin Instance, Assembly Assembly, PluginLoadContext Context);
|
||||
|
||||
public static class PluginLoader
|
||||
@@ -48,7 +50,7 @@ public sealed class PluginLoadContext : AssemblyLoadContext
|
||||
}
|
||||
|
||||
// Разрешаем управляемые зависимости плагина из его папки.
|
||||
// Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, Plugin.Abstractions).
|
||||
// Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, SfeduSchedule.Plugin.Abstractions).
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
var path = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
|
@@ -1,4 +1,6 @@
|
||||
using System.Threading.RateLimiting;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.Identity.Web;
|
||||
using Quartz;
|
||||
using SfeduSchedule;
|
||||
@@ -7,6 +9,7 @@ using SfeduSchedule.Services;
|
||||
using X.Extensions.Logging.Telegram.Extensions;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using SfeduSchedule.Auth;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -50,6 +53,12 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
|
||||
var mvcBuilder = builder.Services.AddControllers();
|
||||
builder.Services.AddHttpClient<ModeusService>();
|
||||
|
||||
builder.Services.AddAuthentication()
|
||||
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
|
||||
ApiKeyAuthenticationDefaults.Scheme, _ => { });
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
|
||||
|
||||
// Загружаем плагины (изолированно), даём им зарегистрировать DI и подключаем контроллеры
|
||||
@@ -63,7 +72,6 @@ foreach (var p in loaded)
|
||||
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly));
|
||||
}
|
||||
|
||||
|
||||
var jobKey = new JobKey("UpdateJWTJob");
|
||||
|
||||
if (string.IsNullOrEmpty(preinstalledJwtToken))
|
||||
@@ -88,6 +96,17 @@ builder.Services.AddSwaggerGen(options =>
|
||||
var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
options.IncludeXmlComments(xmlPath);
|
||||
|
||||
// Добавляем только схему авторизации по ApiKey
|
||||
options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.Scheme, new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Description = $"Api Key needed to access the endpoints. {ApiKeyAuthenticationDefaults.HeaderName}: Your_API_Key",
|
||||
Name = ApiKeyAuthenticationDefaults.HeaderName,
|
||||
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
|
||||
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
|
||||
Scheme = ApiKeyAuthenticationDefaults.Scheme
|
||||
});
|
||||
options.OperationFilter<SwaggerAuthorizeOperationFilter>();
|
||||
});
|
||||
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
@@ -169,14 +188,10 @@ if (string.IsNullOrEmpty(preinstalledJwtToken))
|
||||
await scheduler.TriggerJob(jobKey);
|
||||
}
|
||||
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseStaticFiles();
|
||||
|
@@ -10,7 +10,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8"/>
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.0" />
|
||||
<PackageReference Include="Microsoft.Playwright" Version="1.55.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.0" />
|
||||
|
@@ -15,6 +15,7 @@ services:
|
||||
- MS_PASSWORD=${MS_PASSWORD}
|
||||
- TG_CHAT_ID=${TG_CHAT_ID}
|
||||
- TG_TOKEN=${TG_TOKEN}
|
||||
- API_KEY=${API_KEY}
|
||||
# - TOKEN=${TOKEN}
|
||||
volumes:
|
||||
- data:/app/data
|
||||
|
@@ -15,6 +15,7 @@ services:
|
||||
- MS_PASSWORD=${MS_PASSWORD}
|
||||
- TG_CHAT_ID=${TG_CHAT_ID}
|
||||
- TG_TOKEN=${TG_TOKEN}
|
||||
- API_KEY=${API_KEY}
|
||||
# - TOKEN=${TOKEN}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
|
Reference in New Issue
Block a user