Run maSSO in Docker? USE_AS_DOCKER.md covers Docker Compose, profiles, ports, environment variables, and in-container Quick Tunnel.
Without --issuer, maSSO defaults to http://localhost:<port>/. Remote SPs might not allow localhost endpoints, and the iss claim or SAML assertion issuer will mismatch. Setting --issuer propagates the URL into discovery, metadata, token claims, and assertion issuers.
No account or token required. Ephemeral URL, valid for the lifetime of the process.
./masso oidc --cloudflaredmaSSO starts cloudflared, parses the https://xxxx.trycloudflare.com URL from its output, and sets --issuer automatically. The dashboard Settings tab shows the live tunnel URL.
./masso oidc --issuer https://my-tunnel.example.com/Any tunnel that forwards to 0.0.0.0:<port> works (ngrok, Cloudflare named tunnels, SSH port forwarding, etc.).
When maSSO is reachable directly on its listen port (e.g. an EC2 host with the security group open on 8081, no proxy in front), pass the public hostname as the issuer:
./masso oidc --issuer http://ec2-xxxx.compute.amazonaws.com/Because the endpoints are served on the IdP port, maSSO automatically appends the listen port when a --issuer is http:// and has no explicit port:
Issuer : http://ec2-xxxx.compute.amazonaws.com:8081/
Discovery : http://ec2-xxxx.compute.amazonaws.com:8081/.well-known/openid-configuration
The port is added consistently to the iss claim, discovery/metadata, and the printed banner, so clients reach the right place. maSSO logs a warning when it does this.
Rules:
https://…issuers are never changed: TLS almost always means a proxy/tunnel on 443.- An explicit port disables the behaviour: if a reverse proxy fronts the IdP on a standard port, set it explicitly, e.g.
--issuer http://myhost:80/(or usehttps://).
Each endpoint can be set independently from the dashboard. Available modes vary by endpoint type.
| Mode | Behaviour |
|---|---|
| Pass-through | Normal IdP behaviour - traffic is logged but not modified |
| Mock | Return a fixed pre-configured response; the real IdP is not called |
| Intercept | Buffer the real response, push it to the dashboard, block until the operator releases it (with optional edits) |
| Intercept (Before Sign) | Capture content before signing - SAML assertion XML before samlp signs it, or OIDC id_token payload before re-signing |
| Intercept (Response) | (SAML /saml/sso only) - Capture the already-signed HTTP POST before it is forwarded; useful for signature-stripping tests |
When /oauth/token is set to Intercept (Before Sign):
- maSSO buffers the real token endpoint response.
- It decodes the
id_tokenJWT payload and presents the raw JSON to the dashboard operator. - The operator edits any claim (
sub,email,groups, custom claims - anything). - maSSO re-signs the modified payload with its own RSA private key using RS256.
- The new JWT passes SP signature verification because the matching public key is served at
/keys(JWKS).
The SP receives a cryptographically valid id_token containing exactly what the operator typed.
When /saml/sso is set to Intercept (Before Sign):
- The Node.js engine builds the SAML assertion but has not signed it yet.
- The unsigned XML is sent to the Go interceptor via an internal HTTP call.
- The interceptor pushes it to the dashboard WebSocket.
- The operator edits the assertion using a split-pane editor:
- Left - Structured form: NameID, Conditions (NotBefore / NotOnOrAfter / Audience), and per-attribute fields (name, value, NameFormat).
- Right - Raw XML textarea, synchronized with the form in real time.
- The modified XML is returned to Node.js, which signs it with the SAML IdP private key.
- The SP receives a cryptographically valid SAML Response with attacker-controlled content.
Full SAML IdP settings reference (signing algorithms, attribute mappings, SP overrides): docs/saml-settings.md.
http://127.0.0.1:8082/dashboard - local only, adapts automatically to OIDC or SAML mode.
- Endpoints tab - per-endpoint mode selector; mock response editor.
- Intercept queue - live WebSocket feed; blocked requests wait here until released.
- Flow log - full HTTP/1.1 wire-format log of every request and response.
- Users tab - create, edit, and delete test users stored in SQLite.
- Settings tab - issuer URL, tunnel URL (if
--cloudflared), OIDC token lifetimes, client credentials, and SAML IdP options. - SCIM tab - (with
--scim-url) import SP users, run attack payloads against/Usersand/Groups. - Signer tab - standalone token signing and verification tool (see below).
The Signer tab lets you craft, sign, and verify tokens at any time, no active authentication flow is needed. Useful for preparing test payloads, verifying that SP signature enforcement actually works, and confirming that a token you built is cryptographically valid before replaying it.
In duo mode the Signer always uses the currently selected IdP's own key, each instance has an independent OIDC RSA key (distinct
kid, served at that instance's/keys) and SAML certificate (advertised in that instance's/metadata). Switch instances with the top-bar pills before signing to target the other IdP. Both instances share the same users pool (one SQLite user database), so the account picker offers the same test accounts regardless of the selected IdP.
Signing
- Open the Signer tab. An editable JSON payload is pre-populated with a standard
sub,email,iat,expskeleton. - Edit any claims, add, remove, or modify fields freely.
- Click Sign JWT. maSSO signs the payload with its current RSA private key (RS256) and returns a compact JWT string.
- Copy the JWT with the Copy button or send it directly to the SP.
Verification
- Paste any JWT into the Verify JWT field.
- Click Verify. maSSO checks the structure, RS256 signature against its own public key,
exp, andnbf. - A per-check table reports
ok/failfor each condition.
The signing key is the same one served at
/keys(JWKS). Tokens signed here will pass the SP's signature verification.
Signing
- Open the Signer tab. An unsigned SAML Assertion XML is pre-populated and editable byte-for-byte.
- Edit the XML directly, change
NameID, attributes, conditions, audience, or any element. - Click Sign Assertion. The Node.js SAML engine wraps the assertion in a
samlp:Response, signs it with the IdP private key, and returns a base64-encodedSAMLResponse. - Use Copy Base64 to copy the encoded response.
- Optionally paste back the base64 into Verify SAMLResponse to confirm the signature is accepted.
Verification
- Paste a base64-encoded
SAMLResponseinto the Verify SAMLResponse field. - Click Verify. maSSO checks the XML-DSIG signature against the IdP certificate,
NotOnOrAfter,NotBefore, and that aNameIDis present. - A per-check table reports each condition.
The verification path also catches common test cases: tampered base64, missing <Signature> element, expired conditions, and future-dated NotBefore.
Duo mode starts two fully independent IdP instances from a single command, each with its own port, engine, interceptor state, flow log, and signing key. Both are controlled from the same dashboard with color-coded pills in the top bar for switching between them.
- Testing cross-organization access control, configure IdP A as "Acme Corp" and IdP B as "Contoso Corp", then verify the SP correctly rejects tokens from the wrong organization's IdP.
- Running parallel flows, trigger an authorization in both instances simultaneously without restarting.
- Comparing SP behaviour, different clients or configurations against the same shared user database.
Both instances share the SQLite user database, so test accounts are available in both without duplication.
# Two OIDC IdPs - A on :8081, B on :8083 (auto), dashboard on :8082
# Names default to "IdP A" and "IdP B", no flags needed
./masso oidc --duo
# Explicit ports
./masso oidc --duo --port 8081 --port-b 8084 --dash-port 8085
# Two SAML IdPs - --acs/--audience are not accepted with --duo; configure each
# IdP's ACS/audience independently from the dashboard's Settings tab once both are up
./masso saml --duo
# Custom colors (any CSS hex)
./masso oidc --duo --color-a "#a78bfa" --color-b "#34d399" --name-a "Purple IdP" --name-b "Green IdP"Single-IdP "quick setup" flags are rejected with --duo: --client-id,
--client-secret, --redirect-uri, --any-redirect (OIDC) and --acs,
--audience (SAML) each only have one obvious target once there are two IdPs -
maSSO exits immediately (before starting any tunnel or subprocess) if one of
these is passed together with --duo. Start duo mode with --duo alone, then
configure each IdP independently from the dashboard's Settings tab once
both instances are up. Single mode keeps all of these flags for fast one-shot
runs.
The local listen ports are the same whether or not --cloudflared is used, the
tunnels simply forward to them. With the defaults (--port 8081), duo mode uses:
| Component | Flag | Default |
|---|---|---|
| IdP A | --port |
8081 (same as single mode) |
| IdP B | --port-b |
8083 (auto: --port + 2) |
| Dashboard (local only) | --dash-port |
8082 |
| SAML Node.js engine A | --node-port |
7070 (SAML mode only) |
| SAML Node.js engine B | (automatic) | 7071 (auto: --node-port + 1, SAML mode only) |
--port-bdefaults to--port + 2(not+ 1) so it never collides with the default--dash-port(--port + 1, i.e. 8082). maSSO refuses to start if--port-bequals--portor--dash-port.
The issuer each IdP advertises (in OIDC discovery / iss claims and SAML
metadata / assertion issuer) depends on --cloudflared:
| Mode | IdP A issuer | IdP B issuer |
|---|---|---|
without --cloudflared (default) |
http://localhost:8081/ |
http://localhost:8083/ |
with --cloudflared |
https://<random-a>.trycloudflare.com/ |
https://<random-b>.trycloudflare.com/ |
With --cloudflared, each IdP gets its own quick tunnel (two cloudflared
processes). IdP A's tunnel is required, startup fails if it can't be established;
IdP B's is best-effort and falls back to http://localhost:8083/ (with a warning)
if it can't come up. Both tunnels are terminated on shutdown.
Finding each IdP's public URL: selecting an IdP in the dashboard shows that instance's real issuer (its own tunnel URL in cloudflared mode) in the Settings tab, and
/dashboard/api/inforeports a per-instanceissuer/issuer_source. The startup log also printsCloudflare Tunnel established instance=A/B url=….
The top bar gains two colored pills, one per IdP. Click a pill to switch the dashboard context:
- All panel data (Endpoints, Intercept queue, Flow Log, Settings, Signer) updates to the selected instance. Editing and saving from the Settings tab (OIDC token lifetimes / client config, or SAML IdP options) applies only to the currently selected IdP; each instance's settings are stored separately.
- The
--accentcolor changes to the instance's color, including a colored bar across the top of the tabs header, so you always know which IdP you are looking at. The header pill readsOIDC IdPs Duo Mode/SAML IdPs Duo Mode(versusOIDC IdP/SAML IdPin single mode). - The Signer signs with the selected instance's own key, so the resulting token/
kid(OIDC) or certificate (SAML) matches that IdP; switching instances regenerates the signer payload for the new one. - The WebSocket reconnects to the selected instance's channel; intercepts from both instances are tracked independently.
- The Users and SCIM tabs are shared; they always show the common user database.
test/duo-sp/ is a single SP that logs in against both duo IdPs (A and B) over both OIDC and SAML, and verifies which IdP issued each credential, ideal for cross-organization access tests.
# OIDC duo
./masso oidc --duo
cd test/duo-sp && go build -o duo-sp . && ./duo-sp # open http://localhost:9999
# SAML duo (--acs/--audience are not accepted with --duo; set each IdP's
# ACS/audience to match the SP from the dashboard's Settings tab after startup)
./masso saml --duo
cd test/duo-sp && ./duo-spThe --idp-a/--idp-b/--name-a/--name-b flags only seed initial values - the
SP's Settings tab lets you repoint either IdP at any URL (different ports,
container-mapped ports, or a trycloudflare.com tunnel) at runtime, with no
restart. See test/duo-sp/README.md for details.
- Persisted endpoint config and OIDC settings are stored and restored per instance. Each IdP keeps its own endpoint modes (mock / intercept / mock response), SAML config, and OIDC token settings, keyed by instance index in SQLite. Changing a setting on one IdP never affects the other, and each instance reloads exactly its own configuration on restart. Single mode uses instance 0's config, so duo-mode edits to IdP B do not leak into a later single-mode run.
- OIDC client config (client ID, secret, redirect URIs) is shared in SQLite. Both instances serve the same registered clients. Configure per-instance redirect URIs via the Settings → OIDC Client panel after startup.
- Dynamically-registered clients (RFC 7591
/register) are per-instance, not shared - a client registered on IdP A is invisible to IdP B's/register/{client_id}and vice versa. This is the opposite of the static/dashboard-managed clients above; it matters for testing dynamic-registration flows where the SP under test is expected to keep separate client registrations per organization. --cloudflaredtunnels both IdPs (see Ports and issuers in duo mode above); selecting an IdP in the dashboard shows that instance's real issuer, so each IdP's public tunnel URL is visible from the single dashboard.
All cryptographic material is auto-generated on first run. Nothing is hardcoded.
| Material | Location | Lifetime |
|---|---|---|
| OIDC AES session key | crypto.key (next to DB) |
Persisted - reused across restarts |
| OIDC JWT signing key | In-memory RSA 2048 | Ephemeral - regenerated each startup |
| SAML IdP certificate | saml-engine/idp-public-cert.pem |
Persisted - generated once if missing |
| SAML IdP private key | saml-engine/idp-private-key.pem |
Persisted - generated once if missing |
| SAML session secret | Environment variable | Ephemeral - random 32-byte hex per startup |
To force fresh keys or certificates, delete the relevant files and restart.
Pre-built test clients live in test/. Each accepts flags to target a remote maSSO instance (e.g. running behind a tunnel). Full usage: docs/test-clients.md.
| Directory | Client | Purpose |
|---|---|---|
test/oidc-client/ |
vulncorp-client |
Authorization Code + PKCE RP with ID token validation |
test/oidc-client-dynreg/ |
dynreg-client |
Dynamic registration (RFC 7591), introspection, revocation |
test/oidc-client-mcp/ |
mcp-client |
Protected Resource Metadata, RFC 8414 AS Metadata, MCP session headers |
test/saml-sp/ |
saml-sp |
SAML SP - generates AuthnRequests, validates assertions and attributes |
test/scim-sp/ |
scim-sp |
SCIM SP - tests User/Group provisioning and token authentication |
test/duo-sp/ |
duo-sp |
Duo-mode SP - one SP that logs in against both duo IdPs (A/B) over both OIDC and SAML; verifies and shows which IdP issued each credential |
# Build and run any test client (example: OIDC RP against a tunneled maSSO)
cd test/oidc-client && go build -o vulncorp-client .
MASSO_IDP=https://random-name.trycloudflare.com/ ./vulncorp-client
# or: ./vulncorp-client --idp https://random-name.trycloudflare.com/| Path | Purpose |
|---|---|
/.well-known/openid-configuration |
OIDC Discovery |
/authorize |
Authorization endpoint |
/oauth/token |
Token endpoint |
/userinfo |
UserInfo endpoint |
/keys |
JWKS |
/register |
Dynamic Client Registration (RFC 7591) |
/register/{client_id} |
Dynamic Client Management - GET / PUT / DELETE (RFC 7592) |
/oauth/introspect |
Token Introspection (RFC 7662) |
/revoke |
Token Revocation (RFC 7009) |
/end_session |
Session termination |
/device_authorization |
Device Authorization (RFC 8628) - start the flow |
/device |
Device Authorization - user-facing approval page (enter user_code, sign in, approve/deny) |
/.well-known/oauth-protected-resource |
Protected Resource Metadata (RFC 9728) |
/.well-known/oauth-authorization-server |
OAuth AS Metadata (RFC 8414) |
| Path | Purpose |
|---|---|
/metadata |
IdP Metadata XML |
/saml/sso |
Single Sign-On (HTTP-Redirect + HTTP-POST) |
/saml/slo |
Single Logout |
/signin |
Post-login relay (Go → Node.js) |