Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 101 additions & 1 deletion Modules/OpenApi/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using GenHTTP.Api.Content;
using System;
using System.Collections.Generic;

using GenHTTP.Api.Content;
using GenHTTP.Modules.OpenApi.Discovery;
using GenHTTP.Modules.OpenApi.Handler;

using NSwag;

namespace GenHTTP.Modules.OpenApi;

Expand Down Expand Up @@ -27,4 +33,98 @@ public static T AddOpenApi<T>(this T builder, ApiDiscoveryRegistryBuilder regist
return builder;
}

/// <summary>
/// Configures the generated OpenAPI specification to describe an
/// API-key based authentication scheme.
/// </summary>
/// <remarks>
/// This helper registers an <c>apiKey</c> security scheme that reads
/// the key from a given HTTP header and marks all matching paths as
/// requiring that scheme. A <c>401 Unauthorized</c> response is added
/// to such operations if it is not already present.
/// </remarks>
/// <param name="builder">
/// The OpenAPI concern builder returned by <see cref="ApiDescription.Create"/>.
/// </param>
/// <param name="schemeName">
/// Logical name of the security scheme to be referenced by security requirements.
/// Defaults to <c>"X-API-Key"</c>.
/// </param>
/// <param name="headerName">
/// Name of the HTTP header that will contain the API key.
/// Defaults to <c>"X-API-Key"</c>.
/// </param>
/// <param name="includePath">
/// Optional predicate used to determine for which paths the security
/// requirement should be applied. If <c>null</c>, the requirement is
/// added for all paths.
/// </param>
/// <returns>
/// The same <see cref="OpenApiConcernBuilder"/> instance to allow fluent configuration.
/// </returns>
public static OpenApiConcernBuilder WithApiKeyAuthentication(
this OpenApiConcernBuilder builder,
string schemeName = "X-API-Key",
string headerName = "X-API-Key",
Func<string, bool>? includePath = null)
{
if (builder is null) throw new ArgumentNullException(nameof(builder));

includePath ??= _ => true;

return builder.PostProcessor((request, doc) =>
{
var securityDefinitions = doc.SecurityDefinitions;

// Register the API key security scheme if not already present
if (securityDefinitions != null && !securityDefinitions.ContainsKey(schemeName))
{
securityDefinitions.Add(schemeName, new OpenApiSecurityScheme
{
Name = headerName,
Type = OpenApiSecuritySchemeType.ApiKey,
In = OpenApiSecurityApiKeyLocation.Header,
});
}

if (doc.Paths is null)
{
return;
}

var requirement = new OpenApiSecurityRequirement
{
{ schemeName, Array.Empty<string>() }
};

foreach (var pathEntry in doc.Paths)
{
var path = pathEntry.Key;
var pathItem = pathEntry.Value;

if (!includePath(path) || pathItem is null)
{
continue;
}

foreach (var operation in pathItem.Values)
{
if (operation is null) continue;

operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(requirement);

if (!operation.Responses.ContainsKey("401"))
{
operation.Responses.Add("401", new OpenApiResponse
{
Description = "Unauthorized"
});
}
}
}
});
}


}
11 changes: 10 additions & 1 deletion Modules/OpenApi/Handler/OpenApiConcernBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,19 @@ public OpenApiConcernBuilder Caching(bool enabled)
/// Registers a function that will be called when an OpenAPI document has been
/// generated, directly before it is served to the client.
/// </summary>
/// <remarks>
/// This method can be called multiple times. All registered post processors
/// will be invoked in the order of registration.
/// </remarks>
/// <param name="action">The method to be invoked to adjust the generated document</param>
public OpenApiConcernBuilder PostProcessor(Action<IRequest, OpenApiDocument> action)
{
_postProcessor = action;
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}

_postProcessor += action;
return this;
}

Expand Down
42 changes: 42 additions & 0 deletions Testing/Acceptance/Modules/OpenApi/DiscoveryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.OpenApi;
using GenHTTP.Modules.OpenApi.Discovery;
using System.Linq;
using NSwag;
using OpenApiDocument = NSwag.OpenApiDocument;

namespace GenHTTP.Testing.Acceptance.Modules.OpenApi;
Expand Down Expand Up @@ -63,6 +65,46 @@ public async Task TestSamePathWithDifferentMethods(TestEngine engine)
Assert.IsTrue(operations?.ContainsKey(HttpMethod.Put));
}

[TestMethod]
[MultiEngineTest]
public async Task TestApiKeyAuthenticationIsReflected(TestEngine engine)
{
var api = Inline.Create()
.Get("/secure", () => 42)
.Add(ApiDescription.Create()
.WithApiKeyAuthentication(
schemeName: "X-API-Key",
headerName: "X-API-Key",
includePath: _ => true));

var result = await api.GetOpenApiAsync(engine);
var doc = result.Document;
Assert.IsNotNull(doc, "OpenAPI document should not be null.");

var paths = doc!.Paths;
Assert.IsNotNull(paths, "Paths collection should not be null.");

Assert.IsTrue(paths!.TryGetValue("/secure", out var pathItem),
"Path '/secure' should be present.");
Assert.IsNotNull(pathItem, "Path item for '/secure' should not be null.");

var operations = pathItem!.Operations;
Assert.IsNotNull(operations, "Operations collection should not be null.");

Assert.IsTrue(operations!.TryGetValue(HttpMethod.Get, out var operation),
"GET operation for '/secure' should be present.");
Assert.IsNotNull(operation, "Operation for GET '/secure' should not be null.");
var responses = operation!.Responses;
Assert.IsNotNull(responses, "Responses collection should not be null.");
Assert.IsTrue(responses!.ContainsKey("401"),
"401 Unauthorized response should be documented.");

var security = operation.Security;
Assert.IsNotNull(security, "Security requirements should be set on the operation.");
Assert.IsTrue(security!.Any(),
"There should be at least one security requirement.");
}

#region Supporting data structures

public class CustomExplorer : IApiExplorer
Expand Down