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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NextQuantumSystem.Protect.SwaggerGuard" Version="1.5.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NextQuantumSystem.Protect.SwaggerGuard" Version="1.5.1" />
                    
Directory.Packages.props
<PackageReference Include="NextQuantumSystem.Protect.SwaggerGuard" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NextQuantumSystem.Protect.SwaggerGuard --version 1.5.1
                    
#r "nuget: NextQuantumSystem.Protect.SwaggerGuard, 1.5.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NextQuantumSystem.Protect.SwaggerGuard@1.5.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NextQuantumSystem.Protect.SwaggerGuard&version=1.5.1
                    
Install as a Cake Addin
#tool nuget:?package=NextQuantumSystem.Protect.SwaggerGuard&version=1.5.1
                    
Install as a Cake Tool

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 extra Use... 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. AddSwaggerProtection registers the services (options + token validator); UseSwaggerProtection puts the middleware in the pipeline. Calling only Use… 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… after UseSwaggerUI() (guard never triggers), calling only Use… without Add… (startup exception), or putting TokenInputPath inside ProtectedPaths (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 .Abp package.

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.

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 shared appsettings.shared.json, environment variables (Swagger__Authority=...), or the in-code AddSwaggerProtection(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
  1. Everything is OR. Any claim type × any allowed value that matches is enough. There is no way to require two conditions together (AND).
  2. Values are compared case-insensitively. admin, Admin and ADMIN are the same. Claim names are matched exactly.
  3. Empty AllowedRoles means no restriction — any valid token passes, and RoleClaimTypes is not consulted.
  4. 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 Authority is 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 own role=admin token.
  • 🔁 Infinite-redirect guard: TokenInputPath is 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: returnUrl allows only local (same-site) paths; // or absolute URLs are rejected.
  • ⚠️ Protecting api/abp/api-definition may 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; tune ProtectedPaths accordingly.

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.0 and 1.4.0 as well — those versions were prepared but never published to nuget.org.

  • Target frameworks stay at net9.0 / net10.0. Widening the core to net7.0/net8.0 was explored and reverted: net7.0 is 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.0 is 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, 403 body — now ships in both languages, with a language link on the page itself. See Language.
  • Language per request: ?lang= → Language option → 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.
  • DocumentTitle defaults to null and follows the display language; an explicitly configured title still wins.
  • Responses carry a matching Content-Language header; log and internal validation messages are now consistently English.
  • Documentation: AddSwaggerProtection / UseSwaggerProtection sections 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.0 and net10.0.

1.3.0 — 2026-07-20

  • Symmetric-key (HS256) validation. Set SigningKey (plus optional ValidIssuer) 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: SigningKey set → SymmetricJwtTokenValidator, otherwise JwksJwtTokenValidator. Signature + issuer + expiry are still always verified.
  • Fully backward compatible; Authority/JWKS setups are unaffected.
  • 12/12 tests passing on net9.0 and net10.0.

1.2.0 — 2026-07-18

  • Multi-targeting: supports net9.0 and net10.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.0 was dropped because its ABP line (8.3.x) drags in transitive packages with known vulnerability advisories. net6.0/net7.0 are out of support & dropped by ABP. The floor is net9.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.2 was published earlier with identical code due to a version mix-up (the tag wasn't wired to the package version yet); it is superseded by 1.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 / UseSwaggerProtection extensions for plain ASP.NET Core.
  • ProtectSwaggerGuardModule for ABP hosts (automatic wiring via a single [DependsOn]).
  • 10 integration tests (in-memory TestServer).
  • NuGet packaging: SwaggerGuard (plain) and SwaggerGuard.Abp (ABP) via dotnet 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 bir Use... ç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. AddSwaggerProtection servisleri (options + token doğrulayıcı) kaydeder; UseSwaggerProtection middleware'i pipeline'a koyar. Yalnızca Use… ç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…'u UseSwaggerUI()'den sonra çağırmak (koruma hiç tetiklenmez), Add… olmadan yalnızca Use… çağırmak (açılışta exception), TokenInputPath'i ProtectedPaths iç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 .Abp paketini 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: ortak appsettings.shared.json, environment variable (Swagger__Authority=...) veya kod-içi AddSwaggerProtection(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
  1. 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.
  2. Değerler büyük/küçük harf duyarsız karşılaştırılır. admin, Admin ve ADMIN aynıdır. Claim adları ise birebir eşleşir.
  3. AllowedRoles boşsa kısıt yoktur — geçerli her token girer ve RoleClaimTypes'a hiç bakılmaz.
  4. 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. Authority yapı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 kendi role=admin token'ını üretmesini engeller.
  • 🔁 Sonsuz redirect koruması: TokenInputPath asla 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ı: returnUrl yalnı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; ProtectedPaths eş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.0 ve 1.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.0 olarak kalıyor. Çekirdeği net7.0/net8.0'a genişletmek denendi ve geri alındı: net7.0 destek 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.0 henü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ı, 403 gövdesi — artık iki dilde; sayfanın kendisinde dil bağlantısı var. Bkz. Dil.
  • İstek başına dil: ?lang= → Language ayarı → 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.
  • DocumentTitle varsayılanı null oldu ve gösterim dilini izliyor; açıkça verilen başlık yine kazanır.
  • Yanıtlar uygun Content-Language başlığını taşıyor; log ve iç doğrulama mesajları tutarlı biçimde İngilizce'ye çevrildi.
  • Dokümantasyon: AddSwaggerProtection / UseSwaggerProtection bö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.0 ve net10.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: SigningKey doluysa SymmetricJwtTokenValidator, değilse JwksJwtTokenValidator. İmza + issuer + expiry yine her zaman doğrulanır.
  • Tümüyle geriye dönük uyumlu; Authority/JWKS kurulumları etkilenmez.
  • net9.0 ve net10.0 üzerinde 12/12 test geçiyor.

1.2.0 — 2026-07-18

  • Çoklu hedefleme: net9.0 ve net10.0 desteğ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.0 zaten destek dışı & ABP kaldırdı. Taban net9.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.0 onun 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 / UseSwaggerProtection extension'ları.
  • ABP host'ları için ProtectSwaggerGuardModule (tek [DependsOn] ile otomatik wiring).
  • 10 integration testi (in-memory TestServer).
  • NuGet paketleme: SwaggerGuard (düz) ve SwaggerGuard.Abp (ABP) paketleri.
  • Açıklamalı örnek config: samples/appsettings.Swagger.sample.jsonc.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.5.1 206 7/20/2026
1.0.2 135 7/18/2026
1.0.1 130 7/18/2026
1.0.0 131 7/18/2026