Closed
Description
Hello, the logic I'm using works fine in my API, but when I attempt to implement it in my GraphQL project, it fails to function. Here is the code from my Program.cs:
namespace Koble.GraphQL;
using Core;
using Core.Authorization;
using Core.Authorization.ApiKeyAuthorizationSchema;
using Core.Extensions;
using Entity;
using global::GraphQL;
using global::GraphQL.DataLoader;
using global::GraphQL.MicrosoftDI;
using Microsoft.EntityFrameworkCore;
using Stripe;
/// <summary>
/// GraphQL program.
/// </summary>
public class Program
{
/// <summary>
/// Main task.
/// </summary>
/// <param name="args">Main args.</param>
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
// Add Cache for GraphQL.
builder.Services.AddDistributedMemoryCache();
// Add GraphQl extra services, like dataloader.
builder.Services.AddSingleton<IDataLoaderContextAccessor, DataLoaderContextAccessor>();
builder.Services.AddSingleton<DataLoaderDocumentListener>();
builder.Services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
// Add GraphQL service, and set all the configuration.
builder.Services.AddSingleton(services => new Schema(new SelfActivatingServiceProvider(services)))
.AddGraphQLUpload()
.AddGraphQL(options =>
options.ConfigureExecution((opt, next) =>
{
opt.EnableMetrics = true;
opt.ThrowOnUnhandledException = true;
opt.MaxParallelExecutionCount = 100;
var services = opt.RequestServices;
var listener = services.GetRequiredService<DataLoaderDocumentListener>();
opt.Listeners.Add(listener);
return next(opt);
})
.AddSystemTextJson()
.AddAuthorizationRule());
// Add DbContextFactory for PSQL Koble database.
builder.Services.AddDbContextFactory<PsqlKobleContext>(
options =>
options.UseNpgsql(configuration.GetConnectionString("PSQLDB_KOBLE")));
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
// Add Koble.GraphQL settings (environment variables).
builder.Services.ConfigureEnvironmentSettings<Settings>(configuration);
builder.Services.Configure<GraphQlSettings>(configuration.GetSection("ConnectionStrings"));
builder.Services.Configure<GraphQlSettings>(configuration.GetSection("Google"));
builder.Services.Configure<GraphQlSettings>(configuration.GetSection("GraphQLSettings"));
builder.Services.Configure<GraphQlSettings>(configuration.GetSection("Sendgrid"));
builder.Services.Configure<GraphQlSettings>(configuration.GetSection("Twilio"));
// Add Cors configuration.
builder.Services.AddCors(options => options.AddDefaultPolicy(policy =>
{
policy
.WithOrigins(new string[]
{
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:3002",
"http://localhost:80",
"http://localhost",
"http://localhost:5173",
})
.AllowAnyHeader()
.AllowAnyMethod();
}));
// Add authentication and authorization.
builder.Services.AddAuthentication("Bearer")
.AddOAuth2Introspection(options =>
{
options.Authority = configuration.GetSection("GraphQLSettings")["KOBLE_IDENTITY_URL"];
options.ClientId = "koble_graphql";
options.ClientSecret = configuration.GetSection("GraphQLSettings")["KOBLE_GRAPHQL_SECRET"];
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromSeconds(30);
})
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyHandler>("ApiKey", options => { });
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiKeyPolicy", policy =>
{
policy.AddAuthenticationSchemes("ApiKey");
policy.RequireAuthenticatedUser();
});
options.AddUserStudentPolicies();
options.AddUserRecruiterPolicies();
options.AddUserStudentUserRecruiterPolicies();
options.AddApiKeyAuthorizationPolicy();
});
// Add controllers, for SchemaController.
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
// Add Stripe configuration.
StripeConfiguration.ApiKey = configuration.GetSection("Stripe")["STRIPE_API_KEY"];
var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors();
app.MapControllers();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseGraphQLAltair("/");
app.UseGraphQLUpload<Schema>().UseGraphQL<Schema>();
app.Run();
}
}
I am attempting to implement the apiKey Policy in this manner:
namespace Koble.GraphQL.Resolvers;
using Microsoft.AspNetCore.Http;
using global::GraphQL;
using global::GraphQL.Types;
// TODO: Delete this file after adding the first query extension that use the api key authentication.
/// <summary>
/// Query extension for initializing the add api key test resolver.
/// </summary>
public static class QueryAddApiKeyTestExtension
{
public static void AddAddApiKeyTestResolvers(this Query query)
{
query.Field<StringGraphType>("addApiKeyTest")
.Description("Add api key test.")
.AuthorizeWithPolicy("ApiKeyPolicy")
.Resolve(context =>
{
var httpContext = context.UserContext as HttpContext;
var etst = query.HttpContextAccessor.HttpContext;
return httpContext?.Request.Headers["api_key"];
});
}
}
I have applied this policy in a controller, and it works as expected. The setup in my API's program is quite similar, especially the .AddAuthentication()
part, which uses the same code.
Below is the code for my ApiKeyHandler:
namespace Koble.Core.Authorization.ApiKeyAuthorizationSchema;
using System.Security.Claims;
using System.Text.Encodings.Web;
using Entity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Utils;
/// <summary>
/// Defines the koble api key handler.
/// </summary>
public class ApiKeyHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private readonly IDbContextFactory<PsqlKobleContext> psqlKobleContext;
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyHandler"/> class.
/// </summary>
/// <param name="options">Api key options class.</param>
/// <param name="logger">Logger instance.</param>
/// <param name="encoder">Encode url.</param>
/// <param name="clock">System clock.</param>
/// <param name="psqlKobleContext">Koble db factory context.</param>
public ApiKeyHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IDbContextFactory<PsqlKobleContext> psqlKobleContext)
: base(options, logger, encoder, clock)
{
this.psqlKobleContext = psqlKobleContext;
}
/// <summary>
/// Handle authenticate async.
/// </summary>
/// <returns>A <see cref="Task{AuthenticationResult}"/> representing the result of the asynchronous operation.</returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var isHeaderName = this.Request.Headers.TryGetValue(this.Options.HeaderName, out var apiKeyValue);
if (!isHeaderName)
{
return AuthenticateResult.Fail("API key was not provided.");
}
var apiKey = apiKeyValue.FirstOrDefault();
if (string.IsNullOrWhiteSpace(apiKey))
{
return AuthenticateResult.Fail("API key was not provided.");
}
if (!apiKey.StartsWith(this.Options.ApiKeyPrefix, StringComparison.InvariantCulture))
{
return AuthenticateResult.Fail("API key format is not valid, it should start with SLT-.");
}
var authenticationInfo = await this.GetAuthenticationInfo(apiKey);
// If the authenticationInfo is null, then return null.
if (authenticationInfo == null)
{
return AuthenticateResult.Fail("API key is not valid.");
}
// Create the claims and put them in an identity.
var claims = new List<Claim>
{
new("sub", authenticationInfo.Id.ToString()),
new("scope", authenticationInfo.Scope),
};
var identity = new ClaimsIdentity(claims, this.Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
return AuthenticateResult.Success(ticket);
}
private async Task<AuthenticationInfo> GetAuthenticationInfo(string apiKey)
{
await using var context = await this.psqlKobleContext.CreateDbContextAsync();
var integrationService = await context.IntegrationServices
.FirstOrDefaultAsync(x => x.ApiKey == CommonMethods.GetSha256(apiKey));
if (integrationService != null)
{
return new AuthenticationInfo()
{
Id = integrationService.IntegrationServiceId,
Scope = "integration_service",
};
}
return null;
}
private class AuthenticationInfo
{
public Guid Id { get; set; }
public string Scope { get; set; }
}
}
Sorry for the inconvenience, the truth is I've been stuck on this for a couple of days, thank you!
Metadata
Metadata
Assignees
Labels
No labels