Skip to content

Commit b064169

Browse files
author
vp
committed
Add OpenAPI security transformer and options classes
Introduce OpenApiSecurityDocumentTransformer for injecting OAuth2 and HTTP bearer security schemes into OpenAPI docs, with configurable global requirements. Add OpenApiSecurityOptions for flexible scheme and endpoint configuration. Both files include MIT license headers and XML docs.
1 parent 7b94495 commit b064169

2 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// MIT-License
2+
// Copyright BridgingIT GmbH - All Rights Reserved
3+
// Use of this source code is governed by an MIT-style license that can be
4+
// found in the LICENSE file at https://github.com/bridgingit/bitdevkit/license
5+
6+
namespace BridgingIT.DevKit.Presentation.Web.OpenApi;
7+
8+
using Microsoft.AspNetCore.OpenApi;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.OpenApi;
11+
12+
/// <summary>
13+
/// Adds configurable OAuth2 and HTTP bearer security schemes to an OpenAPI document.
14+
/// </summary>
15+
/// <remarks>
16+
/// This transformer is OpenAPI-focused rather than Scalar-specific. It updates the generated
17+
/// document components and global security requirements so any OpenAPI consumer can understand
18+
/// the configured authentication model.
19+
/// </remarks>
20+
public sealed class OpenApiSecurityDocumentTransformer : IOpenApiDocumentTransformer
21+
{
22+
/// <summary>
23+
/// Applies the configured OpenAPI security schemes and global requirements to the document.
24+
/// </summary>
25+
/// <param name="document">The OpenAPI document being transformed.</param>
26+
/// <param name="context">The transformation context, including access to application services.</param>
27+
/// <param name="cancellationToken">A cancellation token for the operation.</param>
28+
/// <returns>A completed task after the document has been updated.</returns>
29+
/// <exception cref="InvalidOperationException">
30+
/// Thrown when <see cref="OpenApiSecurityOptions"/> has not been registered in the DI container.
31+
/// </exception>
32+
/// <remarks>
33+
/// The transformer can add:
34+
/// <list type="bullet">
35+
/// <item>A reusable OAuth2 authorization code security scheme.</item>
36+
/// <item>A reusable HTTP bearer security scheme for JWT tokens.</item>
37+
/// <item>Global OpenAPI security requirements that reference one or both schemes.</item>
38+
/// </list>
39+
/// </remarks>
40+
public Task TransformAsync(
41+
OpenApiDocument document,
42+
OpenApiDocumentTransformerContext context,
43+
CancellationToken cancellationToken)
44+
{
45+
var optionsAccessor = context.ApplicationServices
46+
.GetService<Microsoft.Extensions.Options.IOptions<OpenApiSecurityOptions>>();
47+
48+
if (optionsAccessor is null)
49+
{
50+
throw new InvalidOperationException(
51+
$"{nameof(OpenApiSecurityDocumentTransformer)} requires {nameof(OpenApiSecurityOptions)} to be registered in the DI container. ");
52+
}
53+
54+
var options = optionsAccessor.Value;
55+
56+
document.Components ??= new OpenApiComponents();
57+
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>(StringComparer.OrdinalIgnoreCase);
58+
document.Components.SecuritySchemes.Remove(options.OAuth2SchemeName);
59+
document.Components.SecuritySchemes.Remove(options.BearerSchemeName);
60+
61+
if (options.AddOAuth2AuthorizationCodeScheme &&
62+
!string.IsNullOrWhiteSpace(options.AuthorizationUrl) &&
63+
!string.IsNullOrWhiteSpace(options.TokenUrl))
64+
{
65+
document.Components.SecuritySchemes[options.OAuth2SchemeName] = new OpenApiSecurityScheme
66+
{
67+
Type = SecuritySchemeType.OAuth2,
68+
Description = "Authenticate using the OAuth2 authorization code flow.",
69+
Flows = new OpenApiOAuthFlows
70+
{
71+
AuthorizationCode = new OpenApiOAuthFlow
72+
{
73+
AuthorizationUrl = new Uri(options.AuthorizationUrl),
74+
TokenUrl = new Uri(options.TokenUrl),
75+
Scopes = (options.Scopes ?? []).ToDictionary(
76+
scope => scope,
77+
scope => $"Request the {scope} scope.",
78+
StringComparer.OrdinalIgnoreCase),
79+
},
80+
},
81+
};
82+
}
83+
84+
if (options.AddHttpBearerScheme)
85+
{
86+
document.Components.SecuritySchemes[options.BearerSchemeName] = new OpenApiSecurityScheme
87+
{
88+
Type = SecuritySchemeType.Http,
89+
Scheme = "bearer",
90+
BearerFormat = "JWT",
91+
Description = "The JWT token in the format: Bearer {token}",
92+
};
93+
}
94+
95+
if (!options.AddGlobalSecurityRequirements)
96+
{
97+
return Task.CompletedTask;
98+
}
99+
100+
document.Security =
101+
[
102+
.. (document.Security ?? [])
103+
.Where(requirement => !requirement.Keys.Any(scheme =>
104+
string.Equals(scheme.Reference?.Id, options.OAuth2SchemeName, StringComparison.OrdinalIgnoreCase) ||
105+
string.Equals(scheme.Reference?.Id, options.BearerSchemeName, StringComparison.OrdinalIgnoreCase))),
106+
];
107+
108+
if (options.AddOAuth2AuthorizationCodeScheme &&
109+
!string.IsNullOrWhiteSpace(options.AuthorizationUrl) &&
110+
!string.IsNullOrWhiteSpace(options.TokenUrl))
111+
{
112+
document.Security.Add(new OpenApiSecurityRequirement
113+
{
114+
[new OpenApiSecuritySchemeReference(options.OAuth2SchemeName, document)] = [.. (options.Scopes ?? [])],
115+
});
116+
}
117+
118+
if (options.AddHttpBearerScheme)
119+
{
120+
document.Security.Add(new OpenApiSecurityRequirement
121+
{
122+
[new OpenApiSecuritySchemeReference(options.BearerSchemeName, document)] = [],
123+
});
124+
}
125+
126+
return Task.CompletedTask;
127+
}
128+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// MIT-License
2+
// Copyright BridgingIT GmbH - All Rights Reserved
3+
// Use of this source code is governed by an MIT-style license that can be
4+
// found in the LICENSE file at https://github.com/bridgingit/bitdevkit/license
5+
6+
namespace BridgingIT.DevKit.Presentation.Web.OpenApi;
7+
8+
using Microsoft.AspNetCore.Authentication.JwtBearer;
9+
10+
/// <summary>
11+
/// Configures generated OpenAPI security schemes and related OAuth2 endpoint metadata.
12+
/// </summary>
13+
public class OpenApiSecurityOptions
14+
{
15+
/// <summary>
16+
/// Gets or sets the name of the HTTP bearer security scheme in the OpenAPI document.
17+
/// </summary>
18+
public string BearerSchemeName { get; set; } = JwtBearerDefaults.AuthenticationScheme;
19+
20+
/// <summary>
21+
/// Gets or sets the name of the OAuth2 security scheme in the OpenAPI document.
22+
/// </summary>
23+
public string OAuth2SchemeName { get; set; } = "OAuth2";
24+
25+
/// <summary>
26+
/// Gets or sets a value indicating whether a HTTP bearer security scheme should be added.
27+
/// </summary>
28+
public bool AddHttpBearerScheme { get; set; } = true;
29+
30+
/// <summary>
31+
/// Gets or sets a value indicating whether an OAuth2 authorization code security scheme should be added.
32+
/// </summary>
33+
public bool AddOAuth2AuthorizationCodeScheme { get; set; } = true;
34+
35+
/// <summary>
36+
/// Gets or sets a value indicating whether global OpenAPI security requirements should be added.
37+
/// </summary>
38+
public bool AddGlobalSecurityRequirements { get; set; } = true;
39+
40+
/// <summary>
41+
/// Gets or sets the authority base URL used to build OAuth2 authorization and token endpoint URLs.
42+
/// </summary>
43+
public string Authority { get; set; }
44+
45+
/// <summary>
46+
/// Gets or sets the relative authorization endpoint path appended to <see cref="Authority"/>.
47+
/// </summary>
48+
public string AuthorizationPath { get; set; } = "/api/_system/identity/connect/authorize";
49+
50+
/// <summary>
51+
/// Gets or sets the relative token endpoint path appended to <see cref="Authority"/>.
52+
/// </summary>
53+
public string TokenPath { get; set; } = "/api/_system/identity/connect/token";
54+
55+
/// <summary>
56+
/// Gets or sets the scopes exposed by the OAuth2 authorization code flow.
57+
/// </summary>
58+
public string[] Scopes { get; set; } = ["openid", "profile", "email", "roles"];
59+
60+
/// <summary>
61+
/// Gets the full OAuth2 authorization endpoint URL derived from <see cref="Authority"/> and <see cref="AuthorizationPath"/>.
62+
/// </summary>
63+
public string AuthorizationUrl
64+
{
65+
get
66+
{
67+
var authority = this.Authority?.TrimEnd('/');
68+
return string.IsNullOrWhiteSpace(authority)
69+
? null
70+
: $"{authority}{this.AuthorizationPath}";
71+
}
72+
}
73+
74+
/// <summary>
75+
/// Gets the full OAuth2 token endpoint URL derived from <see cref="Authority"/> and <see cref="TokenPath"/>.
76+
/// </summary>
77+
public string TokenUrl
78+
{
79+
get
80+
{
81+
var authority = this.Authority?.TrimEnd('/');
82+
return string.IsNullOrWhiteSpace(authority)
83+
? null
84+
: $"{authority}{this.TokenPath}";
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)