Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swagger client generation with api key authentication

I have an existing C# ASP.NET Web API 2 project (.NET 4.6.1) where I need to integrate Swagger to generate the documentation as well as the client SDKs (only C# for the moment). This as been done using Swashbuckle in its latest version (5.5.3).

Everything went well except one thing. The issue I'm having is that the security (apiKey through HTTP headers) defined in my SwaggerConfig.cs ends up in the output JSON but somehow it's not linked to any of the methods (even though it is mandatory).

My security config is defined as follow:

GlobalConfiguration.Configuration.EnableSwagger(c =>
{
    c.SingleApiVersion("v1", "Dummy API")
    c.ApiKey("apiKey")
        .Description("API Key Authentication")
        .Name("X-API-Key")
        .In("header");
}).EnableSwaggerUi(c =>
{
    c.EnableApiKeySupport("X-API-Key", "header");
});

and the result in the generated Swagger JSON:

"securityDefinitions": {
        "apiKey": {
        "type": "apiKey",
        "description": "API Key Authentication",
        "name": "X-API-Key",
        "in": "header"
    }
}

And here is what I obtain:

"/api/ping": {
  "get": {
    "tags": [
      "Dummy"
    ],
    "summary": "Ping.",
    "operationId": "ping",
    "consumes": [],
    "produces": [
      "application/json"
    ],
    "responses": {
      "200": {
        "description": "OK",
        "schema": {
          "type": "string"
        }
      }
    }
  }
}

compared to what I want to obtain:

"/api/ping": {
  "get": {
    "tags": [
      "Dummy"
    ],
    "summary": "Ping.",
    "operationId": "ping",
    "consumes": [],
    "produces": [
      "application/json"
    ],
    "responses": {
      "200": {
        "description": "OK",
        "schema": {
          "type": "string"
        }
      }
    },
    "security": [
      {
        "apiKey": []
      }
    ]
  }
}

Any idea what I should change in the project so the security part is generated?

like image 776
albator1932 Avatar asked Jul 25 '26 10:07

albator1932


1 Answers

This question and others were great in helping me along the path. In my case, I was always just missing one more thing - the SwaggerUI wasn't passing the header name/value I chose (X-API-KEY) to my authentication handler when decorating actions/controllers with [Authorize]. My project uses .NET Core 3.1 and Swashbuckle 5. I made a custom class that inherits IOperationFilter that uses the Swashbuckle.AspNetCore.Filters nuget package below to piggyback off their implementation for oauth2.

// Startup.cs
// ...
services.AddSwaggerGen(options =>
{
  options.SwaggerDoc("v1", new OpenApiInfo { Title = nameof(BoardMinutes), Version = "v1" });

  // Adds authentication to the generated json which is also picked up by swagger.
  options.AddSecurityDefinition(ApiKeyAuthenticationOptions.DefaultScheme, new OpenApiSecurityScheme
  {
      In = ParameterLocation.Header,
      Name = ApiKeyAuthenticationHandler.ApiKeyHeaderName,
      Type = SecuritySchemeType.ApiKey
  });

  options.OperationFilter<ApiKeyOperationFilter>();
});

The key components are the options.AddSecurityDefinition() (I have some open endpoints and didn't want to provide a global filter) as well as options.OperationFilter<ApiKeyOperationFilter>().

// ApiKeyOperationFilter.cs
// ...
internal class ApiKeyOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        // Piggy back off of SecurityRequirementsOperationFilter from Swashbuckle.AspNetCore.Filters which has oauth2 as the default security scheme.
        var filter = new SecurityRequirementsOperationFilter(securitySchemaName: ApiKeyAuthenticationOptions.DefaultScheme);
        filter.Apply(operation, context);
    }
}

And finally - for the complete picture here's the authentication handler and authentication options

// ApiKeyAuthenticationOptions.cs
// ... 
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "API Key";
    public string Scheme => DefaultScheme;
    public string AuthenticationType = DefaultScheme;
}

// ApiKeyAuthenticationHandler.cs
// ...
internal class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private const string ProblemDetailsContentType = "application/problem+json";
    public const string ApiKeyHeaderName = "X-Api-Key";

    private readonly IApiKeyService _apiKeyService;
    private readonly ProblemDetailsFactory _problemDetailsFactory;

    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IApiKeyService apiKeyService,
        ProblemDetailsFactory problemDetailsFactory) : base(options, logger, encoder, clock)
    {
        _apiKeyService = apiKeyService ?? throw new ArgumentNullException(nameof(apiKeyService));
        _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory));
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues))
        {
            return AuthenticateResult.NoResult();
        }

        Guid.TryParse(apiKeyHeaderValues.FirstOrDefault(), out var apiKey);

        if (apiKeyHeaderValues.Count == 0 || apiKey == Guid.Empty)
        {
            return AuthenticateResult.NoResult();
        }

        var existingApiKey = await _apiKeyService.FindApiKeyAsync(apiKey);

        if (existingApiKey == null)
        {
            return AuthenticateResult.Fail("Invalid API Key provided.");
        }

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, existingApiKey.Owner)
        };

        var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
        var identities = new List<ClaimsIdentity> { identity };
        var principal = new ClaimsPrincipal(identities);
        var ticket = new AuthenticationTicket(principal, Options.Scheme);

        return AuthenticateResult.Success(ticket);
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = StatusCodes.Status401Unauthorized;
        Response.ContentType = ProblemDetailsContentType;
        var problemDetails = _problemDetailsFactory.CreateProblemDetails(Request.HttpContext, StatusCodes.Status401Unauthorized, nameof(HttpStatusCode.Unauthorized),
            detail: "Bad API key.");

        await Response.WriteAsync(JsonSerializer.Serialize(problemDetails));
    }

    protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = StatusCodes.Status403Forbidden;
        Response.ContentType = ProblemDetailsContentType;
        var problemDetails = _problemDetailsFactory.CreateProblemDetails(Request.HttpContext, StatusCodes.Status403Forbidden, nameof(HttpStatusCode.Forbidden),
            detail: "This API Key cannot access this resource.");

        await Response.WriteAsync(JsonSerializer.Serialize(problemDetails));
    }
}
like image 94
Ben Sampica Avatar answered Jul 27 '26 01:07

Ben Sampica



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!