Skip to content

Commit de2ca6b

Browse files
committed
Add configurable SecuritySchemes for OpenAPI auth
Introduce SignalROpenApiOptions.SecuritySchemes to allow custom authentication schemes in OpenAPI docs. Remove hardcoded Bearer scheme; generator now uses user-configured schemes. Update README and Program.cs with JWT Bearer examples. Expand tests to verify scheme inclusion and security requirements. Enables flexible, explicit authentication configuration.
1 parent 4c323e4 commit de2ca6b

5 files changed

Lines changed: 123 additions & 33 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ builder.Services.AddSignalROpenApi(options =>
8282
// Each entry appears as an apiKey security scheme (in: header) so users
8383
// can enter a value at runtime before invoking hub methods.
8484
options.ApiKeyHeaders["X-Custom-Header"] = "A custom header sent with every hub connection.";
85+
86+
// Security schemes applied to operations with [Authorize].
87+
// Define the authentication methods that SwaggerUI exposes in the Authorize dialog.
88+
options.SecuritySchemes["Bearer"] = new Microsoft.OpenApi.Models.OpenApiSecurityScheme
89+
{
90+
Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
91+
Scheme = "bearer",
92+
BearerFormat = "JWT",
93+
Description = "JWT Bearer token for SignalR hub authentication.",
94+
};
8595
});
8696

8797
builder.Services.AddSignalRSwaggerUi(options =>

samples/SignalR.OpenApi.Sample/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
22

33
using FluentValidation;
4+
using Microsoft.OpenApi.Models;
45
using SignalR.OpenApi.Extensions;
56
using SignalR.OpenApi.Sample.Hubs;
67

@@ -33,6 +34,16 @@
3334
// Each entry appears as an apiKey security scheme (in: header) so users
3435
// can enter a value at runtime before invoking hub methods.
3536
options.ApiKeyHeaders["X-Custom-Header"] = "A custom header sent with every hub connection.";
37+
38+
// Security schemes applied to operations with [Authorize].
39+
// Define the authentication methods that SwaggerUI exposes in the Authorize dialog.
40+
options.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
41+
{
42+
Type = SecuritySchemeType.Http,
43+
Scheme = "bearer",
44+
BearerFormat = "JWT",
45+
Description = "JWT Bearer token for SignalR hub authentication. The token is passed via the SignalR connection's accessTokenFactory.",
46+
};
3647
});
3748
builder.Services.AddSignalRFluentValidation();
3849
builder.Services.AddSignalRSwaggerUi(options =>

src/SignalR.OpenApi/Generation/SignalROpenApiDocumentGenerator.cs

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public OpenApiDocument GenerateDocument(IReadOnlyList<SignalRHubInfo> hubs)
7676

7777
if (requiresAuth)
7878
{
79-
AddSecuritySchemes(document);
79+
this.AddConfiguredSecuritySchemes(document);
8080
}
8181

8282
this.AddApiKeyHeaderSchemes(document);
@@ -138,19 +138,6 @@ private static void AddSignalRExtension(
138138
operation.Extensions["x-signalr"] = extension;
139139
}
140140

141-
private static void AddSecuritySchemes(OpenApiDocument document)
142-
{
143-
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>();
144-
145-
document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
146-
{
147-
Type = SecuritySchemeType.Http,
148-
Scheme = "bearer",
149-
BearerFormat = "JWT",
150-
Description = "JWT Bearer token for SignalR hub authentication. The token is passed via the SignalR connection's accessTokenFactory.",
151-
};
152-
}
153-
154141
private static void ApplyDataAnnotations(OpenApiSchema schema, ICustomAttributeProvider member)
155142
{
156143
var stringLength = GetAttribute<StringLengthAttribute>(member);
@@ -380,6 +367,25 @@ private static Microsoft.OpenApi.Any.OpenApiArray ConvertJsonArray(JsonElement e
380367
return arr;
381368
}
382369

370+
/// <summary>
371+
/// Adds the user-configured security schemes from
372+
/// <see cref="SignalROpenApiOptions.SecuritySchemes"/> to the document.
373+
/// </summary>
374+
private void AddConfiguredSecuritySchemes(OpenApiDocument document)
375+
{
376+
if (this.options.SecuritySchemes.Count == 0)
377+
{
378+
return;
379+
}
380+
381+
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>();
382+
383+
foreach (var scheme in this.options.SecuritySchemes)
384+
{
385+
document.Components.SecuritySchemes[scheme.Key] = scheme.Value;
386+
}
387+
}
388+
383389
/// <summary>
384390
/// Adds <c>apiKey</c> security schemes for each configured
385391
/// <see cref="SignalROpenApiOptions.ApiKeyHeaders"/> entry.
@@ -1151,24 +1157,26 @@ private OpenApiOperation CreateOperation(SignalRHubInfo hub, SignalRMethodInfo m
11511157
};
11521158
}
11531159

1154-
// Security
1155-
if (method.RequiresAuthorization || (hub.RequiresAuthorization && !method.AllowAnonymous))
1160+
// Security — reference every configured scheme so the lock icon
1161+
// and Authorize dialog cover all authentication methods.
1162+
if ((method.RequiresAuthorization || (hub.RequiresAuthorization && !method.AllowAnonymous))
1163+
&& this.options.SecuritySchemes.Count > 0)
11561164
{
1157-
operation.Security =
1158-
[
1159-
new OpenApiSecurityRequirement
1165+
var requirement = new OpenApiSecurityRequirement();
1166+
foreach (var schemeId in this.options.SecuritySchemes.Keys)
1167+
{
1168+
var schemeRef = new OpenApiSecurityScheme
11601169
{
1161-
[new OpenApiSecurityScheme
1170+
Reference = new OpenApiReference
11621171
{
1163-
Reference = new OpenApiReference
1164-
{
1165-
Type = ReferenceType.SecurityScheme,
1166-
Id = "Bearer",
1167-
},
1168-
}
1169-
] = [],
1170-
},
1171-
];
1172+
Type = ReferenceType.SecurityScheme,
1173+
Id = schemeId,
1174+
},
1175+
};
1176+
requirement[schemeRef] = [];
1177+
}
1178+
1179+
operation.Security = [requirement];
11721180
}
11731181

11741182
var isFlattened = method.Parameters.Count == 1

src/SignalR.OpenApi/SignalROpenApiOptions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using System.Reflection;
44
using System.Text.Json;
5+
using Microsoft.OpenApi.Models;
56

67
namespace SignalR.OpenApi;
78

@@ -104,4 +105,23 @@ public sealed class SignalROpenApiOptions
104105
/// </code>
105106
/// </example>
106107
public IDictionary<string, string> ApiKeyHeaders { get; } = new Dictionary<string, string>(StringComparer.Ordinal);
108+
109+
/// <summary>
110+
/// Gets the security schemes to include in the OpenAPI document when hubs
111+
/// require authorization. Each entry is added to <c>components/securitySchemes</c>
112+
/// and referenced in the <c>security</c> section of operations with
113+
/// <c>[Authorize]</c>. When empty, no authentication scheme is emitted.
114+
/// </summary>
115+
/// <example>
116+
/// <code>
117+
/// options.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
118+
/// {
119+
/// Type = SecuritySchemeType.Http,
120+
/// Scheme = "bearer",
121+
/// BearerFormat = "JWT",
122+
/// Description = "JWT Bearer token for SignalR hub authentication.",
123+
/// };
124+
/// </code>
125+
/// </example>
126+
public IDictionary<string, OpenApiSecurityScheme> SecuritySchemes { get; } = new Dictionary<string, OpenApiSecurityScheme>(StringComparer.Ordinal);
107127
}

test/SignalR.OpenApi.Tests/SignalROpenApiDocumentGeneratorTests.cs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,50 @@ public void GenerateDocument_ClientEvent_IncludesEventDiscriminators()
172172
}
173173

174174
/// <summary>
175-
/// Verifies security schemes are added when authorized hubs exist.
175+
/// Verifies security schemes are added when authorized hubs exist and schemes are configured.
176176
/// </summary>
177177
[TestMethod]
178-
public void GenerateDocument_AddsSecuritySchemes_WhenAuthorizedHubExists()
178+
public void GenerateDocument_AddsSecuritySchemes_WhenAuthorizedHubExistsAndSchemesConfigured()
179179
{
180-
var (discoverer, generator) = CreateServices();
180+
var (discoverer, generator) = CreateServices(o =>
181+
{
182+
o.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
183+
{
184+
Type = SecuritySchemeType.Http,
185+
Scheme = "bearer",
186+
BearerFormat = "JWT",
187+
Description = "JWT Bearer token.",
188+
};
189+
});
190+
181191
var hubs = discoverer.DiscoverHubs();
182192
var doc = generator.GenerateDocument(hubs);
183193

184194
Assert.IsNotNull(doc.Components.SecuritySchemes);
185195
Assert.IsTrue(doc.Components.SecuritySchemes.ContainsKey("Bearer"));
196+
Assert.AreEqual(SecuritySchemeType.Http, doc.Components.SecuritySchemes["Bearer"].Type);
197+
}
198+
199+
/// <summary>
200+
/// Verifies no security schemes are added when authorized hubs exist but no schemes are configured.
201+
/// </summary>
202+
[TestMethod]
203+
public void GenerateDocument_NoSecuritySchemes_WhenNoneConfigured()
204+
{
205+
var (discoverer, generator) = CreateServices();
206+
var hubs = discoverer.DiscoverHubs();
207+
var doc = generator.GenerateDocument(hubs);
208+
209+
if (doc.Components.SecuritySchemes is not null)
210+
{
211+
foreach (var scheme in doc.Components.SecuritySchemes.Values)
212+
{
213+
Assert.AreNotEqual(
214+
SecuritySchemeType.Http,
215+
scheme.Type,
216+
"Should not have HTTP auth schemes when SecuritySchemes is empty.");
217+
}
218+
}
186219
}
187220

188221
/// <summary>
@@ -247,7 +280,15 @@ public void GenerateDocument_GeneratesResponseSchema()
247280
[TestMethod]
248281
public void GenerateDocument_SetsSecurityOnAuthorizedMethods()
249282
{
250-
var (discoverer, generator) = CreateServices();
283+
var (discoverer, generator) = CreateServices(o =>
284+
{
285+
o.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
286+
{
287+
Type = SecuritySchemeType.Http,
288+
Scheme = "bearer",
289+
};
290+
});
291+
251292
var hubs = discoverer.DiscoverHubs();
252293
var doc = generator.GenerateDocument(hubs);
253294

0 commit comments

Comments
 (0)