feat: implement authentication system for API Gateway and FileService #34

Merged
conco merged 3 commits from claude/issue-17-add-authentication into master 2025-11-28 04:26:23 +00:00
23 changed files with 189 additions and 33 deletions
Showing only changes of commit 75e96cbee5 - Show all commits

View File

@@ -13,6 +13,7 @@
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.1.11" /> <PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.1.11" />
<PackageReference Include="HotChocolate.Fusion" Version="15.1.11" /> <PackageReference Include="HotChocolate.Fusion" Version="15.1.11" />
<PackageReference Include="HotChocolate.Types.Scalars" Version="15.1.11" /> <PackageReference Include="HotChocolate.Types.Scalars" Version="15.1.11" />
<PackageReference Include="Microsoft.AspNetCore.HeaderPropagation" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -12,7 +12,11 @@ public class Program
#region Fusion Gateway #region Fusion Gateway
builder.Services.AddHttpClient("Fusion"); builder.Services.AddHttpClient("Fusion")
.AddHeaderPropagation(opt =>
{
opt.Headers.Add("Authorization");
});
builder.Services builder.Services
.AddFusionGatewayServer() .AddFusionGatewayServer()
@@ -23,7 +27,6 @@ public class Program
// Add authentication // Add authentication
builder.Services.AddOidcAuthentication(builder.Configuration); builder.Services.AddOidcAuthentication(builder.Configuration);
builder.Services.AddFictionArchiveAuthorization();
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
@@ -41,11 +44,10 @@ public class Program
app.UseCors("AllowFictionArchiveOrigins"); app.UseCors("AllowFictionArchiveOrigins");
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz"); app.MapHealthChecks("/healthz");
app.UseHeaderPropagation();
app.MapGraphQL(); app.MapGraphQL();
app.RunWithGraphQLCommands(args); app.RunWithGraphQLCommands(args);

View File

@@ -7,9 +7,10 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"OIDC": { "OIDC": {
"Authority": "https://auth.orfl.xyz/application/o/fictionarchive/", "Authority": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ClientId": "fictionarchive-api", "ClientId": "fictionarchive-api",
"Audience": "fictionarchive-api", "Audience": "fictionarchive-api",
"ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ValidateIssuer": true, "ValidateIssuer": true,
"ValidateAudience": true, "ValidateAudience": true,
"ValidateLifetime": true, "ValidateLifetime": true,

View File

@@ -9,7 +9,7 @@
"BaseUrl": "https://localhost:7247/api" "BaseUrl": "https://localhost:7247/api"
}, },
"RabbitMQ": { "RabbitMQ": {
"ConnectionString": "amqp://localhost2", "ConnectionString": "amqp://localhost",
"ClientIdentifier": "FileService" "ClientIdentifier": "FileService"
}, },
"S3": { "S3": {
@@ -19,9 +19,10 @@
"SecretKey": "REPLACE_ME" "SecretKey": "REPLACE_ME"
}, },
"OIDC": { "OIDC": {
"Authority": "https://auth.orfl.xyz/application/o/fictionarchive/", "Authority": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ClientId": "fictionarchive-files", "ClientId": "fictionarchive-files",
"Audience": "fictionarchive-api", "Audience": "fictionarchive-api",
"ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ValidateIssuer": true, "ValidateIssuer": true,
"ValidateAudience": true, "ValidateAudience": true,
"ValidateLifetime": true, "ValidateLifetime": true,

View File

@@ -1,5 +1,6 @@
using FictionArchive.Service.NovelService.Models.Novels; using FictionArchive.Service.NovelService.Models.Novels;
using FictionArchive.Service.NovelService.Services; using FictionArchive.Service.NovelService.Services;
using HotChocolate.Authorization;
using HotChocolate.Data; using HotChocolate.Data;
using HotChocolate.Types; using HotChocolate.Types;
@@ -7,6 +8,7 @@ namespace FictionArchive.Service.NovelService.GraphQL;
public class Query public class Query
{ {
[Authorize]
[UsePaging] [UsePaging]
[UseProjection] [UseProjection]
[UseFiltering] [UseFiltering]

View File

@@ -43,7 +43,8 @@ public class Program
#region GraphQL #region GraphQL
builder.Services.AddDefaultGraphQl<Query, Mutation>(); builder.Services.AddDefaultGraphQl<Query, Mutation>()
.AddAuthorization();
#endregion #endregion
@@ -75,6 +76,10 @@ public class Program
builder.Services.AddHealthChecks(); builder.Services.AddHealthChecks();
// Authentication & Authorization
builder.Services.AddOidcAuthentication(builder.Configuration);
builder.Services.AddFictionArchiveAuthorization();
var app = builder.Build(); var app = builder.Build();
// Update database (skip in schema export mode) // Update database (skip in schema export mode)
@@ -88,7 +93,10 @@ public class Program
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.MapHealthChecks("/healthz"); app.MapHealthChecks("/healthz");
app.UseAuthentication();
app.UseAuthorization();
app.MapGraphQL(); app.MapGraphQL();
app.RunWithGraphQLCommands(args); app.RunWithGraphQLCommands(args);

View File

@@ -19,5 +19,15 @@
"ConnectionString": "amqp://localhost", "ConnectionString": "amqp://localhost",
"ClientIdentifier": "NovelService" "ClientIdentifier": "NovelService"
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"OIDC": {
"Authority": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ClientId": "ldi5IpEidq2WW0Ka1lehVskb2SOBjnYRaZCpEyBh",
"Audience": "ldi5IpEidq2WW0Ka1lehVskb2SOBjnYRaZCpEyBh",
"ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": true,
"ValidateIssuerSigningKey": true
}
} }

View File

@@ -1,6 +1,6 @@
{ {
"subgraph": "Novels", "subgraph": "Novels",
"http": { "http": {
"baseAddress": "http://localhost:5101/graphql" "baseAddress": "https://localhost:7208/graphql"
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Data; using System.Data;
using FictionArchive.Service.SchedulerService.Models; using FictionArchive.Service.SchedulerService.Models;
using FictionArchive.Service.SchedulerService.Services; using FictionArchive.Service.SchedulerService.Services;
using FictionArchive.Service.Shared.Constants;
using HotChocolate.Authorization; using HotChocolate.Authorization;
using HotChocolate.Types; using HotChocolate.Types;
using Quartz; using Quartz;
@@ -11,21 +12,21 @@ public class Mutation
{ {
[Error<DuplicateNameException>] [Error<DuplicateNameException>]
[Error<FormatException>] [Error<FormatException>]
[Authorize(Roles = new[] { "admin" })] [Authorize(Roles = [AuthorizationConstants.Roles.Admin])]
public async Task<SchedulerJob> ScheduleEventJob(string key, string description, string eventType, string eventData, string cronSchedule, JobManagerService jobManager) public async Task<SchedulerJob> ScheduleEventJob(string key, string description, string eventType, string eventData, string cronSchedule, JobManagerService jobManager)
{ {
return await jobManager.ScheduleEventJob(key, description, eventType, eventData, cronSchedule); return await jobManager.ScheduleEventJob(key, description, eventType, eventData, cronSchedule);
} }
[Error<JobPersistenceException>] [Error<JobPersistenceException>]
[Authorize(Roles = new[] { "admin" })] [Authorize(Roles = [AuthorizationConstants.Roles.Admin])]
public async Task<bool> RunJob(string jobKey, JobManagerService jobManager) public async Task<bool> RunJob(string jobKey, JobManagerService jobManager)
{ {
return await jobManager.TriggerJob(jobKey); return await jobManager.TriggerJob(jobKey);
} }
[Error<KeyNotFoundException>] [Error<KeyNotFoundException>]
[Authorize(Roles = new[] { "admin" })] [Authorize(Roles = [AuthorizationConstants.Roles.Admin])]
public async Task<bool> DeleteJob(string jobKey, JobManagerService jobManager) public async Task<bool> DeleteJob(string jobKey, JobManagerService jobManager)
{ {
bool deleted = await jobManager.DeleteJob(jobKey); bool deleted = await jobManager.DeleteJob(jobKey);

View File

@@ -17,10 +17,15 @@ public class Program
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Services // Services
builder.Services.AddDefaultGraphQl<Query, Mutation>(); builder.Services.AddDefaultGraphQl<Query, Mutation>()
.AddAuthorization();
builder.Services.AddHealthChecks(); builder.Services.AddHealthChecks();
builder.Services.AddTransient<JobManagerService>(); builder.Services.AddTransient<JobManagerService>();
// Authentication & Authorization
builder.Services.AddOidcAuthentication(builder.Configuration);
builder.Services.AddFictionArchiveAuthorization();
#region Database #region Database
builder.Services.RegisterDbContext<SchedulerServiceDbContext>( builder.Services.RegisterDbContext<SchedulerServiceDbContext>(
@@ -87,7 +92,10 @@ public class Program
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.MapHealthChecks("/healthz"); app.MapHealthChecks("/healthz");
app.UseAuthentication();
app.UseAuthorization();
app.MapGraphQL(); app.MapGraphQL();
app.RunWithGraphQLCommands(args); app.RunWithGraphQLCommands(args);

View File

@@ -12,5 +12,15 @@
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=FictionArchive_SchedulerService;Username=postgres;password=postgres" "DefaultConnection": "Host=localhost;Database=FictionArchive_SchedulerService;Username=postgres;password=postgres"
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"OIDC": {
"Authority": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ClientId": "fictionarchive-api",
"Audience": "fictionarchive-api",
"ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": true,
"ValidateIssuerSigningKey": true
}
} }

View File

@@ -0,0 +1,15 @@
namespace FictionArchive.Service.Shared.Constants;
public static class AuthorizationConstants
{
public static class Roles
{
public const string Admin = "admin";
}
public static class Policies
{
public const string Admin = "Admin";
public const string User = "User";
}
}

View File

@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using FictionArchive.Service.Shared.Constants;
using FictionArchive.Service.Shared.Models.Authentication; using FictionArchive.Service.Shared.Models.Authentication;
using System.Linq; using System.Linq;
@@ -30,16 +32,59 @@ public static class AuthenticationExtensions
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidateIssuer = oidcConfig.ValidateIssuer, ValidateIssuer = oidcConfig.ValidateIssuer,
ValidIssuer = oidcConfig.ValidIssuer,
ValidateAudience = oidcConfig.ValidateAudience, ValidateAudience = oidcConfig.ValidateAudience,
ValidateLifetime = oidcConfig.ValidateLifetime, ValidateLifetime = oidcConfig.ValidateLifetime,
ValidateIssuerSigningKey = oidcConfig.ValidateIssuerSigningKey, ValidateIssuerSigningKey = oidcConfig.ValidateIssuerSigningKey,
ClockSkew = TimeSpan.FromMinutes(5) ClockSkew = TimeSpan.FromMinutes(5)
}; };
options.Events = CreateLoggingJwtBearerEvents();
}); });
return services; return services;
} }
private static JwtBearerEvents CreateLoggingJwtBearerEvents(JwtBearerEvents? existingEvents = null)
{
return new JwtBearerEvents
{
OnMessageReceived = existingEvents?.OnMessageReceived ?? (_ => Task.CompletedTask),
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
.CreateLogger("JwtBearerAuthentication");
logger.LogWarning(context.Exception, "JWT authentication failed: {Message}", context.Exception.Message);
return existingEvents?.OnAuthenticationFailed?.Invoke(context) ?? Task.CompletedTask;
},
OnChallenge = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
.CreateLogger("JwtBearerAuthentication");
logger.LogDebug(
"JWT challenge issued. Error: {Error}, ErrorDescription: {ErrorDescription}",
context.Error,
context.ErrorDescription);
return existingEvents?.OnChallenge?.Invoke(context) ?? Task.CompletedTask;
},
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
.CreateLogger("JwtBearerAuthentication");
logger.LogDebug(
"JWT token validated for subject: {Subject}",
context.Principal?.FindFirst("sub")?.Value ?? "unknown");
return existingEvents?.OnTokenValidated?.Invoke(context) ?? Task.CompletedTask;
}
};
}
public static IServiceCollection AddOidcCookieAuthentication(this IServiceCollection services, IConfiguration configuration, string cookieName = "fa_session") public static IServiceCollection AddOidcCookieAuthentication(this IServiceCollection services, IConfiguration configuration, string cookieName = "fa_session")
{ {
var oidcConfig = configuration.GetSection("OIDC").Get<OidcConfiguration>(); var oidcConfig = configuration.GetSection("OIDC").Get<OidcConfiguration>();
@@ -58,7 +103,7 @@ public static class AuthenticationExtensions
options.Audience = oidcConfig.Audience; options.Audience = oidcConfig.Audience;
options.RequireHttpsMetadata = !string.IsNullOrEmpty(oidcConfig.Authority) && oidcConfig.Authority.StartsWith("https://"); options.RequireHttpsMetadata = !string.IsNullOrEmpty(oidcConfig.Authority) && oidcConfig.Authority.StartsWith("https://");
options.Events = new JwtBearerEvents var cookieEvents = new JwtBearerEvents
{ {
OnMessageReceived = context => OnMessageReceived = context =>
{ {
@@ -71,10 +116,12 @@ public static class AuthenticationExtensions
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };
options.Events = CreateLoggingJwtBearerEvents(cookieEvents);
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidateIssuer = oidcConfig.ValidateIssuer, ValidateIssuer = oidcConfig.ValidateIssuer,
ValidIssuer = oidcConfig.ValidIssuer,
ValidateAudience = oidcConfig.ValidateAudience, ValidateAudience = oidcConfig.ValidateAudience,
ValidateLifetime = oidcConfig.ValidateLifetime, ValidateLifetime = oidcConfig.ValidateLifetime,
ValidateIssuerSigningKey = oidcConfig.ValidateIssuerSigningKey, ValidateIssuerSigningKey = oidcConfig.ValidateIssuerSigningKey,
@@ -88,8 +135,8 @@ public static class AuthenticationExtensions
public static IServiceCollection AddFictionArchiveAuthorization(this IServiceCollection services) public static IServiceCollection AddFictionArchiveAuthorization(this IServiceCollection services)
{ {
services.AddAuthorizationBuilder() services.AddAuthorizationBuilder()
.AddPolicy("Admin", policy => policy.RequireRole("admin")) .AddPolicy(AuthorizationConstants.Policies.Admin, policy => policy.RequireRole(AuthorizationConstants.Roles.Admin))
.AddPolicy("User", policy => policy.RequireAuthenticatedUser()); .AddPolicy(AuthorizationConstants.Policies.User, policy => policy.RequireAuthenticatedUser());
return services; return services;
} }

View File

@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="GraphQL.Server.Ui.GraphiQL" Version="8.3.3" /> <PackageReference Include="GraphQL.Server.Ui.GraphiQL" Version="8.3.3" />
<PackageReference Include="HotChocolate.AspNetCore" Version="15.1.11" /> <PackageReference Include="HotChocolate.AspNetCore" Version="15.1.11" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="15.1.11" />
<PackageReference Include="HotChocolate.AspNetCore.CommandLine" Version="15.1.11" /> <PackageReference Include="HotChocolate.AspNetCore.CommandLine" Version="15.1.11" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.1.11" /> <PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.1.11" />
<PackageReference Include="HotChocolate.Types.Scalars" Version="15.1.11" /> <PackageReference Include="HotChocolate.Types.Scalars" Version="15.1.11" />

View File

@@ -5,6 +5,7 @@ public class OidcConfiguration
public string Authority { get; set; } = string.Empty; public string Authority { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty; public string ClientId { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty; public string Audience { get; set; } = string.Empty;
public string? ValidIssuer { get; set; }
public bool ValidateIssuer { get; set; } = true; public bool ValidateIssuer { get; set; } = true;
public bool ValidateAudience { get; set; } = true; public bool ValidateAudience { get; set; } = true;
public bool ValidateLifetime { get; set; } = true; public bool ValidateLifetime { get; set; } = true;

View File

@@ -5,15 +5,17 @@ using FictionArchive.Service.TranslationService.Models.Enums;
using FictionArchive.Service.TranslationService.Services; using FictionArchive.Service.TranslationService.Services;
using FictionArchive.Service.TranslationService.Services.Database; using FictionArchive.Service.TranslationService.Services.Database;
using FictionArchive.Service.TranslationService.Services.TranslationEngines; using FictionArchive.Service.TranslationService.Services.TranslationEngines;
using HotChocolate.Authorization;
namespace FictionArchive.Service.TranslationService.GraphQL; namespace FictionArchive.Service.TranslationService.GraphQL;
public class Mutation public class Mutation
{ {
[Authorize]
public async Task<TranslationResult> TranslateText(string text, Language from, Language to, string translationEngineKey, TranslationEngineService translationEngineService) public async Task<TranslationResult> TranslateText(string text, Language from, Language to, string translationEngineKey, TranslationEngineService translationEngineService)
{ {
var result = await translationEngineService.Translate(from, to, text, translationEngineKey); var result = await translationEngineService.Translate(from, to, text, translationEngineKey);
return result; return result;
} }
} }

View File

@@ -2,19 +2,22 @@ using FictionArchive.Service.TranslationService.Models;
using FictionArchive.Service.TranslationService.Models.Database; using FictionArchive.Service.TranslationService.Models.Database;
using FictionArchive.Service.TranslationService.Services.Database; using FictionArchive.Service.TranslationService.Services.Database;
using FictionArchive.Service.TranslationService.Services.TranslationEngines; using FictionArchive.Service.TranslationService.Services.TranslationEngines;
using HotChocolate.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace FictionArchive.Service.TranslationService.GraphQL; namespace FictionArchive.Service.TranslationService.GraphQL;
public class Query public class Query
{ {
[Authorize]
[UseFiltering] [UseFiltering]
[UseSorting] [UseSorting]
public IEnumerable<TranslationEngineDescriptor> GetTranslationEngines(IEnumerable<ITranslationEngine> engines) public IEnumerable<TranslationEngineDescriptor> GetTranslationEngines(IEnumerable<ITranslationEngine> engines)
{ {
return engines.Select(engine => engine.Descriptor); return engines.Select(engine => engine.Descriptor);
} }
[Authorize]
[UsePaging] [UsePaging]
[UseProjection] [UseProjection]
[UseFiltering] [UseFiltering]

View File

@@ -50,7 +50,8 @@ public class Program
#region GraphQL #region GraphQL
builder.Services.AddDefaultGraphQl<Query, Mutation>(); builder.Services.AddDefaultGraphQl<Query, Mutation>()
.AddAuthorization();
#endregion #endregion
@@ -63,9 +64,13 @@ public class Program
builder.Services.AddTransient<ITranslationEngine, DeepLTranslationEngine>(); builder.Services.AddTransient<ITranslationEngine, DeepLTranslationEngine>();
builder.Services.AddTransient<TranslationEngineService>(); builder.Services.AddTransient<TranslationEngineService>();
#endregion #endregion
// Authentication & Authorization
builder.Services.AddOidcAuthentication(builder.Configuration);
builder.Services.AddFictionArchiveAuthorization();
var app = builder.Build(); var app = builder.Build();
// Update database (skip in schema export mode) // Update database (skip in schema export mode)
@@ -79,7 +84,10 @@ public class Program
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.MapHealthChecks("/healthz"); app.MapHealthChecks("/healthz");
app.UseAuthentication();
app.UseAuthorization();
app.MapGraphQL(); app.MapGraphQL();
app.RunWithGraphQLCommands(args); app.RunWithGraphQLCommands(args);

View File

@@ -15,5 +15,15 @@
"ConnectionString": "amqp://localhost", "ConnectionString": "amqp://localhost",
"ClientIdentifier": "TranslationService" "ClientIdentifier": "TranslationService"
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"OIDC": {
"Authority": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ClientId": "fictionarchive-api",
"Audience": "fictionarchive-api",
"ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": true,
"ValidateIssuerSigningKey": true
}
} }

View File

@@ -1,10 +1,13 @@
using FictionArchive.Service.Shared.Constants;
using FictionArchive.Service.UserService.Models.Database; using FictionArchive.Service.UserService.Models.Database;
using FictionArchive.Service.UserService.Services; using FictionArchive.Service.UserService.Services;
using HotChocolate.Authorization;
namespace FictionArchive.Service.UserService.GraphQL; namespace FictionArchive.Service.UserService.GraphQL;
public class Mutation public class Mutation
{ {
[Authorize(Roles = [AuthorizationConstants.Roles.Admin])]
public async Task<User> RegisterUser(string username, string email, string oAuthProviderId, public async Task<User> RegisterUser(string username, string email, string oAuthProviderId,
string? inviterOAuthProviderId, UserManagementService userManagementService) string? inviterOAuthProviderId, UserManagementService userManagementService)
{ {

View File

@@ -1,10 +1,12 @@
using FictionArchive.Service.UserService.Models.Database; using FictionArchive.Service.UserService.Models.Database;
using FictionArchive.Service.UserService.Services; using FictionArchive.Service.UserService.Services;
using HotChocolate.Authorization;
namespace FictionArchive.Service.UserService.GraphQL; namespace FictionArchive.Service.UserService.GraphQL;
public class Query public class Query
{ {
[Authorize]
public async Task<IQueryable<User>> GetUsers(UserManagementService userManagementService) public async Task<IQueryable<User>> GetUsers(UserManagementService userManagementService)
{ {
return userManagementService.GetUsers(); return userManagementService.GetUsers();

View File

@@ -1,3 +1,4 @@
using FictionArchive.Common.Extensions;
using FictionArchive.Service.Shared; using FictionArchive.Service.Shared;
using FictionArchive.Service.Shared.Extensions; using FictionArchive.Service.Shared.Extensions;
using FictionArchive.Service.Shared.Services.EventBus.Implementations; using FictionArchive.Service.Shared.Services.EventBus.Implementations;
@@ -15,7 +16,8 @@ public class Program
var isSchemaExport = SchemaExportDetector.IsSchemaExportMode(args); var isSchemaExport = SchemaExportDetector.IsSchemaExportMode(args);
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.AddLocalAppsettings();
#region Event Bus #region Event Bus
if (!isSchemaExport) if (!isSchemaExport)
@@ -31,7 +33,8 @@ public class Program
#region GraphQL #region GraphQL
builder.Services.AddDefaultGraphQl<Query, Mutation>(); builder.Services.AddDefaultGraphQl<Query, Mutation>()
.AddAuthorization();
#endregion #endregion
@@ -41,7 +44,11 @@ public class Program
builder.Services.AddTransient<UserManagementService>(); builder.Services.AddTransient<UserManagementService>();
builder.Services.AddHealthChecks(); builder.Services.AddHealthChecks();
// Authentication & Authorization
builder.Services.AddOidcAuthentication(builder.Configuration);
builder.Services.AddFictionArchiveAuthorization();
var app = builder.Build(); var app = builder.Build();
// Update database (skip in schema export mode) // Update database (skip in schema export mode)
@@ -52,8 +59,11 @@ public class Program
dbContext.UpdateDatabase(); dbContext.UpdateDatabase();
} }
app.UseAuthentication();
app.UseAuthorization();
app.MapGraphQL(); app.MapGraphQL();
app.MapHealthChecks("/healthz"); app.MapHealthChecks("/healthz");
app.RunWithGraphQLCommands(args); app.RunWithGraphQLCommands(args);

View File

@@ -12,5 +12,15 @@
"ConnectionString": "amqp://localhost", "ConnectionString": "amqp://localhost",
"ClientIdentifier": "UserService" "ClientIdentifier": "UserService"
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"OIDC": {
"Authority": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ClientId": "fictionarchive-api",
"Audience": "fictionarchive-api",
"ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": true,
"ValidateIssuerSigningKey": true
}
} }