# Auth Flow Review

> Use when reviewing authentication and authorization in an ASP.NET Core app — JWT / OIDC / Entra ID configuration, token validation, and scope/role enforcement.

- **Type:** Skill
- **Install:** `agentstack add skill-tunahanaliozturk-secure-dotnet-skills-auth-flow-review`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [tunahanaliozturk](https://agentstack.voostack.com/s/tunahanaliozturk)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [tunahanaliozturk](https://github.com/tunahanaliozturk)
- **Source:** https://github.com/tunahanaliozturk/secure-dotnet-skills/tree/master/skills/auth-flow-review

## Install

```sh
agentstack add skill-tunahanaliozturk-secure-dotnet-skills-auth-flow-review
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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, or `AddAuthorization` registrations.
- 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

1. **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).
2. **Verify token validation parameters end-to-end.** For JWT bearer: confirm `ValidateIssuer`, `ValidateAudience`, `ValidateLifetime`, and `ValidateIssuerSigningKey` are all explicitly `true` in `TokenValidationParameters`. Confirm `Authority` / `MetadataAddress` uses HTTPS and `RequireHttpsMetadata` is `true`. For Entra ID (`AddMicrosoftIdentityWebApi`), confirm the `AzureAd` section supplies `TenantId`, `ClientId` (audience), and the correct `Instance`.
3. **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`/`scp` for delegated flows, `roles` for app-role/app-to-app flows)? Confirm fine-grained endpoints use `[Authorize(Policy = "…")]` rather than bare `[Authorize]`.
4. **Hunt for gaps: anonymous exposure and missing default-deny.** Verify `FallbackPolicy` is set to `RequireAuthenticatedUser` in the `AddAuthorization` options. 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.
5. **Check token lifetime, refresh handling, and clock skew.** Confirm `ValidateLifetime = true` (never `false`). Confirm `ClockSkew` is 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).
6. **Check cookie auth flags and sign-out correctness.** For `AddCookie`: verify `HttpOnly = true`, `Secure = true`, and `SameSite` is `Strict` or `Lax` (never `None` without `Secure`). Confirm `SignOutAsync` clears 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.
7. **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 the `JwtBearerOptions.TokenValidationParameters` block, all four flags must be explicitly `true`: `ValidateIssuer = true`, `ValidateAudience = true`, `ValidateLifetime = true`, `ValidateIssuerSigningKey = true`. Setting any of these to `false` is a deliberate weakening that must be justified in code comments and security sign-off. `IssuerSigningKey` must be populated from Key Vault or from the JWKS metadata endpoint, never hardcoded.
- **`RequireHttpsMetadata` and authority hygiene.** `JwtBearerOptions.RequireHttpsMetadata` must be `true` in any non-development environment. The `Authority` must be the canonical HTTPS issuer URL (e.g. `https://login.microsoftonline.com/{tenantId}/v2.0`). Confirm the `ValidIssuer` or `ValidIssuers` matches what the identity provider actually puts in the `iss` claim — a mismatch silently accepts tokens from the wrong tenant.
- **Entra ID via `Microsoft.Identity.Web` (`AddMicrosoftIdentityWebApi`).** Confirm `services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd"))` is used rather than a hand-rolled `AddJwtBearer` with hardcoded signing keys. The `AzureAd` config section must supply `Instance`, `TenantId`, and `ClientId`; `Audience` should match the Application ID URI. `AddMicrosoftIdentityWebApi` validates issuer, audience, lifetime, and signing keys through the standard OIDC metadata endpoint. **Note:** audience validation is only meaningful when `AzureAd:Audience` (or `AzureAd: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 the `scp` claim; application (client-credentials / daemon) tokens carry `roles`. Do not conflate them — a policy that checks `scp` will silently fail for daemon callers, and vice versa. For **app roles**, use `policy.RequireRole("Orders.Reader")` (or `[Authorize(Roles = "Orders.Reader")]`); under `Microsoft.Identity.Web` the `roles` claim is mapped to `ClaimTypes.Role`, so `RequireClaim("roles", "…")` can fail against valid tokens. For **delegated scopes**, use `Microsoft.Identity.Web`'s `RequireScope("Orders.Read")` policy helper or the `[RequiredScope("Orders.Read")]` attribute — **do not** use `RequireClaim("scp", "Orders.Read")` because the `scp` claim is a space-delimited string (e.g. `"Orders.Read Orders.Write"`), so an exact-value `RequireClaim` will 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 in `AddAuthorization(opts => { opts.AddPolicy("OrdersRead", p => p.RequireAuthenticatedUser().RequireScope("Orders.Read")); })` for delegated flows, or `p.RequireAuthenticatedUser().RequireRole("Orders.Reader")` for app-role flows.
- **Fallback policy = `RequireAuthenticatedUser` (default-deny).** Confirm `options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()` (or the equivalent `options.FallbackPolicy = options.DefaultPolicy`) is set inside `AddAuthorization`. 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]`.
- **`ClockSkew` and lifetime strictness.** The default `ClockSkew` of `TimeSpan.FromMinutes(5)` is acceptable for clock drift. Confirm it has not been raised to tens of minutes or `TimeSpan.MaxValue` to 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.** In `AddCookie(opts => { opts.Cookie.HttpOnly = true; opts.Cookie.SecurePolicy = CookieSecurePolicy.Always; opts.Cookie.SameSite = SameSiteMode.Lax; })`. Use `Cookie.SecurePolicy = CookieSecurePolicy.Always` (the `CookieBuilder` knob) — not a bare `Secure = true` boolean — so the middleware enforces HTTPS for the cookie in all environments. `SameSite = None` without `SecurePolicy = Always` is rejected by modern browsers and exposes the cookie to cross-site requests. On sign-out, call `HttpContext.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/`](../../examples/auth-flow-review/).

## Related skills

- [dotnet-security-review](../dotnet-security-review/SKILL.md) — use for a full security review covering injection, crypto, deserialization, and secrets beyond auth.
- [api-contract-review](../api-contract-review/SKILL.md) — use to review endpoint contracts including authorization requirements and error responses.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [tunahanaliozturk](https://github.com/tunahanaliozturk)
- **Source:** [tunahanaliozturk/secure-dotnet-skills](https://github.com/tunahanaliozturk/secure-dotnet-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-tunahanaliozturk-secure-dotnet-skills-auth-flow-review
- Seller: https://agentstack.voostack.com/s/tunahanaliozturk
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
