Skip to content

Latest commit

 

History

History
330 lines (233 loc) · 17.5 KB

File metadata and controls

330 lines (233 loc) · 17.5 KB

Run maSSO in Docker? USE_AS_DOCKER.md covers Docker Compose, profiles, ports, environment variables, and in-container Quick Tunnel.

Tunneling

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.

Option A - Cloudflare Quick Tunnel (zero-config)

No account or token required. Ephemeral URL, valid for the lifetime of the process.

./masso oidc --cloudflared

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

Option B - Manual

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

Option C - Direct public access (no reverse proxy)

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 use https://).

Interception Modes

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

Before-Sign Interception

OIDC - JWT Payload Editor

When /oauth/token is set to Intercept (Before Sign):

  1. maSSO buffers the real token endpoint response.
  2. It decodes the id_token JWT payload and presents the raw JSON to the dashboard operator.
  3. The operator edits any claim (sub, email, groups, custom claims - anything).
  4. maSSO re-signs the modified payload with its own RSA private key using RS256.
  5. 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.

SAML - Assertion XML Editor

When /saml/sso is set to Intercept (Before Sign):

  1. The Node.js engine builds the SAML assertion but has not signed it yet.
  2. The unsigned XML is sent to the Go interceptor via an internal HTTP call.
  3. The interceptor pushes it to the dashboard WebSocket.
  4. 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.
  5. The modified XML is returned to Node.js, which signs it with the SAML IdP private key.
  6. 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.


Attacker Dashboard

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 /Users and /Groups.
  • Signer tab - standalone token signing and verification tool (see below).

Signer Tab

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.

OIDC Mode: JWT Signer / Verifier

Signing

  1. Open the Signer tab. An editable JSON payload is pre-populated with a standard sub, email, iat, exp skeleton.
  2. Edit any claims, add, remove, or modify fields freely.
  3. Click Sign JWT. maSSO signs the payload with its current RSA private key (RS256) and returns a compact JWT string.
  4. Copy the JWT with the Copy button or send it directly to the SP.

Verification

  1. Paste any JWT into the Verify JWT field.
  2. Click Verify. maSSO checks the structure, RS256 signature against its own public key, exp, and nbf.
  3. A per-check table reports ok / fail for each condition.

The signing key is the same one served at /keys (JWKS). Tokens signed here will pass the SP's signature verification.

SAML Mode: Assertion Signer / Verifier

Signing

  1. Open the Signer tab. An unsigned SAML Assertion XML is pre-populated and editable byte-for-byte.
  2. Edit the XML directly, change NameID, attributes, conditions, audience, or any element.
  3. 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-encoded SAMLResponse.
  4. Use Copy Base64 to copy the encoded response.
  5. Optionally paste back the base64 into Verify SAMLResponse to confirm the signature is accepted.

Verification

  1. Paste a base64-encoded SAMLResponse into the Verify SAMLResponse field.
  2. Click Verify. maSSO checks the XML-DSIG signature against the IdP certificate, NotOnOrAfter, NotBefore, and that a NameID is present.
  3. 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

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.

When to use it

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

Starting duo mode

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

Ports and issuers in duo mode

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-b defaults 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-b equals --port or --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/info reports a per-instance issuer / issuer_source. The startup log also prints Cloudflare Tunnel established instance=A/B url=….

Using the dashboard in duo mode

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 --accent color 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 reads OIDC IdPs Duo Mode / SAML IdPs Duo Mode (versus OIDC IdP / SAML IdP in 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.

Testing duo mode with a Service Provider

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-sp

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

Limitations

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

Keys and Certificates

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.


Test Clients

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/

Endpoint Reference

OIDC

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)

SAML

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)