Skip to content

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.

  • 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.

Create the directory:

Terminal window
mkdir -p backend/internal/protocols/newprotocol

Create 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 is acme, 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.

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-Session or lg_session.
  • Use EmitFlowStep and EmitTokenIssued to add semantic context.
  • For browser redirects where headers are hard to set, use the lg_session query parameter.

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.

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},
},
}

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*` },
]
},

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.

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, nonce in 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.Parameters map (see section 3). Mismatched names will silently fail to render an explainer.

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).

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.ts
  • frontend/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' })
}
}

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.

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/:

Terminal window
go test ./internal/protocols/oid4vci ./internal/protocols/oid4vp -count=1

This command executes real end-to-end OID4VCI and OID4VP HTTP flows, including issuance-to-presentation lineage checks.

External interoperability checks:

  • Configure CONFORMANCE_BASE_URL and CONFORMANCE_EXTERNAL_WALLET_SUBMIT_URL GitHub secrets.
  • The Protocol Conformance workflow runs nightly/manual external interop checks via TestExternalInteropConformance.
  • For local/manual external checks, run:
Terminal window
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 -v

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-known path 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.
  • 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