diff --git a/AzureFunctionsUtils/AzureFunctionsUtils.csproj b/AzureFunctionsUtils/AzureFunctionsUtils.csproj index 5729005..2ffbd08 100644 --- a/AzureFunctionsUtils/AzureFunctionsUtils.csproj +++ b/AzureFunctionsUtils/AzureFunctionsUtils.csproj @@ -7,7 +7,7 @@ WebGate.Azure.FunctionsUtils - 10.0.0 + 10.0.1 WebGate Consulting AG WebGate Consulting AG Apache-2.0 diff --git a/AzureFunctionsUtils/FunctionRunContext/Internal/AzureFunctionsEnvironment.cs b/AzureFunctionsUtils/FunctionRunContext/Internal/AzureFunctionsEnvironment.cs index 9e47a67..7f296b7 100644 --- a/AzureFunctionsUtils/FunctionRunContext/Internal/AzureFunctionsEnvironment.cs +++ b/AzureFunctionsUtils/FunctionRunContext/Internal/AzureFunctionsEnvironment.cs @@ -2,18 +2,16 @@ namespace WebGate.Azure.FunctionsUtils.Internal; internal static class AzureFunctionsEnvironment { - private const string AZURE_FUNCTIONS_ENVIRONMENT_VARIABLE = "AZURE_FUNCTIONS_ENVIRONMENT"; - private const string LOCAL_DEVELOPMENT_ENVIRONMENT_NAME = "LocalDevelopment"; + private const string LOCAL_DEVELOPMENT_VARIABLE = "FUNCTIONS_UTILS_LOCAL_DEVELOPMENT"; /// - /// True when functions run on a developer machine. - /// Azure cloud environments use other values (e.g. Development, Staging, Production). + /// True when FUNCTIONS_UTILS_LOCAL_DEVELOPMENT is exactly true (case-insensitive). + /// Set only in local.settings.json — never in Azure. + /// Enables and Bearer JWT validation. /// public static bool IsLocalDevelopment() { - return string.Equals( - Environment.GetEnvironmentVariable(AZURE_FUNCTIONS_ENVIRONMENT_VARIABLE, EnvironmentVariableTarget.Process), - LOCAL_DEVELOPMENT_ENVIRONMENT_NAME, - StringComparison.OrdinalIgnoreCase); + var value = Environment.GetEnvironmentVariable(LOCAL_DEVELOPMENT_VARIABLE, EnvironmentVariableTarget.Process); + return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); } } diff --git a/AzureFunctionsUtils/FunctionRunContext/Internal/BearerJwtClaimsPrincipalParser.cs b/AzureFunctionsUtils/FunctionRunContext/Internal/BearerJwtClaimsPrincipalParser.cs index daa1ac0..10fdad0 100644 --- a/AzureFunctionsUtils/FunctionRunContext/Internal/BearerJwtClaimsPrincipalParser.cs +++ b/AzureFunctionsUtils/FunctionRunContext/Internal/BearerJwtClaimsPrincipalParser.cs @@ -20,6 +20,11 @@ internal static class BearerJwtClaimsPrincipalParser public static ClaimsPrincipal? TryParse(HttpRequest request) { + if (!AzureFunctionsEnvironment.IsLocalDevelopment()) + { + return null; + } + if (!TryGetBearerToken(request, out var token)) { return null; @@ -44,7 +49,7 @@ internal static class BearerJwtClaimsPrincipalParser $"https://sts.windows.net/{tenantId}/" ], ValidateAudience = true, - ValidAudiences = [audience], + ValidAudiences = BuildValidAudiences(audience), ValidateIssuerSigningKey = true, IssuerSigningKeys = openIdConfig.SigningKeys, ValidateLifetime = true, @@ -85,6 +90,22 @@ private static bool TryGetBearerToken(HttpRequest request, out string token) return token.Length > 0; } + private static string[] BuildValidAudiences(string audience) + { + var audiences = new HashSet(StringComparer.OrdinalIgnoreCase) { audience }; + + if (audience.StartsWith("api://", StringComparison.OrdinalIgnoreCase)) + { + audiences.Add(audience["api://".Length..]); + } + else + { + audiences.Add($"api://{audience}"); + } + + return [.. audiences]; + } + private static OpenIdConnectConfiguration GetOpenIdConnectConfiguration(string tenantId) { var metadataAddress = diff --git a/README.md b/README.md index b63cb19..822bfa6 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,34 @@ dotnet add package WebGate.Azure.FunctionsUtils The **NuGet package major version matches the .NET target framework major version**. - `net10.0` → package version `10.x.x` -- A future uplift to `net11.0` would start at package version `11.0.0` +- Future uplifts follow **.NET LTS** releases only (e.g. the next LTS after .NET 10); the package major then matches that TFM major. Within a major line, use minor/patch for library changes that stay on the same TFM. --- -## Environments +## Local development flag -`AZURE_FUNCTIONS_ENVIRONMENT` distinguishes **where** the functions run: +Local vs cloud is controlled by one setting: -| Value | Meaning | Auth behavior | `IsDev()` | -|---|---|---|---| -| `LocalDevelopment` | Functions on a developer machine | Easy Auth if present, otherwise validated Bearer JWT | `true` | -| `Development` | Azure cloud **DEV** environment | Easy Auth only | `false` | -| `Staging` / `Production` / other | Azure cloud environments | Easy Auth only | `false` | +| Variable | Value | Effect | +|---|---|---| +| `FUNCTIONS_UTILS_LOCAL_DEVELOPMENT` | `true` | `IsDev() == true`, Bearer JWT allowed | +| unset / other | — | Cloud mode: Easy Auth only, no Bearer | -Important: Azure’s usual `Development` value means the cloud DEV slot, **not** local execution. For local runs set `AZURE_FUNCTIONS_ENVIRONMENT=LocalDevelopment` in `local.settings.json`. +Set it **only** in `local.settings.json`. Do not set it in Azure App Settings. + +```json +{ + "Values": { + "FUNCTIONS_UTILS_LOCAL_DEVELOPMENT": "true", + "FUNCTIONS_UTILS_AAD_TENANT_ID": "{tenant-id}", + "FUNCTIONS_UTILS_AAD_AUDIENCE": "api://{api-app-id}" + } +} +``` + +Do not use `AZURE_FUNCTIONS_ENVIRONMENT` for this — Core Tools overwrites it to `Development`. --- @@ -47,45 +58,21 @@ Headers alone are not trusted. Protect cloud Function Apps like this: Internet → Azure API Management (validate-jwt) → Function App (Easy Auth Required) → UserFunctionRunContext ``` -1. **Easy Auth required (cloud environments)** - On the Function App, enable App Service Authentication / Easy Auth and set unauthenticated requests to **Return HTTP 401**. Easy Auth strips client-supplied `x-ms-client-principal*` headers and replaces them after a successful Entra ID login. - Whenever the environment is **not** `LocalDevelopment`, this library accepts **only** Easy Auth (`x-ms-client-principal`). Bearer fallback is disabled. +1. **Easy Auth required (cloud)** + Enable App Service Authentication / Easy Auth and set unauthenticated requests to **Return HTTP 401**. Without `FUNCTIONS_UTILS_LOCAL_DEVELOPMENT`, this library accepts **only** Easy Auth (`x-ms-client-principal`). 2. **Do not expose the Function App publicly** - Prefer private networking and put **Azure API Management** in front. Use an APIM `validate-jwt` policy against Entra ID (issuer, audience, signing keys) before traffic reaches the Function App. - - Example APIM fragment: - - ```xml - - - - {api-app-id-or-uri} - - - ``` - -3. **Bearer JWT cryptographic validation (`LocalDevelopment` only)** - On a developer machine (`AZURE_FUNCTIONS_ENVIRONMENT=LocalDevelopment`), if Easy Auth is absent, the library may fall back to `Authorization: Bearer`. That token is validated (signature, issuer, audience, lifetime) via Entra OpenID metadata — decode-only is not used. - - Example `local.settings.json`: - - ```json - { - "Values": { - "AZURE_FUNCTIONS_ENVIRONMENT": "LocalDevelopment", - "FUNCTIONS_UTILS_AAD_TENANT_ID": "{tenant-id}", - "FUNCTIONS_UTILS_AAD_AUDIENCE": "{api-app-id-or-uri}" - } - } - ``` + Prefer private networking and put **Azure API Management** in front with `validate-jwt`. + +3. **Bearer JWT (local only)** + When `FUNCTIONS_UTILS_LOCAL_DEVELOPMENT=true` and Easy Auth is absent, `Authorization: Bearer` is validated (signature, issuer, audience, lifetime) via Entra OpenID metadata. | Variable | Purpose | |---|---| | `FUNCTIONS_UTILS_AAD_TENANT_ID` | Entra tenant ID | - | `FUNCTIONS_UTILS_AAD_AUDIENCE` | API audience (app ID or Application ID URI) | + | `FUNCTIONS_UTILS_AAD_AUDIENCE` | API audience (GUID or `api://{app-id}`; both accepted) | - If either variable is missing, Bearer fallback fails closed (`IsAuthenticated() == false`). + If tenant/audience are missing, Bearer fails closed. The SPA must send an API access token (Expose an API), e.g. MSAL scope `api://{clientId}/access_as_user`. --- @@ -107,14 +94,14 @@ Shared API (`IFunctionRunContext`): - `GetEnvironmentVariable(name)` `UserFunctionRunContext` also exposes `GetClaimsPrincipal()`. -`IsDev()` follows the [Environments](#environments) table (`LocalDevelopment` only). +`IsDev()` is `true` when `FUNCTIONS_UTILS_LOCAL_DEVELOPMENT` is enabled. ### UserFunctionRunContext Identity is resolved in this order: 1. Azure Easy Auth payload from `x-ms-client-principal` (all environments) -2. Validated JWT from `Authorization: Bearer ` (`LocalDevelopment` only; requires tenant/audience env vars) +2. Validated JWT from `Authorization: Bearer ` (only when `FUNCTIONS_UTILS_LOCAL_DEVELOPMENT=true`) Claim mapping: