Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,31 +1,63 @@
# Dependencies
node_modules/

# Local helper scripts
scripts/

# Python
.venv/
venv/
*.pyc
.pytest_cache/

# Azure Functions local settings — may contain secrets; never commit.
local.settings.json
# Local secrets (keep the shared sample)
local.settings*.json
!local.settings.sample.json
.env
.env.*
!.env.example
.keys/

# Keys / certificates — never commit private keys.
# Keys and publish credentials
*.pem
*.pfx
*.key
*.p12
*.publishsettings
*.pubxml
*.pubxml.user

# Azure Functions build/runtime
bin/
obj/
[Bb]in/
[Oo]bj/
publish/
artifacts/
.azure/
.python_packages/
__pycache__/
coverage/
[Tt]est[Rr]esults*/

# .NET developer output
[Dd]ebug*/
[Rr]elease*/
*.nupkg
*.snupkg
*.suo
*.user
*.userprefs
*.userosscache
*.sln.docstates
_ReSharper*/
*.DotSettings.user

# Logs
*.log
npm-debug.log*

# Editor / OS
.vscode/
.vs/
.idea/
.DS_Store
Thumbs.db
53 changes: 34 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,54 @@ secrets in Key Vault.

| Language | Status | Folder |
|----------|--------|--------|
| JavaScript (Node.js) | Available | [`javascript/`](javascript/) |
| C# (.NET isolated worker) | Available | [`dotnet/`](dotnet/) |
| Python (v2 model) | Available | [`python/`](python/) |
| JavaScript (Node.js) | Available | [javascript/](javascript/) |
| C# (.NET isolated worker) | Available | [dotnet/](dotnet/) |
| Python (v2 model) | Available | [python/](python/) |

All implementations conform to the **language-agnostic contract** in
[`docs/CONTRACT.md`](docs/CONTRACT.md) — identical HTTP API, provider-adapter shape, config/env var
[docs/CONTRACT.md](docs/CONTRACT.md) — identical HTTP API, provider-adapter shape, config/env var
names, Key Vault secret names, and behaviors (fail-closed, managed identity, privacy). Pick any folder
and follow its README.

Choose one language and configure the adapter for your provider. No provider is preferred or selected
by default. Deploy each language separately, not all three to the same Function App. See the
[shared configuration](docs/CONTRACT.md#default-provider-and-configuration-readers).

New here? Start with **[docs/ONBOARDING.md](docs/ONBOARDING.md)** — setup, config, running, securing,
and deploying, step by step.

## The design in one line

`POST /api/SendOtp` → validate token → resolve provider → fetch secret from Key Vault (managed
identity) → provider adapter builds the request → send with a timeout → map the provider status to an
outcome and an HTTP status. **Fail-closed:** only a `Continue` outcome returns `200 accepted`.
SAS → Easy Auth → anonymous HTTP handler (`POST /api/SendOtp`, validate envelope + decrypt JWE) →
configured provider (API key) → HTTP result with nonce on success.
Only provider acceptance returns the nonce for live requests. Incoming `mode: 2` (evaluation) is the
generic shutter: after platform authentication, validate and decrypt, then echo the nonce without
calling a provider.

See [docs/CONTRACT.md](docs/CONTRACT.md) for the full specification every implementation follows.

See [`docs/CONTRACT.md`](docs/CONTRACT.md) for the full specification every implementation follows.
Request `tenantId`, `channel`, `mode` and `ttlSeconds` are request data, not extra environment settings.
The trusted tenant issuer, endpoint-app audience and authorized SAS caller are configured in Easy Auth,
not in application environment settings or incoming request data.

## Security

**Turn Easy Auth (App Service Authentication) ON — that is the primary gate.** Set
`unauthenticatedClientAction` to `Return401` and pin `allowedApplications` to Microsoft's app id. The
HTTP trigger is `authLevel: anonymous`, so with Easy Auth off nothing stands in front of the endpoint.
**Easy Auth (App Service Authentication) is the only caller-authentication gate, before the anonymous
Function.** Enable it with `requireAuthentication=true`, `unauthenticatedClientAction=Return401` and
`requireHttps=true`. Configure the trusted tenant issuer and `allowedAudiences` for the endpoint app,
and a **nonempty `allowedApplications`** list pinned to the authorized SAS caller application ID.
Do not exclude the SendOtp path. The handler does not parse or validate bearer tokens, and there is
no backup application validation or function-key gate. **Never expose this endpoint to the public
internet with Easy Auth disabled or bypassed.** See [platform setup](docs/ONBOARDING.md#2-provision-encryption-and-deployment-trust).

JWE decryption protects the payload but **does not authenticate SAS**: anyone with the public key can
encrypt a request. A nonce echo, including a fixed nonce, is not caller authentication. Provider API
keys are read from **Key Vault** via **managed identity**; they authenticate the outbound provider call,
not the inbound request.

**Also set `EPP_REQUIRE_AUTH=true` in any real deployment.** Easy Auth lives outside the code, so a
portal change or slot swap can drop it silently; in-process validation is the backstop. The Function
then validates the caller's **Entra JWT** (audience = `EPP_EXPECTED_AUDIENCE`, issuer tenant =
`EPP_TENANT_ID`, signature via JWKS) and returns **401** without a valid token. Provider secrets are read
from **Key Vault** via **managed identity** — no keys or connection strings in code or config. Locally
(`func start`) there is no Easy Auth, so `EPP_REQUIRE_AUTH` is the only gate. See
[docs/ONBOARDING.md §5](docs/ONBOARDING.md) for how to test it with a token.
Core Tools does not provide Easy Auth. Local execution is unauthenticated: bind only to loopback,
with no tunnels or public forwarding. Offline tests cover application behavior, not platform
authentication; [separate deployed security checks](docs/ONBOARDING.md#4-package-deploy-and-verify) are required.

## Docs

Expand All @@ -53,4 +68,4 @@ from **Key Vault** via **managed identity** — no keys or connection strings in
- **New provider** (in any language): add one adapter file exposing `manifest` + `buildRequest` +
`parseResponse` — no engine changes. See the language folder's README.
- **New language**: mirror the folder structure, implement the contract, add the same test scenarios,
and wire it into [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
and wire it into [.github/workflows/ci.yml](.github/workflows/ci.yml).
Loading
Loading