diff --git a/CLAUDE.md b/CLAUDE.md index 5d75feb..bdb2b4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,8 @@ The repository ships a Claude skills library at [plugin/skills/](plugin/skills/) The library's default "build me an app" entry point is [`skills/formio-application/`](skills/formio-application/) — a framework-agnostic orchestrator that runs the full pipeline from plain-language intent through `formio-resource-planner`, the `project_import` MCP tool, and handoff to a framework-specific scaffolding skill. Today the only framework implementor is [`skills/formio-angular/`](skills/formio-angular/) (with its sub-skill [`skills/formio-angular/resources/`](skills/formio-angular/resources/) for per-resource NgModule work); `formio-angular` is no longer a top-level build-an-app skill — it is the Angular-specific implementor that `formio-application` delegates to. Future framework skills (`formio-react`, etc.) add themselves as rows in `skills/formio-application/FRAMEWORK.md`'s registry table; no other change to the orchestrator is required. +Authentication and authorization are owned by a stand-alone peer skill at [`skills/formio-auth/`](plugin/skills/formio-auth/). The planner ↔ auth handoff contract is explicit: `formio-resource-planner` owns the data model (roles, the `user` Resource, login/registration forms, group joins) and emits the canonical `template.json` shapes for the Login Action, Role Assignment Action, Group Assignment Action, `access` arrays, `submissionAccess` arrays, and field-based `submissionAccess` on group-reference selects. `formio-auth` owns the auth configuration that runs on top of that model — SSO (OIDC / OAuth, SAML, LDAP) with provider Role Mapping, Token Swap from an external OIDC token, Custom JWT (Enterprise / on-prem, signed with `JWT_SECRET`), email-token (passwordless) authentication, JWT and session mechanics (the `x-jwt-token` header, `jti` Session ID, logout semantics, 2FA, reCAPTCHA), and RBAC tuning beyond default roles. Action JSON shapes are NOT duplicated across the two skills; `formio-auth` references the planner's `references/template-json.md` by file path. When a planner-produced Resource Map's `Users & Auth` section emits a non-`none` `SSO` field, a `Custom JWT: yes`, or any other auth concern beyond resource-backed login plus Role Assignment plus Group Assignment, hand off to `formio-auth` immediately after the Resource Map is approved. + The library also ships [`skills/formio-sdk/`](plugin/skills/formio-sdk/) — a source-derived reference for the `@formio/js` SDK and `@formio/js/utils` Utilities, authored directly from the Form.io source code (`packages/core/src/sdk`, `packages/core/src/utils`, `packages/formio.js/src/Formio.js`, `packages/formio.js/src/utils`) rather than from drift-prone online docs. Reference docs cover SDK setup, auth, forms, submissions, projects, roles, files, plugins, VanillaJS rendering (`Formio.createForm`), and the Utils surface (Evaluator, form traversal, conditions, logic, JSONLogic, mask/sanitize, misc). The skill mandates ESM imports (`import { Formio } from '@formio/js'`, `import { Utils } from '@formio/js/utils'`) and explicit Hosted-vs-SaaS URL configuration. The router skill's `description` follows a three-clause template: capability statement, a "Use when the user asks to …" trigger clause, and a "Not for: …" negative-trigger clause disambiguating from `formio-application` (orchestrator) and `formio-resource-planner` (planner). Each reference document includes a `## MCP Tool Preference` section instructing Claude to prefer the MCP server's first-party tools (`form_*`, `role_*`, `project_*`, `authenticate`) when they cover the requested operation. diff --git a/openspec/changes/archive/2026-05-22-add-formio-auth-skill/.openspec.yaml b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/.openspec.yaml new file mode 100644 index 0000000..a5aaf30 --- /dev/null +++ b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-tdd +created: 2026-05-22 diff --git a/openspec/changes/archive/2026-05-22-add-formio-auth-skill/design.md b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/design.md new file mode 100644 index 0000000..645970b --- /dev/null +++ b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/design.md @@ -0,0 +1,143 @@ +## Context + +Today, Form.io auth knowledge sits inside `plugin/skills/formio-resource-planner/`: + +- `SKILL.md` lines 38–90, 122–129, 150–152, 184–209, 263, 270–295 — role taxonomy, `access` vs `submissionAccess`, action ordering, group-based access "two halves" model +- `references/template-json.md` lines 34–81, 102–158, 297–376, 504–590 — JSON shapes for roles, top-level access, `submissionAccess` patterns, Login / Role Assignment / Group Assignment actions, field-based `submissionAccess` on group-reference selects, transitive group-access mirrors +- `references/template-md.md` lines 68–114, 211–270 — Resource Map "Roles" and "Users & Auth" sections, Access Matrix vocabulary, Access Flow Diagram patterns +- `references/examples/task-manager/template.json` — working end-to-end auth wiring +- `references/examples/complex-crm-transitive/` — multi-level transitive group access + +What is **not** there today: SSO (OIDC, SAML, LDAP), Token Swap, Custom JWT, email-token, 2FA/reCAPTCHA, the full eight-permission RBAC matrix, JWT/session/`jti` semantics, the `x-jwt-token` header on the wire, project-level vs form-definition vs submission-data permission scoping as documented at https://help.form.io/developers/roles-and-permissions. + +The two top-level skills that border this work are: + +- **`formio-resource-planner`** — plans roles, login/registration forms, joins, and `submissionAccess` patterns as part of resource design; emits `template.json`. +- **`formio-application`** — full-stack orchestrator that runs planner → `project_import` → framework scaffolding (Angular today). It calls into auth only transitively. + +Neither is the right home for "explain OIDC Role Mapping" or "how do I forge a Custom JWT for on-prem". We need a peer skill — narrative, reference-driven — that owns auth end-to-end. + +The skills validator (`packages/mcp-server/src/skills-validator.ts`, 476 lines) is scoped only to `formio-api` (the router with `ROUTER_DIR = 'formio-api'`). It does not enforce shape rules on narrative skills, so adding `formio-auth` requires no validator change. Skills are auto-discovered from `plugin/skills/` — no plugin manifest update needed. + +Stakeholders: skill authors, agents using the plugin to build/extend Form.io apps, users with SSO or custom-JWT requirements that the planner cannot address. + +## Goals / Non-Goals + +**Goals:** + +- Stand up `plugin/skills/formio-auth/` as a first-class, activatable skill on auth-only triggers. +- Cover the full Form.io auth surface in topic-scoped reference docs: resource login + Login Action, Role Assignment Action, login/registration form shapes, RBAC (roles + the eight permission types across project/form/submission scopes), Group Permissions (single + transitive), SSO (OIDC, SAML, LDAP), Token Swap, Custom JWT, email-token, JWT/session/`jti` semantics, 2FA, reCAPTCHA. +- Make `formio-auth` and `formio-resource-planner` interoperable: each names the other in handoff prose, and `formio-resource-planner` stops claiming SSO/Token-Swap/Custom-JWT/email-token triggers it does not actually cover. +- Use the existing planner JSON shapes (Login Action, Role Assignment Action, Group Assignment Action, `submissionAccess` patterns) as the single source of truth for emitted `template.json` snippets — `formio-auth` references and reuses those, not forks them. +- Prefer first-party MCP tools (`authenticate`, `role_*`, `form_*`, `action_*`, `project_*`) in MCP Tool Preference sections, matching the convention in `formio-api`'s references. + +**Non-Goals:** + +- No new MCP tools. Auth is configured via existing tools and via the Form.io project portal where appropriate. +- No `formio-auth` eval harness in this change. Pattern is `formio-resource-planner` / `formio-angular`; can be added later under `plugin/skills/formio-auth/evals/`. +- No validator changes. The validator stays scoped to `formio-api`; narrative skills are unchecked for shape today and that stays true here. +- No Angular/React/etc. front-end auth wiring. Framework-specific auth UI is the job of `formio-angular` (and future framework skills), which can reference `formio-auth` for the Form.io side of the contract. +- No removal of auth content from `formio-resource-planner` that is load-bearing for resource emission (action JSON shapes, role objects, `submissionAccess` patterns stay where they are). Only narrative scoping and the trigger surface change. + +## Decisions + +### 1. Layout: SKILL.md + topic-scoped references/, mirroring `formio-resource-planner`, not `formio-api` + +`formio-api` is an endpoint catalog (one file per capability group, strict heading layout enforced by the validator). `formio-auth` is a narrative skill (concepts, configuration walkthroughs, decision trees) — closer in shape to `formio-resource-planner`. + +``` +plugin/skills/formio-auth/ +├── SKILL.md +└── references/ + ├── resource-auth.md + ├── login-forms.md + ├── roles-and-permissions.md + ├── group-permissions.md + ├── sso-oidc.md + ├── sso-saml.md + ├── sso-ldap.md + ├── token-swap.md + ├── custom-jwt.md + ├── email-auth.md + └── jwt-and-sessions.md +``` + +**Alternative considered:** Drop everything into a single big `SKILL.md`. Rejected — auth has eleven distinct sub-topics and an agent should be able to navigate to one without loading the others into context. + +**Alternative considered:** Add `formio-auth` as a sub-skill under `formio-resource-planner/`. Rejected — auth questions are common without a planning context (e.g., wiring SSO into an existing project), and forcing the planner to be the entry point makes activation worse, not better. + +### 2. Activation: three-clause description, auth-only triggers, explicit negative-triggers against planner + application + +Skills activate from `description` content. To avoid stealing planner traffic, `formio-auth`'s description follows the same three-clause template used elsewhere in this repo: + +> *Capability statement.* *Use when the user asks to …* *Not for: …* + +- **Use when** triggers (claimed): login flow, JWT, `x-jwt-token`, SSO, OIDC, OAuth, SAML, LDAP, Token Swap, Custom JWT, `JWT_SECRET`, email token, passwordless, roles, permissions, RBAC, group permissions, "who can read/update/delete", `read_own`/`read_all`/etc., 2FA, reCAPTCHA, Login Action, Role Assignment Action, Group Assignment Action. +- **Not for** (disambiguated): + - Resource/data modeling, building an app from scratch, planning the resource map → `formio-resource-planner`. + - Full-stack scaffolding (run planner → import → scaffold a framework) → `formio-application`. + - Looking up a specific REST endpoint → `formio-api`. + - Front-end UI wiring (Angular login screen, route guards) → `formio-angular`. + +**Alternative considered:** No negative-trigger clause, rely on positive triggers alone. Rejected — the planner today claims terms like "roles", "permissions", "login form" as part of resource design. Without explicit negative-triggers on the planner side AND positive auth-specific claims on the auth side, the two skills race. + +### 3. Cross-skill handoff contract + +Two directions: + +- **Planner → Auth**: After `formio-resource-planner` emits a Resource Map's "Users & Auth" line (`Login:
; Register: ; Role Assignment: ; SSO: ; Custom JWT: `), it prints a "Next steps" footer: + > *"For SSO setup, Token Swap, Custom JWT, email-token auth, or detailed RBAC tuning, activate `formio-auth`."* +- **Auth → Planner**: When a `formio-auth` walkthrough requires a missing resource, role, or form (e.g., "add a `user` resource with email + password fields", "add an Authenticated role"), the doc names the planner explicitly: + > *"This requires a `user` resource and an `authenticated` role. If you do not have them yet, run `formio-resource-planner` to design them."* + +**Alternative considered:** Have `formio-auth` emit `template.json` partials directly. Rejected — there is exactly one canonical place for `template.json` emission (the planner) and forking that into auth doubles maintenance for action shapes that are already 100% accurate in `references/template-json.md`. `formio-auth` references those shapes by file path + line range instead. + +### 4. Reference-doc rules (lighter than `formio-api`) + +`formio-api`'s validator enforces a strict heading layout (`## Overview` / `## Root URL` / `## Authentication` / `## MCP Tool Preference` / `## Endpoints`). That layout makes sense for endpoint catalogs. `formio-auth` is narrative, so each reference doc follows a lighter convention: + +1. `## Overview` — 1-paragraph scope statement +2. `## When to use this` — bulleted activation triggers + "not for" pointers to neighbors +3. `## Configuration` — step-by-step setup, with JSON / config snippets +4. `## MCP Tool Preference` — when an MCP tool can do the work, name it (e.g., `role_create`, `action_create`, `authenticate`); when only the portal can do it, say so explicitly +5. `## See also` — cross-references to `formio-resource-planner` (and back-pointers to neighbor refs in `formio-auth/references/`) + +The canonical portal-login JWT auth paragraph (the one `formio-api` enforces verbatim) is included in `references/jwt-and-sessions.md` and `references/resource-auth.md` because those reference docs describe the on-the-wire JWT directly. Other docs reference it by link instead of copying. + +### 5. Planner scope changes (narrative only, no shape changes) + +`plugin/skills/formio-resource-planner/SKILL.md` changes: + +- Description's "Not for" clause adds: *"SSO (OIDC/SAML/LDAP), Token Swap, Custom JWT, email-token auth, JWT/session mechanics, 2FA — those go through `formio-auth`."* +- "Determine the user/auth model" section keeps its existing scope (asking who the users are, whether the user resource is the built-in `user`, whether roles are needed, what access pattern fits) but adds a one-liner: *"For anything beyond resource-backed login + Role Assignment + Group Assignment, hand off to `formio-auth`."* +- The "Users & Auth" line in the Resource Map (`SKILL.md` ~lines 184–209) gains an SSO field that emits `` and an optional `Custom JWT: ` field, both of which are handoff signals. + +`plugin/skills/formio-resource-planner/references/template-json.md` is **unchanged**. Action shapes (Login, Role Assignment, Group Assignment), role objects, `access`/`submissionAccess` patterns, and group-reference selects remain canonical there. `formio-auth` references them by file path. + +### 6. CLAUDE.md update + +Add `formio-auth` to the Skills Library section as a peer of `formio-resource-planner`, name the planner ↔ auth handoff explicitly, and document the layout convention (`SKILL.md` + narrative `references/`). + +## Risks / Trade-offs + +- **Risk**: Triggers overlap between `formio-auth` ("roles", "permissions") and `formio-resource-planner` ("roles" as part of resource map). → **Mitigation**: planner's "Not for" clause names auth explicitly; `formio-auth`'s positive triggers focus on the *configuration* of auth, not on naming roles inside a resource map. +- **Risk**: Drift between the planner's canonical `template.json` action shapes and any examples copied into `formio-auth`. → **Mitigation**: `formio-auth` references action shapes by `references/template-json.md` file path + line range instead of forking them. JSON examples in `formio-auth` are limited to JWT payloads, SSO settings blocks, and Token Swap headers — content the planner does not own. +- **Risk**: SSO/Token-Swap/Custom-JWT/email-token docs lag the Form.io product if vendors change provider settings. → **Mitigation**: each SSO doc links back to the authoritative `https://help.form.io/developers/auth` page and the per-provider sub-pages; reference docs name "see the linked Form.io docs for current settings" for provider-specific UI fields. +- **Risk**: Validator does not gate `formio-auth` content, so quality drift goes uncaught by `pnpm test`. → **Mitigation**: out of scope for this change; a follow-up can add an eval harness (`evals/evals.json` + `evals/grade.py`) modeled on `formio-resource-planner/evals/`. +- **Trade-off**: Eleven topic files is a lot of surface area. Single-file alternative was rejected (see Decision 1); the cost is real but bounded — each file is short and topic-scoped, and an agent only loads the one it needs. + +## Migration Plan + +This is a documentation-only change; there is no runtime migration. Steps: + +1. Author `plugin/skills/formio-auth/SKILL.md` + the eleven reference docs. +2. Update `plugin/skills/formio-resource-planner/SKILL.md` description (negative-trigger) and "Users & Auth" section (handoff prose + new SSO/Custom-JWT fields). +3. Update `CLAUDE.md` Skills Library section. +4. Run `pnpm test` (validator is scoped to `formio-api` so it should pass without changes), `pnpm lint`, `pnpm format`. +5. No rollback action needed beyond `git revert` — change is additive in `plugin/skills/formio-auth/` and narrative-only in the planner. + +## Open Questions + +- Should `formio-auth` carry its own `examples/` directory (e.g., a working OIDC config snippet, a Custom JWT generator, an email-token webhook) the way `formio-resource-planner/references/examples/` does? Current decision: **no in this change** — keep examples inline in the reference docs to limit surface area. Revisit when the eval harness is added. +- Should the planner's `Users & Auth` line gain a structured `Token Swap: ` field, or is naming SSO + Custom JWT sufficient as a handoff signal? Current decision: **SSO + Custom JWT fields only**; Token Swap is a sub-case of OIDC and the OIDC field carries it. +- 2FA and reCAPTCHA — own reference doc, or sub-section of `resource-auth.md` / `jwt-and-sessions.md`? Current decision: **sub-section of `jwt-and-sessions.md`** (since they layer on top of any auth method and gate JWT issuance), with a one-line pointer from `resource-auth.md`. Open to splitting out if the section grows past ~50 lines. diff --git a/openspec/changes/archive/2026-05-22-add-formio-auth-skill/proposal.md b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/proposal.md new file mode 100644 index 0000000..2f5bf49 --- /dev/null +++ b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/proposal.md @@ -0,0 +1,44 @@ +## Why + +Form.io authentication and authorization knowledge is currently locked inside `formio-resource-planner`, where it lives as supporting material for resource design (role taxonomy, Login/Role Assignment/Group Assignment actions, `access` vs `submissionAccess`, group-based access patterns). Agents asking auth-only questions ("how do I wire OIDC into Form.io?", "what does the JWT payload look like?", "how do I issue a Custom JWT for an on-prem deployment?", "how do I configure email-token authentication?") have no first-class skill to activate, and the planner has no room to cover SSO (OIDC/SAML/LDAP), Token Swap, Custom JWT, email-token, or the eight-permission RBAC matrix in the depth they deserve. A stand-alone `formio-auth` skill makes auth a peer of resource planning, so the planner can stay focused on data modeling and hand off cleanly to an auth-specialist skill once the resource map is settled. + +## What Changes + +- Add a new top-level Claude skill at `plugin/skills/formio-auth/` with `SKILL.md` and a `references/` directory covering the full Form.io auth surface. +- `SKILL.md` activates on auth/authorization-only triggers (login, JWT, SSO, OIDC, SAML, LDAP, Token Swap, Custom JWT, email token, roles, permissions, RBAC, group permissions) and disambiguates against `formio-resource-planner` (data modeling) and `formio-application` (full-stack orchestrator) via "Use when…" + "Not for…" clauses, following the same three-clause description template enforced by `packages/mcp-server/src/skills-validator.ts`. +- Author the following reference documents under `plugin/skills/formio-auth/references/` (one `.md` per topic, no frontmatter, MCP-tool-preference section where applicable): + - `resource-auth.md` — Resource-backed login with Login Action + Role Assignment Action + - `login-forms.md` — Login & registration form patterns (`access`, `submissionAccess`, anonymous self-register) + - `roles-and-permissions.md` — Default roles, eight permission types (`create_own`/`create_all`/`read_own`/`read_all`/`update_own`/`update_all`/`delete_own`/`delete_all`), project/form/submission scopes + - `group-permissions.md` — Group Assignment Action, field-based `submissionAccess` (two-halves pattern), transitive group access via hidden calculated mirrors + - `sso-oidc.md` — OAuth/OpenID Connect provider setup + OAuth Role Mapping + - `sso-saml.md` — SAML provider setup + SAML Role Mapping + - `sso-ldap.md` — LDAP provider setup + LDAP Role Mapping + - `token-swap.md` — Exchanging an external OIDC token for a Form.io JWT + - `custom-jwt.md` — Enterprise/on-prem Custom JWT (`JWT_SECRET`, payload shape, `localStorage.formioToken` injection) + - `email-auth.md` — Email-token (passwordless) authentication + - `jwt-and-sessions.md` — JWT payload, `x-jwt-token` header, `jti` Session ID, logout semantics, 2FA & reCAPTCHA +- Add the canonical Form.io portal-login JWT auth paragraph and MCP Tool Preference section (preferring `role_*` / `form_*` / `project_*` / `authenticate` first-party MCP tools) to references where they apply, matching the patterns enforced by the skills validator. +- Cross-link `formio-auth` ↔ `formio-resource-planner`: planner emits a "next step → run formio-auth" handoff once roles/login forms/group joins are in the resource map, and `formio-auth` references the planner whenever a configuration requires a new resource, role, or form. +- Update `plugin/skills/formio-resource-planner/SKILL.md` and its `references/` so the auth coverage there shrinks to a high-level pointer ("for any auth-only or SSO question, activate `formio-auth`") and stops being the canonical home for SSO/Token-Swap/Custom-JWT/email-token content the planner never actually covered. **BREAKING** for any caller that previously expected the planner to answer SSO questions directly — they will be routed to `formio-auth` instead. Existing planner sections that are load-bearing for resource emission (roles in `template.json`, Login/Role-Assignment/Group-Assignment action shapes, `submissionAccess` patterns) stay in the planner; only narrative scoping changes. +- Update `CLAUDE.md` "Skills Library" section to list `formio-auth` alongside `formio-api`, `formio-application`, `formio-resource-planner`, and `formio-angular`, and to describe the planner ↔ auth handoff contract. + +## Capabilities + +### New Capabilities + +- `formio-auth-skill`: A stand-alone Claude skill that teaches an AI agent the complete Form.io auth/authz surface — resource-backed login, login-action + role-assignment-action wiring, login/registration form shapes, the eight-permission RBAC matrix, group permissions (single-level + transitive), SSO (OIDC/SAML/LDAP), Token Swap, Custom JWT, email-token, JWT/session mechanics — and coordinates with `formio-resource-planner` so an agent can hand off cleanly between data modeling and auth configuration. + +### Modified Capabilities + + + + +## Impact + +- **Affected code**: `plugin/skills/formio-auth/**` (new), `plugin/skills/formio-resource-planner/SKILL.md` + `plugin/skills/formio-resource-planner/references/**` (scoping changes — narrative pointers, no shape changes to action JSON or `submissionAccess` patterns). +- **Affected docs**: `CLAUDE.md` "Skills Library" section to mention `formio-auth` alongside the other top-level skills and describe the planner ↔ auth handoff. +- **APIs**: No MCP tool changes. `formio-auth` references existing tools (`authenticate`, `role_*`, `form_*`, `action_*`, `project_export`/`project_import`) under MCP Tool Preference sections. +- **Dependencies**: No new runtime deps. Skill is documentation-only. +- **Tests**: `pnpm test` already runs the skills validator over `plugin/skills/`. The validator is scoped to the `formio-api` router today and does not enforce shape rules on narrative skills, so no validator changes are needed. The change adds Markdown content only. +- **Eval harness**: Not in scope for this change. A future iteration may add `plugin/skills/formio-auth/evals/` following the `formio-resource-planner` / `formio-angular` pattern. diff --git a/openspec/changes/archive/2026-05-22-add-formio-auth-skill/specs/formio-auth-skill/spec.md b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/specs/formio-auth-skill/spec.md new file mode 100644 index 0000000..e41aa23 --- /dev/null +++ b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/specs/formio-auth-skill/spec.md @@ -0,0 +1,186 @@ +## ADDED Requirements + +### Requirement: Skill directory and entry point + +The skills library SHALL include a stand-alone skill at `plugin/skills/formio-auth/` whose entry point is `plugin/skills/formio-auth/SKILL.md` containing valid YAML frontmatter with `name: formio-auth` and a non-empty `description`. + +#### Scenario: SKILL.md exists and parses + +- **WHEN** `plugin/skills/formio-auth/SKILL.md` is parsed +- **THEN** the file SHALL exist and be non-empty +- **AND** its frontmatter SHALL contain `name: formio-auth` +- **AND** its frontmatter SHALL contain a non-empty `description` field + +#### Scenario: Reference directory exists + +- **WHEN** the skill is inspected +- **THEN** `plugin/skills/formio-auth/references/` SHALL exist as a directory + +### Requirement: Activation description uses the three-clause router template + +`plugin/skills/formio-auth/SKILL.md`'s `description` SHALL contain three discrete clauses in order: (1) a capability statement, (2) a "Use when the user asks to …" trigger clause, and (3) a "Not for: …" negative-trigger clause that explicitly disambiguates `formio-auth` from `formio-resource-planner`, `formio-application`, `formio-api`, and `formio-angular`. + +#### Scenario: Description contains the trigger clause + +- **WHEN** the SKILL.md frontmatter is read +- **THEN** the `description` SHALL contain the phrase `Use when` (case-insensitive) + +#### Scenario: Description contains the negative-trigger clause + +- **WHEN** the SKILL.md frontmatter is read +- **THEN** the `description` SHALL contain the phrase `Not for` (case-insensitive) +- **AND** the negative-trigger clause SHALL name `formio-resource-planner` and at least one of `formio-application` / `formio-api` / `formio-angular` + +### Requirement: Auth-mechanism coverage + +The skill SHALL provide a dedicated reference document for each of the following Form.io authentication and authorization mechanisms: + +- Resource-backed login (Login Action + Role Assignment Action) +- Login and registration form patterns +- Roles and the eight-permission RBAC matrix (`create_own`, `create_all`, `read_own`, `read_all`, `update_own`, `update_all`, `delete_own`, `delete_all`) across project, form-definition, and submission-data scopes +- Group Assignment Action and field-based resource access (single-level + transitive group access) +- SSO via OIDC / OAuth (with OAuth Role Mapping) +- SSO via SAML (with SAML Role Mapping) +- SSO via LDAP (with LDAP Role Mapping) +- Token Swap (exchanging an external OIDC token for a Form.io JWT) +- Custom JWT (Enterprise / on-prem, signed with `JWT_SECRET`) +- Email-token authentication +- JWT and session mechanics (`x-jwt-token` header, `jti` Session ID, logout semantics, 2FA, reCAPTCHA) + +Each topic SHALL be covered by exactly one reference document under `plugin/skills/formio-auth/references/`. + +#### Scenario: Required reference docs all exist + +- **WHEN** the skill is inspected +- **THEN** the following files SHALL all exist and be non-empty under `plugin/skills/formio-auth/references/`: + - `resource-auth.md` + - `login-forms.md` + - `roles-and-permissions.md` + - `group-permissions.md` + - `sso-oidc.md` + - `sso-saml.md` + - `sso-ldap.md` + - `token-swap.md` + - `custom-jwt.md` + - `email-auth.md` + - `jwt-and-sessions.md` + +#### Scenario: Reference docs name their topic in a top-level heading + +- **WHEN** each reference doc is parsed +- **THEN** the first top-level heading (`#` or `##`) SHALL name the topic covered by the file + +### Requirement: Reference docs have no YAML frontmatter + +Files under `plugin/skills/formio-auth/references/` SHALL NOT begin with a YAML frontmatter block. Only `SKILL.md` carries frontmatter. + +#### Scenario: Reference doc without frontmatter passes + +- **WHEN** any `plugin/skills/formio-auth/references/*.md` is parsed +- **THEN** the file SHALL NOT begin with a line equal to `---` + +### Requirement: Reference doc section layout + +Every reference doc under `plugin/skills/formio-auth/references/` SHALL contain these top-level Markdown headings, in this order: + +1. `## Overview` +2. `## When to use this` +3. `## Configuration` +4. `## MCP Tool Preference` +5. `## See also` + +#### Scenario: Reference doc layout present + +- **WHEN** any reference doc is parsed +- **THEN** the parsed top-level (`##`) headings SHALL include `Overview`, `When to use this`, `Configuration`, `MCP Tool Preference`, and `See also` +- **AND** these headings SHALL appear in the order listed above + +### Requirement: MCP Tool Preference names first-party tools + +Each reference doc's `## MCP Tool Preference` section SHALL state explicitly which first-party MCP tools (any of `authenticate`, `role_create`, `role_list`, `role_update`, `form_create`, `form_get`, `form_list`, `form_update`, `action_create`, `action_list`, `action_get`, `action_update`, `action_delete`, `action_type_get`, `action_types_list`, `project_export`, `project_import`) SHOULD be used for the operations the doc covers, OR state explicitly that the configuration MUST be performed via the Form.io project portal because no MCP tool covers it. + +#### Scenario: Tool Preference is non-empty and specific + +- **WHEN** any reference doc's `## MCP Tool Preference` section is read +- **THEN** the section SHALL be non-empty +- **AND** the section SHALL either name at least one first-party MCP tool from the approved list above, or state explicitly that the operation requires the Form.io project portal + +### Requirement: Canonical portal-login JWT paragraph in JWT-aware docs + +`plugin/skills/formio-auth/references/jwt-and-sessions.md` and `plugin/skills/formio-auth/references/resource-auth.md` SHALL each contain the canonical portal-login JWT authentication paragraph used elsewhere in the library (the same paragraph enforced by `packages/mcp-server/src/skills-validator.ts` for `formio-api` references). Other reference docs in `formio-auth` MAY link to that paragraph instead of copying it. + +#### Scenario: Canonical paragraph present in JWT-aware docs + +- **WHEN** `jwt-and-sessions.md` and `resource-auth.md` are read +- **THEN** each file SHALL contain the canonical portal-login JWT auth paragraph verbatim + +### Requirement: Terminology — baseUrl vs projectUrl + +Reference docs under `plugin/skills/formio-auth/references/` SHALL NOT describe the project endpoint using `baseUrl` / `base_url`, and SHALL NOT describe the platform deployment endpoint using `projectUrl` / `project_url`. The canonical mapping is: + +- `baseUrl` / `base_url` → `FORMIO_BASE_URL` (platform deployment endpoint) +- `projectUrl` / `project_url` → `FORMIO_PROJECT_URL` (project endpoint) + +#### Scenario: baseUrl misuse fails review + +- **WHEN** a reference doc contains prose that uses `baseUrl` to describe a project-scoped operation +- **THEN** the doc SHALL be corrected before merge + +### Requirement: Cross-skill handoff to formio-resource-planner + +Every reference doc under `plugin/skills/formio-auth/references/` SHALL contain a `## See also` section that names `formio-resource-planner` whenever the configuration covered by the doc depends on resources, roles, or forms that the planner is responsible for creating (Resource-backed login, login forms, registration forms, roles and permissions, group permissions). For SSO / Token Swap / Custom JWT / email-token docs the `## See also` section SHALL cross-reference at least one neighboring reference inside `plugin/skills/formio-auth/references/` (for example, `jwt-and-sessions.md`). + +#### Scenario: Resource-dependent doc references the planner + +- **WHEN** `resource-auth.md`, `login-forms.md`, `roles-and-permissions.md`, or `group-permissions.md` is read +- **THEN** the `## See also` section SHALL name `formio-resource-planner` + +#### Scenario: SSO / Token-Swap / Custom-JWT / email-token docs cross-link neighbors + +- **WHEN** `sso-oidc.md`, `sso-saml.md`, `sso-ldap.md`, `token-swap.md`, `custom-jwt.md`, or `email-auth.md` is read +- **THEN** the `## See also` section SHALL link to at least one other reference doc in `plugin/skills/formio-auth/references/` + +### Requirement: Planner skill emits a handoff to formio-auth + +`plugin/skills/formio-resource-planner/SKILL.md` SHALL be updated so that: + +- Its `description` includes a "Not for" negative-trigger clause that names SSO (OIDC / SAML / LDAP), Token Swap, Custom JWT, email-token authentication, JWT/session mechanics, and 2FA as topics that route to `formio-auth`. +- Its "Users & Auth" Resource Map section emits an `SSO: ` field and a `Custom JWT: ` field. +- Its narrative includes a "Next steps" pointer naming `formio-auth` whenever the user's requirements include any of: SSO, Token Swap, Custom JWT, email-token auth, JWT customization, 2FA, or RBAC tuning beyond default roles. + +The planner's JSON-shape references (`references/template-json.md`) SHALL remain the single source of truth for Login Action, Role Assignment Action, Group Assignment Action, role objects, `access` arrays, `submissionAccess` arrays, and field-based `submissionAccess` on group-reference selects. The `formio-auth` skill SHALL reference those shapes by file path rather than forking them. + +#### Scenario: Planner description names the auth handoff + +- **WHEN** `plugin/skills/formio-resource-planner/SKILL.md` frontmatter is read +- **THEN** the `description` SHALL contain a "Not for" clause that names `formio-auth` and at least three of: SSO, OIDC, SAML, LDAP, Token Swap, Custom JWT, email token, JWT, 2FA + +#### Scenario: Planner Users & Auth emits SSO and Custom JWT fields + +- **WHEN** the planner's "Users & Auth" Resource Map template is read +- **THEN** the template SHALL include an `SSO:` field whose value is one of `none | OIDC | SAML | LDAP` +- **AND** the template SHALL include a `Custom JWT:` field whose value is `yes` or `no` + +#### Scenario: Planner action JSON shapes remain canonical + +- **WHEN** `plugin/skills/formio-resource-planner/references/template-json.md` is read +- **THEN** the Login Action, Role Assignment Action, and Group Assignment Action JSON shapes SHALL remain present and unmodified except for narrative scoping changes that route SSO / Token-Swap / Custom-JWT / email-token questions to `formio-auth` + +### Requirement: CLAUDE.md lists formio-auth in the Skills Library section + +`CLAUDE.md`'s "Skills Library" section SHALL list `formio-auth` as a peer of `formio-api`, `formio-application`, `formio-resource-planner`, and `formio-angular`, and SHALL describe the planner ↔ auth handoff contract (planner emits roles, login forms, and group joins; `formio-auth` covers SSO, Token Swap, Custom JWT, email-token, and RBAC tuning). + +#### Scenario: CLAUDE.md mentions formio-auth + +- **WHEN** `CLAUDE.md` is read +- **THEN** the "Skills Library" section SHALL contain the string `formio-auth` +- **AND** the section SHALL describe the planner ↔ auth handoff + +### Requirement: Definition of Done — pnpm test, lint, and format pass + +After implementation, the change SHALL satisfy the repository's Definition of Done: `pnpm test`, `pnpm lint`, and `pnpm format` SHALL all complete successfully. + +#### Scenario: All gates green + +- **WHEN** a developer runs `pnpm test`, `pnpm lint`, and `pnpm format` from the repo root +- **THEN** all three commands SHALL exit with status 0 diff --git a/openspec/changes/archive/2026-05-22-add-formio-auth-skill/tasks.md b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/tasks.md new file mode 100644 index 0000000..1217618 --- /dev/null +++ b/openspec/changes/archive/2026-05-22-add-formio-auth-skill/tasks.md @@ -0,0 +1,195 @@ +## 1. Skill scaffold and SKILL.md + + +### Red + +- [x] 1.1 In a new Vitest file `packages/mcp-server/src/formio-auth-skill.test.ts`, write failing tests that assert: `plugin/skills/formio-auth/SKILL.md` exists and is non-empty; its YAML frontmatter parses; `frontmatter.name === 'formio-auth'`; `frontmatter.description` is a non-empty string. +- [x] 1.2 In the same file, write a failing test that asserts `plugin/skills/formio-auth/references/` exists and is a directory. + +### Green + +- [x] 1.3 Create `plugin/skills/formio-auth/SKILL.md` with valid frontmatter (`name: formio-auth`, placeholder description for now), an `## Overview` body section, and a "Map of references" body section that lists each reference doc by filename + one-line topic summary. +- [x] 1.4 Create the empty directory `plugin/skills/formio-auth/references/` (with a `.gitkeep` if needed) so the directory-existence test passes ahead of file creation in later groups. + +### Refactor + +- [x] 1.5 Review implementation and refactor as needed. + +## 2. Activation description — three-clause template + + +### Red + +- [x] 2.1 Write a failing test that the `formio-auth` SKILL.md `description` contains the phrase `Use when` (case-insensitive). +- [x] 2.2 Write a failing test that the `description` contains the phrase `Not for` (case-insensitive) AND mentions `formio-resource-planner` AND mentions at least one of `formio-application` / `formio-api` / `formio-angular`. + +### Green + +- [x] 2.3 Author the final `description` in `plugin/skills/formio-auth/SKILL.md` as a single string with three clauses: (a) capability statement covering resource login, Login/Role-Assignment/Group-Assignment actions, RBAC, group permissions, SSO (OIDC/SAML/LDAP), Token Swap, Custom JWT, email-token, JWT/session mechanics; (b) `Use when the user asks to …` listing the auth-only triggers; (c) `Not for: …` naming `formio-resource-planner` (resource modeling), `formio-application` (full-stack orchestration), `formio-api` (endpoint lookup), and `formio-angular` (front-end UI wiring). + +### Refactor + +- [x] 2.4 Review implementation and refactor as needed. + +## 3. Reference docs — existence, naming heading, no frontmatter + + +### Red + +- [x] 3.1 Write failing tests asserting that all 11 required reference files exist and are non-empty under `plugin/skills/formio-auth/references/`: `resource-auth.md`, `login-forms.md`, `roles-and-permissions.md`, `group-permissions.md`, `sso-oidc.md`, `sso-saml.md`, `sso-ldap.md`, `token-swap.md`, `custom-jwt.md`, `email-auth.md`, `jwt-and-sessions.md`. +- [x] 3.2 Write a failing test that each reference doc's first top-level heading (`#` or `##`) contains the topic word(s) (e.g., `resource-auth.md` first heading mentions "Resource", `sso-oidc.md` first heading mentions "OIDC" or "OAuth"). +- [x] 3.3 Write a failing test that no reference doc begins with a line equal to `---` (i.e., no YAML frontmatter on reference docs). + +### Green + +- [x] 3.4 Create all 11 reference docs with their first heading naming the topic, body content stubbed out (skeleton sections from group 4 will fill them), and no frontmatter. + +### Refactor + +- [x] 3.5 Review implementation and refactor as needed. + +## 4. Reference doc section layout + + +### Red + +- [x] 4.1 Write a failing test that each reference doc contains all five required `##` headings (`Overview`, `When to use this`, `Configuration`, `MCP Tool Preference`, `See also`) AND that they appear in that order. + +### Green + +- [x] 4.2 In each of the 11 reference docs, populate the five required sections with topic-specific content drawn from the design.md "What Changes" and the inputs in the change directory: + - `resource-auth.md` — Login Action + Role Assignment Action, JWT in `x-jwt-token`, bcrypt password compare, six-step Form.io auth flow; references the planner's `template-json.md` for action JSON shapes. + - `login-forms.md` — login + registration form shapes; anonymous `create_own`; admin `create_all`/`read_all`/`update_all`/`delete_all`; brute-force settings; references planner. + - `roles-and-permissions.md` — three default roles + custom roles; the eight permission types; project / form-definition / submission-data scopes; `Everyone` fixed ID `00000000000000000000000`; `update_all` implies `create_all` on submissions. + - `group-permissions.md` — Group Assignment Action JSON shape; field-based `submissionAccess` (two-halves model); transitive group access via hidden calculated mirror. + - `sso-oidc.md` — OAuth/OIDC provider setup + OAuth Role Mapping. + - `sso-saml.md` — SAML provider setup + SAML Role Mapping. + - `sso-ldap.md` — LDAP provider setup + LDAP Role Mapping. + - `token-swap.md` — exchanging an external OIDC token for a Form.io JWT. + - `custom-jwt.md` — `JWT_SECRET` env var, required payload shape (`external: true`, `form._id`, `project._id`, `user._id`, `user.data`, `user.roles`), `localStorage.formioToken` injection. + - `email-auth.md` — email/passwordless token flow. + - `jwt-and-sessions.md` — JWT payload (`iss`, `sub`, `jti`, `iat`, `exp`), `x-jwt-token`, logout invalidates `jti`, 2FA, reCAPTCHA. + +### Refactor + +- [x] 4.3 Review implementation and refactor as needed. + +## 5. MCP Tool Preference content + + +### Red + +- [x] 5.1 Write a failing test that the `## MCP Tool Preference` section in each reference doc is non-empty AND either names at least one approved first-party MCP tool (`authenticate`, `role_create`, `role_list`, `role_update`, `form_create`, `form_get`, `form_list`, `form_update`, `action_create`, `action_list`, `action_get`, `action_update`, `action_delete`, `action_type_get`, `action_types_list`, `project_export`, `project_import`) or contains the phrase `Form.io project portal` (or equivalent "portal-only" wording — define the exact string to test for). + +### Green + +- [x] 5.2 Fill the `## MCP Tool Preference` section in each reference doc: + - `resource-auth.md` → `form_create` + `action_create` (Login + Role Assignment actions), `role_create`/`role_list` for roles; `authenticate` for first-time portal login. + - `login-forms.md` → `form_create` (login + register forms). + - `roles-and-permissions.md` → `role_create` / `role_list` / `role_update` for roles; `form_update` for `access` / `submissionAccess` updates. + - `group-permissions.md` → `action_create` (Group Assignment) + `form_update` (field-based `submissionAccess`). + - `sso-oidc.md` / `sso-saml.md` / `sso-ldap.md` → "Form.io project portal" (UI-only; no MCP tool covers SSO provider configuration today). + - `token-swap.md` → "Form.io project portal" + reference to `runtime-auth` endpoints in the `formio-api` skill. + - `custom-jwt.md` → "Form.io project portal" for `JWT_SECRET`; reference the `runtime-auth` reference in `formio-api` for the token-exchange endpoints. + - `email-auth.md` → `action_create` (Email Authentication action) + `form_create`. + - `jwt-and-sessions.md` → `authenticate` for portal login; reference `runtime-auth` for `/logout` endpoint. + +### Refactor + +- [x] 5.3 Review implementation and refactor as needed. + +## 6. Canonical portal-login JWT paragraph + + +### Red + +- [x] 6.1 Write a failing test that both `plugin/skills/formio-auth/references/jwt-and-sessions.md` and `plugin/skills/formio-auth/references/resource-auth.md` contain the exact same canonical portal-login JWT authentication paragraph used by `formio-api` (import `CANONICAL_AUTH_PARAGRAPH` from `packages/mcp-server/src/skills-validator.ts` for the comparison string). + +### Green + +- [x] 6.2 Insert `CANONICAL_AUTH_PARAGRAPH` verbatim into `jwt-and-sessions.md` (inside its `## Configuration` section or a dedicated `## Authentication` sub-heading) and into `resource-auth.md` (inside `## Configuration`), copied character-for-character from `skills-validator.ts`. + +### Refactor + +- [x] 6.3 Review implementation and refactor as needed. + +## 7. Terminology — baseUrl vs projectUrl + + +### Red + +- [x] 7.1 Write a failing test that no reference doc under `plugin/skills/formio-auth/references/` contains the case-insensitive prose pattern that misuses `baseUrl` for a project-scoped operation or `projectUrl` for a platform-deployment operation. (Strip fenced + inline code blocks first, matching the planner-style stripping in `skills-validator.ts`.) + +### Green + +- [x] 7.2 Audit each reference doc and rewrite any prose that misuses `baseUrl` / `base_url` / `projectUrl` / `project_url` to the canonical `FORMIO_BASE_URL` / `FORMIO_PROJECT_URL` env-var names. + +### Refactor + +- [x] 7.3 Review implementation and refactor as needed. + +## 8. Cross-skill handoff in See also + + +### Red + +- [x] 8.1 Write a failing test that each of `resource-auth.md`, `login-forms.md`, `roles-and-permissions.md`, `group-permissions.md` contains the string `formio-resource-planner` inside its `## See also` section. +- [x] 8.2 Write a failing test that each of `sso-oidc.md`, `sso-saml.md`, `sso-ldap.md`, `token-swap.md`, `custom-jwt.md`, `email-auth.md` contains a link to at least one other reference doc filename inside its `## See also` section. + +### Green + +- [x] 8.3 Add `formio-resource-planner` references to the four resource-dependent docs' `## See also` sections. +- [x] 8.4 Add neighbor cross-links (e.g., `[jwt-and-sessions.md](./jwt-and-sessions.md)`) to the six SSO / Token Swap / Custom JWT / email-token docs' `## See also` sections. + +### Refactor + +- [x] 8.5 Review implementation and refactor as needed. + +## 9. Planner skill update — description and Users & Auth section + + +### Red + +- [x] 9.1 Write a failing test that `plugin/skills/formio-resource-planner/SKILL.md` description contains `Not for` AND names `formio-auth` AND mentions at least three of: `SSO`, `OIDC`, `SAML`, `LDAP`, `Token Swap`, `Custom JWT`, `email token`, `JWT`, `2FA`. +- [x] 9.2 Write a failing test that the planner's "Users & Auth" Resource Map section (in `SKILL.md` or `references/template-md.md`) emits an `SSO:` field with value pattern `none | OIDC | SAML | LDAP` AND a `Custom JWT:` field with value `yes | no`. + +### Green + +- [x] 9.3 Edit `plugin/skills/formio-resource-planner/SKILL.md` description to add the "Not for" auth-handoff clause without breaking existing planner triggers; add a "Next steps → activate `formio-auth`" pointer to the narrative wherever the user's requirements include SSO / Token Swap / Custom JWT / email-token / 2FA / RBAC tuning. +- [x] 9.4 Update the planner's "Users & Auth" Resource Map template (`SKILL.md` ~lines 184–209 and `references/template-md.md` ~lines 68–114) to emit the new `SSO:` and `Custom JWT:` fields. + +### Refactor + +- [x] 9.5 Review implementation and refactor as needed; verify that planner JSON shapes in `references/template-json.md` are unchanged. + +## 10. CLAUDE.md Skills Library section update + + +### Red + +- [x] 10.1 Write a failing test (or an assertion in the existing `formio-auth-skill.test.ts`) that `CLAUDE.md` contains the string `formio-auth` AND contains a description of the planner ↔ auth handoff (e.g., contains both `formio-resource-planner` and `formio-auth` within ~10 lines of each other). + +### Green + +- [x] 10.2 Update the "Skills Library" section of `CLAUDE.md` to list `formio-auth` as a peer of `formio-api`, `formio-application`, `formio-resource-planner`, and `formio-angular`, and to describe the planner ↔ auth handoff (planner owns resources/roles/forms + action JSON shapes; `formio-auth` owns SSO, Token Swap, Custom JWT, email-token, JWT/session mechanics, RBAC tuning). + +### Refactor + +- [x] 10.3 Review implementation and refactor as needed. + +## 11. Definition of Done gates + + +### Red + +- [x] 11.1 Run `pnpm test` from the repo root and confirm that the new `formio-auth-skill.test.ts` tests all pass AND that all pre-existing tests (including the `formio-api` skills validator suite) still pass. +- [x] 11.2 Run `pnpm lint` and confirm zero TypeScript errors. +- [x] 11.3 Run `pnpm format` and confirm no formatting drift. + +### Green + +- [x] 11.4 Fix any failure surfaced by 11.1 / 11.2 / 11.3 until all three commands exit with status 0. + +### Refactor + +- [x] 11.5 Review implementation and refactor as needed. diff --git a/openspec/specs/formio-auth-skill/spec.md b/openspec/specs/formio-auth-skill/spec.md new file mode 100644 index 0000000..e41aa23 --- /dev/null +++ b/openspec/specs/formio-auth-skill/spec.md @@ -0,0 +1,186 @@ +## ADDED Requirements + +### Requirement: Skill directory and entry point + +The skills library SHALL include a stand-alone skill at `plugin/skills/formio-auth/` whose entry point is `plugin/skills/formio-auth/SKILL.md` containing valid YAML frontmatter with `name: formio-auth` and a non-empty `description`. + +#### Scenario: SKILL.md exists and parses + +- **WHEN** `plugin/skills/formio-auth/SKILL.md` is parsed +- **THEN** the file SHALL exist and be non-empty +- **AND** its frontmatter SHALL contain `name: formio-auth` +- **AND** its frontmatter SHALL contain a non-empty `description` field + +#### Scenario: Reference directory exists + +- **WHEN** the skill is inspected +- **THEN** `plugin/skills/formio-auth/references/` SHALL exist as a directory + +### Requirement: Activation description uses the three-clause router template + +`plugin/skills/formio-auth/SKILL.md`'s `description` SHALL contain three discrete clauses in order: (1) a capability statement, (2) a "Use when the user asks to …" trigger clause, and (3) a "Not for: …" negative-trigger clause that explicitly disambiguates `formio-auth` from `formio-resource-planner`, `formio-application`, `formio-api`, and `formio-angular`. + +#### Scenario: Description contains the trigger clause + +- **WHEN** the SKILL.md frontmatter is read +- **THEN** the `description` SHALL contain the phrase `Use when` (case-insensitive) + +#### Scenario: Description contains the negative-trigger clause + +- **WHEN** the SKILL.md frontmatter is read +- **THEN** the `description` SHALL contain the phrase `Not for` (case-insensitive) +- **AND** the negative-trigger clause SHALL name `formio-resource-planner` and at least one of `formio-application` / `formio-api` / `formio-angular` + +### Requirement: Auth-mechanism coverage + +The skill SHALL provide a dedicated reference document for each of the following Form.io authentication and authorization mechanisms: + +- Resource-backed login (Login Action + Role Assignment Action) +- Login and registration form patterns +- Roles and the eight-permission RBAC matrix (`create_own`, `create_all`, `read_own`, `read_all`, `update_own`, `update_all`, `delete_own`, `delete_all`) across project, form-definition, and submission-data scopes +- Group Assignment Action and field-based resource access (single-level + transitive group access) +- SSO via OIDC / OAuth (with OAuth Role Mapping) +- SSO via SAML (with SAML Role Mapping) +- SSO via LDAP (with LDAP Role Mapping) +- Token Swap (exchanging an external OIDC token for a Form.io JWT) +- Custom JWT (Enterprise / on-prem, signed with `JWT_SECRET`) +- Email-token authentication +- JWT and session mechanics (`x-jwt-token` header, `jti` Session ID, logout semantics, 2FA, reCAPTCHA) + +Each topic SHALL be covered by exactly one reference document under `plugin/skills/formio-auth/references/`. + +#### Scenario: Required reference docs all exist + +- **WHEN** the skill is inspected +- **THEN** the following files SHALL all exist and be non-empty under `plugin/skills/formio-auth/references/`: + - `resource-auth.md` + - `login-forms.md` + - `roles-and-permissions.md` + - `group-permissions.md` + - `sso-oidc.md` + - `sso-saml.md` + - `sso-ldap.md` + - `token-swap.md` + - `custom-jwt.md` + - `email-auth.md` + - `jwt-and-sessions.md` + +#### Scenario: Reference docs name their topic in a top-level heading + +- **WHEN** each reference doc is parsed +- **THEN** the first top-level heading (`#` or `##`) SHALL name the topic covered by the file + +### Requirement: Reference docs have no YAML frontmatter + +Files under `plugin/skills/formio-auth/references/` SHALL NOT begin with a YAML frontmatter block. Only `SKILL.md` carries frontmatter. + +#### Scenario: Reference doc without frontmatter passes + +- **WHEN** any `plugin/skills/formio-auth/references/*.md` is parsed +- **THEN** the file SHALL NOT begin with a line equal to `---` + +### Requirement: Reference doc section layout + +Every reference doc under `plugin/skills/formio-auth/references/` SHALL contain these top-level Markdown headings, in this order: + +1. `## Overview` +2. `## When to use this` +3. `## Configuration` +4. `## MCP Tool Preference` +5. `## See also` + +#### Scenario: Reference doc layout present + +- **WHEN** any reference doc is parsed +- **THEN** the parsed top-level (`##`) headings SHALL include `Overview`, `When to use this`, `Configuration`, `MCP Tool Preference`, and `See also` +- **AND** these headings SHALL appear in the order listed above + +### Requirement: MCP Tool Preference names first-party tools + +Each reference doc's `## MCP Tool Preference` section SHALL state explicitly which first-party MCP tools (any of `authenticate`, `role_create`, `role_list`, `role_update`, `form_create`, `form_get`, `form_list`, `form_update`, `action_create`, `action_list`, `action_get`, `action_update`, `action_delete`, `action_type_get`, `action_types_list`, `project_export`, `project_import`) SHOULD be used for the operations the doc covers, OR state explicitly that the configuration MUST be performed via the Form.io project portal because no MCP tool covers it. + +#### Scenario: Tool Preference is non-empty and specific + +- **WHEN** any reference doc's `## MCP Tool Preference` section is read +- **THEN** the section SHALL be non-empty +- **AND** the section SHALL either name at least one first-party MCP tool from the approved list above, or state explicitly that the operation requires the Form.io project portal + +### Requirement: Canonical portal-login JWT paragraph in JWT-aware docs + +`plugin/skills/formio-auth/references/jwt-and-sessions.md` and `plugin/skills/formio-auth/references/resource-auth.md` SHALL each contain the canonical portal-login JWT authentication paragraph used elsewhere in the library (the same paragraph enforced by `packages/mcp-server/src/skills-validator.ts` for `formio-api` references). Other reference docs in `formio-auth` MAY link to that paragraph instead of copying it. + +#### Scenario: Canonical paragraph present in JWT-aware docs + +- **WHEN** `jwt-and-sessions.md` and `resource-auth.md` are read +- **THEN** each file SHALL contain the canonical portal-login JWT auth paragraph verbatim + +### Requirement: Terminology — baseUrl vs projectUrl + +Reference docs under `plugin/skills/formio-auth/references/` SHALL NOT describe the project endpoint using `baseUrl` / `base_url`, and SHALL NOT describe the platform deployment endpoint using `projectUrl` / `project_url`. The canonical mapping is: + +- `baseUrl` / `base_url` → `FORMIO_BASE_URL` (platform deployment endpoint) +- `projectUrl` / `project_url` → `FORMIO_PROJECT_URL` (project endpoint) + +#### Scenario: baseUrl misuse fails review + +- **WHEN** a reference doc contains prose that uses `baseUrl` to describe a project-scoped operation +- **THEN** the doc SHALL be corrected before merge + +### Requirement: Cross-skill handoff to formio-resource-planner + +Every reference doc under `plugin/skills/formio-auth/references/` SHALL contain a `## See also` section that names `formio-resource-planner` whenever the configuration covered by the doc depends on resources, roles, or forms that the planner is responsible for creating (Resource-backed login, login forms, registration forms, roles and permissions, group permissions). For SSO / Token Swap / Custom JWT / email-token docs the `## See also` section SHALL cross-reference at least one neighboring reference inside `plugin/skills/formio-auth/references/` (for example, `jwt-and-sessions.md`). + +#### Scenario: Resource-dependent doc references the planner + +- **WHEN** `resource-auth.md`, `login-forms.md`, `roles-and-permissions.md`, or `group-permissions.md` is read +- **THEN** the `## See also` section SHALL name `formio-resource-planner` + +#### Scenario: SSO / Token-Swap / Custom-JWT / email-token docs cross-link neighbors + +- **WHEN** `sso-oidc.md`, `sso-saml.md`, `sso-ldap.md`, `token-swap.md`, `custom-jwt.md`, or `email-auth.md` is read +- **THEN** the `## See also` section SHALL link to at least one other reference doc in `plugin/skills/formio-auth/references/` + +### Requirement: Planner skill emits a handoff to formio-auth + +`plugin/skills/formio-resource-planner/SKILL.md` SHALL be updated so that: + +- Its `description` includes a "Not for" negative-trigger clause that names SSO (OIDC / SAML / LDAP), Token Swap, Custom JWT, email-token authentication, JWT/session mechanics, and 2FA as topics that route to `formio-auth`. +- Its "Users & Auth" Resource Map section emits an `SSO: ` field and a `Custom JWT: ` field. +- Its narrative includes a "Next steps" pointer naming `formio-auth` whenever the user's requirements include any of: SSO, Token Swap, Custom JWT, email-token auth, JWT customization, 2FA, or RBAC tuning beyond default roles. + +The planner's JSON-shape references (`references/template-json.md`) SHALL remain the single source of truth for Login Action, Role Assignment Action, Group Assignment Action, role objects, `access` arrays, `submissionAccess` arrays, and field-based `submissionAccess` on group-reference selects. The `formio-auth` skill SHALL reference those shapes by file path rather than forking them. + +#### Scenario: Planner description names the auth handoff + +- **WHEN** `plugin/skills/formio-resource-planner/SKILL.md` frontmatter is read +- **THEN** the `description` SHALL contain a "Not for" clause that names `formio-auth` and at least three of: SSO, OIDC, SAML, LDAP, Token Swap, Custom JWT, email token, JWT, 2FA + +#### Scenario: Planner Users & Auth emits SSO and Custom JWT fields + +- **WHEN** the planner's "Users & Auth" Resource Map template is read +- **THEN** the template SHALL include an `SSO:` field whose value is one of `none | OIDC | SAML | LDAP` +- **AND** the template SHALL include a `Custom JWT:` field whose value is `yes` or `no` + +#### Scenario: Planner action JSON shapes remain canonical + +- **WHEN** `plugin/skills/formio-resource-planner/references/template-json.md` is read +- **THEN** the Login Action, Role Assignment Action, and Group Assignment Action JSON shapes SHALL remain present and unmodified except for narrative scoping changes that route SSO / Token-Swap / Custom-JWT / email-token questions to `formio-auth` + +### Requirement: CLAUDE.md lists formio-auth in the Skills Library section + +`CLAUDE.md`'s "Skills Library" section SHALL list `formio-auth` as a peer of `formio-api`, `formio-application`, `formio-resource-planner`, and `formio-angular`, and SHALL describe the planner ↔ auth handoff contract (planner emits roles, login forms, and group joins; `formio-auth` covers SSO, Token Swap, Custom JWT, email-token, and RBAC tuning). + +#### Scenario: CLAUDE.md mentions formio-auth + +- **WHEN** `CLAUDE.md` is read +- **THEN** the "Skills Library" section SHALL contain the string `formio-auth` +- **AND** the section SHALL describe the planner ↔ auth handoff + +### Requirement: Definition of Done — pnpm test, lint, and format pass + +After implementation, the change SHALL satisfy the repository's Definition of Done: `pnpm test`, `pnpm lint`, and `pnpm format` SHALL all complete successfully. + +#### Scenario: All gates green + +- **WHEN** a developer runs `pnpm test`, `pnpm lint`, and `pnpm format` from the repo root +- **THEN** all three commands SHALL exit with status 0 diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 101c096..a280ccf 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -11,7 +11,7 @@ "formio_base_url": { "type": "string", "title": "Form.io base URL", - "description": "Base URL of your Form.io deployment (e.g. https://api.form.io for Form.io Cloud, or your self-hosted host). Used as the parent of FORMIO_PROJECT_URL.", + "description": "Base URL of your Form.io deployment (e.g. https://api.form.io for Form.io SaaS, or your self-hosted host). Used as the parent of FORMIO_PROJECT_URL.", "required": true }, "formio_default_project_url": { diff --git a/plugin/skills/formio-application/DEPLOYMENT.md b/plugin/skills/formio-application/DEPLOYMENT.md index bf10259..6e29546 100644 --- a/plugin/skills/formio-application/DEPLOYMENT.md +++ b/plugin/skills/formio-application/DEPLOYMENT.md @@ -15,7 +15,7 @@ Two URLs that Step 4 (MCP Config), Step 5 (Import), and the Step 6 framework han When asking the user, use descriptions that do NOT assume they know "project" vs. "deployment" vocabulary: -- **Base URL** — "The Form.io deployment your project lives on. If you are using the hosted Form.io cloud, this is `https://api.form.io`. If your team self-hosts Form.io, this is the address of your platform (e.g., `https://forms.acme-corp.com`). This is the platform, not the specific project." +- **Base URL** — "The Form.io deployment your project lives on. If you are using the hosted Form.io SaaS, this is `https://api.form.io`. If your team self-hosts Form.io, this is the address of your platform (e.g., `https://forms.acme-corp.com`). This is the platform, not the specific project." - **Project URL** — "The full URL of the specific Form.io project this template will be imported into and the app will talk to. Example: `https://mycompany.form.io` for a hosted project, or `https://forms.acme-corp.com/crm` for a self-hosted project identified by a path. The project must already exist — we do not create it." ## Run the interview — one batched `AskUserQuestion` @@ -30,7 +30,7 @@ AskUserQuestion({ header: "Base URL", multiSelect: false, options: [ - { label: "https://api.form.io", description: "Hosted Form.io cloud" }, + { label: "https://api.form.io", description: "Hosted Form.io SaaS" }, { label: "https://", description: "Your team's self-hosted Form.io deployment" } ] }, diff --git a/plugin/skills/formio-auth/SKILL.md b/plugin/skills/formio-auth/SKILL.md new file mode 100644 index 0000000..5ff495d --- /dev/null +++ b/plugin/skills/formio-auth/SKILL.md @@ -0,0 +1,74 @@ +--- +name: formio-auth +description: >- + Form.io authentication and authorization specialist — teaches an AI agent the full + Form.io auth surface, including resource-backed login with the Login Action plus + Role Assignment Action, login and registration form shapes, role-based access control + (the eight permission types across project, form-definition, and submission-data scopes), + group permissions (single-level plus transitive via field-based `submissionAccess`), + SSO via OIDC/OAuth/SAML/LDAP with provider role mapping, Token Swap from an external + OIDC token, Custom JWT for Enterprise/on-prem deployments signed with `JWT_SECRET`, + email-token (passwordless) authentication, and JWT/session mechanics (the `x-jwt-token` + header, `jti` Session ID, logout, 2FA, reCAPTCHA). + Use when the user asks to configure login, JWT, sessions, SSO, OIDC, OAuth, SAML, LDAP, + Token Swap, Custom JWT, passwordless/email-token auth, roles, permissions, RBAC, group + permissions, 2FA, reCAPTCHA, the Login Action, the Role Assignment Action, the Group + Assignment Action, or any auth/authorization concern in Form.io. + Not for designing the resource map, planning a new app's data model, or emitting a + `template.json` from scratch (those go through `formio-resource-planner`); orchestrating + the full build-an-app pipeline of plan → import → scaffold a framework (that goes through + `formio-application`); looking up a specific REST endpoint URL or HTTP shape (that goes + through `formio-api`); or wiring a front-end login screen, route guard, or + `FormioAuthService` into an Angular workspace (that goes through `formio-angular`). +--- + +## Overview + +`formio-auth` is the stand-alone skill for everything authentication and authorization in Form.io. It covers how a user proves identity (Resource login, SSO, Token Swap, Custom JWT, email token), how Form.io carries that identity on the wire (`x-jwt-token` header, JWT payload, `jti` Session ID), and how that identity gates access at three scopes (project, form definition, submission data) and through two access models (role-based and group-based). + +The skill is documentation-only. It does not emit `template.json`. When a configuration depends on resources, roles, or forms, this skill points to `formio-resource-planner`, which owns the canonical JSON shapes for roles, the Login Action, the Role Assignment Action, the Group Assignment Action, `access` arrays, `submissionAccess` arrays, and field-based `submissionAccess` on group-reference selects. + +## When to use this + +Activate `formio-auth` when the user is asking about identity, sessions, or access control inside a Form.io project. Sample triggers: + +- "How do I wire OIDC / SAML / LDAP into my Form.io project?" +- "How do I set up Token Swap from my own OAuth provider?" +- "We're on Form.io Enterprise on-prem — how do we forge a Custom JWT?" +- "How do I send the user a magic-link email instead of a password?" +- "Who can read submissions if the role has `read_own` but not `read_all`?" +- "How does group-based access work? What's the difference between single-level and transitive?" +- "How does logout work? What invalidates a JWT?" +- "Add 2FA / reCAPTCHA to my login flow." + +Not for: + +- Designing roles or login forms inside a fresh resource map → `formio-resource-planner`. +- "Build me a CRM" or "scaffold an Angular app for this project" → `formio-application`. +- "What's the URL of the `/login` endpoint?" → `formio-api`. +- "Generate the Angular login component" → `formio-angular`. + +## Map of references + +Each reference doc is self-contained and follows the section layout `Overview` → `When to use this` → `Configuration` → `MCP Tool Preference` → `See also`. + +- [`references/resource-auth.md`](./references/resource-auth.md) — Resource-backed login with the Login Action + Role Assignment Action, the six-step Form.io auth flow, and the `x-jwt-token` response header. +- [`references/login-forms.md`](./references/login-forms.md) — Login and registration form patterns: `access`, `submissionAccess`, anonymous self-register, brute-force protection settings. +- [`references/roles-and-permissions.md`](./references/roles-and-permissions.md) — Default roles, custom roles, the eight permission types (`create_own`, `create_all`, `read_own`, `read_all`, `update_own`, `update_all`, `delete_own`, `delete_all`) across project, form-definition, and submission-data scopes. +- [`references/group-permissions.md`](./references/group-permissions.md) — Group Assignment Action and field-based `submissionAccess`: the two-halves model for single-level group access, and the hidden calculated mirror for transitive group access. +- [`references/sso-oidc.md`](./references/sso-oidc.md) — OAuth / OpenID Connect provider setup plus OAuth Role Mapping. +- [`references/sso-saml.md`](./references/sso-saml.md) — SAML provider setup plus SAML Role Mapping. +- [`references/sso-ldap.md`](./references/sso-ldap.md) — LDAP directory setup plus LDAP Role Mapping. +- [`references/token-swap.md`](./references/token-swap.md) — Exchanging an external OIDC/OAuth bearer token for a Form.io JWT. +- [`references/custom-jwt.md`](./references/custom-jwt.md) — Enterprise/on-prem Custom JWT signed with `JWT_SECRET`, required payload shape, and `localStorage.formioToken` injection. +- [`references/email-auth.md`](./references/email-auth.md) — Email-token (passwordless) authentication via the Email Authentication action. +- [`references/jwt-and-sessions.md`](./references/jwt-and-sessions.md) — JWT payload, `x-jwt-token` header, `jti` Session ID, logout semantics, 2FA, and reCAPTCHA. + +## Handoff with formio-resource-planner + +The planner owns the data model. `formio-auth` owns the auth configuration that runs on top of it. The contract: + +- When the user is still designing roles, the user resource, login/registration forms, or a group join, run `formio-resource-planner` first. The planner emits a `template.json` with role objects, the Login Action, the Role Assignment Action, the Group Assignment Action, `submissionAccess` arrays, and field-based `submissionAccess` on group-reference selects. +- When the user is configuring SSO, Token Swap, Custom JWT, email-token auth, JWT customization, 2FA, reCAPTCHA, or tuning RBAC beyond the planner's defaults, hand off to `formio-auth`. + +Action JSON shapes are NOT duplicated here — they live in `plugin/skills/formio-resource-planner/references/template-json.md` and are referenced by file path from this skill. diff --git a/plugin/skills/formio-auth/references/custom-jwt.md b/plugin/skills/formio-auth/references/custom-jwt.md new file mode 100644 index 0000000..21a7743 --- /dev/null +++ b/plugin/skills/formio-auth/references/custom-jwt.md @@ -0,0 +1,116 @@ +# Custom JWT + +## Overview + +Custom JWT is Form.io Enterprise's escape hatch for on-prem deployments that want to issue Form.io tokens from their own backend. The customer mints a JWT signed with the deployment's `JWT_SECRET` environment variable; Form.io accepts it as if it had issued the token itself. Useful when Form.io is embedded behind another auth boundary (a Backend-for-Frontend, an internal portal, a session-tracked microservice) and a full OAuth/SAML/LDAP integration would be overkill. + +## When to use this + +Reach for Custom JWT when: + +- The deployment is Form.io Enterprise (on-prem). Custom JWT requires control over the deployment's `JWT_SECRET`, which Form.io SaaS does not expose. +- The customer's backend is already the source of truth for sessions and user identity. +- Embedding Form.io inside an existing app and re-using its session, not federating against an IdP, is the goal. + +Not for: + +- Form.io SaaS — `JWT_SECRET` is platform-managed and cannot be used to forge tokens. +- Federated identity → see [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md). +- Swapping an existing OIDC token for a Form.io JWT → see [`token-swap.md`](./token-swap.md). +- Issuing magic-link emails → see [`email-auth.md`](./email-auth.md). + +## Configuration + +### Deployment prerequisites + +1. On-prem Form.io Enterprise with a unique `JWT_SECRET` environment variable set in the Docker / Kubernetes deployment. Document the value in a secrets manager — it MUST match between the API server and any worker that issues tokens. +2. Roles created in the project. Each role you intend to assign has a stable MongoDB ID — note them down (or fetch them via `role_list`). +3. A `user` Resource (or whichever Form.io Resource you treat as the canonical user record) to point `form._id` at. This is a **passive association only** — it records where the user conceptually "belongs". The user forged by the Custom JWT is **ephemeral**: it is never written to this Resource and no submission row is ever created or required for it. +4. Form Access configured so the roles you mint into the JWT actually grant the access you expect — see [`roles-and-permissions.md`](./roles-and-permissions.md). + +### Required JWT payload shape + +The token MUST be signed with `JWT_SECRET` and carry this payload: + +```js +{ + external: true, + form: { _id: 'USER_RESOURCE_FORM_ID' }, + project: { _id: 'PROJECT_ID' }, + user: { + _id: 'external', + data: { name: 'joe' }, + roles: ['ROLE_ID_1', 'ROLE_ID_2'] + } +} +``` + +Required claim semantics: + +- `external: true` — flags the token as customer-issued so Form.io skips the normal credential lookup. +- `form._id` — the MongoDB ID of the `user` Resource form. This is a **passive association only** (which Resource the user conceptually belongs to); Form.io does not read a submission from it or write one to it. +- `project._id` — the MongoDB ID of the Form.io project. +- `user._id` — `"external"` is the sentinel for an **ephemeral user** that has no Form.io submission row and never will. The identity lives entirely in this token. +- `user.data` — arbitrary profile data the token carries (the renderer's `Formio.user` is hydrated from this). +- `user.roles` — an array of role MongoDB IDs that gate the user's access. + +### Generating the token (Node example) + +```js +import jwt from 'jsonwebtoken'; + +const token = jwt.sign({ + external: true, + form: { _id: '59795d259be16e3ee58fddaa' }, + project: { _id: '59795d259be16e3ee58fdda6' }, + user: { + _id: 'external', + data: { name: 'joe' }, + roles: ['59795d259be16e3ee58fdda7'] + } +}, process.env.JWT_SECRET); +``` + +The same library that signs (`jsonwebtoken` in Node, equivalents in Python, Go, Ruby, etc.) can be used in any backend the customer controls. Sign with HS256 unless the deployment is configured for a different algorithm. + +### Handing the token to the client + +After signing, the backend hands the JWT to the renderer by writing it into `localStorage` under the key `formioToken`: + +```html + +``` + +Once `formioToken` is present, the Form.io renderer attaches it as the `x-jwt-token` header on every subsequent Form.io request. The user is authenticated from that point on. + +For service-to-service callers (a backend script, a CI pipeline), attach the same `x-jwt-token` header on every HTTP request manually instead of writing to `localStorage`. + +### Rotation and revocation + +- The token's lifetime is whatever your signing library puts in `exp`. Pick a TTL that matches your backend's session policy. +- There is no built-in revocation list. To force logout, rotate `JWT_SECRET` on the deployment — every outstanding Custom JWT immediately becomes invalid (which also invalidates every other Form.io session keyed by the old secret). +- For per-user revocation, prefer issuing short-lived Custom JWTs and refreshing them from your backend on a tight cadence. + +### Security considerations + +- Treat `JWT_SECRET` as a top-tier secret. Leakage lets an attacker forge any user with any roles. +- NEVER ship `JWT_SECRET` to the browser. Sign tokens server-side only. +- Validate the `user.roles` array against the actual roles a session should hold; do not trust client-supplied role lists. +- Avoid putting PII you do not need in `user.data` — the payload lives in `localStorage` on the browser. + +## MCP Tool Preference + +`JWT_SECRET` is a deployment-level environment variable; configuring it MUST be done via the Form.io project portal / deployment configuration (Docker env, Kubernetes secret, etc.) — no MCP tool covers it. Surrounding workflow: + +- Use `role_list` to fetch the MongoDB IDs you will put into `user.roles`. +- Use `form_list` / `form_get` to fetch the `_id` of the `user` Resource form to put into `form._id`. +- Use `project_export` to fetch the project `_id` to put into `project._id`. +- For runtime endpoint documentation (token introspection, token validation), see the `runtime-auth` reference in the `formio-api` skill. + +## See also + +- [`token-swap.md`](./token-swap.md) — the OIDC-token-driven alternative when an IdP is already in play. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the standard Form.io JWT payload (the Custom JWT is the same shape minus `external: true`). +- [`roles-and-permissions.md`](./roles-and-permissions.md) — how the role IDs in `user.roles` gate access at runtime. diff --git a/plugin/skills/formio-auth/references/email-auth.md b/plugin/skills/formio-auth/references/email-auth.md new file mode 100644 index 0000000..3614824 --- /dev/null +++ b/plugin/skills/formio-auth/references/email-auth.md @@ -0,0 +1,141 @@ +# Email-token authentication (SSO Email Token) + +## Overview + +Email-token authentication (Form.io's "SSO Email Token" workflow) issues a one-time login link via email instead of asking for a password. A form submission triggers a standard **Email action**, and the email body contains a `[[token(...)]]` macro. At send time Form.io resolves that macro by **searching a Resource for the record that matches the recipient's email**, and — if a record is found — mints a JWT for that user and substitutes it into the link. The recipient clicks the link, lands on a page in *your* application (the **callback URL**), and that page exchanges the token for a Form.io session. The result is a regular Form.io session indistinguishable from Resource-backed login or SSO. + +## When to use this + +Reach for email-token auth when: + +- The product wants a passwordless sign-in or "magic link" flow. +- The product needs to 'verify' a users email address before continuing a user onboarding process. +- A workflow needs to email a specific, already-known person an authenticated deep link into the app (e.g. "a manager gets a link to review this submission"). +- The user population is comfortable with magic-link UX (consumer apps, light-touch B2B portals, review/approval flows). + +Not for: + +- Backend service authentication → use [`custom-jwt.md`](./custom-jwt.md) or [`token-swap.md`](./token-swap.md). +- High-friction enterprise environments with mandatory IdP federation → see [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md). +- Onboarding brand-new users. **Email-token auth does not create users** — see "The recipient must already exist" below. + +## How it works + +1. A form is submitted (in the tutorial, an Expense Report; for a passwordless login, a small "enter your email" form). +2. An **Email action** on that form fires and sends an email to a recipient address. +3. The email **Message** contains a `[[token(...)]]` macro. When the email is rendered, Form.io searches the referenced Resource for a record whose email matches, and — if found — replaces the macro with a freshly minted JWT for that user. +4. The link in the email points to a **callback URL inside the application** (a page the coding agent builds), carrying the token in the query string. +5. The recipient clicks the link. The callback page reads the token and calls `Formio.setToken()` to establish the session, then routes the user wherever the app needs them (e.g. the submission to review). + +## Configuration + +### Prerequisite: Email Transport + +The Email action sends through an **Email Transport** that must be configured first. Select the transport on the action. Transport secrets (SMTP host, port, credentials, sender address) are set in the Form.io project portal — they are not exposed as MCP tools. + +### The Email action + +This uses the **standard Email action** (not a special "authentication" action). Its relevant fields: + +- **Email Transport** — the configured transport to send through. +- **To** — the recipient's email address (a fixed address, or a value pulled from the triggering submission's data). +- **Message** — the email body, which contains the magic link with the `[[token(...)]]` macro. + +Use `action_type_get` on the `email` action type to inspect its current `settings` schema before authoring, since transport and template fields evolve faster than other actions. For general action shape conventions in `template.json`, see `plugin/skills/formio-resource-planner/references/template-json.md`. + +### The token macro + +The link in the email Message embeds a token macro. + +``` +https://yourapplication.com/?token=[[token(data.email=manager)]]#/project-domain/expensereport/submission/{{ id }}/edit +``` + +In this example, `https://yourapplication.com` would be replaced with the URL of the deployed application that the user is developing that contains the 'Magic Link' email login. + +Reading the `[[token(data.email=manager)]]` macro. + +> the token will then search within the **Manager** resource and try to find a record that matches the **Email** data within the given Resource. If a match is found, a special JWT token will be generated. + +So in `[[token(data.email=manager)]]`: + +- `data.email` — the email value to look up (here, the `email` field from the triggering submission's data). +- `manager` — the **Resource that gets searched**. Form.io looks in this Resource for a record whose email matches `data.email`. + +For a 'default' passwordless **login** flow, the equivalent macro searches the `user` Resource — e.g. `[[token(data.email=user)]]` — where `data.email` is the address the visitor typed into the login form. + +`{{ id }}` is the triggering submission's ID, used in the tutorial to deep-link the recipient into that submission in edit mode. It is an ordinary email-template variable, independent of the token. + +### The application link is a callback URL + +In an application built by the coding agent, the **callback URL** is a page that exists *inside the application the agent is building*. Only the host/path changes; the `?token=[[token(...)]]` macro stays exactly as above. For example: + +``` +https://your-app.example.com/auth/callback?token=[[token(data.email=user)]] +``` + +(Append whatever app-specific route or query the page needs to send the user onward after login, e.g. the submission ID to open.) + +The callback page is developer-authored. Its job is to read the token off the URL and hand it to the Form.io SDK, which stores it as the active session: + +```js +const query = Formio.pageQuery(); +if (query.token) { + const user = await Formio.setToken(query.token); +} +``` + +`Formio.pageQuery()` parses the token out of the URL (including parameters after the `#`), and `Formio.setToken()` persists the JWT and returns the authenticated user. After this runs, the application has a live Form.io session for that user and can route them to the intended page. + +### The recipient must already exist for token macro to work + +The token macro does not automatically create users that it does not find. The macro only mints a JWT if the search finds a matching record in the referenced Resource. If no record matches the recipient's email, no token is generated and the link cannot authenticate anyone. The recipient (manager, user, etc.) must already exist as a submission in the searched Resource before the email goes out. Below is a common application workflow that is capable of User Creation => Email Verification => Onboarding user flow. + +### Typical user onboarding workflow + +For many applications, you may wish to accomplish a typical workflow where anyone can create an account, that account is verified with their email, and then they click on a "Magic Link" to complete their user onboarding process. This process uses the email authentication process by using the following process: + + - User lands on an application 'register' page, where they see an embedded form with ONLY an email address. + - This is a Form.io form that only contains an Email field. + - This form 'Create Own' permission is set to allow 'Anonymous' submissions. + - This form contains two actions: Save Submission (pointing to User resource) and Email + - The email action contains the token macro described above, with a callback url navigating to an 'onboarding' page within the application. + - The user clicks on the link, and it navigates them to the onboarding page with the `token=...` set within the url. + - The onboarding page has a controller that reads the token (using `Formio.pageQuery()`) and then authenticates the user (which was created with the 'Save Submission' action on the register form). They are authenticated with `Formio.setToken(token)`. + +This onboarding page could then contain whatever content is needed to complete the user registration. This could be to set the 'password' of the user, or complete filling out their profile. This user is now 'verified' since they needed to click on a link within their email in order to complete the registration. + +### Roles and permissions + +The recipient authenticates **as their existing Resource record**, carrying whatever role that record already holds. In the tutorial, a **Manager** role is assigned to Manager-Resource records (via a Role Assignment action), and the target form grants that role the access it needs: + +- **Create Own** → Authenticated (employees submit). +- **Read All** / **Update All** → Manager (managers review and edit submissions). +- Remove Anonymous access to the form so it requires login. + +For the canonical Role Assignment Action JSON shape used to assign a role to a Resource's records, see `plugin/skills/formio-resource-planner/references/template-json.md`. Design the underlying Resource and role with `formio-resource-planner` if they are not yet in place. + +### Security considerations + +- The token in the email is a credential. Anyone who reads the email can use the link until the token is no longer valid — treat the inbox as part of the trust boundary. +- The token can ONLY be generated for a user whose email matches the recipient of the email. +- The token will not work if the email is sent to multiple emails. +- Use HTTPS for the callback URL. A token traveling over plaintext is as exposed as a password. +- Restrict what the searched Resource and assigned role can do, so a leaked link grants only the recipient's intended access. +- Consider rate-limiting the form that triggers the email to slow inbox-flood abuse. + +## MCP Tool Preference + +- `form_create` / `form_update` — create the triggering form (and, if you model the callback as a Form.io-hosted form, that page too), including their `access` and `submissionAccess` arrays. +- `action_create` — attach the Email action to the triggering form. Run `action_type_get` on the `email` action type first to inspect its current `settings` schema. +- `action_list` / `action_update` — adjust transport, To, or Message after the fact. +- `role_create` / `role_list` — for the role assigned to the searched Resource's records. + +For Email Transport secrets (host, port, credentials), use the Form.io project portal — those values are not exposed as MCP tools today. + +## See also + +- `formio-resource-planner` — owns the canonical Role Assignment Action shape and the underlying Resource model. See `plugin/skills/formio-resource-planner/references/template-json.md`. +- [`resource-auth.md`](./resource-auth.md) — the underlying Form.io auth flow that email-token auth slots into. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the JWT the token resolves to and how sessions / logout work after `setToken`. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — what the recipient's role can do once authenticated. diff --git a/plugin/skills/formio-auth/references/group-permissions.md b/plugin/skills/formio-auth/references/group-permissions.md new file mode 100644 index 0000000..6ec90ce --- /dev/null +++ b/plugin/skills/formio-auth/references/group-permissions.md @@ -0,0 +1,168 @@ +# Group permissions + +## Overview + +Group permissions are Form.io's mechanism for "users on the same team see the same records, users on other teams do not." They layer on top of role-based access using two pieces working in tandem: a Group Assignment Action on a join Resource (records the user's membership in a group), and a field-based `submissionAccess` block on each child Resource's group-reference `select` (resolves the membership at runtime). For multi-level hierarchies, a hidden calculated mirror on grandchild Resources propagates group membership transitively. + +## When to use this + +Reach for group permissions when the user wants: + +- "Members of project X can read tasks belonging to project X." +- "Team members see only their team's customers." +- Multi-tenant data isolation inside a single Form.io project. +- Hierarchies where group membership cascades down two or more levels. + +Not for: + +- Owner-only patterns (use direct `submissionAccess: read_own`/`update_own`) → see [`roles-and-permissions.md`](./roles-and-permissions.md). +- Pure role-based access without group affinity → see [`roles-and-permissions.md`](./roles-and-permissions.md). +- Field-Match-Based Access (gates by literal field values, not group joins) → see Form.io docs at `/developers/roles-and-permissions/field-match-based-access.md`. + +## Configuration + +### The group model in three parts + +1. **A "group" Resource** — the entity that owns records. Examples: `project`, `team`, `account`. +2. **A membership carrier** — the record that ties a user to a group. Two shapes, picked by cardinality: + - **One-to-many** (user belongs to exactly one group): a group-reference `select` on the `user` Resource itself. No join Resource. + - **Many-to-many** (user belongs to many groups): a separate join Resource (`UserTeam`, `UserDepartment`, etc.) with a `user` reference and a group reference, one row per membership. +3. **A "child" Resource** — the data owned by the group. Examples: `task` (owned by `project`), `customer` (owned by `team`). Holds a group-reference `select` component that points back to the group Resource. + +The planner's `complex-crm-transitive` example walks through the many-to-many shape with multiple levels: see `plugin/skills/formio-resource-planner/references/examples/complex-crm-transitive/`. + +### Picking the membership shape + +Decide cardinality first, because it dictates where the Group Assignment Action lives: + +| User belongs to … | Shape | Group Assignment Action attaches to | +|-------------------|-------|-------------------------------------| +| Exactly one group | One-to-many | The `user` Resource | +| Many groups | Many-to-many | The join Resource (`UserTeam`, etc.) | + +If the requirement might evolve from one-to-many to many-to-many later, start with many-to-many — migrating from a `select` on `user` to a join Resource after data has been written is a non-trivial backfill. + +### One-to-many group access (user → single group) + +Use when a user belongs to exactly one team / department / tenant at a time. No join Resource is needed; the membership lives directly on the `user` Resource as a single group-reference field. + +Setup steps: + +1. **Create the group Resource** — `Team`, `Department`, `Tenant`, `Organization`, etc. Standard Form.io Resource with whatever profile fields the group itself needs. +2. **Add a group-reference `select` to the `user` Resource** — a `select` component (`reference: true`) keyed (for example) `team` that points at the group Resource. This is the field the platform reads to determine membership. +3. **Attach a Group Assignment Action to the `user` Resource** — `name: "group"`, `priority: 5`, `method: ["create"]`, `handler: ["after"]`. The Action's settings name the `user` Resource's own keys: + - `settings.group` — the key of the group-reference field on the `user` Resource (e.g. `"team"`). + - `settings.user` — `"_id"` (or whichever field on the user submission represents the user; with the join-less shape, the submission itself IS the user, so the user reference is the submission `_id`). +4. **Add the field-based `submissionAccess` block** to every child Resource's group-reference `select` exactly as documented under "Single-level group access (two halves)" below. The platform's runtime resolver looks the same in either shape — it does not care whether the membership came from a join Resource or from a field on the `user`. + +Update semantics for one-to-many: + +- Changing the user's `team` field re-issues their group ACLs on next login (or on next token refresh). Old-group rows fall out of read access immediately. +- If the requirement is "user can be a member of multiple teams simultaneously," this shape will not work — switch to many-to-many. + +### Many-to-many group access (user ↔ multiple groups via join) + +Use when a user belongs to multiple teams / departments / tenants concurrently. Memberships are first-class rows in a join Resource so each (user, group) pairing can be created or revoked independently. + +Setup steps: + +1. **Create the group Resource** — same as the one-to-many case. +2. **Create a join Resource** — `UserTeam`, `UserDepartment`, `ProjectUser`, etc. One row per (user, group) membership. Carries at minimum two `select` components (both `reference: true`): + - `user` — points at the `user` Resource. + - A group-reference field (e.g. `team`) — points at the group Resource. + Add metadata fields (`role`, `joinedAt`, `invitedBy`) on the join itself when needed. +3. **Attach a Group Assignment Action to the join Resource** — `name: "group"`, `priority: 5`, `method: ["create"]`, `handler: ["after"]`. Settings name the join's own keys: + - `settings.group` — the group-reference field on the join (e.g. `"team"`). + - `settings.user` — the user-reference field on the join (e.g. `"user"`). +4. **Add the field-based `submissionAccess` block** to every child Resource's group-reference `select` (same shape as below). + +Revocation semantics: + +- Deleting a join row revokes the user's membership in that group; their ACL on the group's records drops on next token refresh. +- Add a Save-Submission filter or Delete Action to the join Resource if you need an audit log of membership changes. + +For the canonical Group Assignment Action JSON shape and the join Resource shape, see `plugin/skills/formio-resource-planner/references/template-json.md` lines 555–590. + +### Single-level group access (two halves) + +**Half 1 — Group Assignment Action on the join Resource:** + +- `name: "group"`, `priority: 5`, `method: ["create"]`, `handler: ["after"]`. +- `settings.group` names the join field that holds the group reference (e.g. `"project"`). +- `settings.user` names the join field that holds the user reference (e.g. `"user"`). +- On every join submission the platform stores an ACL on the referenced group submission tying the user to the group. + +For the canonical Group Assignment Action JSON shape, see `plugin/skills/formio-resource-planner/references/template-json.md` lines 555–576. + +**Half 2 — Field-based `submissionAccess` on the child Resource's group-reference select:** + +The `select` component on the child Resource that references the group must carry a four-entry `submissionAccess` block with empty `roles` arrays: + +```json +{ + "type": "select", + "key": "project", + "reference": true, + "submissionAccess": [ + { "type": "read", "roles": [] }, + { "type": "create", "roles": [] }, + { "type": "update", "roles": [] }, + { "type": "delete", "roles": [] } + ] +} +``` + +Empty `roles` is intentional. The platform resolves permissions at runtime from the group's ACL — the user has `read` / `create` / `update` / `delete` on a child submission if and only if they are a member of the group named by this field. The platform's resolver fills in the effective roles per-request; you do not enumerate them statically. + +For the canonical group-reference select shape, see `plugin/skills/formio-resource-planner/references/template-json.md` lines 297–327. + +### Transitive group access (three levels) + +When a Resource is a grandchild of the group (e.g. `lineItem` belongs to `order` belongs to `account`), the child carries the group reference but the grandchild does not. Without help, the grandchild has no way to inherit the account's ACL. + +The fix is a **hidden calculated mirror**: a hidden `select` on the grandchild that mirrors the child's group reference, with the same four-entry `submissionAccess` block. The platform resolves the grandchild's permissions against the mirrored group exactly as if the grandchild had its own group field: + +```json +{ + "label": "Team", + "key": "team", + "type": "select", + "hidden": true, + "calculateValue": "value = data.account.data.team;", + "refreshOn": "account", + "reference": true, + "submissionAccess": [ + { "type": "read", "roles": [] }, + { "type": "create", "roles": [] }, + { "type": "update", "roles": [] }, + { "type": "delete", "roles": [] } + ] +} +``` + +For the canonical transitive-mirror shape, see `plugin/skills/formio-resource-planner/references/template-json.md` lines 297–376. + +### Resource Map vocabulary + +The planner annotates group-based access in its Resource Map with tokens like: + +- `group()` — submission access via a specific join. +- `group` — submission access via the canonical join for that child. + +Use those tokens when planning the project; once the resources are deployed, this skill takes over for any tuning beyond what the planner emits. + +## MCP Tool Preference + +- `form_get` — inspect the existing group-reference `select` on a child Resource before editing. +- `form_update` — add or modify the four-entry field-based `submissionAccess` block on a group-reference select, or add the hidden calculated mirror on a grandchild. +- `action_create` — attach the Group Assignment Action to the join Resource. +- `action_list`, `action_get`, `action_update` — inspect or change an existing Group Assignment Action's `settings.group` / `settings.user` keys. +- `project_export` / `project_import` — round-trip the full group graph (join Resource + Group Assignment Action + child group-reference selects + grandchild mirrors) in a `template.json`. + +`action_type_get` for the `group` action type is also useful to confirm the `settings` schema before you create the action. + +## See also + +- `formio-resource-planner` — owns the canonical Group Assignment Action JSON shape, group-reference select shape, and transitive mirror shape. Run the planner first if your data model does not yet include a join Resource or a group-reference select. See `plugin/skills/formio-resource-planner/references/template-json.md` lines 297–376 and 555–590, and the `complex-crm-transitive` example. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — how the eight permission types interact with group ACLs. +- [`resource-auth.md`](./resource-auth.md) — how a user's roles + group memberships combine into the effective access set the JWT carries. diff --git a/plugin/skills/formio-auth/references/jwt-and-sessions.md b/plugin/skills/formio-auth/references/jwt-and-sessions.md new file mode 100644 index 0000000..e87f28e --- /dev/null +++ b/plugin/skills/formio-auth/references/jwt-and-sessions.md @@ -0,0 +1,104 @@ +# JWT and sessions + +## Overview + +Every authenticated Form.io request rides a JSON Web Token (JWT) on the `x-jwt-token` header. The JWT is the unifying currency of every auth method in this skill: Resource login, OIDC SSO, SAML SSO, LDAP, Token Swap, Custom JWT, and email-token auth all return one to the caller. This reference documents the payload, the Session ID, the on-the-wire header, the logout semantics that invalidate a session, and the two additional security controls (2FA and reCAPTCHA) that layer on top of any underlying auth method. + +## When to use this + +Reach for this reference when: + +- You need to know what's in the JWT (decode the payload, name the claims, explain `jti`). +- You need to know which header carries it and when. +- You need to invalidate a session (logout one device or all devices). +- You need to add 2FA or reCAPTCHA. +- You need to integrate Form.io auth with another system that consumes JWTs. + +Not for: + +- Choosing an auth mechanism in the first place — that's the rest of `formio-auth`'s reference docs. +- Designing role-keyed access — see [`roles-and-permissions.md`](./roles-and-permissions.md). + +## Configuration + +### The on-the-wire header + +Every request to these endpoints MUST include an `x-jwt-token` header holding the user JWT issued by the MCP server's browser-based portal-login flow. The MCP server attaches this header automatically via `formioFetch`; external clients must obtain the JWT through the same portal-login flow. Do not use any other authentication mechanism with these endpoints. + +The same header is used by the in-browser renderer, by service-to-service callers, and by Custom JWTs forged on a customer's backend. The token is also persisted by the renderer into `localStorage` under the key `formioToken`; the renderer re-attaches it on every subsequent Form.io request. + +### JWT payload + +A decoded Form.io JWT looks like this: + +```json +{ + "user": { "_id": "5e5411ba1e29ee1aab5031d9" }, + "iss": "https://api.form.io:3000", + "sub": "5e5411ba1e29ee1aab5031d9", + "jti": "5fffbb5646d76c292a7b5df1", + "iat": 1610595158, + "exp": 1610609558 +} +``` + +Claim semantics: + +- `user._id` — MongoDB ID of the user submission when the identity is backed by a `user` Resource row (Resource login). For SSO (Remote Authentication) and Custom JWTs there is no Resource row — the user is ephemeral and `user._id` is either an IDP identifier (for SSO), or the `"external"` sentinel for Custom JWTs. +- `iss` — issuer; the Form.io API base URL. +- `sub` — subject; same as `user._id`. +- `jti` — Session ID. Logging out invalidates this; see below. +- `iat`, `exp` — issued-at and expiry timestamps (unix seconds). + +SSO (OIDC/OAuth, SAML, LDAP, Token Swap) uses Remote Authentication: the ephemeral user built from the IdP is encoded directly into the JWT, so the token carries `user.data` (the mapped profile) and `user.roles` (from Role Mapping) rather than pointing at a Resource row. SSO tokens also carry `external: true`, `project: { _id: ... }`, and sometimes `form: { _id: ... }`. This is the same in-token user shape a Custom JWT carries — However, custom JWTs always carry `form: { _id: ... }`. See [`custom-jwt.md`](./custom-jwt.md) for the full payload. + +### Session ID (`jti`) and logout + +`jti` is a Form.io-issued Session ID. The relationship between JWTs and sessions: + +- A login (Resource, OIDC, SAML, LDAP, Token Swap, email-token) creates a Session ID and issues a JWT carrying that `jti`. +- A logout API call invalidates the Session ID. Every JWT that carries the invalidated `jti` immediately stops working — so logging out one device logs the user out of every device that holds a JWT for the same session. +- A user can hold multiple concurrent sessions (different devices, different logins) — each has its own `jti`. Invalidating one does not touch the others. + +Custom JWTs (where the customer signs with `JWT_SECRET`) typically do not carry a `jti`. Revocation for those is done by rotating `JWT_SECRET` or by short TTLs — see [`custom-jwt.md`](./custom-jwt.md). + +For the runtime logout endpoint URL and shape, see the `runtime-auth` reference in the `formio-api` skill. + +### Token lifetime + +Form.io JWTs expire at `exp`. After expiry, the renderer treats the user as anonymous and prompts for re-authentication. The deployment configures the lifetime; pick a value that balances UX against blast radius. Custom JWTs follow whatever `exp` the signing backend writes. + +### 2FA + +Two-Factor Authentication adds a second factor (typically a TOTP from an authenticator app) before Form.io issues the JWT. The 2FA flow: + +1. User submits the login form with email + password (or completes the SSO handshake). +2. Form.io intercepts before issuing the JWT and presents the 2FA challenge. +3. User supplies the TOTP code. +4. On success, Form.io issues the JWT and the session begins normally. + +2FA is configured at the project level in the portal. It layers on top of any of the underlying auth methods in this skill — the user-facing experience changes, but the JWT issued at the end is identical. + +### CAPTCHA Component + +The CAPTCHA Component gates form submission against bot abuse. It is wired as a premium component on the login or registration form, configured against a Google reCAPTCHA site key. When enabled, the Login Action / Email Authentication action will not run unless the reCAPTCHA token is present and valid. + +Use a CAPTCHA Component on any user-facing auth form that anonymous users can hit (login, registration, send-magic-link). For SSO buttons (OIDC, SAML, LDAP) CAPTCHA usually adds little because the bot would also have to defeat the IdP. + +### Decoding a JWT for debugging + +The Form.io JWT is a standard JWS. Decode it with `https://jwt.io` or any standard JWT library to inspect the payload. Do not decode it client-side from untrusted input as a substitute for server-side validation. + +## MCP Tool Preference + +- `authenticate` — drive the MCP server's browser-based portal-login flow and obtain the portal JWT that `formioFetch` attaches to every subsequent call. +- For the runtime endpoint that invalidates a session (`/logout`) and the token-introspection endpoints, see the `runtime-auth` reference in the `formio-api` skill — those calls are HTTP endpoints, not MCP tools. +- For 2FA / reCAPTCHA configuration, use the Form.io project portal. No MCP tool covers premium component configuration today. + +## See also + +- [`resource-auth.md`](./resource-auth.md) — the six-step Form.io auth flow that issues the JWT this reference describes. +- [`custom-jwt.md`](./custom-jwt.md) — the customer-signed variant of this payload. +- [`token-swap.md`](./token-swap.md) — exchanging an external OIDC bearer token for one of these JWTs. +- [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md) — alternate methods that return this same JWT. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — the role-keyed authorization the JWT identity is fed into at runtime. diff --git a/plugin/skills/formio-auth/references/login-forms.md b/plugin/skills/formio-auth/references/login-forms.md new file mode 100644 index 0000000..471c967 --- /dev/null +++ b/plugin/skills/formio-auth/references/login-forms.md @@ -0,0 +1,90 @@ +# Login and registration forms + +## Overview + +Login and registration forms are the user-facing surface of resource-backed authentication. Both are standard Form.io forms — the difference is in their access rules, the components they expose, and which Actions they carry. A login form gates an existing user submission and issues a JWT; a registration form writes a new submission into the `user` Resource and (typically) issues a JWT immediately after. + +## When to use this + +Reach for this reference when the user wants to: + +- Build a login form from scratch. +- Add a self-register flow with anonymous create access. +- Tighten or loosen `access` / `submissionAccess` on an existing login form. +- Configure brute-force protection on a login form. + +Not for: + +- The Login Action's role in the six-step Form.io auth flow → see [`resource-auth.md`](./resource-auth.md). +- Assigning roles or extending the permission matrix → see [`roles-and-permissions.md`](./roles-and-permissions.md). +- Federated identity → see [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md). + +## Configuration + +### Login form + +Components: + +- `email` (type `email`, required). +- `password` (type `password`, `persistent: false` so it is not stored on the submission). +- A submit `button`. + +Access: + +- `access`: `read_all` granted to all three default roles (Administrator, Authenticated, Anonymous), so anonymous visitors can load the form definition. +- `submissionAccess`: `create_own` granted to Anonymous so visitors can post the form. + +Actions: + +- One Login Action with `settings.resources: ["user"]`, `settings.username: "email"`, `settings.password: "password"`, plus brute-force settings: + - `allowedAttempts` — typically 5. + - `attemptWindow` — seconds during which `allowedAttempts` is counted (typical 30). + - `lockWait` — seconds the account stays locked after exceeding `allowedAttempts` (typical 1800 = 30 minutes). + +For the full Login Action JSON shape (priority, handler, method, settings), see `plugin/skills/formio-resource-planner/references/template-json.md` lines 504–534. + +### Registration form + +Components: + +- `email` (type `email`, required, `persistent: true`). +- `password` (type `password`, required, `persistent: true` — this row is the credential row). +- Any additional profile fields you want to capture at signup. +- A submit `button`. + +Access: + +- `access`: `read_all` to all three default roles. +- `submissionAccess`: `create_own` to Anonymous. + +Actions (order matters because `priority` is the tie-breaker among handlers at the same phase): + +1. **Save Submission** (built-in) — persists the new submission. Priority 10, `handler: ["before"]`. +2. **Role Assignment Action** — `settings.association: "new"`, `settings.type: "add"`, `settings.role: "authenticated"`. Priority 1, `handler: ["after"]`. Runs after the save so the new submission already has an `_id`. +3. **Login Action** — `settings.resources: ["user"]`, same field names as the login form. Priority 2, `handler: ["before"]`. Issues the JWT immediately so the new user is logged in without a second round-trip. + +NEVER include `"admin"` in the Login Action's `settings.resources`. Admin work is performed via the Form.io project portal, not via an app-side login form, and including `"admin"` breaks project import. + +For the canonical action JSON shapes, see `plugin/skills/formio-resource-planner/references/template-json.md` lines 504–553. + +### Anonymous vs admin write paths + +The Resource Map terminology used by the planner: + +- **Anonymous self-register**: registration form has `submissionAccess: create_own` for Anonymous. End users sign themselves up. +- **Admin-issued accounts**: registration form has `submissionAccess: create_all` for Administrator only. Admins seed users via the Form.io project portal (not via the app's UI). The login form is the only user-facing form. + +## MCP Tool Preference + +- `form_create` — create the login form and the registration form. Each form's `access` and `submissionAccess` arrays are part of the form payload. +- `form_get`, `form_update` — inspect or change `access` / `submissionAccess` on an existing form. +- `action_create` — attach the Login Action to the login form, and the Role Assignment + Login Actions to the registration form. +- `action_type_get` — inspect the Login Action and Role Assignment Action `settings` schemas before creating. +- `project_import` — when you are seeding from a planner-produced `template.json`, this single call creates both forms and all three actions at once. Prefer it over driving `form_create` + `action_create` individually for greenfield projects. + +## See also + +- `formio-resource-planner` — owns the canonical login and registration form JSON shapes plus the Login Action and Role Assignment Action shapes. Use the planner first if the forms do not yet exist. See `plugin/skills/formio-resource-planner/references/template-json.md` lines 504–553 and `plugin/skills/formio-resource-planner/references/examples/task-manager/template.json` for a working end-to-end example. +- [`resource-auth.md`](./resource-auth.md) — the six-step auth flow, the `x-jwt-token` header, and the `user` Resource shape. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — what `access` and `submissionAccess` actually permit, and how the eight permission types interact. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the JWT that the Login Action returns and how the renderer carries it. diff --git a/plugin/skills/formio-auth/references/resource-auth.md b/plugin/skills/formio-auth/references/resource-auth.md new file mode 100644 index 0000000..8d6f735 --- /dev/null +++ b/plugin/skills/formio-auth/references/resource-auth.md @@ -0,0 +1,82 @@ +# Resource-backed authentication + +## Overview + +Resource-backed authentication is Form.io's first-party identity mechanism. A submission of a Form.io Resource (typically the built-in `user` resource) represents an authenticated user; a Login Action on a login form verifies credentials against that Resource and issues a JWT, and a Role Assignment Action on a registration form attaches a Form.io Role to the new submission immediately on signup. Together they cover the full "log in" and "sign up" surface for any Form.io project that does not rely on an external Identity Provider. + +## When to use this + +Reach for resource auth when the user wants: + +- Email-and-password login backed by a Form.io Resource. +- A self-register flow that assigns an initial role on signup. +- A first-party identity store (no external IdP, no SSO, no Custom JWT). +- Brute-force protection on a login form (allowed attempts, attempt window, lock duration). + +Not for: + +- Federated identity (OIDC / SAML / LDAP) → see [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md). +- Exchanging an external bearer token for a Form.io JWT → see [`token-swap.md`](./token-swap.md). +- Forging a JWT in your own backend → see [`custom-jwt.md`](./custom-jwt.md). +- Passwordless email-link auth → see [`email-auth.md`](./email-auth.md). + +## Configuration + +### Six-step Form.io authentication flow + +1. **Authentication Method Selection** — pick one of OAuth/OIDC, SAML, LDAP, or Resource-based. For this doc, Resource-based. +2. **Authentication Form Configuration** — build a login form that collects `email` and `password`, plus a registration form that collects the same fields and any profile data. +3. **Authentication Request** — the user submits the login form. The submission payload reaches the Form.io server. +4. **Verification** — the Login Action looks up a matching submission of the configured Resource (`settings.resources`), runs a one-way hash comparison on the password, and gates the submission. +5. **Authentication Success** — Form.io generates a JWT representing the matched user submission, attaches it to the response, and assigns roles via the Role Assignment configured on the user (Resource Role Assignment). +6. **Additional Security Measures** — layer 2FA and reCAPTCHA via [`jwt-and-sessions.md`](./jwt-and-sessions.md) when policy demands them. + +### JWT on the wire + +Every request to these endpoints MUST include an `x-jwt-token` header holding the user JWT issued by the MCP server's browser-based portal-login flow. The MCP server attaches this header automatically via `formioFetch`; external clients must obtain the JWT through the same portal-login flow. Do not use any other authentication mechanism with these endpoints. + +On the renderer side, the response header `x-jwt-token` is persisted into the browser's `localStorage` under the key `formioToken`, and the renderer attaches it to every subsequent Form.io request. + +### Login form + Login Action + +The login form is a normal Form.io form with two components — `email` (type `email`) and `password` (type `password`, `persistent: false`). It needs: + +- `access`: `read_all` for all three default roles (Administrator, Authenticated, Anonymous), so unauthenticated visitors can load the form definition. +- `submissionAccess`: `create_own` for `anonymous` (so visitors can submit it). +- One Login Action attached to the form. + +The Login Action's `settings.resources` MUST be `["user"]` (or whichever Resource holds the credentials). `settings.username` names the field that holds the username/email (typically `"email"`); `settings.password` names the password field (typically `"password"`). Brute-force protection is controlled by `allowedAttempts`, `attemptWindow`, and `lockWait`. + +For the canonical Login Action JSON shape (priority, handler, method, all field names), see `plugin/skills/formio-resource-planner/references/template-json.md` lines 504–534. + +### Registration form + Role Assignment Action + +The registration form is a separate form (typically `userRegister`) that writes a new submission into the `user` Resource. It needs: + +- A Role Assignment Action (`name: "role"`, `settings.association: "new"`, `settings.type: "add"`, `settings.role: "authenticated"`, `priority: 1`, `handler: ["after"]`) to attach the initial role to the new submission. +- A Login Action immediately afterward (priority 2, `handler: ["before"]`) so the new user is logged in without a second round-trip. + +For the canonical Role Assignment Action JSON shape, see `plugin/skills/formio-resource-planner/references/template-json.md` lines 535–553. + +### The `user` Resource + +The canonical `user` Resource holds `email` (unique, `protected: false`) and `password` (`protected: true`). Its `submissionAccess` grants the administrator full CRUD and the authenticated role `read_own` + `update_own`. See the planner reference at `plugin/skills/formio-resource-planner/references/template-json.md` lines 409–437. + +## MCP Tool Preference + +Prefer the first-party MCP tools when wiring this auth flow: + +- `authenticate` — obtain a Form.io portal JWT for the MCP server itself the first time you connect. +- `role_list`, `role_create`, `role_update` — inspect and create the roles you assign (typically `administrator`, `authenticated`, `anonymous`). +- `form_create`, `form_get`, `form_update` — create the login form, the registration form, and the `user` Resource. Use `form_update` to adjust `access` / `submissionAccess` on each. +- `action_create`, `action_list`, `action_update` — attach the Login Action and Role Assignment Action to the appropriate forms. Use `action_type_get` to inspect each action's `settings` schema before creating. +- `project_export` / `project_import` — round-trip a full project (resources, forms, actions, roles) as a `template.json`. The planner emits this shape and `project_import` consumes it. + +If you are seeding a fresh project from a planner-produced `template.json`, `project_import` is the single call that creates the user Resource, the login/registration forms, and all three actions in one shot — no need to drive `form_create` + `action_create` individually. + +## See also + +- `formio-resource-planner` — owns the canonical JSON shapes for the user Resource, login/registration forms, the Login Action, and the Role Assignment Action. Run the planner first if any of these do not yet exist in the project. See `plugin/skills/formio-resource-planner/SKILL.md` and `plugin/skills/formio-resource-planner/references/template-json.md`. +- [`login-forms.md`](./login-forms.md) — login + registration form shapes in more detail. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — what each role can do and how the eight permission types layer onto these forms. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the JWT payload Form.io returns, `jti` Session ID, logout, 2FA, reCAPTCHA. diff --git a/plugin/skills/formio-auth/references/roles-and-permissions.md b/plugin/skills/formio-auth/references/roles-and-permissions.md new file mode 100644 index 0000000..afe4b9d --- /dev/null +++ b/plugin/skills/formio-auth/references/roles-and-permissions.md @@ -0,0 +1,138 @@ +# Roles and permissions (RBAC) + +## Overview + +Form.io authorization is role-based. Every authenticated user object carries a `roles` array of MongoDB IDs; every project has its own set of Roles; and every Project, Form Definition, and Submission carries `access` / `submissionAccess` arrays that map permission types to roles. This reference covers the default roles, custom roles, the eight permission types, the three permission scopes (project / form-definition / submission-data), and the layered access models (Self Access, Field Match-Based Access, Field-Based Resource Access, Group Permissions). + +## When to use this + +Reach for this reference when the user wants to: + +- Understand who can read / update / delete what. +- Tune `access` or `submissionAccess` on a Form or Resource. +- Add a custom role and decide what it can do. +- Reason about "own" vs "all" semantics. +- Pick between role-based, field-match-based, field-based-resource, and group-based access models. + +Not for: + +- Wiring the actual login flow → see [`resource-auth.md`](./resource-auth.md). +- Designing group joins → see [`group-permissions.md`](./group-permissions.md). +- Configuring SSO role mapping → see [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md). + +## Configuration + +### Default roles + +Every new Form.io project is seeded with these roles, each with a unique MongoDB ID: + +- **Anonymous** — for unauthenticated users. Cannot be deleted. +- **Everyone** — baseline applied to all users. Fixed ID `00000000000000000000000`. Cannot be deleted. +- **Administrator** — preconfigured with full CRUD across the project. +- **Authenticated** — preconfigured for logged-in workflows; assigned by the Role Assignment Action at signup. + +In a planner-produced `template.json` the default roles are emitted as objects with `title`, `description`, `admin`, and `default` fields — see `plugin/skills/formio-resource-planner/references/template-json.md` lines 34–59 for the canonical shape. + +### Custom roles + +Add custom roles when the default trio is not enough — for example `salesRep`, `moderator`, `supportAgent`. A custom role: + +- Has `admin: false` and `default: false`. +- Is referenced by MongoDB ID in `access` / `submissionAccess` arrays on the Forms and Resources you want it to reach. +- Can be mapped from an SSO provider's role claim via OAuth / SAML / LDAP Role Mapping. + +### The eight permission types + +The same eight types appear across every scope: + +| Type | Meaning | +|------|---------| +| `create_own` | Create an entity; the actor becomes the owner. | +| `create_all` | Create an entity; the actor may set `owner` to any user. | +| `read_own` | Read entities the actor owns. | +| `read_all` | Read every entity, regardless of ownership. | +| `update_own` | Update entities the actor owns. | +| `update_all` | Update every entity. On Submissions, also lets the actor change `owner`. | +| `delete_own` | Delete entities the actor owns. | +| `delete_all` | Delete every entity. | + +Key rules: + +- `update_all` on Submissions implicitly grants `create_all`. +- `read_all` at the Project scope controls index access for forms and roles. +- Submission access is disabled by default — every role that needs to see submissions (including Anonymous on a public form) must be granted explicit `submissionAccess`. +- Only the Project owner can delete the Project itself. + +### The three permission scopes + +| Scope | Where it lives | Controls | +|-------|----------------|----------| +| Project | `access[]` on the Project object | Who can create, read, update, delete forms/resources/roles inside the project. | +| Form Definition | `access[]` on each Form/Resource | Who can read/update/delete the form's JSON definition. `read_all` is required for users to load the form's renderer. | +| Submission Data | `submissionAccess[]` on each Form/Resource | Who can create/read/update/delete actual submission rows. This is "the real access-control story". | + +The planner reference at `plugin/skills/formio-resource-planner/references/template-json.md` lines 61–158 carries the canonical `access` and `submissionAccess` JSON shapes; common patterns (admin-only, owner-level, public-submit, group-based) are documented there. + +### Layered access models + +Beyond the role-keyed `access` / `submissionAccess` arrays, Form.io supports four overlay models: + +1. **Self Access Permissions** — write the submission's own `_id` into its `owner` property. The submission becomes its own owner; useful for "users see only their own records" patterns without a separate user lookup. +2. **Field Match-Based Access** — submission access gated on field values. Configured at `/developers/roles-and-permissions/field-match-based-access.md`. +3. **Field-Based Resource Access** — permissions assigned via Resource references inside a form. Configured at `/developers/roles-and-permissions/field-based-resource-access.md`. +4. **Group Permissions** — an extension of Field-Based Resource Access where permissions derive from group associations. See [`group-permissions.md`](./group-permissions.md). + +### Worked examples + +**Admin-only resource:** + +```json +"submissionAccess": [ + { "type": "create_all", "roles": [""] }, + { "type": "read_all", "roles": [""] }, + { "type": "update_all", "roles": [""] }, + { "type": "delete_all", "roles": [""] } +] +``` + +**Owner-only resource (user sees only own records):** + +```json +"submissionAccess": [ + { "type": "create_all", "roles": [""] }, + { "type": "read_all", "roles": [""] }, + { "type": "update_all", "roles": [""] }, + { "type": "delete_all", "roles": [""] }, + { "type": "read_own", "roles": [""] }, + { "type": "update_own", "roles": [""] } +] +``` + +**Public submit (anonymous feedback form):** + +```json +"submissionAccess": [ + { "type": "create_own", "roles": [""] }, + { "type": "read_all", "roles": [""] }, + { "type": "update_all", "roles": [""] }, + { "type": "delete_all", "roles": [""] } +] +``` + +## MCP Tool Preference + +- `role_list` — discover existing role IDs in the project (default trio plus any custom). +- `role_create` — add a custom role (`title`, `description`, `admin: false`, `default: false`). +- `role_update` — adjust an existing role's metadata. +- `form_get` — read the current `access` / `submissionAccess` on a form before editing it. +- `form_update` — write new `access` or `submissionAccess` arrays onto a Form or Resource. +- `project_export` / `project_import` — round-trip the entire role + permission graph in a `template.json`. + +Use the Form.io project portal for direct Role and Permission edits when you are administering a single project interactively; prefer the MCP tools when scripting or driving multi-project changes from an agent. + +## See also + +- `formio-resource-planner` — owns the canonical role objects, `access` arrays, and `submissionAccess` arrays for `template.json`. Start there when designing a new project's permission matrix. See `plugin/skills/formio-resource-planner/references/template-json.md` lines 34–158. +- [`resource-auth.md`](./resource-auth.md) — how roles get attached to a user at login and signup. +- [`group-permissions.md`](./group-permissions.md) — group-based access overlay. +- [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md) — provider role mapping into Form.io roles. diff --git a/plugin/skills/formio-auth/references/sso-ldap.md b/plugin/skills/formio-auth/references/sso-ldap.md new file mode 100644 index 0000000..bae3424 --- /dev/null +++ b/plugin/skills/formio-auth/references/sso-ldap.md @@ -0,0 +1,78 @@ +# SSO — LDAP + +## Overview + +Form.io supports LDAP authentication against any LDAP-compliant directory (OpenLDAP, Microsoft Active Directory via LDAP, FreeIPA, etc.). The user supplies their LDAP credentials on a Form.io login form, and Form.io binds against the directory and returns a first-party Form.io JWT on success. + +Like every SSO method here, LDAP uses **Remote Authentication**: Form.io does not look up or create a `user` Resource submission. After a successful bind it reads the directory entry's attributes, builds an **ephemeral user object** from them, applies **LDAP Role Mapping** to attach Form.io Roles, and encodes that user — profile data plus mapped roles — **entirely within the Form.io JWT**. The identity lives in the token, not in a database row. + +## When to use this + +Reach for LDAP when: + +- The organization runs a corporate LDAP directory and wants Form.io to authenticate against it without SAML or OIDC in front. +- Active Directory is the source of truth for users and groups, and an LDAP bind is acceptable (no Kerberos/IWA). +- Group membership in LDAP (`memberOf`) should drive Form.io Role assignment. + +Not for: + +- Federated SSO with redirect-based handshakes → see [`sso-oidc.md`](./sso-oidc.md) or [`sso-saml.md`](./sso-saml.md). +- Token-based handoff from another system → see [`token-swap.md`](./token-swap.md) or [`custom-jwt.md`](./custom-jwt.md). + +## Configuration + +### Directory connection + +In the project portal's LDAP settings: + +- **Host** and **Port** — typically `389` (LDAP) or `636` (LDAPS). Use LDAPS in production. +- **Base DN** — the subtree to search (e.g. `dc=corp,dc=example,dc=com`). +- **Bind DN** + **Bind Password** — a service account that can search the directory. Form.io binds with these credentials before issuing user lookups. +- **User search filter** — typically `(&(objectClass=person)(uid={{username}}))` for OpenLDAP or `(&(objectClass=user)(sAMAccountName={{username}}))` for AD. The `{{username}}` placeholder is replaced with whatever the user typed on the login form. + +### Login form + +A Form.io login form drives LDAP exactly like Resource-backed login from the user's point of view: an LDAP `email`/`username` field and a `password` field. The difference is the Action attached to the form: + +- For Resource login, attach a Login Action with `settings.resources: ["user"]`. +- For LDAP login, attach the LDAP Action (project portal — managed via the LDAP settings page, not as a Resource Login Action). + +On submit, the LDAP Action attempts a bind with the supplied credentials, then runs the User search filter to locate the matching directory entry. + +### User mapping + +After a successful bind, Form.io maps directory attributes onto the ephemeral user object that gets encoded into the JWT (not onto a Resource submission): + +- The `email` attribute (or `mail`, depending on the schema) populates the user's `email`. +- Optional profile attributes (`displayName`, `givenName`, `sn`, etc.) populate matching fields on the ephemeral user's `data`. + +There is no auto-create / require-existing-user setting: LDAP login never creates or requires a `user` Resource row. See "Remote Authentication" in [`sso-oidc.md`](./sso-oidc.md) for the shared model and [`custom-jwt.md`](./custom-jwt.md) for the in-token user shape. + +### LDAP Role Mapping + +LDAP Role Mapping uses the directory's group attribute (typically `memberOf`) to assign Form.io Roles: + +- Pick the attribute name (`memberOf` is the default). +- For each group DN that the directory can return (e.g. `cn=admins,ou=groups,dc=corp,dc=example,dc=com`), choose the Form.io Role. +- A user's final `roles` array is the union of matched group → Role rows, falling back to the configured default Role if nothing matches. + +### TLS and security + +- Use LDAPS (`636`) or StartTLS in production. Sending bind credentials over plain LDAP exposes them on the wire. +- Use a low-privilege service account for the Bind DN. +- Set a tight User search filter to avoid leaking unrelated directory entries. + +## MCP Tool Preference + +LDAP provider configuration (host, port, Bind DN, search filter, Role Mapping) MUST be performed via the Form.io project portal — no MCP tool covers LDAP directory wiring today. After the provider is configured: + +- Use `role_list` / `role_create` to ensure the Form.io Roles that the LDAP Role Mapping targets exist. +- Use `form_create` / `form_update` to build the login form that drives the LDAP bind. + +For runtime endpoint documentation, see the `runtime-auth` reference in the `formio-api` skill. + +## See also + +- [`sso-oidc.md`](./sso-oidc.md), [`sso-saml.md`](./sso-saml.md) — sibling SSO references for OIDC/OAuth and SAML. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — the Form.io Roles your LDAP Role Mapping targets. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the Form.io JWT the LDAP login returns, plus session and logout semantics. diff --git a/plugin/skills/formio-auth/references/sso-oidc.md b/plugin/skills/formio-auth/references/sso-oidc.md new file mode 100644 index 0000000..afe7090 --- /dev/null +++ b/plugin/skills/formio-auth/references/sso-oidc.md @@ -0,0 +1,122 @@ +# SSO — OAuth / OIDC + +## Overview + +Form.io integrates with any OAuth 2.0 / OpenID Connect (OIDC) Identity Provider. The user authenticates against the IdP, the provider returns standard OAuth/OIDC claims, and Form.io exchanges those claims for a first-party Form.io JWT. + +SSO in Form.io is **Remote Authentication**: Form.io does not look up or create a `user` Resource submission. Instead it retrieves user data from the IDP (using the `userinfo_endpoint`), dynamically builds an **ephemeral user object** from that information, and encodes that user — profile data plus mapped roles — **entirely within the Form.io JWT**. There is no database row for the user; everything the application needs about the identity travels inside the token. Provider roles are translated onto Form.io Roles via **OAuth Role Mapping**, so an SSO user lands with the same role-keyed `access` / `submissionAccess` evaluation that resource-backed users get. + +## When to use this + +Reach for OIDC SSO when: + +- The organization runs an IdP like Okta, Auth0, Azure AD (Entra ID), Keycloak, or Google Workspace. +- Users should authenticate against the IdP, not against a Form.io `user` Resource. +- Provider role claims should govern Form.io Role assignment. +- A "Sign in with..." button on the Form.io login form is acceptable. + +Not for: + +- SAML-only providers → see [`sso-saml.md`](./sso-saml.md). +- LDAP directories → see [`sso-ldap.md`](./sso-ldap.md). +- Already-issued OIDC bearer tokens you want to swap for a Form.io JWT without re-authenticating → see [`token-swap.md`](./token-swap.md). +- Forging a JWT yourself in your backend → see [`custom-jwt.md`](./custom-jwt.md). + +## Configuration + +### Provider registration + +OAuth/OIDC providers are configured on the Project's OAuth settings page in the Form.io portal. For each provider you supply: + +- **Client ID** and **Client Secret** from the IdP. +- **Authorization URL** (authorization_endpoint), **Token URL** (token_endpoint), and **User Info URL** (userinfo_endpoint): (All three of these can be easily retrieved using the OIDC discovery document, e.g. `https:///.well-known/openid-configuration`). +- **Scopes** — typically `openid profile email` plus any custom scopes that carry the role claim. + +Refer to the per-provider sub-pages linked from `https://help.form.io/developers/auth/oauth#openid-connect-oidc` for the exact field names and any provider-specific quirks. + +### Login form integration + +In order to create a Login Form that performs an OIDC authentication, you must first add the following button schema to your Login Form. + +``` +{ + "label": "Sign in with OIDC", + "action": "oauth", + "key": "oidcLogin", + "type": "button", + "input": true, + "oauthProvider": "openid" +} +``` + +Once this button is part of the form, the `OAuth` Action is then added to that form with the following configurations. + +- `settings.provider` = "openid" +- `settings.association` = "remote" +- `settings.button` = "oidcLogin" <== Must match the key for the OIDC login button component. +- `settings.redirectURI` = "..." <== The application url to navigate to after the OIDC handshake (this defaults to `window.location.origin` of the application and must also be added to the list of redirect URIs in the OIDC application). +- `settings.roles` = [{...}] <== This contains an array of the following object. + +OAuth Role Mapping is the bridge between an IdP role claim (e.g. `groups`, `roles`, `https://my-app/roles`) and Form.io Roles. The role map settings should provide the following: + +- Pick the claim path (e.g. `roles`) the IdP returns. Leave empty to mean `any authenticated user` +- For each claim value (e.g. `admin`, `marketing`, `external`), choose the Form.io Role it maps to. +- A user may match multiple rows; the resulting `roles` array is the union. + +`settings.roles` = +``` +[ + { + "claim": "", // Leave empty to mean "any authenticated user" + "value": "", + "role": "69dfb6dcbb04c38a9102977c" // This would be the 'Authenticated' role + }, + { + "claim": "groups", + "value": "Admin", + "role": "69dfb6dcbb04c38a9102977d" // This would be the 'Administrator' role + } +] +``` + +With these settings in place, and saved within the OAuth Action, when a user is using the form (embedded within the application), and clicks on the button, the following occurs. + +1. User is taken to IDP authentication page and logs in. +2. IDP auth performs a redirect to the URI defined in `settings.redirectURI` in the OAuth action with the auth code. +3. Form.io renderer then makes a request to the Form.io API server with the auth code. +4. Form.io server sends the auth code to the IDP to obtain the ID and access tokens. +5. Calls the IDP's User Info endpoint with the OAuth access token. +6. Builds an ephemeral user object from the returned user information (no `user` Resource submission is created or looked up). +7. Applies OAuth Role Mapping (configured in action settings) to attach Form.io Roles to that ephemeral user. +8. Encodes the ephemeral user (profile data + roles) into a Form.io JWT and returns it via the `x-jwt-token` response header. From this point on the user is indistinguishable from a Resource-authenticated user — except that the identity lives in the token rather than in a Resource row. + +### Remote Authentication: the ephemeral user + +SSO does not create or require a `user` Resource submission. On each login Form.io constructs an ephemeral user from the IdP's user information and encodes it in the JWT: + +- The IdP user information (email, name, and any other claims you map) becomes the user's `data`. +- OAuth Role Mapping (above) determines the user's `roles`. +- The result is encoded into the Form.io JWT — the same ephemeral, in-token user shape that a Custom JWT carries (`user.data`, `user.roles`, no Resource row). See [`custom-jwt.md`](./custom-jwt.md) for that payload shape and [`jwt-and-sessions.md`](./jwt-and-sessions.md) for how the application reads it. + +Because the identity lives in the token, the application gets everything it needs about the user from the decoded JWT (`Formio.user`) without a Resource lookup. Nothing is persisted to the database on login, and no "first login" provisioning step exists. + +### MFA and provider-level controls + +When MFA is enforced at the IdP, Form.io receives the post-MFA token automatically — there is nothing to wire on the Form.io side. To layer Form.io-side controls (rate-limiting at the project boundary, brute-force on a fallback Resource login form, reCAPTCHA on a non-IdP path), see [`jwt-and-sessions.md`](./jwt-and-sessions.md). + +## MCP Tool Preference + +OAuth provider configuration MUST be performed via the Form.io project portal — no MCP tool covers IdP credentials, scope tables, or Role Mapping at the time of writing. After the provider is configured: + +- Use `role_list` / `role_create` in the MCP server to ensure the Form.io Roles that the OAuth mapping table targets actually exist. +- Use `form_create` / `form_update` to scaffold the login form that hosts the OAuth Actions. +- Use `action_list` to confirm an OAuth Action has been attached to that form (typically managed in the portal, not via `action_create`). + +For documentation of the runtime endpoints the renderer uses to complete the OAuth handshake, see the `runtime-auth` reference in the `formio-api` skill. + +## See also + +- [`token-swap.md`](./token-swap.md) — exchanging an externally-issued OIDC bearer token for a Form.io JWT without rendering an OAuth Action button. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — the Form.io Roles your OAuth mapping targets. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the Form.io JWT the OAuth flow returns, the `jti` Session ID, and logout semantics that invalidate the session even if the IdP session is still alive. +- [`sso-saml.md`](./sso-saml.md), [`sso-ldap.md`](./sso-ldap.md) — sibling SSO references for other provider types. diff --git a/plugin/skills/formio-auth/references/sso-saml.md b/plugin/skills/formio-auth/references/sso-saml.md new file mode 100644 index 0000000..4dd5bbb --- /dev/null +++ b/plugin/skills/formio-auth/references/sso-saml.md @@ -0,0 +1,78 @@ +# SSO — SAML + +## Overview + +Form.io supports SAML 2.0 SSO with any SAML-compliant Identity Provider (ADFS, Okta SAML, OneLogin, Shibboleth, etc.). The user authenticates against the IdP, the IdP posts a signed SAML assertion back to Form.io, and Form.io validates the signature and returns a first-party Form.io JWT. + +Like every SSO method here, SAML uses **Remote Authentication**: Form.io does not look up or create a `user` Resource submission. It reads the attributes from the assertion, builds an **ephemeral user object** from them, applies **SAML Role Mapping** to attach Form.io Roles, and encodes that user — profile data plus mapped roles — **entirely within the Form.io JWT**. From the application's point of view the user is indistinguishable from a Resource-authenticated user; the difference is that the identity lives in the token, not in a database row. + +## When to use this + +Reach for SAML SSO when: + +- The IdP only speaks SAML (or the organization mandates it for compliance reasons). +- ADFS, legacy enterprise SSO, or SAML-only education/government tenants are involved. +- Assertions carry the role/group claims you want to map onto Form.io Roles. + +Not for: + +- OIDC / OAuth providers → see [`sso-oidc.md`](./sso-oidc.md). +- LDAP directories → see [`sso-ldap.md`](./sso-ldap.md). +- Backend-issued tokens you want to swap without an interactive login → see [`token-swap.md`](./token-swap.md) and [`custom-jwt.md`](./custom-jwt.md). + +## Configuration + +### IdP-side configuration + +In the IdP's admin console: + +- **Entity ID** — the project's SAML entity identifier (Form.io generates a canonical value per project). +- **ACS / Reply URL** — the Form.io endpoint that consumes the SAML assertion. Form.io generates this; copy it into the IdP. +- **NameID format** — typically `EmailAddress` so the assertion carries an identifier that matches a `user` Resource submission. +- **Attribute statements** — at minimum include `email`; include any role/group claim you intend to map (`role`, `memberOf`, `groups`, etc.). +- **Signing certificate** — the IdP's X.509 cert that Form.io uses to validate assertions. + +### Form.io-side configuration + +In the Form.io project portal's SAML settings: + +- **Issuer / Metadata URL** — paste the IdP's metadata XML URL, or import the metadata document directly. +- **Signing certificate** — uploaded from the IdP. +- **NameID claim → user field mapping** — which field on the ephemeral user object the NameID resolves to (usually `email`). +- **Attribute mapping** — for each non-role attribute you want carried on the user, name the source attribute and the target field. These attributes populate the ephemeral user's `data` that gets encoded into the JWT — they are not written to a Resource submission. + +There is no auto-create / require-existing-user setting: SAML SSO never creates or requires a `user` Resource row. See "Remote Authentication" in [`sso-oidc.md`](./sso-oidc.md) for the shared model and [`custom-jwt.md`](./custom-jwt.md) for the in-token user shape. + +### SAML Role Mapping + +SAML Role Mapping translates an IdP attribute (typically `memberOf`, `groups`, or a custom `role` attribute) onto Form.io Roles: + +- Pick the attribute name. +- For each value the IdP can return, choose the Form.io Role. +- The user's final `roles` array is the union of matched mappings, falling back to the configured default Role if nothing matches. + +Multi-valued attributes (the SAML default for `memberOf`) produce multi-row matches naturally — no special handling needed. + +### Login button + +Attach a SAML Action (one per provider) to the project's login form. The Action renders a button that initiates the SAML handshake (SP-initiated). IdP-initiated flows are also supported when the IdP posts directly to the ACS URL. + +### Just-in-time deprovisioning + +SAML assertions do not include a refresh mechanism. To enforce session revocation at the IdP, combine SAML SSO with a short Form.io session TTL and rely on `jti`-based logout (see [`jwt-and-sessions.md`](./jwt-and-sessions.md)). Form.io has no built-in SAML Single Logout (SLO) handshake at the time of writing; revoke at the Form.io side by invalidating the `jti`. + +## MCP Tool Preference + +SAML provider configuration (Entity ID, certificates, attribute mappings, Role Mapping) MUST be performed via the Form.io project portal — no MCP tool covers SAML metadata or signing certs today. After the provider is configured: + +- Use `role_list` / `role_create` to ensure the Form.io Roles that the SAML Role Mapping targets actually exist. +- Use `form_create` / `form_update` to scaffold the login form hosting the SAML Action button. +- Use `action_list` to confirm a SAML Action is attached to the login form. + +For runtime endpoint documentation, see the `runtime-auth` reference in the `formio-api` skill. + +## See also + +- [`sso-oidc.md`](./sso-oidc.md), [`sso-ldap.md`](./sso-ldap.md) — sibling SSO references for OIDC/OAuth and LDAP. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — the Form.io Roles your SAML Role Mapping targets. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the Form.io JWT the SAML handshake returns and the `jti` Session ID used for logout. diff --git a/plugin/skills/formio-auth/references/token-swap.md b/plugin/skills/formio-auth/references/token-swap.md new file mode 100644 index 0000000..0665607 --- /dev/null +++ b/plugin/skills/formio-auth/references/token-swap.md @@ -0,0 +1,108 @@ +# Token Swap + +## Overview + +Token Swap exchanges an externally-issued OAuth/OIDC bearer token for a first-party Form.io token. The canonical use case is **embedding Form.io forms inside an existing application that already has its own OAuth authentication**. That host application already holds a Bearer token from the OAuth provider; Token Swap trades that token for a Form.io token so that every subsequent interaction with Form.io is authenticated with the new Form.io token — no separate "Sign in with..." step inside Form.io. + +Token Swap is **Remote Authentication**, exactly like interactive OIDC SSO: Form.io does not look up or create a `user` Resource submission. It uses the supplied OAuth token to fetch the user information from the provider, builds an **ephemeral user object**, applies OAuth Role Mappings (defined in the project settings), and encodes that user — profile data plus mapped roles — **entirely within the Form.io token**. + +## When to use this + +Reach for Token Swap when: + +- Form.io forms are embedded in an existing application that already authenticated the user via OAuth, and you do not want a second "Sign in with..." step inside Form.io. +- The host app (mobile shell, Backend-for-Frontend, federated portal) already holds a valid OAuth bearer token and wants to reuse it to authenticate Form.io. + +Not for: + +- Interactive in-browser OIDC SSO → see [`sso-oidc.md`](./sso-oidc.md). +- SAML assertions → see [`sso-saml.md`](./sso-saml.md). +- LDAP credentials → see [`sso-ldap.md`](./sso-ldap.md). +- Issuing a Form.io JWT entirely from your own backend without an IdP at all → see [`custom-jwt.md`](./custom-jwt.md). + +## Configuration +In order to perform a token swap, the project's OpenID settings must be configured. See [`sso-oidc.md`](./sso-oidc.md) for instructions on these configurations. + +**Important: You must also ensure you have the role mappings configured within the project settings to properly map the OIDC claims with the Form.io Roles.** + +OAuth Role Mapping is the bridge between an IdP role claim (e.g. `groups`, `roles`, `https://my-app/roles`) and Form.io Roles. The Project's OAuth settings page exposes a mapping table: + +- Pick the claim path (e.g. `roles`) the IdP returns. +- For each claim value (e.g. `admin`, `marketing`, `external`), choose the Form.io Role it maps to. +- A user may match multiple rows; the resulting `roles` array is the union. + +### Prerequisites + +- An existing OAuth authorization token (Bearer or other) issued to the user by the OAuth provider. In the embedded use case the host application already holds this token. +- **OpenID / OpenID Connect settings configured in the Form.io project** (the provider configuration Form.io uses to validate the token and locate the provider's user-info endpoint, plus OAuth Role Mapping). See [`sso-oidc.md`](./sso-oidc.md) for the provider configuration. +- The provider's **`/userInfo` endpoint must be exposed** — Form.io calls it with the supplied authorization token to retrieve the user information that becomes the ephemeral user. + +### Performing the swap + +The swap is driven by the Form.io JavaScript SDK's `currentUser` call. Instantiate `Formio` against the project URL, attach the OAuth token as an `Authorization` header, and call `currentUser` with `external: true`: + +```js +import { Formio } from '@formio/js'; + +// For token swap to work, your application must set the baseUrl and projectUrl. +const projectUrl = 'https://yourdomain.com/yourproject'; +Formio.setBaseUrl('https://yourdomain.com'); +Formio.setProjectUrl(projectUrl); + +// A simple token swap function. +async function tokenSwap(authToken) { + return await (new Formio(projectUrl)).currentUser({ + external: true, + headers: { + Authorization: authToken + }, + }); +} + +// Swap the bearer token with an authenticated Form.io user with a valid JWT token. +const user = await tokenSwap('Bearer 2e762950-9498-4079-a699-xxxxxxxxxxxx'); + +// Any other calls will now use the `x-jwt-token` swapped. In this example, this would be a submission +// made to the 'myform' using the correct JWT token. +(new Formio(`${projectUrl}/myform`)).saveSubmission({ + data: { + firstName: user.data.firstName, // This data comes from the OIDC userInfo + lastName: user.data.lastName + } +}); +``` + +`external: true` tells `currentUser` to treat the `Authorization` header as an external OAuth token to swap, rather than an existing Form.io token. On that call Form.io: + +1. Takes the bearer token off the `Authorization` header. +2. Calls the OAuth provider's `/userInfo` endpoint with that token to retrieve the user information. +3. Builds an ephemeral user from that information and applies OAuth Role Mapping to attach Form.io Roles (Remote Authentication — no `user` submission is created or looked up). +4. Mints a new Form.io token and passes it back to the Form.io library. +5. The SDK stores the Form.io token and attaches it as the `x-jwt-token` header on every subsequent Form.io request — the OAuth token is no longer needed for Form.io calls. + +### Caching and rotation + +- The SDK holds the minted Form.io token after the swap; subsequent Form.io calls reuse it automatically. No need to re-send the OAuth token on every request. +- When the host application's OAuth token rotates or expires, re-run the `currentUser({ external: true, header })` swap with the new OAuth token to mint a fresh Form.io token. +- Logout invalidates the Form.io token's `jti` Session ID on the Form.io side; it does NOT log the user out of the OAuth provider. See [`jwt-and-sessions.md`](./jwt-and-sessions.md). + +### Failure modes + +- **OpenID / OIDC not configured** — Form.io cannot validate the token or find the provider; configure the provider's OpenID settings in the project first. +- **`/userInfo` endpoint not exposed or unreachable** — Form.io cannot fetch the user information, so no Form.io token is minted. Expose the provider's `/userInfo` endpoint. +- **OAuth token invalid or expired** — the provider rejects the `/userInfo` call; the swap fails. Refresh the OAuth token in the host app, then swap again. +- **Role Mapping returns no rows** — the user is granted the default Form.io Role (typically Authenticated) and the swap still succeeds. + +## MCP Tool Preference + +Token Swap provider configuration (OpenID / OIDC settings, Role Mapping) MUST be performed via the Form.io project portal. The swap itself is driven client-side by the Form.io JavaScript SDK (`currentUser({ external: true, header })`), not by an MCP tool. Surrounding workflow: + +- Use `role_list` / `role_create` to ensure the Form.io Roles your OAuth Role Mapping targets exist. +- Use `authenticate` once on the MCP server to obtain a portal JWT for project administration calls (the portal JWT is separate from any user token produced by Token Swap). + +## See also + +- [`sso-oidc.md`](./sso-oidc.md) — interactive OIDC SSO and OAuth Role Mapping. For role mapping, token swap gets its roles from the project settings, whereas standard OIDC SSO gets the mappings from the login form's OAuth action. +- [`custom-jwt.md`](./custom-jwt.md) — when there is no IdP and the backend signs Form.io JWTs directly with `JWT_SECRET`. +- [`jwt-and-sessions.md`](./jwt-and-sessions.md) — the Form.io JWT payload that Token Swap returns, the `jti` Session ID, and logout semantics. +- [`roles-and-permissions.md`](./roles-and-permissions.md) — the role IDs OAuth Role Mapping references. diff --git a/plugin/skills/formio-resource-planner/SKILL.md b/plugin/skills/formio-resource-planner/SKILL.md index 88f9ce7..57b1585 100644 --- a/plugin/skills/formio-resource-planner/SKILL.md +++ b/plugin/skills/formio-resource-planner/SKILL.md @@ -1,7 +1,7 @@ --- name: formio-resource-planner description: >- - Plan the Resource structure, field configurations, and access/permission model for a Form.io application from a user's high-level requirements, then emit a ready-to-import Form.io project `template.json`. Use this skill whenever the user wants to design, architect, model, or plan a Form.io app, project, portal, or data model — phrases like "build a app in Form.io", "model in Form.io", "design the resources for...", "plan the schema for...", "I want to build a task manager / CRM / inventory / booking system in Form.io". Two-phase output — (Phase A) a human-readable Resource Map for the user to review and approve; (Phase B) after explicit approval, a full `template.json` containing roles, resources, forms, and actions that can be POSTed to `/{projectName}/import`, handed to `form_create`, or passed to the formio-api skill. Interview-driven — infers resources from the description and asks about relationships, auth, and access before committing. Not for looking up an endpoint (see the formio-api skill). Trigger even if the user does not say the word "Form.io" — if they describe an app and you are in a Form.io project, plan the resources. + Plan the Resource structure, field configurations, and access/permission model for a Form.io application from a user's high-level requirements, then emit a ready-to-import Form.io project `template.json`. Use this skill whenever the user wants to design, architect, model, or plan a Form.io app, project, portal, or data model — phrases like "build a app in Form.io", "model in Form.io", "design the resources for...", "plan the schema for...", "I want to build a task manager / CRM / inventory / booking system in Form.io". Two-phase output — (Phase A) a human-readable Resource Map for the user to review and approve; (Phase B) after explicit approval, a full `template.json` containing roles, resources, forms, and actions that can be POSTed to `/{projectName}/import`, handed to `form_create`, or passed to the formio-api skill. Interview-driven — infers resources from the description and asks about relationships, auth, and access before committing. Not for looking up an endpoint (see the formio-api skill). Not for configuring SSO (OIDC, SAML, LDAP), Token Swap, Custom JWT signed with `JWT_SECRET`, email-token (passwordless) authentication, JWT/session mechanics, 2FA, or RBAC tuning beyond default roles and group permissions — those hand off to `formio-auth`. Trigger even if the user does not say the word "Form.io" — if they describe an app and you are in a Form.io project, plan the resources. --- # Form.io Resource Planner @@ -278,7 +278,9 @@ When the interview has enough signal, emit the resource map as a single fenced m - Login resources: `user` - Admin operations: - Registration: ` with Role Assignment action | admin-invite only> -- SSO: +- SSO: +- Custom JWT: +- Next steps for auth: when `SSO` is anything other than `none`, or `Custom JWT` is `yes`, or the plan calls for Token Swap, email-token auth, 2FA, reCAPTCHA, or RBAC tuning beyond default roles and group permissions, hand off to the `formio-auth` skill after this Resource Map is approved. ## Roles diff --git a/plugin/skills/formio-resource-planner/references/template-md.md b/plugin/skills/formio-resource-planner/references/template-md.md index 3b5b2a1..25c84b5 100644 --- a/plugin/skills/formio-resource-planner/references/template-md.md +++ b/plugin/skills/formio-resource-planner/references/template-md.md @@ -97,9 +97,12 @@ Bulleted facts only. Keep it parseable. - User resource: `> - Login form: (Login action) - Registration: with Role Assignment → | admin-invite only | none> -- SSO: +- SSO: +- Custom JWT: ``` +When `SSO` is anything other than `none`, or `Custom JWT` is `yes`, downstream auth configuration (OAuth/SAML/LDAP Role Mapping, Token Swap, `JWT_SECRET`-signed Custom JWT, email-token auth, 2FA, reCAPTCHA) is owned by the `formio-auth` skill — hand off there after this Resource Map is approved. + ## Roles section One bullet per role. Include the three Form.io defaults (`administrator`, `authenticated`, `anonymous`) plus any custom roles.