Add A Protocol
This is a concrete, end-to-end checklist for adding a new protocol plugin. It includes exact files to touch and a worked example.
0. Choose IDs and naming
Section titled “0. Choose IDs and naming”- Protocol ID: lowercase, URL-safe, no spaces. This becomes the base route
/<id>and API id. - Package dir:
backend/internal/protocols/<id> - Flow IDs: lowercase, snake_case or kebab-case. The UI normalizes underscores to hyphens.
- Demo scenario IDs: use the same ID as the flow you want to execute.
- Use the exact backend flow ID in
flowMeta. The Looking Glass executor uses the hyphen-normalized flow ID.
1. Backend package
Section titled “1. Backend package”Create the directory:
mkdir -p backend/internal/protocols/newprotocolCreate plugin.go and implement plugin.ProtocolPlugin:
package newprotocol
import ( "context"
"github.com/ParleSec/ProtocolSoup/internal/crypto" "github.com/ParleSec/ProtocolSoup/internal/lookingglass" "github.com/ParleSec/ProtocolSoup/internal/mockidp" "github.com/ParleSec/ProtocolSoup/internal/plugin" "github.com/go-chi/chi/v5")
type Plugin struct { *plugin.BasePlugin mockIdP *mockidp.MockIdP keySet *crypto.KeySet lookingGlass *lookingglass.Engine baseURL string}
func NewPlugin() *Plugin { return &Plugin{ BasePlugin: plugin.NewBasePlugin(plugin.PluginInfo{ ID: "newprotocol", Name: "New Protocol", Version: "1.0.0", Description: "Short description", Tags: []string{"tag1", "tag2"}, RFCs: []string{"RFC XXXX"}, }), }}
func (p *Plugin) Initialize(ctx context.Context, config plugin.PluginConfig) error { p.SetConfig(config) p.baseURL = config.BaseURL
if idp, ok := config.MockIdP.(*mockidp.MockIdP); ok { p.mockIdP = idp } if ks, ok := config.KeySet.(*crypto.KeySet); ok { p.keySet = ks } if lg, ok := config.LookingGlass.(*lookingglass.Engine); ok { p.lookingGlass = lg } return nil}
func (p *Plugin) Shutdown(ctx context.Context) error { return nil }
func (p *Plugin) RegisterRoutes(router chi.Router) { router.Get("/endpoint", p.handleEndpoint)}
func (p *Plugin) GetInspectors() []plugin.Inspector { return nil }func (p *Plugin) GetFlowDefinitions() []plugin.FlowDefinition { return nil }func (p *Plugin) GetDemoScenarios() []plugin.DemoScenario { return nil }Notes:
- Routes are mounted at
/<protocol_id>. If your plugin ID isacme, the route above becomes/acme/endpoint. - Use real protocol execution and real cryptographic operations. Do not hardcode tokens or protocol artifacts; if you return demo metadata, include real values (timestamps, IDs) from execution.
2. Handlers and Looking Glass capture
Section titled “2. Handlers and Looking Glass capture”Implement handlers in handlers.go. To emit events, you need the session id:
sessionID := r.Header.Get("X-Looking-Glass-Session")if sessionID == "" { sessionID = r.URL.Query().Get("lg_session")}if sessionID != "" && p.lookingGlass != nil { b := p.lookingGlass.NewEventBroadcaster(sessionID) b.EmitFlowStep(1, "Step Name", "Client", "Server", map[string]interface{}{ "note": "What happened", })}Important:
- The capture middleware automatically emits HTTP request/response exchanges when the request includes
X-Looking-Glass-Sessionorlg_session. - Use
EmitFlowStepandEmitTokenIssuedto add semantic context. - For browser redirects where headers are hard to set, use the
lg_sessionquery parameter.
3. Flow definitions (UI + diagram)
Section titled “3. Flow definitions (UI + diagram)”Flow definitions power the protocol pages and the Looking Glass diagram.
plugin.FlowDefinition{ ID: "flow_id", Name: "Flow Name", Description: "What the flow demonstrates", Executable: true, Category: "authorization", Steps: []plugin.FlowStep{ { Order: 1, Name: "Request", Description: "Client sends request to server", From: "Client", To: "Authorization Server", Type: "request", // request | response | redirect | internal Parameters: map[string]string{"param": "description"}, Security: []string{"RFC XXXX Section Y - security note"}, }, },}Use Executable: false for flows that are documentation-only. These still appear on protocol pages but are hidden from the Looking Glass executor list.
4. Demo scenarios
Section titled “4. Demo scenarios”Add a demo scenario with the same ID as the flow you want to execute:
plugin.DemoScenario{ ID: "flow_id", Name: "Flow Demo", Description: "Interactive demonstration", Steps: []plugin.DemoStep{ {Order: 1, Name: "Start", Description: "Initialize", Auto: true}, {Order: 2, Name: "User Action", Description: "User interaction", Auto: false}, },}5. Register the plugin
Section titled “5. Register the plugin”Add the plugin in the correct entrypoint:
- Monolith:
backend/cmd/server/main.go - Split services:
backend/cmd/server-federation/main.go,server-scim,server-ssf,server-spiffe, etc.
If you create a new service, update docker compose and gateway routing as needed.
6. Frontend wiring (minimum to appear in UI)
Section titled “6. Frontend wiring (minimum to appear in UI)”Add the new protocol metadata (inside the object literal):
1) frontend/src/protocols/registry.ts:
export const protocolMeta = { // ... newprotocol: { icon: 'Shield', color: 'orange', gradient: 'from-orange-500 to-amber-500', features: ['Key feature 1', 'Key feature 2', 'Key feature 3'], },}2) frontend/src/lookingglass/registry.ts:
const PROTOCOL_COLORS = { // ... newprotocol: 'orange',}
const PROTOCOL_ICONS = { // ... newprotocol: 'shield',}3) frontend/src/views/ProtocolDemo.tsx:
const flowMeta = { // ... 'flow_id': { icon: Shield, color: 'from-orange-500 to-amber-600', features: ['Feature A', 'Feature B'], recommended: true, },}Use the exact flow.id string from the backend, not the URL slug.
4) frontend/next.config.ts: add a rewrite entry for the protocol base path so executors can call /${protocolId} in local/dev and production runtimes.
async rewrites() { return [ // ... { source: '/newprotocol/:path*', destination: `${backendOrigin}/newprotocol/:path*` }, ]},7. Parameter explainers and references
Section titled “7. Parameter explainers and references”Two UI surfaces give learners security context for the new protocol: per-parameter explainer panels (clickable (?) icons beside flow parameters) and the “Specs & references” panels on the protocol overview and flow detail pages. Both are populated from data files; adding a new protocol means adding entries on both surfaces.
7.1 Per-parameter explainers
Section titled “7.1 Per-parameter explainers”Create frontend/src/protocols/explainers/<protocol>.ts exporting a map keyed by parameter name:
import type { ParameterExplainer } from './index'
export const NEWPROTOCOL_EXPLAINERS: Record<string, ParameterExplainer> = { param_name: { purpose: 'What this parameter does and why it exists.', attacks: [ { id: 'named-attack', name: 'Named attack pattern (CVE or research label)', scenario: 'How an attacker exploits the parameter when it is absent or mishandled.', impact: 'What the attacker achieves.', }, ], mitigations: [ { action: 'Concrete mitigation step.', rationale: 'Optional one-line reason this works.', mitigates: ['named-attack'], }, ], references: [ { label: 'RFC/spec section', href: 'https://...' }, ], },}Then register the map in frontend/src/protocols/explainers/index.ts by adding an import and spreading it into the EXPLAINERS record:
import { NEWPROTOCOL_EXPLAINERS } from './newprotocol'
const EXPLAINERS: Record<string, ParameterExplainer> = { ...OAUTH2_EXPLAINERS, // ... ...NEWPROTOCOL_EXPLAINERS,}Notes:
- Mitigations link back to attacks via
mitigates: string[]of attack IDs. A single mitigation may cover several attacks. - For parameters whose semantics differ across protocols (for example,
noncein OIDC versus OID4VP), use the override key form`${protocolId}:${name}`instead of the bare name. - Source content from canonical specs and named real-world incidents (CVE numbers, research disclosures). Avoid universal-baseline mitigations such as “use HTTPS”; reviewers can be assumed to know those.
- See any existing protocol’s explainer file (for example
oauth2.ts) for a worked reference of tone, length, and structure. - The map keys must match the parameter names emitted by the backend’s
FlowStep.Parametersmap (see section 3). Mismatched names will silently fail to render an explainer.
7.2 Per-protocol references panel
Section titled “7.2 Per-protocol references panel”Update frontend/src/protocols/presentation/protocol-catalog-data.ts to add a references array on the protocol’s catalog entry:
{ id: 'newprotocol', name: 'New Protocol', // ...existing fields references: [ { category: 'core', label: 'Core spec name', href: 'https://...' }, { category: 'security', label: 'Security & privacy considerations', href: 'https://...' }, { category: 'companion', label: 'Companion RFC', href: 'https://...' }, { category: 'profile', label: 'Deployment profile', href: 'https://...' }, ],}The categories render as separate sections in the UI:
core: specs that normatively define the protocol.security: dedicated security and privacy considerations documents (or deep-anchored security sections of core specs).companion: extension RFCs and related standards the protocol composes with.profile: deployment profiles that constrain the protocol for specific assurance regimes (FAPI, HAIP, eIDAS, and so on).
7.3 Per-flow references panel
Section titled “7.3 Per-flow references panel”In the same file, add a references array to each flow definition. Prefer deep-anchored URLs to specific sections rather than spec landing pages:
{ id: 'flow-id', name: 'Flow Name', rfc: '§X.Y', references: [ { category: 'core', label: 'Spec §X.Y - Section title', href: 'https://...#anchor' }, { category: 'security', label: 'Security considerations §Z', href: 'https://...#anchor' }, ],}The same ProtocolReferences component renders both surfaces, so flow references should be focused (typically two to four per flow) and non-overlapping with the protocol-level reading list.
8. Optional: Looking Glass executor (for real execution)
Section titled “8. Optional: Looking Glass executor (for real execution)”If your flow is executable, implement a flow executor.
frontend/src/lookingglass/flows/newprotocol-flow.tsfrontend/src/lookingglass/flows/index.ts(export it)frontend/src/lookingglass/flows/executor-factory.ts(map the flow ID)
Looking Glass normalizes flow IDs by replacing underscores with hyphens. Use the hyphenated ID in FLOW_EXECUTOR_MAP and add a snake_case alias if needed.
Minimal skeleton:
export class NewProtocolExecutor extends FlowExecutorBase { readonly flowType = 'newprotocol-flow' readonly flowName = 'New Protocol Flow' readonly rfcReference = 'RFC XXXX'
async execute(): Promise<void> { this.updateState({ status: 'executing', currentStep: 'Starting' }) await this.makeRequest('GET', `${this.config.baseUrl}/endpoint`, { step: 'Call endpoint', rfcReference: this.rfcReference, }) this.updateState({ status: 'completed', currentStep: 'Done' }) }}Documentation
Section titled “Documentation”Add backend/internal/protocols/<id>/README.md:
- Supported flows
- RFC/spec references
- Any config knobs or special behavior
When user-facing or deployment behavior changes, also update public docs under docs/starlight/src/content/docs/, OpenAPI under openapi/v1/ (and keep docs/starlight/public/openapi/ in sync), and package docs under docs/packages/ as described in Development setup.
Protocol verification
Section titled “Protocol verification”Keep normative behavior tied directly to executable tests and Looking Glass events rather than maintaining a duplicate JSON compliance manifest.
Run runtime conformance checks from backend/:
go test ./internal/protocols/oid4vci ./internal/protocols/oid4vp -count=1This command executes real end-to-end OID4VCI and OID4VP HTTP flows, including issuance-to-presentation lineage checks.
External interoperability checks:
- Configure
CONFORMANCE_BASE_URLandCONFORMANCE_EXTERNAL_WALLET_SUBMIT_URLGitHub secrets. - The
Protocol Conformanceworkflow runs nightly/manual external interop checks viaTestExternalInteropConformance. - For local/manual external checks, run:
RUN_EXTERNAL_INTEROP_CONFORMANCE=1 CONFORMANCE_BASE_URL=https://<verifier-base> CONFORMANCE_EXTERNAL_WALLET_SUBMIT_URL=https://<wallet-harness>/submit go test ./internal/protocols/oid4vp -run TestExternalInteropConformance -count=1 -vVC plugin patterns (OID4VCI/OID4VP)
Section titled “VC plugin patterns (OID4VCI/OID4VP)”When adding Verifiable Credential flows, follow these additional constraints:
- Keep flow execution real: no placeholder credentials, proofs, or verifier outcomes in UI paths.
- Capture VC artifacts explicitly in executor state (
vcArtifacts) for request objects, proofs, credentials, wallet handoff payloads, and policy evidence. - For wallet-mediated flows, expose deep-link/QR payloads and support an external wallet agent that performs real HTTP + signing operations.
- Enforce actor key separation: issuer, verifier, and wallet roles must use distinct key material, and validators must reject cross-role signatures.
- Maintain credential lineage: credentials presented in OID4VP should come from wallet-held issuance state (or explicit documented fallback), not ad hoc hardcoded claims.
- For OID4VCI metadata, serve the issuer-derived canonical
.well-knownpath from the root router (for path-bound issuer identifiers). - Emit parameter-level denial diagnostics (nonce/audience/expiry/holder-binding plus policy reasons) so failures are teachable, not opaque.
- Keep negative/replay validation in handler logic, tests, and conformance checks; avoid exposing diagnostic-only failure scenarios as primary executable flow catalog entries.
- Document only behavior exercised by tests or live Looking Glass flows.
Acceptance criteria
Section titled “Acceptance criteria”- Real HTTP requests and real tokens (no placeholders)
- Looking Glass shows request/response + semantic events
- Flow definitions match the spec and contain RFC references
- UI metadata added so the protocol and flows are discoverable
Related
Section titled “Related”- Development setup — local validation commands
- Pull request workflow — review expectations and DCO