JanssenProject / JanssenProject/jans
feat(jans-fido2): native app readiness checker for AASA / assetlinks.json (sandboxed, SSRF-safe)
- Dominant language
- Java
- Stars
- 648
- Forks
- 174
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 110
Description
**Is your feature request related to a problem? Please describe.**
Once an admin configures an RP (structured origin/RP-ID model) and deploys the generated `assetlinks.json` / AASA files, they currently have to verify everything by hand: that the files are reachable, served correctly, and actually match the configured fingerprints/Team IDs. There's no way to see readiness at a glance, and stale/mismatched files surface only as generic registration failures. **But** any checker that fetches admin-supplied domains from inside the Config API is a serious SSRF risk: the RP ID is customer-controlled, DNS can be rebound to internal/metadata IPs, and redirects can bounce to `169.254.169.254`/localhost/internal admin endpoints. Jans commonly runs inside a cluster/VPC alongside databases, LDAP, and MDS.
**Describe the solution you'd like**
A **two-layered** readiness checker:
1. **Config/API layer** (Config API fido2 plugin) — owns RP config, expected file generation, result caching, and the status the UI renders. Accepts an **RP config ID, never a URL**: `POST /jans-fido2/restv1/rp/{rpConfigId}/readiness`.
2. **Sandboxed readiness worker** — performs the actual outbound fetch under strict constraints.
Checks returned as structured per-check status (`pass` / `warn` / `fail` + short reason), not raw file contents:
- **Reachability** — HTTP 200, correct `Content-Type` (AASA = `application/json`), served from the RP-ID domain (not a subdomain), **no redirects** (flag redirects explicitly — neither Apple nor Android follows them).
- **Content / fingerprint match** — diff deployed file vs. configured values: Android package + SHA-256 fingerprint(s) + `delegate_permission/common.get_login_creds`; iOS `TeamID.BundleID` in `webcredentials`. Report the *specific* mismatch.
- **Freshness / drift** — deployed file no longer matches what Jans would generate (e.g. after a signing-key rotation).
- **AASA CDN caveat** — iOS goes through `app-site-association.cdn-apple.com` (caches aggressively); surface as an **informational `warn`**, not a hard fail (origin can be correct while CDN is stale).
**Mandatory SSRF safeguards:**
1. **Derive URLs, never accept them** — server derives exactly `https:///.well-known/assetlinks.json` and `…/apple-app-site-association`. No user-supplied scheme/host/port/path/query/redirect.
2. **Strict RP-ID validation** — require a DNS hostname; reject IP literals, `localhost`, `.local`/`.internal`/`.corp`, embedded credentials, ports, paths, punycode/mixed-script (unless normalized); optionally require the RP already exists in FIDO2 config.
3. **Block private/sensitive ranges after DNS resolution** — `127/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254/16`, IPv6 loopback/link-local/ULA/multicast, cloud metadata, K8s service CIDRs, Jans internal domains.
4. **Disable redirects entirely** (security *and* correctness — Apple requires none).
5. **Re-check the resolved IP immediately before connect** (DNS-rebinding defense).
6. **Sandboxed worker / restricted egress** — not a broadly privileged Config API plugin; no cluster/metadata access, no Jans tokens/cookies attached, tight timeout + response-size cap.
7. **Opt-in & auditable** config, e.g. `fido2.nativeReadiness.enabled=false`, `allowExternalFetch=false`, `maxResponseBytes=131072`, `timeoutMs=3000`, `cacheTtlSeconds=3600`; audit each check (rpId, check, requestedBy, timestamp, result, resolvedIpClass).
8. **Cache + rate-limit** — no fetch on every page load; "last checked" + manual "Recheck now" + background refresh + per-RP/per-tenant limits (so it can't be used as a scanner).
9. **Structured status only** — return the per-check result JSON, not the whole remote file.
**Describe alternatives you've considered**
- *Generic URL-fetcher endpoint (`{"url": "…"}`)* — explicitly rejected: SSRF/unsafe-egress. Accept `rpConfigId` only.
- *Run the fetch inside the Config API plugin* — rejected: too privileged a network position; isolate in the sandboxed worker.
- *Client-side (Admin UI) fetch* — rejected: CORS + no server-side reuse for CLI/ops guide; keep check logic server-side.
- *Abandon the checker over SSRF concerns* — rejected; build it constrained instead.
**Additional context**
Depends on the mobile origin/RP-ID policy model (config) and the `assetlinks.json` / AASA generation (expected content). Surfaces in the Admin UI as inline pass/warn/fail indicators on the RP page. References: OWASP SSRF cheat sheet;
Corbado article — https://www.corbado.com/blog/native-ios-android-passkey-implementation-challenges ;
ChatGPT analysis — https://chatgpt.com/share/6a5129bc-a8f4-83ea-8649-3e463e0a7db3
Contributor guide
Assessment
This issue has not been assessed yet.