Auth flow review
Skill tunahanaliozturk/secure-dotnet-skills/skills/auth-flow-review
Aegis — 12 judgment-style agent skills for secure, production-grade .NET on Azure (security, design, performance, concurrency, observability). Works with Claude Code, Codex, Cursor, Gemini.
npx -y skills add tunahanaliozturk/secure-dotnet-skills --skill auth-flow-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when reviewing authentication and authorization in an ASP.NET Core app — JWT / OIDC / Entra ID configuration, token validation, and scope/role enforcement.
SKILL.md
11.4 KB, as published. Nobody here has run it
Auth Flow Review
Directs the agent to audit the full authentication and authorization surface of an ASP.NET Core app: token validation completeness, policy granularity, default-deny posture, cookie hygiene, and lifetime/refresh handling — producing a severity-rated finding per gap with the concrete ASP.NET Core API that closes it.
When to use
- A PR introduces or modifies
AddAuthentication,AddJwtBearer,AddMicrosoftIdentityWebApi, cookie auth, orAddAuthorizationregistrations. - New protected endpoints are added and their policy coverage must be verified.
- An incident or review flag suggests tokens may be accepted without audience, issuer, or lifetime validation.
- Entra ID / OIDC integration is being wired for the first time or reconfigured.
Process
- Identify the auth scheme(s) and their registration sites. Locate every
AddAuthentication(…)call and each.Add…scheme attached to it (AddJwtBearer,AddCookie,AddMicrosoftIdentityWebApi,AddOpenIdConnect). Note whether a default scheme is set and whether multiple schemes co-exist (and which is the challenge/forbid scheme). - Verify token validation parameters end-to-end. For JWT bearer: confirm
ValidateIssuer,ValidateAudience,ValidateLifetime, andValidateIssuerSigningKeyare all explicitlytrueinTokenValidationParameters. ConfirmAuthority/MetadataAddressuses HTTPS andRequireHttpsMetadataistrue. For Entra ID (AddMicrosoftIdentityWebApi), confirm theAzureAdsection suppliesTenantId,ClientId(audience), and the correctInstance. - Check authorization policies and their enforcement. Enumerate every named policy registered in
AddAuthorization. For each policy: does it require both authentication (RequireAuthenticatedUser) and a meaningful claim assertion (scope/scpfor delegated flows,rolesfor app-role/app-to-app flows)? Confirm fine-grained endpoints use[Authorize(Policy = "…")]rather than bare[Authorize]. - Hunt for gaps: anonymous exposure and missing default-deny. Verify
FallbackPolicyis set toRequireAuthenticatedUserin theAddAuthorizationoptions. Flag every[AllowAnonymous]and decide whether it is intentional (health checks, OIDC callbacks) or accidental (admin endpoints). Confirm no controller omits an[Authorize]attribute while the fallback is absent. - Check token lifetime, refresh handling, and clock skew. Confirm
ValidateLifetime = true(neverfalse). ConfirmClockSkewis not set to an absurd value (the ASP.NET Core default of 5 minutes is acceptable; anything over 15 minutes is a flag). For cookie auth, verify that the session or sliding expiry matches business requirements and that refresh tokens are rotated on use (not long-lived and non-rotating). - Check cookie auth flags and sign-out correctness. For
AddCookie: verifyHttpOnly = true,Secure = true, andSameSiteisStrictorLax(neverNonewithoutSecure). ConfirmSignOutAsyncclears the auth cookie and — for Entra ID / OIDC — triggers a back-channel or front-channel sign-out so the identity provider session is also terminated. - Output findings with fixes. Rate each gap Critical / High / Medium / Low. Pair each finding with the exact property name or method call to fix it. Re-check the same patterns across all scheme registrations and all protected controllers before closing.
.NET / Azure checks
AddAuthentication().AddJwtBearer— validation completeness. In theJwtBearerOptions.TokenValidationParametersblock, all four flags must be explicitlytrue:ValidateIssuer = true,ValidateAudience = true,ValidateLifetime = true,ValidateIssuerSigningKey = true. Setting any of these tofalseis a deliberate weakening that must be justified in code comments and security sign-off.IssuerSigningKeymust be populated from Key Vault or from the JWKS metadata endpoint, never hardcoded.RequireHttpsMetadataand authority hygiene.JwtBearerOptions.RequireHttpsMetadatamust betruein any non-development environment. TheAuthoritymust be the canonical HTTPS issuer URL (e.g.https://login.microsoftonline.com/{tenantId}/v2.0). Confirm theValidIssuerorValidIssuersmatches what the identity provider actually puts in theissclaim — a mismatch silently accepts tokens from the wrong tenant.- Entra ID via
Microsoft.Identity.Web(AddMicrosoftIdentityWebApi). Confirmservices.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd"))is used rather than a hand-rolledAddJwtBearerwith hardcoded signing keys. TheAzureAdconfig section must supplyInstance,TenantId, andClientId;Audienceshould match the Application ID URI.AddMicrosoftIdentityWebApivalidates issuer, audience, lifetime, and signing keys through the standard OIDC metadata endpoint. Note: audience validation is only meaningful whenAzureAd:Audience(orAzureAd:ClientId) is present in configuration — without it the library cannot enforce which application the token was issued for. - Scope (
scp) vs app-role (roles) claims — use the right claim and the right API for the flow. Delegated (on-behalf-of-user) tokens carry thescpclaim; application (client-credentials / daemon) tokens carryroles. Do not conflate them — a policy that checksscpwill silently fail for daemon callers, and vice versa. For app roles, usepolicy.RequireRole("Orders.Reader")(or[Authorize(Roles = "Orders.Reader")]); underMicrosoft.Identity.Webtherolesclaim is mapped toClaimTypes.Role, soRequireClaim("roles", "…")can fail against valid tokens. For delegated scopes, useMicrosoft.Identity.Web'sRequireScope("Orders.Read")policy helper or the[RequiredScope("Orders.Read")]attribute — do not useRequireClaim("scp", "Orders.Read")because thescpclaim is a space-delimited string (e.g."Orders.Read Orders.Write"), so an exact-valueRequireClaimwill deny tokens that carry additional scopes alongside the required one. - Authorization policies and
[Authorize(Policy = "…")]. Every endpoint that gates on a specific permission must use[Authorize(Policy = "OrdersRead")](or equivalent named policy), not bare[Authorize]. Bare[Authorize]only checks that a principal is authenticated — it does not enforce scope or role. Register policies inAddAuthorization(opts => { opts.AddPolicy("OrdersRead", p => p.RequireAuthenticatedUser().RequireScope("Orders.Read")); })for delegated flows, orp.RequireAuthenticatedUser().RequireRole("Orders.Reader")for app-role flows. - Fallback policy =
RequireAuthenticatedUser(default-deny). Confirmoptions.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()(or the equivalentoptions.FallbackPolicy = options.DefaultPolicy) is set insideAddAuthorization. Without a fallback policy, any controller or minimal-API endpoint that omits[Authorize]is publicly reachable. Opt-out for genuinely public routes should be explicit[AllowAnonymous]. ClockSkewand lifetime strictness. The defaultClockSkewofTimeSpan.FromMinutes(5)is acceptable for clock drift. Confirm it has not been raised to tens of minutes orTimeSpan.MaxValueto paper over a clock-sync problem.ValidateLifetime = false— even temporarily — means expired tokens are accepted indefinitely; treat this as a Critical finding.- Cookie auth:
HttpOnly,SecurePolicy,SameSite, and sign-out. InAddCookie(opts => { opts.Cookie.HttpOnly = true; opts.Cookie.SecurePolicy = CookieSecurePolicy.Always; opts.Cookie.SameSite = SameSiteMode.Lax; }). UseCookie.SecurePolicy = CookieSecurePolicy.Always(theCookieBuilderknob) — not a bareSecure = trueboolean — so the middleware enforces HTTPS for the cookie in all environments.SameSite = NonewithoutSecurePolicy = Alwaysis rejected by modern browsers and exposes the cookie to cross-site requests. On sign-out, callHttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme)and, for OIDC federated sessions,HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme)to trigger the end-session endpoint.
Red flags
| Signal | Why it matters |
|---|---|
ValidateAudience = false in TokenValidationParameters | Any JWT issued by the same authority for any application is accepted — including tokens issued to other relying parties in the same tenant. A compromised client app's tokens become valid here. |
ValidateIssuerSigningKey = false | The middleware no longer verifies the token's signature. Any syntactically valid JWT, including attacker-crafted ones with arbitrary claims, is accepted as authentic. |
RequireHttpsMetadata = false in a non-development environment | The OIDC metadata endpoint (and the JWKS endpoint it references) is fetched over HTTP. An attacker who can intercept that response can substitute their own signing keys and issue tokens the app accepts. |
Bare [Authorize] guarding an admin or elevated-privilege endpoint | [Authorize] alone asserts only that the caller is authenticated, not that they hold the required scope or role. Any authenticated user — including low-privilege users — satisfies the check. |
No FallbackPolicy in AddAuthorization | Every controller or minimal-API handler that omits [Authorize] is publicly reachable. Adding a new endpoint without explicitly opting in to authentication silently exposes it. |
ValidateLifetime = false | Expired tokens are accepted indefinitely. A stolen token remains valid forever, removing the window-of-opportunity constraint that short-lived tokens are designed to provide. |
ClockSkew = TimeSpan.FromHours(1) or larger | Dramatically extends the validity window of expired tokens. A token valid for 15 minutes becomes valid for over an hour, negating the security benefit of short expiry. |
SameSite = SameSiteMode.None without Cookie.SecurePolicy = CookieSecurePolicy.Always | The cookie is sent on cross-site requests (CSRF vector) and the None attribute is rejected by browsers if the Secure flag is not also set, breaking authentication entirely in secure contexts. Use CookieSecurePolicy.Always (not a bare Secure = true boolean) so the middleware enforces HTTPS for the cookie. |
Checking scp claim for a daemon / client-credentials flow | Daemon tokens (issued via client-credentials grant) carry roles, not scp. A policy checking scp will fail open or deny all daemon callers depending on the fallback, masking the authorization gap. |
[AllowAnonymous] on an endpoint that mutates privileged state | Bypasses all authorization middleware including the fallback policy. Even if the intent is deliberate, it must be code-reviewed and documented — a mis-applied attribute here is a full auth bypass. |
Example
See examples/auth-flow-review/.
Related skills
- dotnet-security-review — use for a full security review covering injection, crypto, deserialization, and secrets beyond auth.
- api-contract-review — use to review endpoint contracts including authorization requirements and error responses.