NextQuantumSystem.Protect.SwaggerGuard
1.5.1
dotnet add package NextQuantumSystem.Protect.SwaggerGuard --version 1.5.1
NuGet\Install-Package NextQuantumSystem.Protect.SwaggerGuard -Version 1.5.1
<PackageReference Include="NextQuantumSystem.Protect.SwaggerGuard" Version="1.5.1" />
<PackageVersion Include="NextQuantumSystem.Protect.SwaggerGuard" Version="1.5.1" />
<PackageReference Include="NextQuantumSystem.Protect.SwaggerGuard" />
paket add NextQuantumSystem.Protect.SwaggerGuard --version 1.5.1
#r "nuget: NextQuantumSystem.Protect.SwaggerGuard, 1.5.1"
#:package NextQuantumSystem.Protect.SwaggerGuard@1.5.1
#addin nuget:?package=NextQuantumSystem.Protect.SwaggerGuard&version=1.5.1
#tool nuget:?package=NextQuantumSystem.Protect.SwaggerGuard&version=1.5.1
NextQuantumSystem.Protect — SwaggerGuard
🌐 Language / Dil: English (below) · Türkçe
Puts Swagger UI and related endpoints behind a JWT + role gate — a reusable protection module you can drop into any project as a DLL.
| Version | 1.5.0 |
| Date | 2026-07-20 |
| Target Frameworks | .NET 9 · 10 (net9.0, net10.0) |
| ABP | per-TFM: 9.3.7 / 10.5.0 (optional — plain ASP.NET Core is also supported) |
| Status | Core + ABP wrapper + 39 integration tests, green on every target framework |
⚡ Quick Start (TL;DR)
ABP host — 3 steps:
// 1) Add package: dotnet add package NextQuantumSystem.Protect.SwaggerGuard.Abp
// 2) Depend on the module:
[DependsOn(typeof(ProtectSwaggerGuardModule))]
public class MyHttpApiHostModule : AbpModule { }
// 3) Add the "Swagger" section to appsettings.json (below). Middleware wires up automatically.
Plain ASP.NET Core host — 3 steps:
// 1) Add package: dotnet add package NextQuantumSystem.Protect.SwaggerGuard
// 2) Program.cs:
builder.Services.AddSwaggerProtection(builder.Configuration);
app.UseSwaggerProtection(); // ← BEFORE app.UseSwaggerUI()
// 3) Add the "Swagger" section to appsettings.json (below).
Minimal appsettings.json:
{
"Swagger": {
"ProtectedPaths": [ "swagger", "swagger/v1/swagger.json" ],
"AllowedRoles": [ "admin" ],
"Authority": "https://id.example.com" // ← the IdP that issues the token; REQUIRED
}
}
Result: everyone hitting /swagger first sees a token-input page → only a valid JWT with the admin role gets in.
The page is English by default and Turkish on request (Accept-Language, a ?lang= link on the page, or pin it with "Language": "en" | "tr").
1. Purpose — Why & What For
Problem
In ABP / ASP.NET Core projects the Swagger UI (/swagger) is often publicly open. On dev/staging this leaks your API schema, endpoint list and example payloads to unauthorized people.
Solution
Swagger no longer opens directly. Instead a token-input page appears; the user pastes a valid JWT, the token's signature + issuer + expiry are verified, and only users with an allowed role (AllowedRoles) can reach Swagger.
Why a separate module / DLL?
Instead of copy-pasting the same protection into dozens of microservices, you drop one DLL into every project for consistent, defense-in-depth protection (each service protects itself; you don't rely on the gateway alone). Each project only changes values in its own appsettings.json; the code and the DLL stay identical — no rebuild needed.
2. How It Works — Flow
Request ──▶ SwaggerProtectionMiddleware
│
├─ IsEnabled=false? ──▶ pass through
│
├─ Path == TokenInputPath (auth/swagger)?
│ ├─ GET ──▶ show token-input page (200)
│ └─ POST ──▶ validate token
│ ├─ invalid/expired ──▶ page + 401
│ ├─ role not allowed ──▶ page + 403
│ └─ valid + role OK ──▶ set cookie + 302 to returnUrl
│
├─ Path not in ProtectedPaths? ──▶ pass through
│
└─ Protected path:
├─ no cookie ──▶ 302 to login
├─ invalid cookie ──▶ clear cookie + 302 to login
├─ role not allowed ──▶ 403
└─ all OK ──▶ pass to Swagger
Cookie: the validated token is written as __auth_token, HttpOnly + Secure (on HTTPS) + SameSite=Lax, valid for CookieExpirationHours.
3. Architecture
Decision: the core is ABP-independent, so both ABP and plain ASP.NET Core hosts can use it. The ABP side is a thin wrapper.
Protect/
├── src/
│ ├── NextQuantumSystem.Protect.SwaggerGuard/ ← ABP-free core
│ │ ├── SwaggerProtectionOptions.cs · all settings
│ │ ├── SwaggerProtectionMiddleware.cs · path/cookie/role/redirect logic
│ │ ├── SwaggerProtectionExtensions.cs · AddSwaggerProtection / UseSwaggerProtection
│ │ └── Internal/
│ │ ├── IJwtTokenValidator.cs · validation abstraction
│ │ ├── JwksJwtTokenValidator.cs · signature validation via Authority/JWKS
│ │ ├── SymmetricJwtTokenValidator.cs · signature validation via SigningKey (HS256)
│ │ ├── SwaggerGuardTexts.cs · user-facing strings (en/tr)
│ │ ├── LanguageResolver.cs · ?lang → option → Accept-Language → en
│ │ └── TokenInputPageRenderer.cs · token-input page (HTML)
│ │
│ └── NextQuantumSystem.Protect.SwaggerGuard.Abp/ ← thin ABP module
│ └── ProtectSwaggerGuardModule.cs · AbpModule, automatic wiring
│
└── test/
└── NextQuantumSystem.Protect.SwaggerGuard.Tests/ ← in-memory TestServer, 39 tests
| Layer | Dependency | Purpose |
|---|---|---|
SwaggerGuard |
Microsoft.AspNetCore.App, Microsoft.IdentityModel.* |
Framework-agnostic core. Plain ASP.NET Core hosts consume this directly. |
SwaggerGuard.Abp |
Volo.Abp.AspNetCore + core |
Automatic wiring for ABP hosts. |
4. Setup & Usage
4.a) ABP Host
1. Add the package:
dotnet add package NextQuantumSystem.Protect.SwaggerGuard.Abp
2. Depend on the module:
[DependsOn(
typeof(ProtectSwaggerGuardModule) // ← one line, everything else is automatic
// ... your other modules
)]
public class MyServiceHttpApiHostModule : AbpModule { }
3. Add the "Swagger" section to appsettings.json (see Configuration).
Because the module is a dependency of the host, its middleware registers before the host's own
UseAbpSwaggerUI()call (ABP init order: dependencies first). No extraUse...call needed.
What the module does for you — exactly the two calls you'd otherwise write by hand:
public override void ConfigureServices(ServiceConfigurationContext context)
=> context.Services.AddSwaggerProtection(context.Services.GetConfiguration());
public override void OnApplicationInitialization(ApplicationInitializationContext context)
=> context.GetApplicationBuilder().UseSwaggerProtection();
Overriding options on an ABP host — the module binds the "Swagger" section; add a Configure<> in your own module to patch individual values:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<SwaggerProtectionOptions>(o =>
{
o.Language = "en";
o.IsEnabled = !context.Services.GetHostingEnvironment().IsDevelopment();
});
}
Not using the ABP package? Any ABP host can also consume the core package directly — the module is only a convenience:
public override void ConfigureServices(ServiceConfigurationContext context)
=> context.Services.AddSwaggerProtection(context.Services.GetConfiguration());
public override void OnApplicationInitialization(ApplicationInitializationContext context)
=> context.GetApplicationBuilder().UseSwaggerProtection(); // before UseAbpSwaggerUI()
4.b) Plain ASP.NET Core Host
1. Add the package:
dotnet add package NextQuantumSystem.Protect.SwaggerGuard
2. Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSwaggerProtection(builder.Configuration); // binds the "Swagger" section
var app = builder.Build();
app.UseSwaggerProtection(); // ← BEFORE Swagger UI
app.UseSwagger();
app.UseSwaggerUI();
app.Run();
⚠️ Both calls are required.
AddSwaggerProtectionregisters the services (options + token validator);UseSwaggerProtectionputs the middleware in the pipeline. Calling onlyUse…throws at startup:Unable to resolve service for type 'IJwtTokenValidator' while attempting to activate 'SwaggerProtectionMiddleware'.
4.b.1) AddSwaggerProtection — the three overloads
| Overload | Use it when | What it does |
|---|---|---|
AddSwaggerProtection(IConfiguration) |
Standard case | Binds the "Swagger" section of the given configuration. |
AddSwaggerProtection(IConfigurationSection) |
Your settings live under a different section name | Binds exactly the section you hand it. |
AddSwaggerProtection(Action<SwaggerProtectionOptions>) |
Values come from code, or you want to combine sources | Configures options in code; nothing is bound implicitly. |
All three register the same services and are interchangeable — pick one, don't chain them.
a) Bind the "Swagger" section (most common):
builder.Services.AddSwaggerProtection(builder.Configuration);
b) Bind a differently named section — e.g. your settings sit under "ApiDocs":
builder.Services.AddSwaggerProtection(builder.Configuration.GetSection("ApiDocs"));
c) Configure in code — no config file involvement:
builder.Services.AddSwaggerProtection(o =>
{
o.Authority = "https://id.example.com";
o.ProtectedPaths = new() { "swagger", "swagger/v1/swagger.json" };
o.AllowedRoles = new() { "admin" };
o.Language = "en";
});
d) Config file + code override — bind first, then patch the values you don't want duplicated in appsettings.json (useful when the API signs its own tokens and the key already lives elsewhere):
builder.Services.AddSwaggerProtection(o =>
{
builder.Configuration.GetSection("Swagger").Bind(o); // everything from config…
o.SigningKey = builder.Configuration["TokenOptions:SecurityKey"]; // …key from a single source
o.ValidIssuer = builder.Configuration["TokenOptions:Issuer"];
});
e) Environment-dependent setup — protection off locally, on everywhere else:
builder.Services.AddSwaggerProtection(o =>
{
builder.Configuration.GetSection("Swagger").Bind(o);
o.IsEnabled = !builder.Environment.IsDevelopment();
});
f) Custom token validator — replace the built-in validation entirely (the package registers its own with TryAdd, so register yours before the Add… call, or RemoveAll first):
builder.Services.AddSwaggerProtection(builder.Configuration);
builder.Services.RemoveAll<IJwtTokenValidator>();
builder.Services.AddSingleton<IJwtTokenValidator, MyOwnValidator>();
4.b.2) UseSwaggerProtection — where it goes in the pipeline
The middleware must run before whatever serves the protected paths, otherwise Swagger answers first and the guard never sees the request.
a) Minimal API / typical host:
app.UseSwaggerProtection(); // ← before both
app.UseSwagger();
app.UseSwaggerUI();
b) With authentication/authorization — put it after them; it uses its own cookie, but this keeps HttpContext.User populated for anything downstream:
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSwaggerProtection(); // ← after auth, before Swagger
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
c) Behind a path base / reverse proxy — call UsePathBase and UseForwardedHeaders first so the guard matches the paths the user actually sees:
app.UsePathBase("/api-gateway");
app.UseForwardedHeaders();
app.UseSwaggerProtection();
d) Only in some environments — either gate the call or use IsEnabled (see (e) above); prefer IsEnabled so the behaviour stays visible in config:
if (!app.Environment.IsDevelopment())
{
app.UseSwaggerProtection();
}
❗ Common mistakes: calling
Use…afterUseSwaggerUI()(guard never triggers), calling onlyUse…withoutAdd…(startup exception), or puttingTokenInputPathinsideProtectedPaths(infinite redirect).
4.c) NuGet Distribution
Two packages are produced:
| Package | When to use | Dependencies |
|---|---|---|
NextQuantumSystem.Protect.SwaggerGuard |
Plain ASP.NET Core host | Microsoft.IdentityModel.* |
NextQuantumSystem.Protect.SwaggerGuard.Abp |
ABP host | ↑ core + Volo.Abp.AspNetCore |
When you install the ABP package the core comes in automatically (declared as a dependency). On an ABP host you only need the
.Abppackage.
Build the packages (two .nupkg under ./artifacts):
dotnet pack NextQuantumSystem.Protect.slnx -c Release -o ./artifacts
Push to public nuget.org:
dotnet nuget push "artifacts/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key <NUGET_API_KEY> --skip-duplicate
⚠️ A version published to nuget.org cannot be deleted (only "unlisted") and the package ID is reserved globally.
Automated publishing (CI/CD — recommended)
With .github/workflows/build.yml:
- Every push/PR → build + test + pack (uploaded as artifacts).
- On GitHub Release → auto-push to nuget.org.
Publishing uses NuGet Trusted Publishing (OIDC) — no long-lived API key secret is stored; a short-lived key is obtained via the production environment.
Releasing a new version — the Release tag is the single source of truth for the version:
# Create a GitHub Release with tag vX.Y.Z (e.g. v1.2.0)
# → build.yml derives the package version from the tag (v1.2.0 → 1.2.0),
# then packs + pushes automatically. No csproj edit required.
# Alternatively: Actions → build → "Run workflow" with an optional "version" input.
5. Configuration Reference
The "Swagger" section in appsettings.json binds to SwaggerProtectionOptions.
{
"Swagger": {
"IsEnabled": true,
"RoutePrefix": "swagger",
"DocumentTitle": "API Docs",
"CookieName": "__auth_token",
"CookieExpirationHours": 8,
"TokenInputPath": "auth/swagger",
"ProtectedPaths": [ "swagger", "api/abp/api-definition", "swagger/v1/swagger.json" ],
"AllowedRoles": [ "admin" ],
"Authority": "https://id.example.com",
"Audience": "my-service",
"ValidateAudience": false,
"ValidateIssuer": true,
"RequireHttpsMetadata": true,
"RoleClaimTypes": [ "role", "roles" ]
}
}
| Setting | Type | Default | Description |
|---|---|---|---|
IsEnabled |
bool | true |
If false the middleware does nothing (protection off). |
RoutePrefix |
string | "swagger" |
Informational; used as the returnUrl fallback. |
DocumentTitle |
string? | null |
<title> of the token-input page. Empty → picked from the display language (API Documentation / API Dokümanı). |
Language |
string? | null |
UI language: "en" or "tr". Empty → Accept-Language, falling back to English. See Language. |
CookieName |
string | "__auth_token" |
Cookie holding the validated JWT. |
CookieExpirationHours |
int | 8 |
Cookie lifetime (hours). |
TokenInputPath |
string | "auth/swagger" |
Path of the token-input page. Must never be inside ProtectedPaths (infinite redirect). |
ProtectedPaths |
string[] | [] |
Path prefixes (no leading slash) to protect. |
AllowedRoles |
string[] | [] |
Values allowed through the gate. Empty: any validated token is accepted. Not limited to roles — see Who gets in. |
Authority |
string? | null |
The OpenID/OAuth authority issuing tokens; JWKS is fetched from it. Required unless SigningKey is set. |
SigningKey |
string? | null |
Symmetric HMAC key (HS256). When set, validation uses this key instead of Authority/JWKS — for APIs that mint their own tokens with no IdP. Must be the same key the token issuer signs with (≥32 chars). |
ValidIssuer |
string? | null |
Expected issuer, SigningKey mode only (in JWKS mode the issuer comes from Authority metadata). Falls back to Authority when empty. |
Audience |
string? | null |
Expected audience (required when ValidateAudience=true). |
ValidateAudience |
bool | false |
Validate the audience? |
ValidateIssuer |
bool | true |
Validate the issuer (from the Authority)? |
RequireHttpsMetadata |
bool | true |
Require HTTPS for authority metadata? (false in dev). |
RoleClaimTypes |
string[] | ["role","roles",".../role"] |
Which claim(s) AllowedRoles is matched against. Any claim works — username, id, GUID… see Who gets in. |
📄 Full example + annotated scenarios:
samples/appsettings.Swagger.sample.jsonc— every option explained inline plus 5 ready scenarios (production, development, no-role, audience-validated, disabled).
Values that differ per project (
Authority,Audience) live in each host's own config, not in the DLL. To reduce repetition: a sharedappsettings.shared.json, environment variables (Swagger__Authority=...), or the in-codeAddSwaggerProtection(o => ...)overload.
5.1) Language — en / tr
Every string the user can see — the token-input page, its error messages and the 403 body — ships in English and Turkish. English is the default; nothing has to be configured.
How the language for a request is picked (first match wins):
| # | Source | Example |
|---|---|---|
| 1 | Explicit choice on the request | /auth/swagger?lang=tr, or the hidden lang field the form posts back |
| 2 | Language option |
"Swagger": { "Language": "tr" } |
| 3 | Accept-Language header |
tr-TR,tr;q=0.9,en;q=0.8 → Turkish |
| 4 | Fallback | English |
Regional tags are accepted (tr-TR → tr); unsupported languages (de-DE) fall back to English.
Pin the UI to one language:
{ "Swagger": { "Language": "en" } } // or "tr"
builder.Services.AddSwaggerProtection(o => o.Language = "en");
Leave it automatic — omit Language and each visitor gets their browser's preference, English if it doesn't match.
The page always shows a link to the other language in the bottom-right corner, so the choice is never a dead end. The selected language survives a failed login (it is posted back with the form) and the response carries a matching Content-Language header.
Log messages and internal validation errors are always English — they go to your logs, not to the user.
5.2) Who gets in — the gate can be any claim, not just roles
Despite the name, AllowedRoles is not limited to roles. Two settings work together:
| Setting | Question it answers |
|---|---|
RoleClaimTypes |
Which claim do we look at? |
AllowedRoles |
Which values of that claim are allowed in? |
So you can open Swagger by username, user id, courier id, GUID, e-mail — anything your token already carries. No change to how you issue tokens.
The example token
Every example below uses this real payload:
{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "161",
"email": "admin",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Example Admin",
"UserId": "161",
"CourierId": "18",
"UserGuid": "3f1a7c9e-1111-4a2b-8c3d-5e6f70819a2b",
"UserName": "admin",
"iss": "api.example.com",
"aud": "api.example.com"
}
Note it has no role claim at all — and that is fine. Pick any other one.
Example 1 — Anyone with a valid token
"AllowedRoles": []
Leave the list empty and the claim check is skipped entirely: a correctly signed, unexpired token is enough. RoleClaimTypes is ignored in this mode.
Example 2 — By username
"RoleClaimTypes": [ "UserName" ],
"AllowedRoles": [ "admin" ]
Only the user whose UserName is admin gets in. ✅ our token passes.
Example 3 — By username, several people
"RoleClaimTypes": [ "UserName" ],
"AllowedRoles": [ "admin", "john", "sarah" ]
Any one of the three is enough. ✅ our token passes on admin.
Example 4 — By user id
"RoleClaimTypes": [ "UserId" ],
"AllowedRoles": [ "161" ]
Only user 161. ✅ passes. Change it to 162 and the same token gets 403.
Example 5 — By courier id
"RoleClaimTypes": [ "CourierId" ],
"AllowedRoles": [ "18" ]
Only courier 18. ✅ passes.
Example 6 — By GUID
"RoleClaimTypes": [ "UserGuid" ],
"AllowedRoles": [ "3f1a7c9e-1111-4a2b-8c3d-5e6f70819a2b" ]
Pinned to one specific account, immune to a renamed username. ✅ passes.
Example 7 — By e-mail
"RoleClaimTypes": [ "email" ],
"AllowedRoles": [ "admin" ]
✅ passes — in this token email happens to hold admin.
Example 8 — By a long URI claim
"RoleClaimTypes": [ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" ],
"AllowedRoles": [ "161" ]
Long claim names work too — just write them in full, exactly as they appear in the token. ✅ passes.
Example 9 — Two doors at once
"RoleClaimTypes": [ "UserName", "CourierId" ],
"AllowedRoles": [ "admin", "99" ]
Reads as: UserName is admin or 99, or CourierId is admin or 99. Any single match opens the door. ✅ passes on UserName = admin.
Example 10 — A real role claim (classic IdP)
"RoleClaimTypes": [ "role", "roles" ],
"AllowedRoles": [ "admin" ]
The classic setup, for tokens that do carry roles. ❌ our example token has no role claim, so it gets 403 — a good reminder to check what your token actually contains.
The four rules behind all of this
- Everything is OR. Any claim type × any allowed value that matches is enough. There is no way to require two conditions together (AND).
- Values are compared case-insensitively.
admin,AdminandADMINare the same. Claim names are matched exactly. - Empty
AllowedRolesmeans no restriction — any valid token passes, andRoleClaimTypesis not consulted. - Claim names are taken verbatim. No inbound mapping is applied, so write the name exactly as it appears in the token payload. Paste your JWT into jwt.io to see the real names.
⚠️ These checks run after the signature, issuer and expiry are verified. A claim gate is never a substitute for that — it narrows down who among the holders of a valid token may open Swagger.
6. Security Notes
- 🔴 The JWT signature is always verified. If
Authorityis not configured, validation fails — a token is never accepted by "just decoding" it. Signature + issuer + expiry are always checked. This stops an attacker from forging their ownrole=admintoken. - 🔁 Infinite-redirect guard:
TokenInputPathis never protected; it is handled on its own. - 🍪 Cookie:
HttpOnly(JS can't read it) +Secure(on HTTPS) +SameSite=Lax. Stale/invalid cookies are cleared automatically. - ↩️ Open-redirect guard:
returnUrlallows only local (same-site) paths;//or absolute URLs are rejected. - ⚠️ Protecting
api/abp/api-definitionmay affect ABP dynamic client-proxy generation (tools calling without a cookie get 302). Make it a conscious choice. - 🌐 Gateway/reverse-proxy:
X-Forwarded-*/ PathBase can change the perceived path; tuneProtectedPathsaccordingly.
7. Tests
39 integration tests over real HTTP on an in-memory TestServer (test/NextQuantumSystem.Protect.SwaggerGuard.Tests), run against both target frameworks.
dotnet test NextQuantumSystem.Protect.slnx
Middleware flow (SwaggerProtectionMiddlewareTests) — a symmetric-key validator is injected instead of JWKS so the full logic is exercised without a live authority:
| Scenario | Expected |
|---|---|
Unprotected path (/healthz) |
200 (passes) |
| Protected path, no cookie | 302 → /auth/swagger?returnUrl=... |
| Login page GET | 200 + HTML |
| POST valid admin token | 302 + Set-Cookie |
| POST valid non-admin token | 403 |
| POST invalid token | 401 |
| POST expired token | 401 |
| Protected path + valid admin cookie | 200 (passes) |
| Protected path + non-admin cookie | 403 |
| Protected path + garbage cookie | 302 + cookie cleared |
Symmetric key (SymmetricSigningKeyTests) — the real DI selection, no validator swapped in:
| Scenario | Expected |
|---|---|
Token signed with the configured SigningKey |
200 (passes) |
| Token signed with a different key | 401 |
Language (LocalizationTests):
| Scenario | Expected |
|---|---|
| Nothing configured | English page |
?lang=tr / ?lang=en |
that language |
Language option vs. Accept-Language |
option wins |
Accept-Language: tr-TR;q=0.9, en;q=0.8 |
Turkish |
Accept-Language: de-DE |
English (fallback) |
Failed login with lang=tr |
error message in Turkish |
| Role error / 403 body | localized |
| Page contains a link to the other language | yes |
Claim gate (ClaimMatchingTests) — every example from Who gets in is pinned here, using the documented sample token:
| Scenario | Expected |
|---|---|
UserName = admin / ADMIN |
200 (case-insensitive) |
UserName = john |
403 |
UserId = 161 / 162 |
200 / 403 |
CourierId = 18, UserGuid, email |
200 |
Long URI claims (.../nameidentifier, .../name) |
200 |
role claim the token doesn't have |
403 |
Empty AllowedRoles |
200 (no restriction) |
| Several values / several claim types | OR semantics |
| Value present in another claim type | 403 (no cross-matching) |
| Claim names taken verbatim (no inbound mapping) | 200 |
Turkish claim names (KullaniciAdi) |
200 (identical behaviour) |
Result: 39/39 passing on net9.0 and net10.0.
8. FAQ
Q: Is this an ABP module or a plain library?
Both. The core (SwaggerGuard) is an ABP-free plain library; SwaggerGuard.Abp is a thin module that auto-wires it for ABP hosts.
Q: Do I need a separate DLL per microservice?
No. The same DLL is used everywhere; only appsettings.json changes.
Q: Does NuGet support multi-language READMEs? NuGet shows a single README per package. The convention (used here) is one README with language sections and a selector at the top.
9. Version History
1.5.0 — 2026-07-20
Ships everything from
1.3.0and1.4.0as well — those versions were prepared but never published to nuget.org.
- Target frameworks stay at
net9.0/net10.0. Widening the core tonet7.0/net8.0was explored and reverted:net7.0is out of support (May 2024) and its runtime can no longer be provisioned on CI, so it could not be tested, and keeping the whole solution on one pair of frameworks is simpler to reason about than a core and an ABP package that drift apart. net11.0is not targeted yet — it has not been released.
1.4.0 — 2026-07-20
- Bilingual UI (en/tr). Every user-facing string — token-input page, error messages,
403body — now ships in both languages, with a language link on the page itself. See Language. - Language per request:
?lang=→Languageoption →Accept-Language→ English. Regional tags (tr-TR) are accepted; unsupported ones fall back to English. - English is now the default. Previously the page was Turkish-only. Set
"Language": "tr"to keep the old wording. DocumentTitledefaults tonulland follows the display language; an explicitly configured title still wins.- Responses carry a matching
Content-Languageheader; log and internal validation messages are now consistently English. - Documentation:
AddSwaggerProtection/UseSwaggerProtectionsections rewritten with per-overload guidance and worked examples (config binding, named sections, code-only, config+override, environment-gated, custom validator, pipeline placement, common mistakes). - New documentation section Who gets in: 10 standalone, copy-paste examples showing that the gate can be built on ANY claim (username, user id, courier id, GUID, e-mail, long URI claims) — not just roles. Every example is pinned by a test.
- 39/39 tests passing on
net9.0andnet10.0.
1.3.0 — 2026-07-20
- Symmetric-key (HS256) validation. Set
SigningKey(plus optionalValidIssuer) and the guard validates with that key instead of Authority/JWKS. Unblocks APIs that issue their own tokens and have no OpenID authority — previously every token was rejected because the JWKS metadata fetch failed. - The validator is chosen at resolve time:
SigningKeyset →SymmetricJwtTokenValidator, otherwiseJwksJwtTokenValidator. Signature + issuer + expiry are still always verified. - Fully backward compatible; Authority/JWKS setups are unaffected.
- 12/12 tests passing on
net9.0andnet10.0.
1.2.0 — 2026-07-18
- Multi-targeting: supports
net9.0andnet10.0. More projects can now consume the package. - The ABP package references the matching ABP major per framework (
net9.0→9.3.7,net10.0→10.5.0). - Tests run on both frameworks — 10/10 passing on each.
net8.0was dropped because its ABP line (8.3.x) drags in transitive packages with known vulnerability advisories.net6.0/net7.0are out of support & dropped by ABP. The floor isnet9.0.- CI: the package version is now derived from the Release tag (
vX.Y.Z), so the tag is the single source of truth. - Note:
1.0.2was published earlier with identical code due to a version mix-up (the tag wasn't wired to the package version yet); it is superseded by1.2.0.
1.0.1 — 2026-07-18
- Multilingual README (English + Türkçe) with a language selector at the top.
1.0.0 — 2026-07-18
- First release.
SwaggerProtectionMiddleware: path matching, cookie-based JWT gate, role check, redirect flow.JwksJwtTokenValidator: signature + issuer + expiry validation via Authority/JWKS.- Token-input page (no MVC, HTML rendered).
AddSwaggerProtection/UseSwaggerProtectionextensions for plain ASP.NET Core.ProtectSwaggerGuardModulefor ABP hosts (automatic wiring via a single[DependsOn]).- 10 integration tests (in-memory TestServer).
- NuGet packaging:
SwaggerGuard(plain) andSwaggerGuard.Abp(ABP) viadotnet pack. - Annotated sample config:
samples/appsettings.Swagger.sample.jsonc.
<a name="-türkçe"></a>
🇹🇷 Türkçe
🌐 Language / Dil: English · Türkçe (aşağıda)
Swagger UI'yi ve ilişkili endpoint'leri JWT + rol geçidinin arkasına alan, DLL olarak her projeye takılabilen yeniden kullanılabilir koruma modülü.
| Sürüm | 1.5.0 |
| Tarih | 2026-07-20 |
| Hedef Framework'ler | .NET 9 · 10 (net9.0, net10.0) |
| ABP | TFM'e göre: 9.3.7 / 10.5.0 (opsiyonel — düz ASP.NET Core da desteklenir) |
| Durum | Çekirdek + ABP sarmalayıcı + 39 integration testi, her hedef framework'te yeşil |
⚡ Nasıl Kullanılır — Özet (TL;DR)
ABP host'ta 3 adım:
// 1) Paketi ekle: dotnet add package NextQuantumSystem.Protect.SwaggerGuard.Abp
// 2) Modülü bağla:
[DependsOn(typeof(ProtectSwaggerGuardModule))]
public class MyHttpApiHostModule : AbpModule { }
// 3) appsettings.json'a "Swagger" bölümünü ekle (aşağıda). Middleware otomatik devreye girer.
Düz ASP.NET Core host'ta 3 adım:
// 1) Paketi ekle: dotnet add package NextQuantumSystem.Protect.SwaggerGuard
// 2) Program.cs:
builder.Services.AddSwaggerProtection(builder.Configuration);
app.UseSwaggerProtection(); // ← app.UseSwaggerUI()'DEN ÖNCE
// 3) appsettings.json'a "Swagger" bölümünü ekle (aşağıda).
Minimum appsettings.json:
{
"Swagger": {
"ProtectedPaths": [ "swagger", "swagger/v1/swagger.json" ],
"AllowedRoles": [ "admin" ],
"Authority": "https://id.example.com" // ← token'ı üreten IdP; ZORUNLU
}
}
Sonuç: /swagger'a giden herkes önce token giriş sayfasını görür → geçerli JWT + admin rolü olan girer, olmayan giremez.
Sayfa varsayılan İngilizce, talep hâlinde Türkçe'dir (Accept-Language, sayfadaki ?lang= bağlantısı veya "Language": "en" | "tr" ile sabitleme).
1. Amaç — Neden & Niçin
Problem
ABP / ASP.NET Core projelerinde Swagger UI (/swagger) çoğu zaman herkese açık gelir. Development/staging ortamlarında bu, API şemasının, endpoint listesinin ve örnek payload'ların yetkisiz kişilere sızması demektir.
Çözüm
Swagger direkt açılmaz. Yerine token giriş sayfası çıkar; kullanıcı geçerli bir JWT girer, token imzası + issuer + expiry doğrulanır ve yalnızca izin verilen rollere (AllowedRoles) sahip kullanıcılar Swagger'a erişebilir.
Neden ayrı bir modül / DLL?
Aynı korumayı onlarca mikroservise kopyala-yapıştır yapmak yerine, tek bir DLL'i her projeye takıp her serviste tutarlı koruma sağlanır (defense-in-depth: her servis kendini korur, sadece gateway'e güvenilmez). Her proje sadece kendi appsettings.json'ındaki değerleri değiştirir; kod ve DLL aynı kalır, yeniden derleme gerekmez.
2. Nasıl Çalışır — Akış
İstek ──▶ SwaggerProtectionMiddleware
│
├─ IsEnabled=false? ──▶ dokunma, geç
│
├─ Yol == TokenInputPath (auth/swagger)?
│ ├─ GET ──▶ token giriş sayfasını göster (200)
│ └─ POST ──▶ token doğrula
│ ├─ geçersiz/expired ──▶ sayfa + 401
│ ├─ rol uygun değil ──▶ sayfa + 403
│ └─ geçerli+rol OK ──▶ cookie yaz + returnUrl'e 302
│
├─ Yol ProtectedPaths içinde değil? ──▶ geç
│
└─ Korunan yol:
├─ cookie yok ──▶ giriş sayfasına 302
├─ cookie geçersiz ──▶ cookie sil + giriş sayfasına 302
├─ rol uygun değil ──▶ 403
└─ her şey OK ──▶ Swagger'a geç
Cookie: doğrulanan token __auth_token adıyla, HttpOnly + Secure (HTTPS'te) + SameSite=Lax + CookieExpirationHours süreli olarak yazılır.
3. Mimari
Karar: çekirdek ABP'den bağımsız, böylece hem ABP hem düz ASP.NET Core host'lar kullanabilir. ABP tarafı ince bir sarmalayıcıdır.
Protect/
├── src/
│ ├── NextQuantumSystem.Protect.SwaggerGuard/ ← ABP'siz düz çekirdek
│ │ ├── SwaggerProtectionOptions.cs · tüm ayarlar
│ │ ├── SwaggerProtectionMiddleware.cs · path/cookie/rol/redirect mantığı
│ │ ├── SwaggerProtectionExtensions.cs · AddSwaggerProtection / UseSwaggerProtection
│ │ └── Internal/
│ │ ├── IJwtTokenValidator.cs · doğrulama soyutlaması
│ │ ├── JwksJwtTokenValidator.cs · Authority/JWKS ile imza doğrulama
│ │ ├── SymmetricJwtTokenValidator.cs · SigningKey (HS256) ile imza doğrulama
│ │ ├── SwaggerGuardTexts.cs · kullanıcıya görünen metinler (en/tr)
│ │ ├── LanguageResolver.cs · ?lang → ayar → Accept-Language → en
│ │ └── TokenInputPageRenderer.cs · token giriş sayfası (HTML)
│ │
│ └── NextQuantumSystem.Protect.SwaggerGuard.Abp/ ← ince ABP modülü
│ └── ProtectSwaggerGuardModule.cs · AbpModule, otomatik wiring
│
└── test/
└── NextQuantumSystem.Protect.SwaggerGuard.Tests/ ← in-memory TestServer, 39 test
| Katman | Bağımlılık | Amaç |
|---|---|---|
SwaggerGuard |
Microsoft.AspNetCore.App, Microsoft.IdentityModel.* |
Framework-agnostik çekirdek. Düz ASP.NET Core host'lar buna doğrudan takılır. |
SwaggerGuard.Abp |
Volo.Abp.AspNetCore + çekirdek |
ABP host'ları için otomatik wiring. |
4. Kurulum & Kullanım
4.a) ABP Host
1. Paketi ekle:
dotnet add package NextQuantumSystem.Protect.SwaggerGuard.Abp
2. Modülü bağımlılık olarak ekle:
[DependsOn(
typeof(ProtectSwaggerGuardModule) // ← tek satır, gerisi otomatik
// ... diğer modülleriniz
)]
public class MyServiceHttpApiHostModule : AbpModule { }
3. appsettings.json'a "Swagger" bölümünü ekle (bkz. Konfigürasyon).
Middleware, modül host'un bağımlılığı olduğu için host'un kendi
UseAbpSwaggerUI()çağrısından önce kaydolur (ABP init sırası: bağımlılıklar önce). Ekstra birUse...çağrısı gerekmez.
Modül senin yerine ne yapıyor — elle yazsaydın atacağın iki adım:
public override void ConfigureServices(ServiceConfigurationContext context)
=> context.Services.AddSwaggerProtection(context.Services.GetConfiguration());
public override void OnApplicationInitialization(ApplicationInitializationContext context)
=> context.GetApplicationBuilder().UseSwaggerProtection();
ABP host'ta ayarları ezme — modül "Swagger" bölümünü bind eder; tek tek değerleri kendi modülünde Configure<> ile değiştirebilirsin:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<SwaggerProtectionOptions>(o =>
{
o.Language = "en";
o.IsEnabled = !context.Services.GetHostingEnvironment().IsDevelopment();
});
}
ABP paketini kullanmak istemiyorsan — her ABP host çekirdek paketi doğrudan da tüketebilir; modül yalnızca kolaylık:
public override void ConfigureServices(ServiceConfigurationContext context)
=> context.Services.AddSwaggerProtection(context.Services.GetConfiguration());
public override void OnApplicationInitialization(ApplicationInitializationContext context)
=> context.GetApplicationBuilder().UseSwaggerProtection(); // UseAbpSwaggerUI()'den önce
4.b) Düz ASP.NET Core Host
1. Paketi ekle:
dotnet add package NextQuantumSystem.Protect.SwaggerGuard
2. Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSwaggerProtection(builder.Configuration); // "Swagger" bölümünden bind eder
var app = builder.Build();
app.UseSwaggerProtection(); // ← Swagger UI'DEN ÖNCE
app.UseSwagger();
app.UseSwaggerUI();
app.Run();
⚠️ İki çağrı da zorunlu.
AddSwaggerProtectionservisleri (options + token doğrulayıcı) kaydeder;UseSwaggerProtectionmiddleware'i pipeline'a koyar. YalnızcaUse…çağırırsan uygulama açılışta patlar:Unable to resolve service for type 'IJwtTokenValidator' while attempting to activate 'SwaggerProtectionMiddleware'.
4.b.1) AddSwaggerProtection — üç overload
| Overload | Ne zaman | Ne yapar |
|---|---|---|
AddSwaggerProtection(IConfiguration) |
Standart kullanım | Verilen konfigürasyonun "Swagger" bölümünü bind eder. |
AddSwaggerProtection(IConfigurationSection) |
Ayarların farklı bir bölüm adı altında | Verdiğin bölümü aynen bind eder. |
AddSwaggerProtection(Action<SwaggerProtectionOptions>) |
Değerler koddan gelecekse veya kaynakları harmanlayacaksan | Options'ı kodla ayarlar; örtük bir bind yapmaz. |
Üçü de aynı servisleri kaydeder ve birbirinin yerine geçer — birini seç, zincirleme çağırma.
a) "Swagger" bölümünü bind et (en yaygın):
builder.Services.AddSwaggerProtection(builder.Configuration);
b) Farklı adlı bölümü bind et — ör. ayarlar "ApiDocs" altındaysa:
builder.Services.AddSwaggerProtection(builder.Configuration.GetSection("ApiDocs"));
c) Kod içinde ayarla — konfigürasyon dosyasına hiç dokunmadan:
builder.Services.AddSwaggerProtection(o =>
{
o.Authority = "https://id.example.com";
o.ProtectedPaths = new() { "swagger", "swagger/v1/swagger.json" };
o.AllowedRoles = new() { "admin" };
o.Language = "tr";
});
d) Konfigürasyon + kod ile ezme — önce bind et, sonra appsettings.json'da tekrarlamak istemediğin değerleri yamala (API kendi token'ını üretiyorsa ve anahtar zaten başka yerdeyse çok işe yarar):
builder.Services.AddSwaggerProtection(o =>
{
builder.Configuration.GetSection("Swagger").Bind(o); // her şey config'ten…
o.SigningKey = builder.Configuration["TokenOptions:SecurityKey"]; // …anahtar tek kaynaktan
o.ValidIssuer = builder.Configuration["TokenOptions:Issuer"];
});
e) Ortama göre kurulum — lokalde koruma kapalı, diğer her yerde açık:
builder.Services.AddSwaggerProtection(o =>
{
builder.Configuration.GetSection("Swagger").Bind(o);
o.IsEnabled = !builder.Environment.IsDevelopment();
});
f) Kendi token doğrulayıcın — dahili doğrulamayı tümüyle değiştir (paket kendi kaydını TryAdd ile yaptığı için seninkini Add…'den önce kaydet, ya da önce RemoveAll çağır):
builder.Services.AddSwaggerProtection(builder.Configuration);
builder.Services.RemoveAll<IJwtTokenValidator>();
builder.Services.AddSingleton<IJwtTokenValidator, MyOwnValidator>();
4.b.2) UseSwaggerProtection — pipeline'da yeri
Middleware, korunan yolları servis eden bileşenden önce çalışmalı; yoksa Swagger isteği önce yanıtlar ve koruma hiç devreye girmez.
a) Minimal API / tipik host:
app.UseSwaggerProtection(); // ← ikisinden de önce
app.UseSwagger();
app.UseSwaggerUI();
b) Authentication/authorization ile birlikte — onlardan sonra koy; koruma kendi cookie'sini kullanır ama bu sıra HttpContext.User'ın dolu kalmasını sağlar:
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSwaggerProtection(); // ← auth'tan sonra, Swagger'dan önce
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
c) PathBase / reverse proxy arkasında — UsePathBase ve UseForwardedHeaders'ı önce çağır ki koruma kullanıcının gördüğü yolları eşleştirsin:
app.UsePathBase("/api-gateway");
app.UseForwardedHeaders();
app.UseSwaggerProtection();
d) Yalnızca bazı ortamlarda — ya çağrıyı koşula bağla ya da IsEnabled kullan; davranış config'te görünür kalsın diye IsEnabled tercih edilir (bkz. (e)):
if (!app.Environment.IsDevelopment())
{
app.UseSwaggerProtection();
}
❗ Sık yapılan hatalar:
Use…'uUseSwaggerUI()'den sonra çağırmak (koruma hiç tetiklenmez),Add…olmadan yalnızcaUse…çağırmak (açılışta exception),TokenInputPath'iProtectedPathsiçine koymak (sonsuz redirect).
4.c) NuGet ile Dağıtım
İki paket üretilir:
| Paket | Ne zaman kullanılır | Bağımlılıkları |
|---|---|---|
NextQuantumSystem.Protect.SwaggerGuard |
Düz ASP.NET Core host | Microsoft.IdentityModel.* |
NextQuantumSystem.Protect.SwaggerGuard.Abp |
ABP host | ↑ çekirdek + Volo.Abp.AspNetCore |
ABP paketini kurduğunuzda çekirdek paket otomatik gelir (bağımlılık olarak tanımlı). ABP host'ta sadece
.Abppaketini kurmanız yeterli.
Paketleri üret (./artifacts altına iki .nupkg):
dotnet pack NextQuantumSystem.Protect.slnx -c Release -o ./artifacts
Genel nuget.org'a gönder:
dotnet nuget push "artifacts/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key <NUGET_API_KEY> --skip-duplicate
⚠️ nuget.org'a çıkan bir sürüm silinemez (yalnızca "unlist" edilebilir) ve paket ID'si global olarak rezerve olur.
Otomatik yayın (CI/CD — önerilen)
.github/workflows/build.yml ile:
- Her push/PR → derle + test + paketle (artifact olarak yüklenir).
- GitHub Release yayınlandığında → nuget.org'a otomatik push.
Yayın, NuGet Trusted Publishing (OIDC) ile yapılır — repoda uzun ömürlü API key secret'ı tutulmaz; production ortamı üzerinden kısa ömürlü key alınır.
Yeni sürüm yayınlama — sürümün tek doğruluk kaynağı Release tag'idir:
# GitHub'da vX.Y.Z etiketiyle Release oluştur (ör. v1.2.0)
# → build.yml paket sürümünü tag'den türetir (v1.2.0 → 1.2.0),
# sonra otomatik pack + push eder. csproj düzenlemek gerekmez.
# Alternatif: Actions → build → "Run workflow", opsiyonel "version" girdisiyle.
5. Konfigürasyon Referansı
appsettings.json içindeki "Swagger" bölümü SwaggerProtectionOptions'a bind edilir.
{
"Swagger": {
"IsEnabled": true,
"RoutePrefix": "swagger",
"DocumentTitle": "API Dokümanı",
"CookieName": "__auth_token",
"CookieExpirationHours": 8,
"TokenInputPath": "auth/swagger",
"ProtectedPaths": [ "swagger", "api/abp/api-definition", "swagger/v1/swagger.json" ],
"AllowedRoles": [ "admin" ],
"Authority": "https://id.example.com",
"Audience": "my-service",
"ValidateAudience": false,
"ValidateIssuer": true,
"RequireHttpsMetadata": true,
"RoleClaimTypes": [ "role", "roles" ]
}
}
| Ayar | Tip | Varsayılan | Açıklama |
|---|---|---|---|
IsEnabled |
bool | true |
false ise middleware hiçbir şey yapmaz (koruma kapalı). |
RoutePrefix |
string | "swagger" |
Bilgilendirme amaçlı; returnUrl fallback'i olarak kullanılır. |
DocumentTitle |
string? | null |
Token giriş sayfasının <title>'ı. Boşsa gösterim diline göre seçilir (API Documentation / API Dokümanı). |
Language |
string? | null |
Arayüz dili: "en" veya "tr". Boşsa Accept-Language'e bakılır, o da tutmazsa İngilizce. Bkz. Dil. |
CookieName |
string | "__auth_token" |
Doğrulanmış JWT'nin saklandığı cookie. |
CookieExpirationHours |
int | 8 |
Cookie geçerlilik süresi (saat). |
TokenInputPath |
string | "auth/swagger" |
Token giriş sayfasının yolu. Asla ProtectedPaths içinde olmamalı (sonsuz redirect). |
ProtectedPaths |
string[] | [] |
Korunacak yol önekleri (leading slash'siz). |
AllowedRoles |
string[] | [] |
Kapıdan geçmesine izin verilen değerler. Boşsa: doğrulanmış herhangi bir token yeterli. Rollerle sınırlı değil — bkz. Kime açılır. |
Authority |
string? | null |
Token'ı üreten OpenID/OAuth otoritesi; JWKS buradan çekilir. SigningKey verilmediyse zorunlu. |
SigningKey |
string? | null |
Simetrik HMAC anahtarı (HS256). Doluysa Authority/JWKS yerine bu anahtarla doğrulanır — token'ını kendi üreten, IdP'si olmayan API'ler için. Token'ı imzalayan tarafla aynı anahtar olmalı (≥32 karakter). |
ValidIssuer |
string? | null |
Beklenen issuer; yalnızca SigningKey modunda geçerli (JWKS modunda issuer Authority metadata'sından gelir). Boşsa Authority kullanılır. |
Audience |
string? | null |
Beklenen audience (ValidateAudience=true ise zorunlu). |
ValidateAudience |
bool | false |
Audience doğrulaması yapılsın mı? |
ValidateIssuer |
bool | true |
Issuer doğrulaması (Authority'den gelen issuer ile). |
RequireHttpsMetadata |
bool | true |
Authority metadata için HTTPS zorunlu mu? (dev'de false). |
RoleClaimTypes |
string[] | ["role","roles",".../role"] |
AllowedRoles'ın hangi claim'lerle karşılaştırılacağı. Her claim olur — kullanıcı adı, id, GUID… bkz. Kime açılır. |
📄 Tam örnek + açıklamalı senaryolar:
samples/appsettings.Swagger.sample.jsonc— her ayarın satır-içi açıklaması ve 5 hazır senaryo (production, development, rol-kısıtsız, audience-doğrulamalı, kapalı).
Her projede değişen değerler (
Authority,Audience) DLL içinde değil, her host'un kendi config'inde tutulur. Tekrarı azaltmak için: ortakappsettings.shared.json, environment variable (Swagger__Authority=...) veya kod-içiAddSwaggerProtection(o => ...)overload'u kullanılabilir.
5.1) Dil — en / tr
Kullanıcının görebildiği tüm metinler — token giriş sayfası, hata mesajları ve 403 gövdesi — İngilizce ve Türkçe gelir. Varsayılan İngilizce'dir; hiçbir ayar yapmadan çalışır.
Bir isteğin dili nasıl seçilir (ilk eşleşen kazanır):
| # | Kaynak | Örnek |
|---|---|---|
| 1 | İstekteki açık seçim | /auth/swagger?lang=tr veya formun geri gönderdiği gizli lang alanı |
| 2 | Language ayarı |
"Swagger": { "Language": "tr" } |
| 3 | Accept-Language başlığı |
tr-TR,tr;q=0.9,en;q=0.8 → Türkçe |
| 4 | Fallback | İngilizce |
Bölgesel kodlar kabul edilir (tr-TR → tr); desteklenmeyen diller (de-DE) İngilizce'ye düşer.
Arayüzü tek dile sabitle:
{ "Swagger": { "Language": "tr" } } // veya "en"
builder.Services.AddSwaggerProtection(o => o.Language = "tr");
Otomatik bırak — Language'i hiç verme; her ziyaretçi tarayıcı tercihini görür, eşleşmezse İngilizce.
Sayfanın sağ altında her zaman diğer dile geçiş bağlantısı vardır; seçim çıkmaz sokak değildir. Seçilen dil başarısız girişten sonra da korunur (form ile geri gönderilir) ve yanıt uygun Content-Language başlığını taşır.
Log mesajları ve iç doğrulama hataları her zaman İngilizce'dir — bunlar kullanıcıya değil, log'larına gider.
5.2) Kime açılır — kapı herhangi bir claim ile kurulabilir, sadece rol değil
Adına rağmen AllowedRoles yalnızca rollerle sınırlı değildir. İki ayar birlikte çalışır:
| Ayar | Cevapladığı soru |
|---|---|
RoleClaimTypes |
Hangi claim'e bakalım? |
AllowedRoles |
O claim'in hangi değerleri girebilsin? |
Yani Swagger'ı kullanıcı adına, kullanıcı ID'sine, kurye ID'sine, GUID'e, e-postaya — token'ında zaten ne varsa ona göre açabilirsin. Token üretme şeklini değiştirmene gerek yok.
Örnek token
Aşağıdaki tüm örnekler bu gerçek payload üzerinden:
{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "161",
"email": "admin",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Örnek Yönetici",
"KullaniciId": "161",
"KuryeId": "18",
"KullaniciGuid": "3f1a7c9e-1111-4a2b-8c3d-5e6f70819a2b",
"KullaniciAdi": "admin",
"iss": "api.example.com",
"aud": "api.example.com"
}
Dikkat: bu token'da hiç role claim'i yok — sorun değil. Başka herhangi birini seç.
Örnek 1 — Geçerli token'ı olan herkes
"AllowedRoles": []
Listeyi boş bırakırsan claim kontrolü tümüyle atlanır: doğru imzalı, süresi dolmamış bir token yeterlidir. Bu modda RoleClaimTypes hiç kullanılmaz.
Örnek 2 — Kullanıcı adına göre
"RoleClaimTypes": [ "KullaniciAdi" ],
"AllowedRoles": [ "admin" ]
Yalnızca KullaniciAdi değeri admin olan kullanıcı girer. ✅ örnek token geçer.
Örnek 3 — Kullanıcı adına göre, birden çok kişi
"RoleClaimTypes": [ "KullaniciAdi" ],
"AllowedRoles": [ "admin", "mehmet", "ayse" ]
Üçünden biri olması yeterli. ✅ örnek token admin üzerinden geçer.
Örnek 4 — Kullanıcı ID'sine göre
"RoleClaimTypes": [ "KullaniciId" ],
"AllowedRoles": [ "161" ]
Yalnızca 161 numaralı kullanıcı. ✅ geçer. 162 yazarsan aynı token 403 alır.
Örnek 5 — Kurye ID'sine göre
"RoleClaimTypes": [ "KuryeId" ],
"AllowedRoles": [ "18" ]
Yalnızca 18 numaralı kurye. ✅ geçer.
Örnek 6 — GUID'e göre
"RoleClaimTypes": [ "KullaniciGuid" ],
"AllowedRoles": [ "3f1a7c9e-1111-4a2b-8c3d-5e6f70819a2b" ]
Tek bir hesaba sabitlenir; kullanıcı adı değişse bile etkilenmez. ✅ geçer.
Örnek 7 — E-postaya göre
"RoleClaimTypes": [ "email" ],
"AllowedRoles": [ "admin" ]
✅ geçer — bu token'da email claim'i admin değerini taşıyor.
Örnek 8 — Uzun URI'li claim'e göre
"RoleClaimTypes": [ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" ],
"AllowedRoles": [ "161" ]
Uzun claim adları da çalışır — yeter ki token'da göründüğü gibi tam yazılsın. ✅ geçer.
Örnek 9 — Aynı anda iki kapı
"RoleClaimTypes": [ "KullaniciAdi", "KuryeId" ],
"AllowedRoles": [ "admin", "99" ]
Şöyle okunur: KullaniciAdi admin veya 99 ise, ya da KuryeId admin veya 99 ise. Tek bir eşleşme kapıyı açar. ✅ KullaniciAdi = admin üzerinden geçer.
Örnek 10 — Gerçek rol claim'i (klasik IdP)
"RoleClaimTypes": [ "role", "roles" ],
"AllowedRoles": [ "admin" ]
Rol taşıyan token'lar için klasik kurulum. ❌ örnek token'da role claim'i olmadığı için 403 alır — token'ının gerçekte ne taşıdığını kontrol etmen gerektiğinin iyi bir hatırlatıcısı.
Bunların arkasındaki dört kural
- Her şey VEYA (OR) mantığında. Herhangi bir claim tipi × herhangi bir izinli değer tutuyorsa yeterlidir. İki koşulu birlikte zorunlu kılmanın (AND) bir yolu yoktur.
- Değerler büyük/küçük harf duyarsız karşılaştırılır.
admin,AdminveADMINaynıdır. Claim adları ise birebir eşleşir. AllowedRolesboşsa kısıt yoktur — geçerli her token girer veRoleClaimTypes'a hiç bakılmaz.- Claim adları olduğu gibi alınır. Hiçbir dönüştürme (inbound mapping) uygulanmaz; adı token payload'ında göründüğü gibi yaz. Gerçek adları görmek için JWT'ni jwt.io'ya yapıştır.
⚠️ Bu kontroller imza, issuer ve expiry doğrulandıktan sonra çalışır. Claim kapısı bunların yerine geçmez — geçerli token sahipleri arasından kimin Swagger'ı açabileceğini daraltır.
6. Güvenlik Notları
- 🔴 JWT imzası MUTLAKA doğrulanır.
Authorityyapılandırılmazsa doğrulama başarısız döner — token asla "sadece decode edilip" kabul edilmez. İmza + issuer + expiry hep doğrulanır. Bu, saldırganın kendirole=admintoken'ını üretmesini engeller. - 🔁 Sonsuz redirect koruması:
TokenInputPathasla korunan değildir; kendi içinde ele alınır. - 🍪 Cookie:
HttpOnly(JS erişemez) +Secure(HTTPS'te) +SameSite=Lax. Bayat/geçersiz cookie otomatik silinir. - ↩️ Open-redirect koruması:
returnUrlyalnızca site-içi (yerel) yollara izin verir;//veya mutlak URL'ler reddedilir. - ⚠️
api/abp/api-definition'ı korumak, ABP dinamik client-proxy üretimini etkileyebilir (cookie'siz çağıran araçlar 302 alır). Bilinçli tercih olmalı. - 🌐 Gateway/reverse-proxy:
X-Forwarded-*/ PathBase algılanan yolu değiştirebilir;ProtectedPathseşleştirmesi buna göre ayarlanmalı.
7. Test
In-memory TestServer üzerinde gerçek HTTP akışıyla 39 integration testi; her iki hedef framework'te de koşar. Ayrıntılı senaryo tabloları için İngilizce bölüme bakabilirsin — başlıca gruplar: middleware akışı, simetrik anahtar, dil seçimi ve claim kapısı (Kime açılır bölümündeki 10 örneğin hepsi testle sabitlenmiştir).
Aşağıdaki tablo middleware akışı testlerini gösterir; JWKS yerine simetrik anahtarlı bir doğrulayıcı enjekte edilerek tüm mantık canlı bir authority olmadan sınanır.
dotnet test NextQuantumSystem.Protect.slnx
| Senaryo | Beklenen |
|---|---|
Serbest yol (/healthz) |
200 (geçer) |
| Korunan yol, cookie yok | 302 → /auth/swagger?returnUrl=... |
| Giriş sayfası GET | 200 + HTML |
| POST geçerli admin token | 302 + Set-Cookie |
| POST geçerli non-admin token | 403 |
| POST geçersiz token | 401 |
| POST expired token | 401 |
| Korunan yol + geçerli admin cookie | 200 (geçer) |
| Korunan yol + non-admin cookie | 403 |
| Korunan yol + bozuk cookie | 302 + cookie silinir |
Sonuç: net9.0 ve net10.0 üzerinde 39/39 geçiyor.
8. Sık Sorulanlar
S: Bu bir ABP modülü mü, yoksa düz kütüphane mi?
İkisi de. Çekirdek (SwaggerGuard) ABP'siz düz kütüphanedir; SwaggerGuard.Abp onu ABP host'ları için otomatik wire eden ince bir modüldür.
S: Her mikroservis için ayrı DLL derlemek gerekir mi?
Hayır. Aynı DLL tüm projelerde kullanılır; sadece appsettings.json değişir.
S: NuGet çoklu dilli README destekler mi? NuGet pakette tek README gösterir. Burada kullanılan yöntem: tek README içinde dil bölümleri + en üstte dil seçici.
9. Sürüm Geçmişi
1.5.0 — 2026-07-20
1.3.0ve1.4.0'daki her şeyi de içerir — o sürümler hazırlandı ama nuget.org'a hiç yayınlanmadı.
- Hedef framework'ler
net9.0/net10.0olarak kalıyor. Çekirdeğinet7.0/net8.0'a genişletmek denendi ve geri alındı:net7.0destek dışı (Mayıs 2024) ve runtime'ı artık CI'da kurulamadığı için test edilemedi; ayrıca tüm çözümü tek bir framework çifti üzerinde tutmak, çekirdek ile ABP paketinin birbirinden ayrışmasından daha anlaşılır. net11.0henüz hedeflenmiyor — daha yayınlanmadı.
1.4.0 — 2026-07-20
- Çift dilli arayüz (en/tr). Kullanıcıya görünen tüm metinler — token giriş sayfası, hata mesajları,
403gövdesi — artık iki dilde; sayfanın kendisinde dil bağlantısı var. Bkz. Dil. - İstek başına dil:
?lang=→Languageayarı →Accept-Language→ İngilizce. Bölgesel kodlar (tr-TR) kabul edilir, desteklenmeyenler İngilizce'ye düşer. - Varsayılan artık İngilizce. Önceden sayfa yalnızca Türkçe'ydi. Eski metinleri korumak için
"Language": "tr"ver. DocumentTitlevarsayılanınulloldu ve gösterim dilini izliyor; açıkça verilen başlık yine kazanır.- Yanıtlar uygun
Content-Languagebaşlığını taşıyor; log ve iç doğrulama mesajları tutarlı biçimde İngilizce'ye çevrildi. - Dokümantasyon:
AddSwaggerProtection/UseSwaggerProtectionbölümleri overload bazında rehber ve işlenmiş örneklerle yeniden yazıldı (config bind, farklı bölüm adı, yalnızca kod, config+ezme, ortama göre, özel doğrulayıcı, pipeline yerleşimi, sık hatalar). - Yeni dokümantasyon bölümü Kime açılır: kapının sadece rolle değil HERHANGİ bir claim ile kurulabildiğini gösteren, kopyala-yapıştır 10 bağımsız örnek (kullanıcı adı, kullanıcı id, kurye id, GUID, e-posta, uzun URI claim'leri). Her örnek bir testle sabitlendi.
net9.0venet10.0üzerinde 39/39 test geçiyor.
1.3.0 — 2026-07-20
- Simetrik anahtar (HS256) ile doğrulama.
SigningKey(ve isteğe bağlıValidIssuer) verildiğinde Authority/JWKS yerine bu anahtarla doğrulanır. Token'ını kendi üreten, OpenID otoritesi olmayan API'lerin önünü açar — önceden JWKS metadata çekilemediği için her token reddediliyordu. - Doğrulayıcı resolve anında seçilir:
SigningKeydoluysaSymmetricJwtTokenValidator, değilseJwksJwtTokenValidator. İmza + issuer + expiry yine her zaman doğrulanır. - Tümüyle geriye dönük uyumlu; Authority/JWKS kurulumları etkilenmez.
net9.0venet10.0üzerinde 12/12 test geçiyor.
1.2.0 — 2026-07-18
- Çoklu hedefleme:
net9.0venet10.0desteği. Artık daha çok proje paketi kurabilir. - ABP paketi her framework için uygun ABP major'ını referanslar (
net9.0→9.3.7,net10.0→10.5.0). - Testler iki framework'te de çalışır — her birinde 10/10 geçiyor.
net8.0çıkarıldı çünkü ABP 8.3.x hattı, bilinen güvenlik açığı uyarısı olan transitif paketler çekiyor.net6.0/net7.0zaten destek dışı & ABP kaldırdı. Tabannet9.0.- CI: paket sürümü artık Release tag'inden (
vX.Y.Z) türetiliyor; tag tek doğruluk kaynağı. - Not:
1.0.2, sürüm karışıklığı nedeniyle (tag henüz paket sürümüne bağlı değildi) aynı kodla daha önce yayınlandı;1.2.0onun yerini alır.
1.0.1 — 2026-07-18
- Çok dilli README (English + Türkçe), en üstte dil seçici.
1.0.0 — 2026-07-18
- İlk sürüm.
SwaggerProtectionMiddleware: path eşleştirme, cookie tabanlı JWT geçidi, rol kontrolü, redirect akışı.JwksJwtTokenValidator: Authority/JWKS ile imza + issuer + expiry doğrulaması.- Token giriş sayfası (MVC'siz, HTML render).
- Düz ASP.NET Core için
AddSwaggerProtection/UseSwaggerProtectionextension'ları. - ABP host'ları için
ProtectSwaggerGuardModule(tek[DependsOn]ile otomatik wiring). - 10 integration testi (in-memory TestServer).
- NuGet paketleme:
SwaggerGuard(düz) veSwaggerGuard.Abp(ABP) paketleri. - Açıklamalı örnek config:
samples/appsettings.Swagger.sample.jsonc.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.IdentityModel.JsonWebTokens (>= 8.16.0)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.16.0)
-
net9.0
- Microsoft.IdentityModel.JsonWebTokens (>= 8.16.0)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.16.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on NextQuantumSystem.Protect.SwaggerGuard:
| Package | Downloads |
|---|---|
|
NextQuantumSystem.Protect.SwaggerGuard.Abp
SwaggerGuard korumasını ABP host'larına tek [DependsOn] ile otomatik ekleyen ABP modülü. NextQuantumSystem.Protect.SwaggerGuard çekirdeğini sarar. |
GitHub repositories
This package is not used by any popular GitHub repositories.