Skip to content

Commit c632276

Browse files
committed
Handle apiKey and Bearer auth schemes distinctly
Refactor JS plugin to properly distinguish apiKey from Bearer/Basic auth, preventing apiKey values from being sent as Bearer tokens. Add unit and Playwright E2E tests to verify correct OpenAPI document emission and SwaggerUI behavior for apiKey schemes. Ensure multiple security schemes are supported and correctly represented in both docs and runtime requests.
1 parent 80ad62b commit c632276

3 files changed

Lines changed: 375 additions & 7 deletions

File tree

src/SignalR.OpenApi.SwaggerUi/Resources/signalr-openapi-plugin.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,29 @@ var SignalROpenApiPlugin = function (system) {
5050

5151
var authState = state.toJS();
5252
var keys = Object.keys(authState);
53-
if (keys.length === 0) {
54-
return undefined;
55-
}
5653

57-
var entry = authState[keys[0]];
58-
if (entry && entry.schema && entry.schema.type === "http" && entry.schema.scheme === "basic") {
59-
return btoa(entry.value.username + ":" + entry.value.password);
54+
for (var i = 0; i < keys.length; i++) {
55+
var entry = authState[keys[i]];
56+
if (!entry || !entry.schema) {
57+
continue;
58+
}
59+
60+
// HTTP Basic: return Base64-encoded credentials
61+
if (entry.schema.type === "http" && entry.schema.scheme === "basic" && entry.value) {
62+
return btoa(entry.value.username + ":" + entry.value.password);
63+
}
64+
65+
// HTTP Bearer or OAuth2: return the token value
66+
if (entry.value
67+
&& ((entry.schema.type === "http" && entry.schema.scheme !== "basic")
68+
|| entry.schema.type === "oauth2")) {
69+
return entry.value;
70+
}
71+
72+
// Skip apiKey schemes — handled by _getApiKeyHeaders()
6073
}
6174

62-
return entry ? entry.value : undefined;
75+
return undefined;
6376
};
6477

6578
// Get apiKey header values entered in the SwaggerUI authorize dialog.

test/SignalR.OpenApi.Tests/SignalROpenApiDocumentGeneratorTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,70 @@ public void GenerateDocument_AddsSecuritySchemes_WhenAuthorizedHubExistsAndSchem
196196
Assert.AreEqual(SecuritySchemeType.Http, doc.Components.SecuritySchemes["Bearer"].Type);
197197
}
198198

199+
/// <summary>
200+
/// Verifies that apiKey security schemes added via SecuritySchemes are emitted
201+
/// in the document's components when an authorized hub exists.
202+
/// </summary>
203+
[TestMethod]
204+
public void GenerateDocument_AddsApiKeySecurityScheme_WhenConfiguredViaSecuritySchemes()
205+
{
206+
var (discoverer, generator) = CreateServices(o =>
207+
{
208+
o.SecuritySchemes["Impersonation"] = new OpenApiSecurityScheme
209+
{
210+
Type = SecuritySchemeType.ApiKey,
211+
In = ParameterLocation.Header,
212+
Name = "X-Session-Id",
213+
Description = "Session ID for impersonation.",
214+
};
215+
});
216+
217+
var hubs = discoverer.DiscoverHubs();
218+
var doc = generator.GenerateDocument(hubs);
219+
220+
Assert.IsNotNull(doc.Components.SecuritySchemes);
221+
Assert.IsTrue(doc.Components.SecuritySchemes.ContainsKey("Impersonation"));
222+
223+
var scheme = doc.Components.SecuritySchemes["Impersonation"];
224+
Assert.AreEqual(SecuritySchemeType.ApiKey, scheme.Type);
225+
Assert.AreEqual(ParameterLocation.Header, scheme.In);
226+
Assert.AreEqual("X-Session-Id", scheme.Name);
227+
Assert.AreEqual("Session ID for impersonation.", scheme.Description);
228+
}
229+
230+
/// <summary>
231+
/// Verifies that both apiKey and HTTP Bearer security schemes can coexist
232+
/// in the document when configured via SecuritySchemes.
233+
/// </summary>
234+
[TestMethod]
235+
public void GenerateDocument_AddsMixedSecuritySchemes_WhenBothApiKeyAndBearerConfigured()
236+
{
237+
var (discoverer, generator) = CreateServices(o =>
238+
{
239+
o.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
240+
{
241+
Type = SecuritySchemeType.Http,
242+
Scheme = "bearer",
243+
BearerFormat = "JWT",
244+
};
245+
o.SecuritySchemes["Impersonation"] = new OpenApiSecurityScheme
246+
{
247+
Type = SecuritySchemeType.ApiKey,
248+
In = ParameterLocation.Header,
249+
Name = "X-Session-Id",
250+
Description = "Session ID for impersonation.",
251+
};
252+
});
253+
254+
var hubs = discoverer.DiscoverHubs();
255+
var doc = generator.GenerateDocument(hubs);
256+
257+
Assert.IsNotNull(doc.Components.SecuritySchemes);
258+
Assert.AreEqual(2, doc.Components.SecuritySchemes.Count);
259+
Assert.IsTrue(doc.Components.SecuritySchemes.ContainsKey("Bearer"));
260+
Assert.IsTrue(doc.Components.SecuritySchemes.ContainsKey("Impersonation"));
261+
}
262+
199263
/// <summary>
200264
/// Verifies no security schemes are added when authorized hubs exist but no schemes are configured.
201265
/// </summary>
@@ -300,6 +364,42 @@ public void GenerateDocument_SetsSecurityOnAuthorizedMethods()
300364
Assert.IsTrue(getUser.Security.Count > 0);
301365
}
302366

367+
/// <summary>
368+
/// Verifies security requirements reference all configured schemes including apiKey.
369+
/// </summary>
370+
[TestMethod]
371+
public void GenerateDocument_SecurityRequirementsIncludeApiKeyScheme()
372+
{
373+
var (discoverer, generator) = CreateServices(o =>
374+
{
375+
o.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
376+
{
377+
Type = SecuritySchemeType.Http,
378+
Scheme = "bearer",
379+
};
380+
o.SecuritySchemes["Impersonation"] = new OpenApiSecurityScheme
381+
{
382+
Type = SecuritySchemeType.ApiKey,
383+
In = ParameterLocation.Header,
384+
Name = "X-Session-Id",
385+
};
386+
});
387+
388+
var hubs = discoverer.DiscoverHubs();
389+
var doc = generator.GenerateDocument(hubs);
390+
391+
var getUser = doc.Paths["/hubs/Attribute/GetUserDetails"]
392+
.Operations[Microsoft.OpenApi.Models.OperationType.Post];
393+
394+
Assert.IsNotNull(getUser.Security);
395+
Assert.AreEqual(1, getUser.Security.Count, "Should have one security requirement.");
396+
397+
var requirement = getUser.Security[0];
398+
var schemeIds = requirement.Keys.Select(k => k.Reference.Id).ToList();
399+
CollectionAssert.Contains(schemeIds, "Bearer", "Should reference Bearer scheme.");
400+
CollectionAssert.Contains(schemeIds, "Impersonation", "Should reference Impersonation scheme.");
401+
}
402+
303403
/// <summary>
304404
/// Verifies data annotations are applied to schemas.
305405
/// </summary>

0 commit comments

Comments
 (0)