Skip to content

I'm attempting to implement a dual authentication scheme and I'm wondering whether it's feasible. If it is possible, could you assist me by pointing out any mistakes I might be making? #1113

Closed
@Lucoky

Description

@Lucoky

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions