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: