diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e7de806..5de4ecf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,6 +28,7 @@ updates: patterns: - "xunit*" - "Microsoft.NET.Test.Sdk" + - "Microsoft.Testing.*" - "NSubstitute" - "Shouldly" - "Microsoft.AspNetCore.Mvc.Testing" diff --git a/.github/workflows/cd-cleanup.yml b/.github/workflows/cd-cleanup.yml index 39c34f6..4ce37da 100644 --- a/.github/workflows/cd-cleanup.yml +++ b/.github/workflows/cd-cleanup.yml @@ -29,13 +29,27 @@ jobs: matrix: env: [ci, ppe] steps: - - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + - name: Skip if tier never bootstrapped + id: precheck + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + run: | + if [ -z "${AZURE_CLIENT_ID}" ]; then + echo "::notice title=Cleanup skipped (${{ matrix.env }})::AZURE_CLIENT_ID not set on this GitHub Environment; tier never bootstrapped." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - if: steps.precheck.outputs.skip == 'false' + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Reset min/max replicas in rg-ftgo-${{ matrix.env }}-${{ vars.LOCATION || 'eastus' }} + - if: steps.precheck.outputs.skip == 'false' + name: Reset min/max replicas in rg-ftgo-${{ matrix.env }}-${{ vars.LOCATION || 'eastus' }} env: LOCATION: ${{ vars.LOCATION || 'eastus' }} run: | diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a4b31b3..adb3630 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -141,7 +141,7 @@ jobs: - name: Upload Trivy SARIF if: always() - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: sarif_file: trivy-${{ steps.meta.outputs.SHORT_NAME }}.sarif category: trivy-${{ steps.meta.outputs.SHORT_NAME }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9e166c..8d5aafa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,7 +188,7 @@ jobs: contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0 + - uses: DavidAnson/markdownlint-cli2-action@6b51ade7a9e4a75a7ad929842dd298a3804ebe8b # v23.1.0 with: globs: | **/*.md @@ -203,7 +203,7 @@ jobs: issues: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.7.0 + - uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.7.0 with: args: --no-progress --max-concurrency 4 --exclude-mail './**/*.md' fail: false diff --git a/.github/workflows/drift-detection.yml b/.github/workflows/drift-detection.yml new file mode 100644 index 0000000..b2310c0 --- /dev/null +++ b/.github/workflows/drift-detection.yml @@ -0,0 +1,70 @@ +name: drift-detection + +# Weekly Bicep what-if against ci/ppe to surface infrastructure drift between +# what main says is deployed and what's actually live in Azure. Output is a +# job summary annotation; no auto-remediation. + +on: + schedule: + - cron: '0 6 * * 1' # Mondays 06:00 UTC + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: drift-${{ github.ref }} + cancel-in-progress: true + +jobs: + what-if: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + id-token: write + contents: read + strategy: + fail-fast: false + matrix: + env: [ci, ppe] + environment: ${{ matrix.env }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Azure login + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Skip if RG missing (tier never provisioned) + id: rg-check + env: + RG: rg-ftgo-${{ matrix.env }}-${{ vars.LOCATION || 'eastus' }} + run: | + if ! az group show --name "$RG" --only-show-errors --output none 2>/dev/null; then + echo "::notice title=Drift skipped::$RG does not exist; tier never provisioned." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Bicep what-if + if: steps.rg-check.outputs.skip == 'false' + env: + RG: rg-ftgo-${{ matrix.env }}-${{ vars.LOCATION || 'eastus' }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + run: | + set -euo pipefail + out=$(az deployment group what-if \ + --resource-group "$RG" \ + --template-file infra/bicep/azure.bicep \ + --parameters infra/bicep/azure.${{ matrix.env }}.bicepparam \ + --no-pretty-print --result-format ResourceIdOnly 2>&1) || true + echo "$out" + if echo "$out" | grep -qE '^(\+|\-|\~|=) '; then + echo "::warning title=Drift detected (${{ matrix.env }})::Bicep what-if reports differences against $RG. Review the run log." + else + echo "::notice title=No drift (${{ matrix.env }})::Live state matches main." + fi diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8bc763f..ab30f55 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: retention-days: 5 - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: sarif_file: results.sarif diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 5781946..17dfbb8 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -11,3 +11,4 @@ MD040: false MD028: false MD036: false MD031: false +MD060: false diff --git a/Directory.Packages.props b/Directory.Packages.props index 019e298..fce1f51 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ - + @@ -19,8 +19,8 @@ - - + + @@ -39,19 +39,19 @@ - + - - + + - + diff --git a/SCOPE.md b/SCOPE.md index 90c2a98..67d534a 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -66,16 +66,16 @@ Call them out so readers don't expect them. ## 5. "If you are X, this guide is Y for you" -| Reader archetype | Recommendation | -|---|---| -| Staff .NET engineer at a 200-person SaaS, multi-team, cloud-native on Azure | **Primary reader.** Read [`docs/decision-trees.md`](./docs/decision-trees.md) → SCOPE → [`docs/matrix.md`](./docs/matrix.md) → [`docs/best-practices.md`](./docs/best-practices.md). | -| Platform / auth-library owner (`EntraAuth.*` shared NuGet, multi-product) | **Primary reader.** Start at [`docs/aks-shared-infra.md`](./docs/aks-shared-infra.md), then [`docs/acquisition.md`](./docs/acquisition.md) and [`docs/validation.md`](./docs/validation.md). | -| Architect picking credential type for a new service | **Primary reader.** [`docs/decision-trees.md`](./docs/decision-trees.md) Tree 2 + Tree 5 → [`docs/credential-patterns/index.md`](./docs/credential-patterns/index.md). | -| Solo developer, one app, pre-PMF | **Skim only.** [`docs/run-locally.md`](./docs/run-locally.md) and [`docs/best-practices.md`](./docs/best-practices.md) are worth an hour; the platform / shared-infra chapters are premature. | -| ASP.NET Core engineer needing JWT validation defaults only | **Targeted reader.** [`docs/validation.md`](./docs/validation.md) is the chapter; the rest is background. | -| Mobile / SPA developer | **Wrong guide.** Read MSAL.js or MSAL mobile docs first; come back when you own a server-side .NET API. | -| IGA / access-review engineer | **Wrong guide.** Use [Microsoft Entra ID Governance](https://learn.microsoft.com/entra/id-governance/identity-governance-overview); this guide is for app developers, not directory operators. | -| Compliance / GRC reviewer mapping controls | **Reference, not source of truth.** Use this as the engineering-side counterpart to your control catalog; mapping work is yours. | +| Reader archetype | Recommendation | +| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Staff .NET engineer at a 200-person SaaS, multi-team, cloud-native on Azure | **Primary reader.** Read [`docs/decision-trees.md`](./docs/decision-trees.md) → SCOPE → [`docs/matrix.md`](./docs/matrix.md) → [`docs/best-practices.md`](./docs/best-practices.md). | +| Platform / auth-library owner (`EntraAuth.*` shared NuGet, multi-product) | **Primary reader.** Start at [`docs/aks-shared-infra.md`](./docs/aks-shared-infra.md), then [`docs/acquisition.md`](./docs/acquisition.md) and [`docs/validation.md`](./docs/validation.md). | +| Architect picking credential type for a new service | **Primary reader.** [`docs/decision-trees.md`](./docs/decision-trees.md) Tree 2 + Tree 5 → [`docs/credential-patterns/index.md`](./docs/credential-patterns/index.md). | +| Solo developer, one app, pre-PMF | **Skim only.** [`docs/run-locally.md`](./docs/run-locally.md) and [`docs/best-practices.md`](./docs/best-practices.md) are worth an hour; the platform / shared-infra chapters are premature. | +| ASP.NET Core engineer needing JWT validation defaults only | **Targeted reader.** [`docs/validation.md`](./docs/validation.md) is the chapter; the rest is background. | +| Mobile / SPA developer | **Wrong guide.** Read MSAL.js or MSAL mobile docs first; come back when you own a server-side .NET API. | +| IGA / access-review engineer | **Wrong guide.** Use [Microsoft Entra ID Governance](https://learn.microsoft.com/entra/id-governance/identity-governance-overview); this guide is for app developers, not directory operators. | +| Compliance / GRC reviewer mapping controls | **Reference, not source of truth.** Use this as the engineering-side counterpart to your control catalog; mapping work is yours. | --- diff --git a/docs/acquisition.md b/docs/acquisition.md index 1b23ee1..928beda 100644 --- a/docs/acquisition.md +++ b/docs/acquisition.md @@ -2,10 +2,10 @@ Two flows you ever acquire on the server: -| Flow | Who is the token *for* | Claim shape | OAuth grant | -|---|---|---|---| -| **App token** | The calling app/workload | `roles` *(when app roles are assigned/required)*, no `scp`; `idtyp=app` may not be present (defense-in-depth only — rely on `roles` + `azp` allow-list for enforcement, see [validation.md §4 App-token specific checks](validation.md#4-app-token-specific-checks)) | `client_credentials` (or FIC assertion) | -| **User token** | The signed-in user | `scp` (delegated scopes); identity in `oid` + `tid` (use these for decisions); `name` / `preferred_username` for display | `authorization_code` (client) → API → **OBO** for downstream | +| Flow | Who is the token *for* | Claim shape | OAuth grant | +| -------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| **App token** | The calling app/workload | `roles` *(when app roles are assigned/required)*, no `scp`; `idtyp=app` may not be present (defense-in-depth only — rely on `roles` + `azp` allow-list for enforcement, see [validation.md §4 App-token specific checks](validation.md#4-app-token-specific-checks)) | `client_credentials` (or FIC assertion) | +| **User token** | The signed-in user | `scp` (delegated scopes); identity in `oid` + `tid` (use these for decisions); `name` / `preferred_username` for display | `authorization_code` (client) → API → **OBO** for downstream | > Rule of thumb: if there is no human in the request, you want an **app token**. If there is, propagate the user identity via **OBO**, don't fall back to an app token. @@ -93,11 +93,11 @@ Same as cert but `.WithClientSecret("…")`. Rotate ≤ 6 months, store only in ### 1e. Picking the library -| You are doing… | Use | -|---|---| -| Calling an **Azure resource SDK** | `Azure.Identity` (`TokenCredential`) | -| Calling **your own / a 3rd-party Entra-protected API** from a worker | `MSAL.NET` (`ConfidentialClientApplication`) | -| Calling a downstream API **from inside an ASP.NET Core API** | `Microsoft.Identity.Web` → `ITokenAcquisition` / `IDownstreamApi` | +| You are doing… | Use | +| -------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Calling an **Azure resource SDK** | `Azure.Identity` (`TokenCredential`) | +| Calling **your own / a 3rd-party Entra-protected API** from a worker | `MSAL.NET` (`ConfidentialClientApplication`) | +| Calling a downstream API **from inside an ASP.NET Core API** | `Microsoft.Identity.Web` → `ITokenAcquisition` / `IDownstreamApi` | --- @@ -155,14 +155,14 @@ The credential the API uses to authenticate **itself** to the token endpoint dur ## 3. Single-tenant vs multi-tenant — at acquisition time -| | Single-tenant | Multi-tenant | -|---|---|---| -| Authority | `https://login.microsoftonline.com/` | `https://login.microsoftonline.com/organizations` (work/school — default for Entra-only APIs). Only use `/common` (work + MSA) if you truly accept personal accounts and filter explicitly. | -| App registration | `signInAudience: AzureADMyOrg` | `AzureADMultipleOrgs` (or `…AndPersonalMicrosoftAccount`) | -| Admin consent | Once, in your tenant | Per-tenant; expose via admin-consent URL | -| App token (`/.default`) | Issued by your tenant | Issued by the **calling tenant** — the app must be provisioned there | -| OBO | Same-tenant | Token is issued in the **user's** home tenant; downstream must accept that issuer. The downstream API **must** enforce a `tid` allow-list via `IssuerValidator` — see [validation.md §3 Issuer & audience](validation.md#3-issuer-audience-v1-vs-v2-single-vs-multi-tenant). | -| MI / FIC | Same | MI is per-resource in your tenant — to call into other tenants you need a **multi-tenant app reg** + cert/FIC, not raw MI | +| | Single-tenant | Multi-tenant | +| ----------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Authority | `https://login.microsoftonline.com/` | `https://login.microsoftonline.com/organizations` (work/school — default for Entra-only APIs). Only use `/common` (work + MSA) if you truly accept personal accounts and filter explicitly. | +| App registration | `signInAudience: AzureADMyOrg` | `AzureADMultipleOrgs` (or `…AndPersonalMicrosoftAccount`) | +| Admin consent | Once, in your tenant | Per-tenant; expose via admin-consent URL | +| App token (`/.default`) | Issued by your tenant | Issued by the **calling tenant** — the app must be provisioned there | +| OBO | Same-tenant | Token is issued in the **user's** home tenant; downstream must accept that issuer. The downstream API **must** enforce a `tid` allow-list via `IssuerValidator` — see [validation.md §3 Issuer & audience](validation.md#3-issuer-audience-v1-vs-v2-single-vs-multi-tenant). | +| MI / FIC | Same | MI is per-resource in your tenant — to call into other tenants you need a **multi-tenant app reg** + cert/FIC, not raw MI | Key takeaway: **MI is single-tenant by nature.** For cross-tenant S2S, use a multi-tenant app registration with a cert or FIC, or use MI to call your own multi-tenant app reg's federated credential. diff --git a/docs/aks-shared-infra.md b/docs/aks-shared-infra.md index 7798090..3e28bf9 100644 --- a/docs/aks-shared-infra.md +++ b/docs/aks-shared-infra.md @@ -71,18 +71,18 @@ Token-forging helpers (signed by a test JWKS) and ready-made negative-test fixtu ## Library vs Service — decision rationale -| Concern | Shared library + IaC (recommended) | Shared auth service (the proposal) | -|---|---|---| -| Solves duplication across 20 services | ✅ One implementation, semver'd | ✅ Only if every service migrates *and stays migrated* | -| Solves drift across 10 products | ✅ Defaults + lint enforce standards | ⚠️ Standards leak into bespoke proxy logic | -| Runtime cost | None — in-process, JWKS cached | +1 hop per request; p99 hit; SPOF when it hiccups | -| Operational cost | Owned like any internal NuGet | New tier-0 service: capacity, on-call, regional HA, DR | -| Conway's-law risk | Low — teams self-serve via package upgrade | High — every product's auth change queues behind one team | -| Token re-issuance temptation | None | Inevitable — you become an IdP | -| OBO / delegated downstream | Lives in the calling service, supported by lib helpers | **Cannot** be centralized — needs the calling service's credential & audience | -| Cross-product S2S | App tokens with `roles`, validated locally; lib handles boilerplate | The proxy adds nothing | -| Failure blast radius | Bad release rolled back per-service, gradual canary | Auth service down ⇒ every product down | -| Rollout | Canary one service, fan out; pin versions per product | Big-bang or risky dual-stack | +| Concern | Shared library + IaC (recommended) | Shared auth service (the proposal) | +| ------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Solves duplication across 20 services | ✅ One implementation, semver'd | ✅ Only if every service migrates *and stays migrated* | +| Solves drift across 10 products | ✅ Defaults + lint enforce standards | ⚠️ Standards leak into bespoke proxy logic | +| Runtime cost | None — in-process, JWKS cached | +1 hop per request; p99 hit; SPOF when it hiccups | +| Operational cost | Owned like any internal NuGet | New tier-0 service: capacity, on-call, regional HA, DR | +| Conway's-law risk | Low — teams self-serve via package upgrade | High — every product's auth change queues behind one team | +| Token re-issuance temptation | None | Inevitable — you become an IdP | +| OBO / delegated downstream | Lives in the calling service, supported by lib helpers | **Cannot** be centralized — needs the calling service's credential & audience | +| Cross-product S2S | App tokens with `roles`, validated locally; lib handles boilerplate | The proxy adds nothing | +| Failure blast radius | Bad release rolled back per-service, gradual canary | Auth service down ⇒ every product down | +| Rollout | Canary one service, fan out; pin versions per product | Big-bang or risky dual-stack | The library approach gives you everything you wanted (one authoritative implementation) without inheriting the downsides of a runtime auth service. diff --git a/docs/credential-patterns/index.md b/docs/credential-patterns/index.md index 11e45cb..33de7ef 100644 --- a/docs/credential-patterns/index.md +++ b/docs/credential-patterns/index.md @@ -13,13 +13,13 @@ the credential type is part of the policy, not an operational detail. ## Decision matrix -| Where does your workload run? | Credential | -|---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| -| Azure (App Service, ACA, AKS, Functions, …) | **must** use [Managed Identity](managed-identity.md) | -| GitHub Actions | **must** use [Workload Identity Federation](federated-identity.md) | -| GKE / EKS / on-prem K8s with SPIFFE / OIDC | **must** use [Workload Identity Federation](federated-identity.md) | -| On-prem service with no IdP / HSM-bound key | **should** use [Certificate](cert.md) | -| Anything else | you almost certainly don't need a [client secret](client-secret.md) — see that page for the three documented exceptions; **strongly prefer** MI or FIC | +| Where does your workload run? | Credential | +| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Azure (App Service, ACA, AKS, Functions, …) | **must** use [Managed Identity](managed-identity.md) | +| GitHub Actions | **must** use [Workload Identity Federation](federated-identity.md) | +| GKE / EKS / on-prem K8s with SPIFFE / OIDC | **must** use [Workload Identity Federation](federated-identity.md) | +| On-prem service with no IdP / HSM-bound key | **should** use [Certificate](cert.md) | +| Anything else | you almost certainly don't need a [client secret](client-secret.md) — see that page for the three documented exceptions; **strongly prefer** MI or FIC | Cross-references: diff --git a/docs/decision-trees.md b/docs/decision-trees.md index fe68fc3..9f48683 100644 --- a/docs/decision-trees.md +++ b/docs/decision-trees.md @@ -73,13 +73,13 @@ References: [`docs/validation.md#3-issuer-audience-v1-vs-v2-single-vs-multi-tena - Trigger: protecting a new endpoint that may be hit by signed-in users (`scp`), app-only callers (`roles` + `azp`), or both. - Cost of wrong call: a single OR-claims policy that lets an app-only token reach a user-only endpoint (no `azp` allow-list, no `scp` check), or a user token satisfy an app-only endpoint by carrying an unrelated `scp`. Both are real privilege-escalation bugs in production APIs. Mirrors dotnet-guide [ch02 §10](https://github.com/mghabin/dotnet-engineering-guide/blob/main/docs/02-aspnetcore.md#10-authnauthz). -- Default per [`validation.md`](../docs/validation.md) §4 + dotnet-guide ch02 §10: **two separate named policies on two separate authorisation attributes**, never an OR-claims policy. Delegated → `[Authorize] + [RequiredScope("orders.read")]`. App-only → `[Authorize(Roles="Orders.Process")] + RequireClientApp` (azp allow-list). Both → list both attributes; each enforces its own invariants. +- Default per [`validation.md`](../docs/validation.md) §4 + dotnet-guide ch02 §10: **two separate named policies on two separate authorisation attributes**, never an OR-claims policy. Delegated → `[Authorize] + [RequiredScope("orders.read")]`. App-only → `[Authorize(Policy = OrdersAuthorizationPolicies.App)]` (one named policy combining role + `azp` allow-list + `scp`-rejection). Both → list both attributes; each enforces its own invariants. **Never `[Authorize(Roles=...)]`** — it silently no-ops on `MapInboundClaims=false` schemes; see [DOCTRINE.md](../DOCTRINE.md#authorization-policy-doctrine-canonical). ```mermaid flowchart TD A[New protected endpoint] --> B{Caller identity model?} B -->|Delegated user only| C["[Authorize] + [RequiredScope(scope)]
reject if roles present without scp — validation.md §4"] - B -->|App-only / daemon only| D["[Authorize(Roles=app-role)]
+ RequireClientApp azp allow-list
+ reject if scp present — validation.md §4"] + B -->|App-only / daemon only| D["[Authorize(Policy=*App)]
via AddAppPolicy: role + azp allow-list
+ reject if scp present — validation.md §4"] B -->|Both flows in scope| E[Two separate named policies
on two separate authorizations
NEVER one OR-claims policy — validation.md §4] C --> F[Each request: presence of scp
+ specific scope value — validation.md §4] D --> G[Each request: presence of roles
+ azp / appid in allow-list
+ absence of scp — validation.md §4] diff --git a/docs/operations.md b/docs/operations.md index 9fa7fcb..7e676fd 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -181,17 +181,21 @@ the workflow still fails with that code, check that 4. The script writes `vars.ENTRA_CONFIG_JSON` for the env so future CD runs are fully declarative. -## Renaming a deployment tier (one-shot live cutover) +## Renaming a deployment tier (template — historical example: `dev` → `ci`) -This runbook is the **only safe order** for renaming a tier (e.g. -`dev` → `ci`, as the `local + ci + ppe + prod` rename did). The +> **Status: template, not active.** This is a generic runbook. The `dev` → +> `ci` rename it walks through happened in **PR #103** and is **complete** +> — current main has no `dev` tier. Reuse this procedure (substituting your +> source/target names) if you ever need to rename another tier. + +This runbook is the **only safe order** for renaming a tier. The non-obvious bit is OIDC: the GitHub OIDC token's `sub` claim is `repo:OWNER/REPO:environment:`, which is matched verbatim against the federated-identity-credential subject on the bootstrap app reg. Rename the GH environment **before** the FIC and the next `azure/login@…` call fails with `AADSTS70021`. -Worked example: cut over from `dev` → `ci`. +Worked example (historical): cut over from `dev` → `ci`. 1. **Codebase first** (no live changes). Land the rename PR (see the Phase A + B commits on `refactor/env-rename-dev-to-ci`). diff --git a/docs/validation.md b/docs/validation.md index 23dd6d8..c6af822 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -42,22 +42,24 @@ builder.Services.Configure(JwtBearerDefaults.AuthenticationSch ## 2. Authorization — `scp` vs `roles` -| Token type | Claim | Source | -|---|---|---| -| User (delegated) | `scp` (space-separated) | Scopes the user consented to | -| App (S2S) | `roles` (array) | App roles you defined on the API and granted to the client app | +| Token type | Claim | Source | +| ---------------- | ----------------------- | -------------------------------------------------------------- | +| User (delegated) | `scp` (space-separated) | Scopes the user consented to | +| App (S2S) | `roles` (array) | App roles you defined on the API and granted to the client app | -Endpoint policy: +Endpoint policy (FTGO sample): ```csharp [Authorize] -[RequiredScope("Files.Read")] // user token must carry scp=Files.Read +[RequiredScope("orders.read")] // user token must carry scp=orders.read public IActionResult ReadAsUser() => … -[Authorize(Roles = "Tasks.Process.All")] // app token must carry that role +[Authorize(Policy = OrdersAuthorizationPolicies.App)] // app token: roles=Orders.Process AND azp ∈ allow-list public IActionResult ProcessAsApp() => … ``` +> **Never** `[Authorize(Roles = "...")]` on a Microsoft.Identity.Web JWT scheme. Those schemes set `MapInboundClaims = false` (defense-in-depth), so the framework's role check looks for `ClaimTypes.Role` while Entra emits the role claim under the short name `roles`. The check silently fails. Always use a named policy via `AddAppPolicy(...)` — see [DOCTRINE.md § "Authorization-policy doctrine"](../DOCTRINE.md#authorization-policy-doctrine-canonical) for the canonical statement. + Distinguish them in code (when an endpoint accepts both): ```csharp @@ -153,8 +155,8 @@ There is nothing magical to validate about MI tokens; treat them like any S2S ca ## 7. Quick checklist per endpoint - [ ] `[Authorize]` present. -- [ ] `[RequiredScope]` (user) or `[Authorize(Roles=…)]` (app) — not both implicit. -- [ ] If endpoint accepts both: explicit branching on `idtyp`/`scp`/`roles`. +- [ ] `[RequiredScope]` (user) or `[Authorize(Policy="...")]` (app) — never `[Authorize(Roles=...)]` on a JWT scheme with `MapInboundClaims=false` (it silently no-ops; see [DOCTRINE.md](../DOCTRINE.md#authorization-policy-doctrine-canonical)). +- [ ] Mixed-claims policies: each named policy rejects tokens that carry the *other* claim type (so a user token never satisfies an app policy and vice versa). - [ ] App-only endpoints additionally allow-list `azp`/`appid`. - [ ] Multi-tenant: tenant allow-list enforced in `IssuerValidator`. - [ ] Audience set explicitly to App ID URI (and GUID during v1↔v2 migration). diff --git a/infra/bicep/azure.bicep b/infra/bicep/azure.bicep index dee4f3a..952034e 100644 --- a/infra/bicep/azure.bicep +++ b/infra/bicep/azure.bicep @@ -42,6 +42,16 @@ param keyVaultEnablePurgeProtection bool = environmentName == 'prod' @maxLength(24) param keyVaultNameOverride string = '' +@description('Monthly cost ceiling in USD. Forecasted+Actual notifications fire at 80%/100%. ci/ppe defaults are deliberately tiny (this sample is $0-idle).') +@minValue(1) +param monthlyBudgetUsd int = environmentName == 'prod' ? 50 : 10 + +@description('Email recipients for budget alerts. Empty array disables email notifications. Wire from each tier bicepparam.') +param budgetContactEmails array = [] + +@description('First-of-month UTC start date (yyyy-MM-01). Pinned in bicepparam for reproducible deployments.') +param budgetStartDate string = '${utcNow('yyyy-MM')}-01' + // Region → short token folded into resource names. Falls back to first 3 chars for unmapped regions. var regionShortMap = { eastus: 'eus' @@ -63,9 +73,10 @@ var kvName = empty(keyVaultNameOverride) ? 'kv-ftgo-${environmentName}-${t module logAnalytics 'modules/log-analytics.bicep' = { name: 'law' params: { - name: lawName - location: location - tags: tags + name: lawName + location: location + tags: tags + retentionInDays: environmentName == 'prod' ? 90 : 30 } } @@ -145,6 +156,20 @@ module kvRbac 'modules/key-vault-rbac.bicep' = { ] } +// Cost guardrail: monthly budget on this RG. ci/ppe defaults are tiny because +// the sample is $0-idle (ACA scale-to-zero); prod allows a higher ceiling with +// the same Forecasted+Actual alert structure. +module budget 'modules/budget.bicep' = { + name: 'budget' + scope: resourceGroup() + params: { + name: 'ftgo-${environmentName}-budget' + monthlyAmount: monthlyBudgetUsd + contactEmails: budgetContactEmails + startDate: budgetStartDate + } +} + // Prod safety net: prevent accidental `az group delete` / portal-delete of the // entire RG. CanNotDelete still lets ARM perform in-place updates but blocks // destructive operations until the lock is removed. ci/PPE intentionally have diff --git a/infra/bicep/modules/budget.bicep b/infra/bicep/modules/budget.bicep new file mode 100644 index 0000000..f985bf7 --- /dev/null +++ b/infra/bicep/modules/budget.bicep @@ -0,0 +1,56 @@ +metadata name = 'budget' +metadata description = 'RG-scoped monthly cost budget with Forecasted+Actual notifications. Defense against runaway spend.' + +extension az + +@description('Budget name (resource-scoped, must be unique within the RG).') +param name string + +@description('Monthly amount in subscription currency (USD by default).') +@minValue(1) +@maxValue(100000) +param monthlyAmount int + +@description('Email addresses notified on threshold breach. Empty = no email channel.') +param contactEmails array = [] + +@description('Start of the budget window (yyyy-MM-01 UTC). Must be the first of a month.') +param startDate string + +resource budget 'Microsoft.Consumption/budgets@2024-08-01' = { + name: name + properties: { + category: 'Cost' + amount: monthlyAmount + timeGrain: 'Monthly' + timePeriod: { + startDate: startDate + } + notifications: empty(contactEmails) ? {} : { + actualAt80: { + enabled: true + operator: 'GreaterThan' + threshold: 80 + thresholdType: 'Actual' + contactEmails: contactEmails + } + actualAt100: { + enabled: true + operator: 'GreaterThanOrEqualTo' + threshold: 100 + thresholdType: 'Actual' + contactEmails: contactEmails + } + forecastAt100: { + enabled: true + operator: 'GreaterThan' + threshold: 100 + thresholdType: 'Forecasted' + contactEmails: contactEmails + } + } + } +} + +@description('Resource ID of the budget.') +output budgetId string = budget.id diff --git a/infra/bicep/modules/container-app.bicep b/infra/bicep/modules/container-app.bicep index f8be212..c6c827b 100644 --- a/infra/bicep/modules/container-app.bicep +++ b/infra/bicep/modules/container-app.bicep @@ -63,7 +63,7 @@ resource app 'Microsoft.App/containerApps@2025-07-01' = { ingress: { external: true targetPort: 8080 - transport: 'auto' + transport: environmentName == 'prod' ? 'https' : 'auto' allowInsecure: false traffic: [ { diff --git a/infra/bicep/modules/log-analytics.bicep b/infra/bicep/modules/log-analytics.bicep index e5ebf4d..8174737 100644 --- a/infra/bicep/modules/log-analytics.bicep +++ b/infra/bicep/modules/log-analytics.bicep @@ -12,6 +12,11 @@ param location string @description('Tags applied to the workspace.') param tags object = {} +@description('Log retention in days. 30 covers ci/ppe ops; prod uses 90+ for the audit-log compliance floor.') +@minValue(30) +@maxValue(730) +param retentionInDays int = 30 + resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { name: name location: location @@ -20,7 +25,7 @@ resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { sku: { name: 'PerGB2018' } - retentionInDays: 30 + retentionInDays: retentionInDays workspaceCapping: { dailyQuotaGb: 1 } diff --git a/src/Ftgo.ApiGateway/packages.lock.json b/src/Ftgo.ApiGateway/packages.lock.json index 2eb022d..ee44b37 100644 --- a/src/Ftgo.ApiGateway/packages.lock.json +++ b/src/Ftgo.ApiGateway/packages.lock.json @@ -10,9 +10,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.54, )", - "resolved": "3.0.54", - "contentHash": "fybpNsTg4ZT+oiQWkrkTvvy1dowdqjfkefFa/7OIPFPmTkHd9eSEjPCpoK1c5oYdp68QjpLTbeCDpZB2TOUQ4A==" + "requested": "[3.0.58, )", + "resolved": "3.0.58", + "contentHash": "EWprZh8ONB5W4T5lajB9H3/ThznWgEVbb8sWkIZe2ZOCRPPzAdLxkFfM8z57SosJtUYGxPXvxlqGmClxw8mA9g==" }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", @@ -25,14 +25,14 @@ }, "Microsoft.Identity.Web": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "rqMufh64Woj/kc39d9iCb12BTtBj9H40haLUemRAwoqvqSomYPlbMOwt1UD1Y5aMqmc8aYF06U5ATY8Qnw2iFg==", - "dependencies": { - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "SshLuFsW0VqCP18O1YUAqPh4KZ6ogu9yYrhlYdGxf/KSRR0wHO1MFlyxh2aOkHdw462XKjbzm36NTspJo1r58g==", + "dependencies": { + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", "Microsoft.IdentityModel.Validators": "8.15.0", "System.IdentityModel.Tokens.Jwt": "8.15.0" @@ -40,11 +40,11 @@ }, "Microsoft.Identity.Web.DownstreamApi": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "MXCBVZO9Oj2hSZkPvEFcRg1/fyKNjqJQzAVxUjdCOzno3rd1BeVhuMddk9aYNJ3r79GD5lA3xJeneXo+SKlKCA==", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "E9RjjTmAiHR7Z8HDtfEDbioW6NOvoYABGlyIhwOPkMpECTP7M69SwO5DJki+QA0pHpIongdUC856GH/lnqyxSA==", "dependencies": { - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0" + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0" } }, "Microsoft.SourceLink.GitHub": { @@ -66,14 +66,14 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.24.0.138807, )", - "resolved": "10.24.0.138807", - "contentHash": "+ZEa1+KhNSulMSatpffgHnovbLGtYL9oukMh8uQp8RBDnPOjEFzvECSa4kfu1D4PJxErksiGOQQ+kDeao1x9jQ==" + "requested": "[10.25.0.139117, )", + "resolved": "10.25.0.139117", + "contentHash": "2iBZIkcgsaUNyIPFqR9ECv5PLbAwV9I/v/PeEiMdkfhWEAnqD7VyNXzukQDlL/D24f5OoUc4MbWF5SgYI7x3CA==" }, "Azure.Core": { "type": "Transitive", - "resolved": "1.53.0", - "contentHash": "x9c/toFMOtRrlTdFuE7rlGCVAduQzWVfKmLz5juj41zJAXEhYD5hluiUyyAEzJ6OxpBnKtiaBztzwpZITAVjtg==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "10.0.3", "Microsoft.Identity.Client": "4.83.1", @@ -171,21 +171,21 @@ }, "Microsoft.Identity.Web.Certificate": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "G4nkrs1pKg6NxuSvyoRzokLfsfd2v5pXpXS1XUvHstvdWkQRBw8kTbSwRCvzdRFA1MW7Ct14zcp1P4kej7dB+g==", + "resolved": "4.9.0", + "contentHash": "kr6ZpxNbWm1+eI3pn6sdgIlduYZiEmoC0TbiiR1gmcREuI87E963xjBMmvXf9+CnHcjbsqhkBcK4huKauif0yA==", "dependencies": { - "Azure.Identity": "1.11.4", + "Azure.Identity": "1.17.2", "Azure.Security.KeyVault.Certificates": "4.6.0", "Azure.Security.KeyVault.Secrets": "4.6.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.Diagnostics": "4.8.0" + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.Diagnostics": "4.9.0" } }, "Microsoft.Identity.Web.Certificateless": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "TegoXg/SX1mca1ygTZWTgLTXIZPQudmg5SLr9f50A8fzC4oFGkNOUfa9wii9SRikBGg4Sc31QxUeM/MMFyIX6A==", + "resolved": "4.9.0", + "contentHash": "hlt8KV1V0JLjyxncmSNAugyqxVr65wQlzcsks6TgqvUyllkQvWAxqgM+Dh6OB8sgJWMoCOmJuKJ6SejAgYjJ/w==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", "Microsoft.IdentityModel.JsonWebTokens": "8.15.0" @@ -193,20 +193,20 @@ }, "Microsoft.Identity.Web.Diagnostics": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "oWYVlq2h86yAmqJHcDK0JrJfhDZTnF1M1vyY9mGx+x5E5GpNml7hTI1N1Kq4Z9JUtaPvz4dN8MgfUn7QyHZ13Q==" + "resolved": "4.9.0", + "contentHash": "5x4Tyg1xFr8vgTAuos5OyCNAXcnq9U+6QWA3pRLOgUF5Am7Bn/CM+rmOjffoyfP6ymQdQ0/zxtNOtyRjTlpiBg==" }, "Microsoft.Identity.Web.TokenAcquisition": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "Nm4YJxUQuIThoRWprg9bv2vCDs2zzlOo7B4nfHoG3Spw5Ege2edFLY+iEtRh+GYW2Fhn97rYmwCgmaQ2IhYZxA==", + "resolved": "4.9.0", + "contentHash": "ap6EFz9Nn3pWlfYQ8AqnRM7UsrH5pAXzDMF/aayKEfcAxLgXe23XKb1jBEzWNYkg5NNrbiLVWjKDIWbPqMrn1w==", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "10.0.0", "Microsoft.AspNetCore.Authentication.OpenIdConnect": "10.0.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Logging": "8.15.0", "Microsoft.IdentityModel.LoggingExtensions": "8.15.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", @@ -215,12 +215,12 @@ }, "Microsoft.Identity.Web.TokenCache": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "2TL0zev2SREJoIzZi9dQ+vv/Us6Q1ZpZD02ti0LhFDpFg/vKGD98pHmcz+f216PbWHzQpERzS3tMgcIGt2lywQ==", + "resolved": "4.9.0", + "contentHash": "3eHtmFu6HJU334bLyOjG/PvWyYQaoGEh9HyomGQ1p/aUV4EiGkdiU4R5yuyOIHdxRs8DKMFPN0Wnqp2nvWayjg==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", - "Microsoft.Identity.Web.Diagnostics": "4.8.0", - "System.Security.Cryptography.Pkcs": "10.0.6" + "Microsoft.Identity.Web.Diagnostics": "4.9.0", + "System.Security.Cryptography.Pkcs": "10.0.7" } }, "Microsoft.IdentityModel.Abstractions": { @@ -289,15 +289,15 @@ }, "OpenTelemetry.PersistentStorage.Abstractions": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + "resolved": "1.0.3", + "contentHash": "7nSE1uLY0KOHRssg1xQydDZJ/s9zECOzFT/gSDIp1KTxbgd1yziuMTsdBKZ/N1BbpUxfH6Pjq5OMd7lYogwReA==" }, "OpenTelemetry.PersistentStorage.FileSystem": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "resolved": "1.0.3", + "contentHash": "3k+yCroJcGBtZIia+0LgzMlBuD0U95BzcWNEGWedusBaRAD8EmkaCPAUwaCtFbb/Mg7HywbIG2zuenANPYFJ0g==", "dependencies": { - "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.3" } }, "Polly.Core": { @@ -350,8 +350,8 @@ }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "10.0.6", - "contentHash": "88tquaGJ1htm4DHWS6x9jwER7sFET2SVRN7HqO1FYZwE0diDcUmz0ajhVa8ZD2HGhDJBueSPjP/gqyP3gXtT2A==" + "resolved": "10.0.7", + "contentHash": "dbdKfF3eA5l+CXiAbDxiCxdezoxeanbue1ck8m49ih1L9uZG6ry8Ul8On6vpragyMDJJP4rQHUY/SWgk66tCYA==" }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", @@ -363,17 +363,17 @@ "dependencies": { "Ftgo.Auth.Client": "[1.0.0, )", "Microsoft.AspNetCore.OpenApi": "[10.0.7, )", - "Microsoft.Identity.Web": "[4.8.0, )", - "Microsoft.Identity.Web.DownstreamApi": "[4.8.0, )", + "Microsoft.Identity.Web": "[4.9.0, )", + "Microsoft.Identity.Web.DownstreamApi": "[4.9.0, )", "Microsoft.IdentityModel.Validators": "[8.17.0, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )", - "Scalar.AspNetCore": "[2.14.4, )" + "Scalar.AspNetCore": "[2.14.9, )" } }, "ftgo.auth.client": { "type": "Project", "dependencies": { - "Azure.Monitor.OpenTelemetry.Exporter": "[1.7.0, )", + "Azure.Monitor.OpenTelemetry.Exporter": "[1.8.0, )", "Microsoft.Extensions.Http.Resilience": "[10.5.0, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", @@ -392,13 +392,13 @@ }, "Azure.Monitor.OpenTelemetry.Exporter": { "type": "CentralTransitive", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "fexzK+HM06C3tmBL6DLNVAcgutBMY7hQ7tGcdpCyq6HaXvbNz6cFtenrJUAdXM8Y4G+3QmZ6PIAL1hFrVe4Mpw==", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "JbNUQIK7uZ7Mg2fZzdsjqC/eso3Ag/tMwCqYvGvedyE1fObe6JAwCaCSMZDMrhrAaFn8x8+ZaKJelfcEK5MCcg==", "dependencies": { - "Azure.Core": "1.52.0", - "OpenTelemetry.Extensions.Hosting": "1.15.1", - "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + "Azure.Core": "1.54.0", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.3" } }, "Azure.Security.KeyVault.Certificates": { @@ -536,9 +536,9 @@ }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.14.4, )", - "resolved": "2.14.4", - "contentHash": "PyuqLRi7JXyTJm/rh+IWCA4Fct1SlZuWrdYiui5hiaBuSY88+roc4eWKfYaE6zrupBIwnEP8pJLe4ifvU7HtZA==" + "requested": "[2.14.9, )", + "resolved": "2.14.9", + "contentHash": "bDc8NjI2JSIAX0C1+02WO11zCJ1SJQhWdMC0s7yi2Nh2atoNRpNRPiYeyIhWSerkc1SkDdJVOeU9QSY6jb0/zQ==" } } } diff --git a/src/Ftgo.Auth.Client/DownstreamApiClient.cs b/src/Ftgo.Auth.Client/DownstreamApiClient.cs index 2566c57..ce0d76e 100644 --- a/src/Ftgo.Auth.Client/DownstreamApiClient.cs +++ b/src/Ftgo.Auth.Client/DownstreamApiClient.cs @@ -22,13 +22,13 @@ public sealed partial class DownstreamApiClient( string scope, CancellationToken cancellationToken) { - var token = await tokenProvider.GetAccessTokenAsync(scope, cancellationToken); + var token = await tokenProvider.GetAccessTokenAsync(scope, cancellationToken).ConfigureAwait(false); using var request = new HttpRequestMessage(HttpMethod.Get, path); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - using var response = await http.SendAsync(request, cancellationToken); - var body = await response.Content.ReadAsStringAsync(cancellationToken); + using var response = await http.SendAsync(request, cancellationToken).ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); LogProbe(logger, (int)response.StatusCode, path); diff --git a/src/Ftgo.Auth.Client/packages.lock.json b/src/Ftgo.Auth.Client/packages.lock.json index 6b83f36..aa7559c 100644 --- a/src/Ftgo.Auth.Client/packages.lock.json +++ b/src/Ftgo.Auth.Client/packages.lock.json @@ -10,20 +10,20 @@ }, "Azure.Monitor.OpenTelemetry.Exporter": { "type": "Direct", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "fexzK+HM06C3tmBL6DLNVAcgutBMY7hQ7tGcdpCyq6HaXvbNz6cFtenrJUAdXM8Y4G+3QmZ6PIAL1hFrVe4Mpw==", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "JbNUQIK7uZ7Mg2fZzdsjqC/eso3Ag/tMwCqYvGvedyE1fObe6JAwCaCSMZDMrhrAaFn8x8+ZaKJelfcEK5MCcg==", "dependencies": { - "Azure.Core": "1.52.0", - "OpenTelemetry.Extensions.Hosting": "1.15.1", - "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + "Azure.Core": "1.54.0", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.3" } }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.54, )", - "resolved": "3.0.54", - "contentHash": "fybpNsTg4ZT+oiQWkrkTvvy1dowdqjfkefFa/7OIPFPmTkHd9eSEjPCpoK1c5oYdp68QjpLTbeCDpZB2TOUQ4A==" + "requested": "[3.0.58, )", + "resolved": "3.0.58", + "contentHash": "EWprZh8ONB5W4T5lajB9H3/ThznWgEVbb8sWkIZe2ZOCRPPzAdLxkFfM8z57SosJtUYGxPXvxlqGmClxw8mA9g==" }, "Microsoft.Extensions.Hosting": { "type": "Direct", @@ -158,16 +158,20 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.24.0.138807, )", - "resolved": "10.24.0.138807", - "contentHash": "+ZEa1+KhNSulMSatpffgHnovbLGtYL9oukMh8uQp8RBDnPOjEFzvECSa4kfu1D4PJxErksiGOQQ+kDeao1x9jQ==" + "requested": "[10.25.0.139117, )", + "resolved": "10.25.0.139117", + "contentHash": "2iBZIkcgsaUNyIPFqR9ECv5PLbAwV9I/v/PeEiMdkfhWEAnqD7VyNXzukQDlL/D24f5OoUc4MbWF5SgYI7x3CA==" }, "Azure.Core": { "type": "Transitive", - "resolved": "1.52.0", - "contentHash": "If2gP0B4kDAwOw3kvMFs7gEounDhLyeleoWMih0xPdGAvhKpcWQwoPI3L/L0gmcQt0hrtqDnRni1jaIaxwdL7w==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "10.0.3", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.3", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.3", + "Microsoft.Identity.Client": "4.83.1", + "Microsoft.Identity.Client.Extensions.Msal": "4.83.1", "System.ClientModel": "1.10.0", "System.Memory.Data": "10.0.3" } @@ -500,6 +504,20 @@ "Microsoft.Extensions.Options": "10.0.6" } }, + "Microsoft.Identity.Client.Extensions.Msal": { + "type": "Transitive", + "resolved": "4.83.1", + "contentHash": "I3k4J4Hj4KbLEFanjeUzzDOVecukETaTgEkJ7h2pP/Yazs6SLp6TVUTo/Eo+ptPXMwvc+iX7rBFtMSUrA7R+Mg==", + "dependencies": { + "Microsoft.Identity.Client": "4.83.1", + "System.Security.Cryptography.ProtectedData": "4.5.0" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "10.0.203", @@ -526,15 +544,15 @@ }, "OpenTelemetry.PersistentStorage.Abstractions": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + "resolved": "1.0.3", + "contentHash": "7nSE1uLY0KOHRssg1xQydDZJ/s9zECOzFT/gSDIp1KTxbgd1yziuMTsdBKZ/N1BbpUxfH6Pjq5OMd7lYogwReA==" }, "OpenTelemetry.PersistentStorage.FileSystem": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "resolved": "1.0.3", + "contentHash": "3k+yCroJcGBtZIia+0LgzMlBuD0U95BzcWNEGWedusBaRAD8EmkaCPAUwaCtFbb/Mg7HywbIG2zuenANPYFJ0g==", "dependencies": { - "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.3" } }, "Polly.Core": { @@ -587,6 +605,11 @@ "resolved": "10.0.3", "contentHash": "MaGhRfGunmrj/nHjtsi9XkhlYJ/ERGWrbA+BiSKNtGnAjc9XlG5EhAvak6VRcX5LYzPF6pBO8nJ613dTgzabig==" }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q==" + }, "System.Threading.RateLimiting": { "type": "Transitive", "resolved": "8.0.0", @@ -604,6 +627,15 @@ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.7" } }, + "Microsoft.Identity.Client": { + "type": "CentralTransitive", + "requested": "[4.83.3, )", + "resolved": "4.83.3", + "contentHash": "XNJJn5uctuGvl3u6qzAof2TNysAZ/PPVKzkAglxvTO5XHgff/Ibs1+yi2G26Xrf6X/f780kS4fMKKdHcGb96hQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.14.0" + } + }, "OpenTelemetry.Api": { "type": "CentralTransitive", "requested": "[1.15.3, )", diff --git a/src/Ftgo.Auth/EntraAuthRateLimiterExtensions.cs b/src/Ftgo.Auth/EntraAuthRateLimiterExtensions.cs index 062c231..82ac6ff 100644 --- a/src/Ftgo.Auth/EntraAuthRateLimiterExtensions.cs +++ b/src/Ftgo.Auth/EntraAuthRateLimiterExtensions.cs @@ -47,13 +47,16 @@ public static IServiceCollection AddEntraAuthRateLimiter( services.AddRateLimiter(rl => { rl.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - rl.OnRejected = static (ctx, _) => + var fallbackRetryAfterSeconds = ((int)opts.Window.TotalSeconds).ToString(CultureInfo.InvariantCulture); + rl.OnRejected = (ctx, _) => { - if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) - { - ctx.HttpContext.Response.Headers.RetryAfter = - ((int)retryAfter.TotalSeconds).ToString(CultureInfo.InvariantCulture); - } + // RFC 6585 §3 mandates Retry-After on 429. Sliding/fixed-window limiters only + // populate the lease metadata when there's a queue; fall back to the window so + // the header is present unconditionally. + var seconds = ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter) + ? ((int)retryAfter.TotalSeconds).ToString(CultureInfo.InvariantCulture) + : fallbackRetryAfterSeconds; + ctx.HttpContext.Response.Headers.RetryAfter = seconds; return ValueTask.CompletedTask; }; diff --git a/src/Ftgo.Auth/packages.lock.json b/src/Ftgo.Auth/packages.lock.json index ad288c2..e963fe2 100644 --- a/src/Ftgo.Auth/packages.lock.json +++ b/src/Ftgo.Auth/packages.lock.json @@ -10,9 +10,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.54, )", - "resolved": "3.0.54", - "contentHash": "fybpNsTg4ZT+oiQWkrkTvvy1dowdqjfkefFa/7OIPFPmTkHd9eSEjPCpoK1c5oYdp68QjpLTbeCDpZB2TOUQ4A==" + "requested": "[3.0.58, )", + "resolved": "3.0.58", + "contentHash": "EWprZh8ONB5W4T5lajB9H3/ThznWgEVbb8sWkIZe2ZOCRPPzAdLxkFfM8z57SosJtUYGxPXvxlqGmClxw8mA9g==" }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", @@ -25,14 +25,14 @@ }, "Microsoft.Identity.Web": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "rqMufh64Woj/kc39d9iCb12BTtBj9H40haLUemRAwoqvqSomYPlbMOwt1UD1Y5aMqmc8aYF06U5ATY8Qnw2iFg==", - "dependencies": { - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "SshLuFsW0VqCP18O1YUAqPh4KZ6ogu9yYrhlYdGxf/KSRR0wHO1MFlyxh2aOkHdw462XKjbzm36NTspJo1r58g==", + "dependencies": { + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", "Microsoft.IdentityModel.Validators": "8.15.0", "System.IdentityModel.Tokens.Jwt": "8.15.0" @@ -40,11 +40,11 @@ }, "Microsoft.Identity.Web.DownstreamApi": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "MXCBVZO9Oj2hSZkPvEFcRg1/fyKNjqJQzAVxUjdCOzno3rd1BeVhuMddk9aYNJ3r79GD5lA3xJeneXo+SKlKCA==", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "E9RjjTmAiHR7Z8HDtfEDbioW6NOvoYABGlyIhwOPkMpECTP7M69SwO5DJki+QA0pHpIongdUC856GH/lnqyxSA==", "dependencies": { - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0" + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0" } }, "Microsoft.IdentityModel.Validators": { @@ -87,20 +87,20 @@ }, "Scalar.AspNetCore": { "type": "Direct", - "requested": "[2.14.4, )", - "resolved": "2.14.4", - "contentHash": "PyuqLRi7JXyTJm/rh+IWCA4Fct1SlZuWrdYiui5hiaBuSY88+roc4eWKfYaE6zrupBIwnEP8pJLe4ifvU7HtZA==" + "requested": "[2.14.9, )", + "resolved": "2.14.9", + "contentHash": "bDc8NjI2JSIAX0C1+02WO11zCJ1SJQhWdMC0s7yi2Nh2atoNRpNRPiYeyIhWSerkc1SkDdJVOeU9QSY6jb0/zQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.24.0.138807, )", - "resolved": "10.24.0.138807", - "contentHash": "+ZEa1+KhNSulMSatpffgHnovbLGtYL9oukMh8uQp8RBDnPOjEFzvECSa4kfu1D4PJxErksiGOQQ+kDeao1x9jQ==" + "requested": "[10.25.0.139117, )", + "resolved": "10.25.0.139117", + "contentHash": "2iBZIkcgsaUNyIPFqR9ECv5PLbAwV9I/v/PeEiMdkfhWEAnqD7VyNXzukQDlL/D24f5OoUc4MbWF5SgYI7x3CA==" }, "Azure.Core": { "type": "Transitive", - "resolved": "1.53.0", - "contentHash": "x9c/toFMOtRrlTdFuE7rlGCVAduQzWVfKmLz5juj41zJAXEhYD5hluiUyyAEzJ6OxpBnKtiaBztzwpZITAVjtg==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "10.0.3", "Microsoft.Identity.Client": "4.83.1", @@ -198,21 +198,21 @@ }, "Microsoft.Identity.Web.Certificate": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "G4nkrs1pKg6NxuSvyoRzokLfsfd2v5pXpXS1XUvHstvdWkQRBw8kTbSwRCvzdRFA1MW7Ct14zcp1P4kej7dB+g==", + "resolved": "4.9.0", + "contentHash": "kr6ZpxNbWm1+eI3pn6sdgIlduYZiEmoC0TbiiR1gmcREuI87E963xjBMmvXf9+CnHcjbsqhkBcK4huKauif0yA==", "dependencies": { - "Azure.Identity": "1.11.4", + "Azure.Identity": "1.17.2", "Azure.Security.KeyVault.Certificates": "4.6.0", "Azure.Security.KeyVault.Secrets": "4.6.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.Diagnostics": "4.8.0" + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.Diagnostics": "4.9.0" } }, "Microsoft.Identity.Web.Certificateless": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "TegoXg/SX1mca1ygTZWTgLTXIZPQudmg5SLr9f50A8fzC4oFGkNOUfa9wii9SRikBGg4Sc31QxUeM/MMFyIX6A==", + "resolved": "4.9.0", + "contentHash": "hlt8KV1V0JLjyxncmSNAugyqxVr65wQlzcsks6TgqvUyllkQvWAxqgM+Dh6OB8sgJWMoCOmJuKJ6SejAgYjJ/w==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", "Microsoft.IdentityModel.JsonWebTokens": "8.15.0" @@ -220,20 +220,20 @@ }, "Microsoft.Identity.Web.Diagnostics": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "oWYVlq2h86yAmqJHcDK0JrJfhDZTnF1M1vyY9mGx+x5E5GpNml7hTI1N1Kq4Z9JUtaPvz4dN8MgfUn7QyHZ13Q==" + "resolved": "4.9.0", + "contentHash": "5x4Tyg1xFr8vgTAuos5OyCNAXcnq9U+6QWA3pRLOgUF5Am7Bn/CM+rmOjffoyfP6ymQdQ0/zxtNOtyRjTlpiBg==" }, "Microsoft.Identity.Web.TokenAcquisition": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "Nm4YJxUQuIThoRWprg9bv2vCDs2zzlOo7B4nfHoG3Spw5Ege2edFLY+iEtRh+GYW2Fhn97rYmwCgmaQ2IhYZxA==", + "resolved": "4.9.0", + "contentHash": "ap6EFz9Nn3pWlfYQ8AqnRM7UsrH5pAXzDMF/aayKEfcAxLgXe23XKb1jBEzWNYkg5NNrbiLVWjKDIWbPqMrn1w==", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "10.0.0", "Microsoft.AspNetCore.Authentication.OpenIdConnect": "10.0.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Logging": "8.15.0", "Microsoft.IdentityModel.LoggingExtensions": "8.15.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", @@ -242,12 +242,12 @@ }, "Microsoft.Identity.Web.TokenCache": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "2TL0zev2SREJoIzZi9dQ+vv/Us6Q1ZpZD02ti0LhFDpFg/vKGD98pHmcz+f216PbWHzQpERzS3tMgcIGt2lywQ==", + "resolved": "4.9.0", + "contentHash": "3eHtmFu6HJU334bLyOjG/PvWyYQaoGEh9HyomGQ1p/aUV4EiGkdiU4R5yuyOIHdxRs8DKMFPN0Wnqp2nvWayjg==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", - "Microsoft.Identity.Web.Diagnostics": "4.8.0", - "System.Security.Cryptography.Pkcs": "10.0.6" + "Microsoft.Identity.Web.Diagnostics": "4.9.0", + "System.Security.Cryptography.Pkcs": "10.0.7" } }, "Microsoft.IdentityModel.Abstractions": { @@ -316,15 +316,15 @@ }, "OpenTelemetry.PersistentStorage.Abstractions": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + "resolved": "1.0.3", + "contentHash": "7nSE1uLY0KOHRssg1xQydDZJ/s9zECOzFT/gSDIp1KTxbgd1yziuMTsdBKZ/N1BbpUxfH6Pjq5OMd7lYogwReA==" }, "OpenTelemetry.PersistentStorage.FileSystem": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "resolved": "1.0.3", + "contentHash": "3k+yCroJcGBtZIia+0LgzMlBuD0U95BzcWNEGWedusBaRAD8EmkaCPAUwaCtFbb/Mg7HywbIG2zuenANPYFJ0g==", "dependencies": { - "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.3" } }, "Polly.Core": { @@ -377,8 +377,8 @@ }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "10.0.6", - "contentHash": "88tquaGJ1htm4DHWS6x9jwER7sFET2SVRN7HqO1FYZwE0diDcUmz0ajhVa8ZD2HGhDJBueSPjP/gqyP3gXtT2A==" + "resolved": "10.0.7", + "contentHash": "dbdKfF3eA5l+CXiAbDxiCxdezoxeanbue1ck8m49ih1L9uZG6ry8Ul8On6vpragyMDJJP4rQHUY/SWgk66tCYA==" }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", @@ -388,7 +388,7 @@ "ftgo.auth.client": { "type": "Project", "dependencies": { - "Azure.Monitor.OpenTelemetry.Exporter": "[1.7.0, )", + "Azure.Monitor.OpenTelemetry.Exporter": "[1.8.0, )", "Microsoft.Extensions.Http.Resilience": "[10.5.0, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", @@ -407,13 +407,13 @@ }, "Azure.Monitor.OpenTelemetry.Exporter": { "type": "CentralTransitive", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "fexzK+HM06C3tmBL6DLNVAcgutBMY7hQ7tGcdpCyq6HaXvbNz6cFtenrJUAdXM8Y4G+3QmZ6PIAL1hFrVe4Mpw==", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "JbNUQIK7uZ7Mg2fZzdsjqC/eso3Ag/tMwCqYvGvedyE1fObe6JAwCaCSMZDMrhrAaFn8x8+ZaKJelfcEK5MCcg==", "dependencies": { - "Azure.Core": "1.52.0", - "OpenTelemetry.Extensions.Hosting": "1.15.1", - "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + "Azure.Core": "1.54.0", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.3" } }, "Azure.Security.KeyVault.Certificates": { diff --git a/src/Ftgo.Kitchen.Worker/packages.lock.json b/src/Ftgo.Kitchen.Worker/packages.lock.json index 8faead8..cbd9fe9 100644 --- a/src/Ftgo.Kitchen.Worker/packages.lock.json +++ b/src/Ftgo.Kitchen.Worker/packages.lock.json @@ -19,9 +19,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.54, )", - "resolved": "3.0.54", - "contentHash": "fybpNsTg4ZT+oiQWkrkTvvy1dowdqjfkefFa/7OIPFPmTkHd9eSEjPCpoK1c5oYdp68QjpLTbeCDpZB2TOUQ4A==" + "requested": "[3.0.58, )", + "resolved": "3.0.58", + "contentHash": "EWprZh8ONB5W4T5lajB9H3/ThznWgEVbb8sWkIZe2ZOCRPPzAdLxkFfM8z57SosJtUYGxPXvxlqGmClxw8mA9g==" }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", @@ -93,14 +93,14 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.24.0.138807, )", - "resolved": "10.24.0.138807", - "contentHash": "+ZEa1+KhNSulMSatpffgHnovbLGtYL9oukMh8uQp8RBDnPOjEFzvECSa4kfu1D4PJxErksiGOQQ+kDeao1x9jQ==" + "requested": "[10.25.0.139117, )", + "resolved": "10.25.0.139117", + "contentHash": "2iBZIkcgsaUNyIPFqR9ECv5PLbAwV9I/v/PeEiMdkfhWEAnqD7VyNXzukQDlL/D24f5OoUc4MbWF5SgYI7x3CA==" }, "Azure.Core": { "type": "Transitive", - "resolved": "1.53.0", - "contentHash": "x9c/toFMOtRrlTdFuE7rlGCVAduQzWVfKmLz5juj41zJAXEhYD5hluiUyyAEzJ6OxpBnKtiaBztzwpZITAVjtg==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "10.0.3", "Microsoft.Extensions.Configuration.Abstractions": "10.0.3", @@ -479,15 +479,15 @@ }, "OpenTelemetry.PersistentStorage.Abstractions": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + "resolved": "1.0.3", + "contentHash": "7nSE1uLY0KOHRssg1xQydDZJ/s9zECOzFT/gSDIp1KTxbgd1yziuMTsdBKZ/N1BbpUxfH6Pjq5OMd7lYogwReA==" }, "OpenTelemetry.PersistentStorage.FileSystem": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "resolved": "1.0.3", + "contentHash": "3k+yCroJcGBtZIia+0LgzMlBuD0U95BzcWNEGWedusBaRAD8EmkaCPAUwaCtFbb/Mg7HywbIG2zuenANPYFJ0g==", "dependencies": { - "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.3" } }, "Polly.Core": { @@ -553,7 +553,7 @@ "ftgo.auth.client": { "type": "Project", "dependencies": { - "Azure.Monitor.OpenTelemetry.Exporter": "[1.7.0, )", + "Azure.Monitor.OpenTelemetry.Exporter": "[1.8.0, )", "Microsoft.Extensions.Hosting": "[10.0.7, )", "Microsoft.Extensions.Http": "[10.0.7, )", "Microsoft.Extensions.Http.Resilience": "[10.5.0, )", @@ -567,13 +567,13 @@ }, "Azure.Monitor.OpenTelemetry.Exporter": { "type": "CentralTransitive", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "fexzK+HM06C3tmBL6DLNVAcgutBMY7hQ7tGcdpCyq6HaXvbNz6cFtenrJUAdXM8Y4G+3QmZ6PIAL1hFrVe4Mpw==", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "JbNUQIK7uZ7Mg2fZzdsjqC/eso3Ag/tMwCqYvGvedyE1fObe6JAwCaCSMZDMrhrAaFn8x8+ZaKJelfcEK5MCcg==", "dependencies": { - "Azure.Core": "1.52.0", - "OpenTelemetry.Extensions.Hosting": "1.15.1", - "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + "Azure.Core": "1.54.0", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.3" } }, "Microsoft.Extensions.Http": { diff --git a/src/Ftgo.Orders.Api/packages.lock.json b/src/Ftgo.Orders.Api/packages.lock.json index 2eb022d..ee44b37 100644 --- a/src/Ftgo.Orders.Api/packages.lock.json +++ b/src/Ftgo.Orders.Api/packages.lock.json @@ -10,9 +10,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.54, )", - "resolved": "3.0.54", - "contentHash": "fybpNsTg4ZT+oiQWkrkTvvy1dowdqjfkefFa/7OIPFPmTkHd9eSEjPCpoK1c5oYdp68QjpLTbeCDpZB2TOUQ4A==" + "requested": "[3.0.58, )", + "resolved": "3.0.58", + "contentHash": "EWprZh8ONB5W4T5lajB9H3/ThznWgEVbb8sWkIZe2ZOCRPPzAdLxkFfM8z57SosJtUYGxPXvxlqGmClxw8mA9g==" }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", @@ -25,14 +25,14 @@ }, "Microsoft.Identity.Web": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "rqMufh64Woj/kc39d9iCb12BTtBj9H40haLUemRAwoqvqSomYPlbMOwt1UD1Y5aMqmc8aYF06U5ATY8Qnw2iFg==", - "dependencies": { - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "SshLuFsW0VqCP18O1YUAqPh4KZ6ogu9yYrhlYdGxf/KSRR0wHO1MFlyxh2aOkHdw462XKjbzm36NTspJo1r58g==", + "dependencies": { + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", "Microsoft.IdentityModel.Validators": "8.15.0", "System.IdentityModel.Tokens.Jwt": "8.15.0" @@ -40,11 +40,11 @@ }, "Microsoft.Identity.Web.DownstreamApi": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "MXCBVZO9Oj2hSZkPvEFcRg1/fyKNjqJQzAVxUjdCOzno3rd1BeVhuMddk9aYNJ3r79GD5lA3xJeneXo+SKlKCA==", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "E9RjjTmAiHR7Z8HDtfEDbioW6NOvoYABGlyIhwOPkMpECTP7M69SwO5DJki+QA0pHpIongdUC856GH/lnqyxSA==", "dependencies": { - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0" + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0" } }, "Microsoft.SourceLink.GitHub": { @@ -66,14 +66,14 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.24.0.138807, )", - "resolved": "10.24.0.138807", - "contentHash": "+ZEa1+KhNSulMSatpffgHnovbLGtYL9oukMh8uQp8RBDnPOjEFzvECSa4kfu1D4PJxErksiGOQQ+kDeao1x9jQ==" + "requested": "[10.25.0.139117, )", + "resolved": "10.25.0.139117", + "contentHash": "2iBZIkcgsaUNyIPFqR9ECv5PLbAwV9I/v/PeEiMdkfhWEAnqD7VyNXzukQDlL/D24f5OoUc4MbWF5SgYI7x3CA==" }, "Azure.Core": { "type": "Transitive", - "resolved": "1.53.0", - "contentHash": "x9c/toFMOtRrlTdFuE7rlGCVAduQzWVfKmLz5juj41zJAXEhYD5hluiUyyAEzJ6OxpBnKtiaBztzwpZITAVjtg==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "10.0.3", "Microsoft.Identity.Client": "4.83.1", @@ -171,21 +171,21 @@ }, "Microsoft.Identity.Web.Certificate": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "G4nkrs1pKg6NxuSvyoRzokLfsfd2v5pXpXS1XUvHstvdWkQRBw8kTbSwRCvzdRFA1MW7Ct14zcp1P4kej7dB+g==", + "resolved": "4.9.0", + "contentHash": "kr6ZpxNbWm1+eI3pn6sdgIlduYZiEmoC0TbiiR1gmcREuI87E963xjBMmvXf9+CnHcjbsqhkBcK4huKauif0yA==", "dependencies": { - "Azure.Identity": "1.11.4", + "Azure.Identity": "1.17.2", "Azure.Security.KeyVault.Certificates": "4.6.0", "Azure.Security.KeyVault.Secrets": "4.6.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.Diagnostics": "4.8.0" + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.Diagnostics": "4.9.0" } }, "Microsoft.Identity.Web.Certificateless": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "TegoXg/SX1mca1ygTZWTgLTXIZPQudmg5SLr9f50A8fzC4oFGkNOUfa9wii9SRikBGg4Sc31QxUeM/MMFyIX6A==", + "resolved": "4.9.0", + "contentHash": "hlt8KV1V0JLjyxncmSNAugyqxVr65wQlzcsks6TgqvUyllkQvWAxqgM+Dh6OB8sgJWMoCOmJuKJ6SejAgYjJ/w==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", "Microsoft.IdentityModel.JsonWebTokens": "8.15.0" @@ -193,20 +193,20 @@ }, "Microsoft.Identity.Web.Diagnostics": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "oWYVlq2h86yAmqJHcDK0JrJfhDZTnF1M1vyY9mGx+x5E5GpNml7hTI1N1Kq4Z9JUtaPvz4dN8MgfUn7QyHZ13Q==" + "resolved": "4.9.0", + "contentHash": "5x4Tyg1xFr8vgTAuos5OyCNAXcnq9U+6QWA3pRLOgUF5Am7Bn/CM+rmOjffoyfP6ymQdQ0/zxtNOtyRjTlpiBg==" }, "Microsoft.Identity.Web.TokenAcquisition": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "Nm4YJxUQuIThoRWprg9bv2vCDs2zzlOo7B4nfHoG3Spw5Ege2edFLY+iEtRh+GYW2Fhn97rYmwCgmaQ2IhYZxA==", + "resolved": "4.9.0", + "contentHash": "ap6EFz9Nn3pWlfYQ8AqnRM7UsrH5pAXzDMF/aayKEfcAxLgXe23XKb1jBEzWNYkg5NNrbiLVWjKDIWbPqMrn1w==", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "10.0.0", "Microsoft.AspNetCore.Authentication.OpenIdConnect": "10.0.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Logging": "8.15.0", "Microsoft.IdentityModel.LoggingExtensions": "8.15.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", @@ -215,12 +215,12 @@ }, "Microsoft.Identity.Web.TokenCache": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "2TL0zev2SREJoIzZi9dQ+vv/Us6Q1ZpZD02ti0LhFDpFg/vKGD98pHmcz+f216PbWHzQpERzS3tMgcIGt2lywQ==", + "resolved": "4.9.0", + "contentHash": "3eHtmFu6HJU334bLyOjG/PvWyYQaoGEh9HyomGQ1p/aUV4EiGkdiU4R5yuyOIHdxRs8DKMFPN0Wnqp2nvWayjg==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", - "Microsoft.Identity.Web.Diagnostics": "4.8.0", - "System.Security.Cryptography.Pkcs": "10.0.6" + "Microsoft.Identity.Web.Diagnostics": "4.9.0", + "System.Security.Cryptography.Pkcs": "10.0.7" } }, "Microsoft.IdentityModel.Abstractions": { @@ -289,15 +289,15 @@ }, "OpenTelemetry.PersistentStorage.Abstractions": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + "resolved": "1.0.3", + "contentHash": "7nSE1uLY0KOHRssg1xQydDZJ/s9zECOzFT/gSDIp1KTxbgd1yziuMTsdBKZ/N1BbpUxfH6Pjq5OMd7lYogwReA==" }, "OpenTelemetry.PersistentStorage.FileSystem": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "resolved": "1.0.3", + "contentHash": "3k+yCroJcGBtZIia+0LgzMlBuD0U95BzcWNEGWedusBaRAD8EmkaCPAUwaCtFbb/Mg7HywbIG2zuenANPYFJ0g==", "dependencies": { - "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.3" } }, "Polly.Core": { @@ -350,8 +350,8 @@ }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "10.0.6", - "contentHash": "88tquaGJ1htm4DHWS6x9jwER7sFET2SVRN7HqO1FYZwE0diDcUmz0ajhVa8ZD2HGhDJBueSPjP/gqyP3gXtT2A==" + "resolved": "10.0.7", + "contentHash": "dbdKfF3eA5l+CXiAbDxiCxdezoxeanbue1ck8m49ih1L9uZG6ry8Ul8On6vpragyMDJJP4rQHUY/SWgk66tCYA==" }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", @@ -363,17 +363,17 @@ "dependencies": { "Ftgo.Auth.Client": "[1.0.0, )", "Microsoft.AspNetCore.OpenApi": "[10.0.7, )", - "Microsoft.Identity.Web": "[4.8.0, )", - "Microsoft.Identity.Web.DownstreamApi": "[4.8.0, )", + "Microsoft.Identity.Web": "[4.9.0, )", + "Microsoft.Identity.Web.DownstreamApi": "[4.9.0, )", "Microsoft.IdentityModel.Validators": "[8.17.0, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )", - "Scalar.AspNetCore": "[2.14.4, )" + "Scalar.AspNetCore": "[2.14.9, )" } }, "ftgo.auth.client": { "type": "Project", "dependencies": { - "Azure.Monitor.OpenTelemetry.Exporter": "[1.7.0, )", + "Azure.Monitor.OpenTelemetry.Exporter": "[1.8.0, )", "Microsoft.Extensions.Http.Resilience": "[10.5.0, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", @@ -392,13 +392,13 @@ }, "Azure.Monitor.OpenTelemetry.Exporter": { "type": "CentralTransitive", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "fexzK+HM06C3tmBL6DLNVAcgutBMY7hQ7tGcdpCyq6HaXvbNz6cFtenrJUAdXM8Y4G+3QmZ6PIAL1hFrVe4Mpw==", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "JbNUQIK7uZ7Mg2fZzdsjqC/eso3Ag/tMwCqYvGvedyE1fObe6JAwCaCSMZDMrhrAaFn8x8+ZaKJelfcEK5MCcg==", "dependencies": { - "Azure.Core": "1.52.0", - "OpenTelemetry.Extensions.Hosting": "1.15.1", - "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + "Azure.Core": "1.54.0", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.3" } }, "Azure.Security.KeyVault.Certificates": { @@ -536,9 +536,9 @@ }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.14.4, )", - "resolved": "2.14.4", - "contentHash": "PyuqLRi7JXyTJm/rh+IWCA4Fct1SlZuWrdYiui5hiaBuSY88+roc4eWKfYaE6zrupBIwnEP8pJLe4ifvU7HtZA==" + "requested": "[2.14.9, )", + "resolved": "2.14.9", + "contentHash": "bDc8NjI2JSIAX0C1+02WO11zCJ1SJQhWdMC0s7yi2Nh2atoNRpNRPiYeyIhWSerkc1SkDdJVOeU9QSY6jb0/zQ==" } } } diff --git a/src/Ftgo.Restaurants.Api/packages.lock.json b/src/Ftgo.Restaurants.Api/packages.lock.json index 2eb022d..ee44b37 100644 --- a/src/Ftgo.Restaurants.Api/packages.lock.json +++ b/src/Ftgo.Restaurants.Api/packages.lock.json @@ -10,9 +10,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.54, )", - "resolved": "3.0.54", - "contentHash": "fybpNsTg4ZT+oiQWkrkTvvy1dowdqjfkefFa/7OIPFPmTkHd9eSEjPCpoK1c5oYdp68QjpLTbeCDpZB2TOUQ4A==" + "requested": "[3.0.58, )", + "resolved": "3.0.58", + "contentHash": "EWprZh8ONB5W4T5lajB9H3/ThznWgEVbb8sWkIZe2ZOCRPPzAdLxkFfM8z57SosJtUYGxPXvxlqGmClxw8mA9g==" }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", @@ -25,14 +25,14 @@ }, "Microsoft.Identity.Web": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "rqMufh64Woj/kc39d9iCb12BTtBj9H40haLUemRAwoqvqSomYPlbMOwt1UD1Y5aMqmc8aYF06U5ATY8Qnw2iFg==", - "dependencies": { - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "SshLuFsW0VqCP18O1YUAqPh4KZ6ogu9yYrhlYdGxf/KSRR0wHO1MFlyxh2aOkHdw462XKjbzm36NTspJo1r58g==", + "dependencies": { + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", "Microsoft.IdentityModel.Validators": "8.15.0", "System.IdentityModel.Tokens.Jwt": "8.15.0" @@ -40,11 +40,11 @@ }, "Microsoft.Identity.Web.DownstreamApi": { "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "MXCBVZO9Oj2hSZkPvEFcRg1/fyKNjqJQzAVxUjdCOzno3rd1BeVhuMddk9aYNJ3r79GD5lA3xJeneXo+SKlKCA==", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "E9RjjTmAiHR7Z8HDtfEDbioW6NOvoYABGlyIhwOPkMpECTP7M69SwO5DJki+QA0pHpIongdUC856GH/lnqyxSA==", "dependencies": { - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0" + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0" } }, "Microsoft.SourceLink.GitHub": { @@ -66,14 +66,14 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.24.0.138807, )", - "resolved": "10.24.0.138807", - "contentHash": "+ZEa1+KhNSulMSatpffgHnovbLGtYL9oukMh8uQp8RBDnPOjEFzvECSa4kfu1D4PJxErksiGOQQ+kDeao1x9jQ==" + "requested": "[10.25.0.139117, )", + "resolved": "10.25.0.139117", + "contentHash": "2iBZIkcgsaUNyIPFqR9ECv5PLbAwV9I/v/PeEiMdkfhWEAnqD7VyNXzukQDlL/D24f5OoUc4MbWF5SgYI7x3CA==" }, "Azure.Core": { "type": "Transitive", - "resolved": "1.53.0", - "contentHash": "x9c/toFMOtRrlTdFuE7rlGCVAduQzWVfKmLz5juj41zJAXEhYD5hluiUyyAEzJ6OxpBnKtiaBztzwpZITAVjtg==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "10.0.3", "Microsoft.Identity.Client": "4.83.1", @@ -171,21 +171,21 @@ }, "Microsoft.Identity.Web.Certificate": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "G4nkrs1pKg6NxuSvyoRzokLfsfd2v5pXpXS1XUvHstvdWkQRBw8kTbSwRCvzdRFA1MW7Ct14zcp1P4kej7dB+g==", + "resolved": "4.9.0", + "contentHash": "kr6ZpxNbWm1+eI3pn6sdgIlduYZiEmoC0TbiiR1gmcREuI87E963xjBMmvXf9+CnHcjbsqhkBcK4huKauif0yA==", "dependencies": { - "Azure.Identity": "1.11.4", + "Azure.Identity": "1.17.2", "Azure.Security.KeyVault.Certificates": "4.6.0", "Azure.Security.KeyVault.Secrets": "4.6.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.Diagnostics": "4.8.0" + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.Diagnostics": "4.9.0" } }, "Microsoft.Identity.Web.Certificateless": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "TegoXg/SX1mca1ygTZWTgLTXIZPQudmg5SLr9f50A8fzC4oFGkNOUfa9wii9SRikBGg4Sc31QxUeM/MMFyIX6A==", + "resolved": "4.9.0", + "contentHash": "hlt8KV1V0JLjyxncmSNAugyqxVr65wQlzcsks6TgqvUyllkQvWAxqgM+Dh6OB8sgJWMoCOmJuKJ6SejAgYjJ/w==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", "Microsoft.IdentityModel.JsonWebTokens": "8.15.0" @@ -193,20 +193,20 @@ }, "Microsoft.Identity.Web.Diagnostics": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "oWYVlq2h86yAmqJHcDK0JrJfhDZTnF1M1vyY9mGx+x5E5GpNml7hTI1N1Kq4Z9JUtaPvz4dN8MgfUn7QyHZ13Q==" + "resolved": "4.9.0", + "contentHash": "5x4Tyg1xFr8vgTAuos5OyCNAXcnq9U+6QWA3pRLOgUF5Am7Bn/CM+rmOjffoyfP6ymQdQ0/zxtNOtyRjTlpiBg==" }, "Microsoft.Identity.Web.TokenAcquisition": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "Nm4YJxUQuIThoRWprg9bv2vCDs2zzlOo7B4nfHoG3Spw5Ege2edFLY+iEtRh+GYW2Fhn97rYmwCgmaQ2IhYZxA==", + "resolved": "4.9.0", + "contentHash": "ap6EFz9Nn3pWlfYQ8AqnRM7UsrH5pAXzDMF/aayKEfcAxLgXe23XKb1jBEzWNYkg5NNrbiLVWjKDIWbPqMrn1w==", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "10.0.0", "Microsoft.AspNetCore.Authentication.OpenIdConnect": "10.0.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Logging": "8.15.0", "Microsoft.IdentityModel.LoggingExtensions": "8.15.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", @@ -215,12 +215,12 @@ }, "Microsoft.Identity.Web.TokenCache": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "2TL0zev2SREJoIzZi9dQ+vv/Us6Q1ZpZD02ti0LhFDpFg/vKGD98pHmcz+f216PbWHzQpERzS3tMgcIGt2lywQ==", + "resolved": "4.9.0", + "contentHash": "3eHtmFu6HJU334bLyOjG/PvWyYQaoGEh9HyomGQ1p/aUV4EiGkdiU4R5yuyOIHdxRs8DKMFPN0Wnqp2nvWayjg==", "dependencies": { "Microsoft.Identity.Client": "4.83.1", - "Microsoft.Identity.Web.Diagnostics": "4.8.0", - "System.Security.Cryptography.Pkcs": "10.0.6" + "Microsoft.Identity.Web.Diagnostics": "4.9.0", + "System.Security.Cryptography.Pkcs": "10.0.7" } }, "Microsoft.IdentityModel.Abstractions": { @@ -289,15 +289,15 @@ }, "OpenTelemetry.PersistentStorage.Abstractions": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + "resolved": "1.0.3", + "contentHash": "7nSE1uLY0KOHRssg1xQydDZJ/s9zECOzFT/gSDIp1KTxbgd1yziuMTsdBKZ/N1BbpUxfH6Pjq5OMd7lYogwReA==" }, "OpenTelemetry.PersistentStorage.FileSystem": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "resolved": "1.0.3", + "contentHash": "3k+yCroJcGBtZIia+0LgzMlBuD0U95BzcWNEGWedusBaRAD8EmkaCPAUwaCtFbb/Mg7HywbIG2zuenANPYFJ0g==", "dependencies": { - "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.3" } }, "Polly.Core": { @@ -350,8 +350,8 @@ }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "10.0.6", - "contentHash": "88tquaGJ1htm4DHWS6x9jwER7sFET2SVRN7HqO1FYZwE0diDcUmz0ajhVa8ZD2HGhDJBueSPjP/gqyP3gXtT2A==" + "resolved": "10.0.7", + "contentHash": "dbdKfF3eA5l+CXiAbDxiCxdezoxeanbue1ck8m49ih1L9uZG6ry8Ul8On6vpragyMDJJP4rQHUY/SWgk66tCYA==" }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", @@ -363,17 +363,17 @@ "dependencies": { "Ftgo.Auth.Client": "[1.0.0, )", "Microsoft.AspNetCore.OpenApi": "[10.0.7, )", - "Microsoft.Identity.Web": "[4.8.0, )", - "Microsoft.Identity.Web.DownstreamApi": "[4.8.0, )", + "Microsoft.Identity.Web": "[4.9.0, )", + "Microsoft.Identity.Web.DownstreamApi": "[4.9.0, )", "Microsoft.IdentityModel.Validators": "[8.17.0, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )", - "Scalar.AspNetCore": "[2.14.4, )" + "Scalar.AspNetCore": "[2.14.9, )" } }, "ftgo.auth.client": { "type": "Project", "dependencies": { - "Azure.Monitor.OpenTelemetry.Exporter": "[1.7.0, )", + "Azure.Monitor.OpenTelemetry.Exporter": "[1.8.0, )", "Microsoft.Extensions.Http.Resilience": "[10.5.0, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", @@ -392,13 +392,13 @@ }, "Azure.Monitor.OpenTelemetry.Exporter": { "type": "CentralTransitive", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "fexzK+HM06C3tmBL6DLNVAcgutBMY7hQ7tGcdpCyq6HaXvbNz6cFtenrJUAdXM8Y4G+3QmZ6PIAL1hFrVe4Mpw==", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "JbNUQIK7uZ7Mg2fZzdsjqC/eso3Ag/tMwCqYvGvedyE1fObe6JAwCaCSMZDMrhrAaFn8x8+ZaKJelfcEK5MCcg==", "dependencies": { - "Azure.Core": "1.52.0", - "OpenTelemetry.Extensions.Hosting": "1.15.1", - "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + "Azure.Core": "1.54.0", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.3" } }, "Azure.Security.KeyVault.Certificates": { @@ -536,9 +536,9 @@ }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.14.4, )", - "resolved": "2.14.4", - "contentHash": "PyuqLRi7JXyTJm/rh+IWCA4Fct1SlZuWrdYiui5hiaBuSY88+roc4eWKfYaE6zrupBIwnEP8pJLe4ifvU7HtZA==" + "requested": "[2.14.9, )", + "resolved": "2.14.9", + "contentHash": "bDc8NjI2JSIAX0C1+02WO11zCJ1SJQhWdMC0s7yi2Nh2atoNRpNRPiYeyIhWSerkc1SkDdJVOeU9QSY6jb0/zQ==" } } } diff --git a/tests/Ftgo.Auth.Tests/DownstreamApiClientClaimsChallengeTests.cs b/tests/Ftgo.Auth.Tests/DownstreamApiClientClaimsChallengeTests.cs index b457c9e..d2890fd 100644 --- a/tests/Ftgo.Auth.Tests/DownstreamApiClientClaimsChallengeTests.cs +++ b/tests/Ftgo.Auth.Tests/DownstreamApiClientClaimsChallengeTests.cs @@ -89,4 +89,60 @@ public async Task ProbeAsync_ReturnsBody_OnSuccess() status.ShouldBe(200); body.ShouldBe("ok"); } + + [Theory] + [InlineData("error=\"insufficient_claims\", claims=\"abc\", error_description=\"step-up\"", "abc")] + [InlineData("error=insufficient_claims, claims=abc, error_description=\"step-up\"", "abc")] + [InlineData("CLAIMS=\"abc\"", "abc")] + [InlineData("Claims=\"abc\", error=\"insufficient_claims\"", "abc")] + [InlineData("error=\"x\", claims=\"value, with, commas\"", "value, with, commas")] + public async Task ProbeAsync_Parses_Claims_From_Various_HeaderShapes(string parameter, string expected) + { + var resp = new HttpResponseMessage(HttpStatusCode.Unauthorized); + resp.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Bearer", parameter)); + + var client = BuildClient(resp); + + var ex = await Should.ThrowAsync(async () => + await client.ProbeAsync("/probe", "scope", TestContext.Current.CancellationToken)); + ex.Claims.ShouldBe(expected); + } + + [Fact] + public async Task ProbeAsync_DoesNotThrow_When_Bearer_Has_No_Parameter() + { + var resp = new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("nope") }; + resp.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Bearer")); + + var client = BuildClient(resp); + var (status, _) = await client.ProbeAsync("/probe", "scope", TestContext.Current.CancellationToken); + + status.ShouldBe(401); + } + + [Fact] + public async Task ProbeAsync_FirstWins_When_Multiple_Bearer_Challenges() + { + var resp = new HttpResponseMessage(HttpStatusCode.Unauthorized); + resp.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Bearer", "error=\"insufficient_claims\", claims=\"first\"")); + resp.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Bearer", "error=\"insufficient_claims\", claims=\"second\"")); + + var client = BuildClient(resp); + + var ex = await Should.ThrowAsync(async () => + await client.ProbeAsync("/probe", "scope", TestContext.Current.CancellationToken)); + ex.Claims.ShouldBe("first"); + } + + [Fact] + public async Task ProbeAsync_DoesNotThrow_When_Scheme_Is_Not_Bearer() + { + var resp = new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("basic-realm") }; + resp.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Basic", "realm=\"test\"")); + + var client = BuildClient(resp); + var (status, _) = await client.ProbeAsync("/probe", "scope", TestContext.Current.CancellationToken); + + status.ShouldBe(401); + } } diff --git a/tests/Ftgo.Auth.Tests/EntraAuthForwardedHeadersExtensionsTests.cs b/tests/Ftgo.Auth.Tests/EntraAuthForwardedHeadersExtensionsTests.cs new file mode 100644 index 0000000..f0ab5f9 --- /dev/null +++ b/tests/Ftgo.Auth.Tests/EntraAuthForwardedHeadersExtensionsTests.cs @@ -0,0 +1,107 @@ +using Ftgo.Auth; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Shouldly; +using Xunit; + +namespace Ftgo.Auth.Tests; + +public sealed class EntraAuthForwardedHeadersExtensionsTests +{ + [Fact] + public void Honours_XForwardedFor_Proto_And_Host() + { + var options = ConfigureAndResolve(); + + options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedFor).ShouldBeTrue(); + options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedProto).ShouldBeTrue(); + options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedHost).ShouldBeTrue(); + } + + [Fact] + public void Caps_ForwardLimit_To_2_To_Limit_TunnelDepth() + { + var options = ConfigureAndResolve(); + options.ForwardLimit.ShouldBe(2); + } + + [Fact] + public async Task Middleware_Updates_RemoteIp_From_XForwardedFor() + { + using var host = await BuildHost(); + var client = host.GetTestClient(); + + var req = new HttpRequestMessage(HttpMethod.Get, "/probe"); + req.Headers.Add("X-Forwarded-For", "203.0.113.42"); + req.Headers.Add("X-Forwarded-Proto", "https"); + req.Headers.Add("X-Forwarded-Host", "api.contoso.com"); + + var response = await client.SendAsync(req, TestContext.Current.CancellationToken); + + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + body.ShouldContain("ip=203.0.113.42"); + body.ShouldContain("scheme=https"); + body.ShouldContain("host=api.contoso.com"); + } + + [Fact] + public async Task Middleware_Honours_ForwardLimit_When_Chain_Too_Long() + { + using var host = await BuildHost(); + var client = host.GetTestClient(); + + var req = new HttpRequestMessage(HttpMethod.Get, "/probe"); + // 3 entries; ForwardLimit=2 means the middleware processes only the last 2 (rightmost). + // The resulting RemoteIp is the deepest *trusted* hop, which is the 2nd-from-right. + req.Headers.Add("X-Forwarded-For", "198.51.100.1, 198.51.100.2, 203.0.113.42"); + + var response = await client.SendAsync(req, TestContext.Current.CancellationToken); + + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + // 198.51.100.1 (the first / least-trusted entry) MUST NOT win — that would be the bug + // ForwardLimit guards against. Either of the other two is acceptable per ForwardedHeaders semantics. + body.ShouldNotContain("ip=198.51.100.1"); + } + + private static ForwardedHeadersOptions ConfigureAndResolve() + { + var services = new ServiceCollection(); + services.AddEntraAuthForwardedHeaders(); + var sp = services.BuildServiceProvider(); + return sp.GetRequiredService>().Value; + } + + private static async Task BuildHost() + { + var host = await new HostBuilder() + .ConfigureWebHost(web => web + .UseTestServer() + .ConfigureServices(services => + { + services.AddEntraAuthForwardedHeaders(); + services.PostConfigure(o => + { + o.KnownIPNetworks.Clear(); + o.KnownProxies.Clear(); + }); + }) + .Configure(app => + { + app.UseForwardedHeaders(); + app.Run(async ctx => + { + var ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + await ctx.Response.WriteAsync( + $"ip={ip} scheme={ctx.Request.Scheme} host={ctx.Request.Host}"); + }); + })) + .StartAsync(TestContext.Current.CancellationToken); + return host; + } +} diff --git a/tests/Ftgo.Auth.Tests/EntraAuthRateLimiterTests.cs b/tests/Ftgo.Auth.Tests/EntraAuthRateLimiterTests.cs index 52eed70..1584b22 100644 --- a/tests/Ftgo.Auth.Tests/EntraAuthRateLimiterTests.cs +++ b/tests/Ftgo.Auth.Tests/EntraAuthRateLimiterTests.cs @@ -102,6 +102,95 @@ public void Partition_key_treats_roles_only_token_as_app_even_without_idtyp() isApp.ShouldBeTrue(); } + [Theory] + [InlineData("tid", "oid", "u|tenant-x|user-y")] + [InlineData("http://schemas.microsoft.com/identity/claims/tenantid", "http://schemas.microsoft.com/identity/claims/objectidentifier", "u|tenant-x|user-y")] + [InlineData("tid", "sub", "u|tenant-x|user-y")] + public void Partition_key_resolves_long_form_user_claim_variants(string tidClaim, string oidClaim, string expected) + { + var ctx = new DefaultHttpContext(); + ctx.User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim(tidClaim, "tenant-x"), + new Claim(oidClaim, "user-y"), + }, authenticationType: "test")); + + EntraAuthRateLimiterExtensions.PartitionKey(ctx).ShouldBe(expected); + } + + [Theory] + [InlineData("azp", "client-x")] + [InlineData("appid", "client-x")] + [InlineData("http://schemas.microsoft.com/identity/claims/appid", "client-x")] + public void Partition_key_resolves_app_appid_claim_variants(string claimType, string value) + { + var ctx = new DefaultHttpContext(); + ctx.User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim("tid", "tenant-x"), + new Claim("idtyp", "app"), + new Claim(claimType, value), + new Claim("roles", "Orders.Process"), + }, authenticationType: "test")); + + EntraAuthRateLimiterExtensions.PartitionKey(ctx).ShouldBe($"a|tenant-x|{value}"); + } + + [Fact] + public void Partition_key_falls_back_to_unknown_when_claims_missing_but_authenticated() + { + var ctx = new DefaultHttpContext(); + ctx.User = new ClaimsPrincipal(new ClaimsIdentity(authenticationType: "test")); + + var key = EntraAuthRateLimiterExtensions.PartitionKey(ctx); + + key.ShouldStartWith("u|unknown-tid|"); + key.ShouldEndWith("|unknown-oid"); + } + + [Fact] + public async Task Default_policy_isolates_partitions_across_distinct_users() + { + using var host = await BuildHostAsync(permitLimit: 2); + using var client = host.GetTestClient(); + + // User A exhausts their budget. + await SendAsAsync(client, tid: "tenant-a", oid: "user-a"); + await SendAsAsync(client, tid: "tenant-a", oid: "user-a"); + var aRejected = await SendAsAsync(client, tid: "tenant-a", oid: "user-a"); + + // User B in same tenant is unaffected. + var bAllowed = await SendAsAsync(client, tid: "tenant-a", oid: "user-b"); + + aRejected.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests); + bAllowed.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task Default_policy_emits_RetryAfter_header_on_429() + { + using var host = await BuildHostAsync(permitLimit: 1); + using var client = host.GetTestClient(); + + await SendAsAsync(client, tid: "tenant-r", oid: "user-r"); + var rejected = await SendAsAsync(client, tid: "tenant-r", oid: "user-r"); + + rejected.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests); + // RFC 6585 §3 requires Retry-After on 429. The OnRejected hook in + // EntraAuthRateLimiterExtensions writes it from the lease metadata. + rejected.Headers.RetryAfter.ShouldNotBeNull(); + rejected.Headers.RetryAfter!.Delta.ShouldNotBeNull(); + rejected.Headers.RetryAfter.Delta!.Value.ShouldBeGreaterThan(TimeSpan.Zero); + } + + private static Task SendAsAsync(HttpClient client, string tid, string oid) + { + var req = new HttpRequestMessage(HttpMethod.Get, "/"); + req.Headers.Add("X-Test-Tid", tid); + req.Headers.Add("X-Test-Oid", oid); + return client.SendAsync(req, TestContext.Current.CancellationToken); + } + private static async Task BuildHostAsync(int permitLimit) { var builder = new HostBuilder().ConfigureWebHost(web => @@ -118,6 +207,22 @@ private static async Task BuildHostAsync(int permitLimit) }); web.Configure(app => { + // Synthesize a ClaimsPrincipal from headers so partition-isolation tests can + // exercise multiple identities through one HttpClient. + app.Use(async (ctx, next) => + { + if (ctx.Request.Headers.TryGetValue("X-Test-Tid", out var tid) && + ctx.Request.Headers.TryGetValue("X-Test-Oid", out var oid)) + { + ctx.User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim("tid", tid!), + new Claim("oid", oid!), + }, authenticationType: "test")); + } + await next(); + }); + app.UseRouting(); app.UseRateLimiter(); app.UseEndpoints(e => diff --git a/tests/Ftgo.Auth.Tests/packages.lock.json b/tests/Ftgo.Auth.Tests/packages.lock.json index ce4c8be..02191af 100644 --- a/tests/Ftgo.Auth.Tests/packages.lock.json +++ b/tests/Ftgo.Auth.Tests/packages.lock.json @@ -16,9 +16,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.54, )", - "resolved": "3.0.54", - "contentHash": "fybpNsTg4ZT+oiQWkrkTvvy1dowdqjfkefFa/7OIPFPmTkHd9eSEjPCpoK1c5oYdp68QjpLTbeCDpZB2TOUQ4A==" + "requested": "[3.0.58, )", + "resolved": "3.0.58", + "contentHash": "EWprZh8ONB5W4T5lajB9H3/ThznWgEVbb8sWkIZe2ZOCRPPzAdLxkFfM8z57SosJtUYGxPXvxlqGmClxw8mA9g==" }, "Microsoft.AspNetCore.TestHost": { "type": "Direct", @@ -28,12 +28,12 @@ }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[18.4.0, )", - "resolved": "18.4.0", - "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==", + "requested": "[18.5.1, )", + "resolved": "18.5.1", + "contentHash": "SfqVaLiIqAbRWuPg5BP4QFwBIirQj/YIL8Dhxl6zntBKbXp0cQykoV480SmwG+yRMiWptxEI6NbHQuGSZ8b97w==", "dependencies": { - "Microsoft.CodeCoverage": "18.4.0", - "Microsoft.TestPlatform.TestHost": "18.4.0" + "Microsoft.CodeCoverage": "18.5.1", + "Microsoft.TestPlatform.TestHost": "18.5.1" } }, "Microsoft.SourceLink.GitHub": { @@ -74,9 +74,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.24.0.138807, )", - "resolved": "10.24.0.138807", - "contentHash": "+ZEa1+KhNSulMSatpffgHnovbLGtYL9oukMh8uQp8RBDnPOjEFzvECSa4kfu1D4PJxErksiGOQQ+kDeao1x9jQ==" + "requested": "[10.25.0.139117, )", + "resolved": "10.25.0.139117", + "contentHash": "2iBZIkcgsaUNyIPFqR9ECv5PLbAwV9I/v/PeEiMdkfhWEAnqD7VyNXzukQDlL/D24f5OoUc4MbWF5SgYI7x3CA==" }, "xunit.runner.visualstudio": { "type": "Direct", @@ -95,8 +95,8 @@ }, "Azure.Core": { "type": "Transitive", - "resolved": "1.53.0", - "contentHash": "x9c/toFMOtRrlTdFuE7rlGCVAduQzWVfKmLz5juj41zJAXEhYD5hluiUyyAEzJ6OxpBnKtiaBztzwpZITAVjtg==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "10.0.3", "Microsoft.Extensions.Configuration.Abstractions": "10.0.3", @@ -167,8 +167,8 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" + "resolved": "18.5.1", + "contentHash": "vMFDR1ZjqzzgKmM0zrPie7Gv9Y+ZppjODB5Quzu9Eq0TlIusUfUCYFPEawO91zQuqwzvdFbJSU7WHNtjStffJQ==" }, "Microsoft.Extensions.AmbientMetadata.Application": { "type": "Transitive", @@ -516,45 +516,45 @@ }, "Microsoft.Identity.Web.Certificate": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "G4nkrs1pKg6NxuSvyoRzokLfsfd2v5pXpXS1XUvHstvdWkQRBw8kTbSwRCvzdRFA1MW7Ct14zcp1P4kej7dB+g==", + "resolved": "4.9.0", + "contentHash": "kr6ZpxNbWm1+eI3pn6sdgIlduYZiEmoC0TbiiR1gmcREuI87E963xjBMmvXf9+CnHcjbsqhkBcK4huKauif0yA==", "dependencies": { - "Azure.Identity": "1.11.4", + "Azure.Identity": "1.17.2", "Azure.Security.KeyVault.Certificates": "4.6.0", "Azure.Security.KeyVault.Secrets": "4.6.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.Diagnostics": "4.8.0" + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.Diagnostics": "4.9.0" } }, "Microsoft.Identity.Web.Certificateless": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "TegoXg/SX1mca1ygTZWTgLTXIZPQudmg5SLr9f50A8fzC4oFGkNOUfa9wii9SRikBGg4Sc31QxUeM/MMFyIX6A==", + "resolved": "4.9.0", + "contentHash": "hlt8KV1V0JLjyxncmSNAugyqxVr65wQlzcsks6TgqvUyllkQvWAxqgM+Dh6OB8sgJWMoCOmJuKJ6SejAgYjJ/w==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", "Microsoft.Identity.Client": "4.83.1", "Microsoft.IdentityModel.JsonWebTokens": "8.15.0" } }, "Microsoft.Identity.Web.Diagnostics": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "oWYVlq2h86yAmqJHcDK0JrJfhDZTnF1M1vyY9mGx+x5E5GpNml7hTI1N1Kq4Z9JUtaPvz4dN8MgfUn7QyHZ13Q==" + "resolved": "4.9.0", + "contentHash": "5x4Tyg1xFr8vgTAuos5OyCNAXcnq9U+6QWA3pRLOgUF5Am7Bn/CM+rmOjffoyfP6ymQdQ0/zxtNOtyRjTlpiBg==" }, "Microsoft.Identity.Web.TokenAcquisition": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "Nm4YJxUQuIThoRWprg9bv2vCDs2zzlOo7B4nfHoG3Spw5Ege2edFLY+iEtRh+GYW2Fhn97rYmwCgmaQ2IhYZxA==", + "resolved": "4.9.0", + "contentHash": "ap6EFz9Nn3pWlfYQ8AqnRM7UsrH5pAXzDMF/aayKEfcAxLgXe23XKb1jBEzWNYkg5NNrbiLVWjKDIWbPqMrn1w==", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "10.0.0", "Microsoft.AspNetCore.Authentication.OpenIdConnect": "10.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0", "Microsoft.Identity.Abstractions": "12.0.0", - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Logging": "8.15.0", "Microsoft.IdentityModel.LoggingExtensions": "8.15.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", @@ -563,16 +563,16 @@ }, "Microsoft.Identity.Web.TokenCache": { "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "2TL0zev2SREJoIzZi9dQ+vv/Us6Q1ZpZD02ti0LhFDpFg/vKGD98pHmcz+f216PbWHzQpERzS3tMgcIGt2lywQ==", + "resolved": "4.9.0", + "contentHash": "3eHtmFu6HJU334bLyOjG/PvWyYQaoGEh9HyomGQ1p/aUV4EiGkdiU4R5yuyOIHdxRs8DKMFPN0Wnqp2nvWayjg==", "dependencies": { - "Microsoft.AspNetCore.DataProtection": "10.0.0", + "Microsoft.AspNetCore.DataProtection": "10.0.7", "Microsoft.Extensions.Caching.Memory": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Microsoft.Identity.Client": "4.83.1", - "Microsoft.Identity.Web.Diagnostics": "4.8.0", - "System.Security.Cryptography.Pkcs": "10.0.6", - "System.Security.Cryptography.Xml": "10.0.6" + "Microsoft.Identity.Web.Diagnostics": "4.9.0", + "System.Security.Cryptography.Pkcs": "10.0.7", + "System.Security.Cryptography.Xml": "10.0.7" } }, "Microsoft.IdentityModel.Abstractions": { @@ -656,15 +656,15 @@ }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" + "resolved": "18.5.1", + "contentHash": "KNZd+M0S0rz5eNAln0pbZX+A/RbokYZCbGKx4fN4CkhtWhkz6nSJDO+9LGYjRE4d0WPVriJ2JnVubkjt3+PpMg==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==", + "resolved": "18.5.1", + "contentHash": "RM+3JNHEoHOCFXzVntUcIiYxzPjzBN0N8wto6HYXi76YyBTZ/3CeRL8U+Pk5zx3AUrOmHxDvKJwGUCdElU9bJg==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.4.0", + "Microsoft.TestPlatform.ObjectModel": "18.5.1", "Newtonsoft.Json": "13.0.3" } }, @@ -699,15 +699,15 @@ }, "OpenTelemetry.PersistentStorage.Abstractions": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + "resolved": "1.0.3", + "contentHash": "7nSE1uLY0KOHRssg1xQydDZJ/s9zECOzFT/gSDIp1KTxbgd1yziuMTsdBKZ/N1BbpUxfH6Pjq5OMd7lYogwReA==" }, "OpenTelemetry.PersistentStorage.FileSystem": { "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "resolved": "1.0.3", + "contentHash": "3k+yCroJcGBtZIia+0LgzMlBuD0U95BzcWNEGWedusBaRAD8EmkaCPAUwaCtFbb/Mg7HywbIG2zuenANPYFJ0g==", "dependencies": { - "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.3" } }, "Polly.Core": { @@ -869,17 +869,17 @@ "dependencies": { "Ftgo.Auth.Client": "[1.0.0, )", "Microsoft.AspNetCore.OpenApi": "[10.0.7, )", - "Microsoft.Identity.Web": "[4.8.0, )", - "Microsoft.Identity.Web.DownstreamApi": "[4.8.0, )", + "Microsoft.Identity.Web": "[4.9.0, )", + "Microsoft.Identity.Web.DownstreamApi": "[4.9.0, )", "Microsoft.IdentityModel.Validators": "[8.17.0, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )", - "Scalar.AspNetCore": "[2.14.4, )" + "Scalar.AspNetCore": "[2.14.9, )" } }, "ftgo.auth.client": { "type": "Project", "dependencies": { - "Azure.Monitor.OpenTelemetry.Exporter": "[1.7.0, )", + "Azure.Monitor.OpenTelemetry.Exporter": "[1.8.0, )", "Microsoft.Extensions.Hosting": "[10.0.7, )", "Microsoft.Extensions.Http": "[10.0.7, )", "Microsoft.Extensions.Http.Resilience": "[10.5.0, )", @@ -902,13 +902,13 @@ }, "Azure.Monitor.OpenTelemetry.Exporter": { "type": "CentralTransitive", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "fexzK+HM06C3tmBL6DLNVAcgutBMY7hQ7tGcdpCyq6HaXvbNz6cFtenrJUAdXM8Y4G+3QmZ6PIAL1hFrVe4Mpw==", + "requested": "[1.8.0, )", + "resolved": "1.8.0", + "contentHash": "JbNUQIK7uZ7Mg2fZzdsjqC/eso3Ag/tMwCqYvGvedyE1fObe6JAwCaCSMZDMrhrAaFn8x8+ZaKJelfcEK5MCcg==", "dependencies": { - "Azure.Core": "1.52.0", - "OpenTelemetry.Extensions.Hosting": "1.15.1", - "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + "Azure.Core": "1.54.0", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.3" } }, "Azure.Security.KeyVault.Certificates": { @@ -1066,14 +1066,14 @@ }, "Microsoft.Identity.Web": { "type": "CentralTransitive", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "rqMufh64Woj/kc39d9iCb12BTtBj9H40haLUemRAwoqvqSomYPlbMOwt1UD1Y5aMqmc8aYF06U5ATY8Qnw2iFg==", - "dependencies": { - "Microsoft.Identity.Web.Certificate": "4.8.0", - "Microsoft.Identity.Web.Certificateless": "4.8.0", - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0", - "Microsoft.Identity.Web.TokenCache": "4.8.0", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "SshLuFsW0VqCP18O1YUAqPh4KZ6ogu9yYrhlYdGxf/KSRR0wHO1MFlyxh2aOkHdw462XKjbzm36NTspJo1r58g==", + "dependencies": { + "Microsoft.Identity.Web.Certificate": "4.9.0", + "Microsoft.Identity.Web.Certificateless": "4.9.0", + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0", + "Microsoft.Identity.Web.TokenCache": "4.9.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.15.0", "Microsoft.IdentityModel.Validators": "8.15.0", "System.IdentityModel.Tokens.Jwt": "8.15.0" @@ -1081,12 +1081,12 @@ }, "Microsoft.Identity.Web.DownstreamApi": { "type": "CentralTransitive", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "MXCBVZO9Oj2hSZkPvEFcRg1/fyKNjqJQzAVxUjdCOzno3rd1BeVhuMddk9aYNJ3r79GD5lA3xJeneXo+SKlKCA==", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "E9RjjTmAiHR7Z8HDtfEDbioW6NOvoYABGlyIhwOPkMpECTP7M69SwO5DJki+QA0pHpIongdUC856GH/lnqyxSA==", "dependencies": { "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0", - "Microsoft.Identity.Web.TokenAcquisition": "4.8.0" + "Microsoft.Identity.Web.TokenAcquisition": "4.9.0" } }, "Microsoft.IdentityModel.JsonWebTokens": { @@ -1176,9 +1176,9 @@ }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.14.4, )", - "resolved": "2.14.4", - "contentHash": "PyuqLRi7JXyTJm/rh+IWCA4Fct1SlZuWrdYiui5hiaBuSY88+roc4eWKfYaE6zrupBIwnEP8pJLe4ifvU7HtZA==" + "requested": "[2.14.9, )", + "resolved": "2.14.9", + "contentHash": "bDc8NjI2JSIAX0C1+02WO11zCJ1SJQhWdMC0s7yi2Nh2atoNRpNRPiYeyIhWSerkc1SkDdJVOeU9QSY6jb0/zQ==" }, "System.Security.Cryptography.Xml": { "type": "CentralTransitive",