!OAuth2 / OIDC / SAML2 Debugger
id-proto-debugger
This is the official home of the community Project.Formerly oauth2-oidc-debugger. The project was renamed on 2026-08-28: it debugs a
good deal more than OAuth2 and OIDC now — SAML 2.0 and 1.1, WS-Trust, WS-Federation,
SD-JWT VC, WebAuthn, Kerberos v5, SPNEGO, LDAP, SCIM 2.0, SPIFFE, X.509 PKI and
standalone encryption — and the old name described the first two of them. Only the
name changed; nothing about the code, the layout or the commands below did. The
deployed sites keep their own names (idptools.com, test.idptools.com).
This is a simple OAuth2 and OpenID Connect (OIDC) debugger (test tool) that I created as part of a Red Hat SSO blog post I wrote in November, 2017. Since then, I have expanded support to include several major Identity Providers (see the complete list below). The blog post uses this debugger for testing the OpenID Connect setup. So, checkout the blog for usage examples. This project builds a docker container that runs the debugger application.
New to this kind of tool? What a protocol debugger is explains what a protocol debugger does, how it differs from a packet sniffer or a client library, and why identity protocols in particular need one.
Supported Specs & Features
This project currently supports the following specs & features:- OAuth2 - RFC 6749
- OAuth2 Refresh Token Support
- OAuth2 application authentication with client_id and client_secret via POST body or Basic Auth (rather than client cert or dsig).
- OpenID Connect Core 1
- OpenID Connect Discovery v1.0
- JWT RFC
- JWS JSON Web Signature
- JWE JSON Web Encryption
- PKCE - RFC 7636
- OIDC RP-Initiated Logout v1.0
- OAuth2 Token Introspection Endpoint (RFC7662) -- client_credentials (basic auth) or bearer token RFC6750 authentication.
- OAuth2 Device Authorization Grant - RFC8628 -- Ever registered an app on your television where it jumped ot your phone? This was probably what was used.
- OAuth2 Token Exchange - RFC8693 -- Exchange one token for another: impersonation or delegation, with a
subject_token, an optionalactor_token, andaudience/resource/scope/requested_token_type. It has its own card on the landing page, which opensoauth2_oidc_1.html— you need a subject token before you can exchange one, so the exchange pane itself is on the results page (oauth2_oidc_2.html). Currently only tested with Keycloak v26.x. - OAuth2 Token Revocation - RFC7009
- OIDC Dynamic Client Registration 1.0 -- a Relying Party registers itself at the OP's registration endpoint, sending its client metadata and getting back a
client_id(plus a Registration Access Token andregistration_client_urifor reading the registration back). Read/update/delete of an existing registration follow RFC 7592, and the request/response bodies are RFC 7591's. It has its own card on the landing page, which opens the Dynamic Client Registration pane onoauth2_oidc_1.html— the same page as the OAuth2 / OIDC workflow, because a registration is what that workflow then uses. - SAML 2.0 -- SP-initiated SSO against an IdP over the HTTP-Redirect, HTTP-POST and HTTP-Artifact bindings, with Single Logout, a signed (and optionally encrypted)
AuthnRequest, and the IdP's response decoded on a page of its own. - SAML 1.1 -- the same workflow's second protocol version, over the same three bindings: Browser/POST and Browser/Artifact, requested with Shibboleth's
urn:mace:shibboleth:1.0:profiles:AuthnRequestprofile, with the artifact resolved over the SOAP binding at the identity provider's SAML responder. It is a different protocol, not an older spelling — there is no request message, no Single Logout, nothing to sign and nothing to encrypt — so the settings that do not apply are greyed out and switched off when 1.1 is selected. See docs/saml11.md. - A SAML Assertion Tool that composes a spec-compliant SAML 1.0 / 1.1 / 2.0 assertion and signs and encrypts it in the browser. See the SAML Assertion Tool section below.
- WS-Federation v1.2 Passive Requestor Profile -- An older protocol, but still common with Microsoft ADFS.
- SD-JWT — Selective Disclosure for JWTs, RFC 9901 -- the format the credential workflows below are built on: salted claim Disclosures hashed into an
_sdarray, a Combined Serialization joined by~, and a Key Binding JWT (section 4.3) whosesd_hashcommits to exactly the bytes presented. In-browser: nothing is disclosed that you did not tick. - SD-JWT VC — SD-JWT-based Verifiable Credentials -- the credential type on top of SD-JWT: a
dc+sd-jwttyped JWT with avct, an issuer resolvable byiss(an HTTPS identifier with JWT VC Issuer Metadata, or adid:jwk), and holder binding throughcnf.
credential_offer_uri / by QR code with a Transaction Code, authorization_details → credential_identifier, a nonce endpoint and openid4vci-proof+jwt proof of possession, batch issuance, deferred issuance, encrypted Credential Responses, the Notification Endpoint, and credential refresh (section 14.5). Appendix H use cases H.1, H.2, H.3 and H.6. See the VC Issuance section below.
- OpenID for Verifiable Presentations 1.0 (OID4VP) -- the VC Presentation workflow: an Authorization Request with
response_type=vp_tokenandresponse_mode=direct_post, a DCQL query decoded into the claim paths being asked for, Client Identifier Prefixes, the request by value or as a signed Request Object by reference, cross-device by QR code, and avp_tokenkeyed by DCQL credential-query id. See the VC Presentation section below. - JAR — JWT-Secured Authorization Request, RFC 9101 -- how a Verifier's OID4VP request is passed by reference and signed, and verified in the browser before anything is disclosed.
- WS-Trust 1.0–1.4 -- Issue / Renew / Validate / Cancel against an STS, selectable protocol version (1.0/1.1/1.2 pre-OASIS, 1.3/1.4 OASIS ws-sx), with WS-Security (UsernameToken / SAML token), WS-Addressing, and optional XML Signature / XML Encryption. See the WS-Trust Test Tools section below.
- Kerberos v5 — RFC 4120 -- the Kerberos workflow: get a ticket-granting ticket from a real Key Distribution Center (a Windows domain controller, MIT or Heimdal krb5, or this project's mock KDC), spend it on a service ticket, and present that to a service with mutual authentication — every field of every message shown on the way past, and the encrypted parts opened when you supply the key. Plus a Kerberos Decoder that parses a pasted message, ticket, GSS-API token, keytab or PAC with no network at all. Unlike every other workflow here this one is not available on the hosted site and its card there is greyed out: Kerberos speaks DER over TCP and UDP port 88, and a browser cannot open a socket, so it needs the api service running locally. The decoder needs no network of its own, but it ships with the rest of the workflow rather than alone — run the debugger locally for any of it.
- Kerberos encryption — RFC 3961, RFC 3962, RFC 8009, RFC 4757 -- the encryption framework and the types actually in use:
aes128/256-cts-hmac-sha1-96(17, 18), the SHA-2 pair (19, 20) andarcfour-hmac-md5(23), implemented in the browser. DES is decode-only — Windows Server 2025 removed it — so an old capture still renders without pretending the protocol is still safe. - GSS-API Kerberos mechanism — RFC 4121 -- the AP exchange as a service actually receives it, including the 0x8003 checksum (which is not a checksum: it carries the channel bindings and the delegation and mutual-authentication flags, with its integers little-endian in a protocol that is otherwise big-endian), and per-message MIC and Wrap tokens.
UserAccountControl flags, signed four different ways with two different keys. "Authentication succeeded but access was denied" is nearly always a question about this structure, and the debugger decodes it and verifies each signature separately.
- Kerberos delegation — MS-SFU -- S4U2Self (a service obtains a ticket for a user who takes no part in it), S4U2Proxy authorized either by
msDS-AllowedToDelegateToon the front end or bymsDS-AllowedToActOnBehalfOfOtherIdentityon the back end — the resource-based form, which is why "I can write to this computer account" becomes "I can reach this service as anybody" — plus unconstrained delegation with forwarded tickets in aKRB-CRED, and renewals. Every refusal in this area isKDC_ERR_BADOPTIONwhatever the cause, so the page's job is to narrow it: which attribute on which account, whether the evidence ticket is forwardable, and whether the required padata was sent. - XML Signature
- XML Encryption
- Base64 Encoding
- [RFC8414]](https://www.rfc-editor.org/rfc/rfc8414.html)
- With the ability to add custom parameters to the Authorization Endpoint call and Token Endpoint call, numerous other protocols can be supported. We'll eventually get around to adding direct support.
- Token Endpoint calls can be initiated from the front-end or back-end depending on what the IdP requires in various use cases.
- The client_id and client_secret can be submitted to the Token Endpoint via POST body or Authorization Request Header.
Supported OAuth2 Authorization Grants
The following OAuth2 Authorization Grants are supported:- Authorization Code Grant
- Implicit Code Grant
- Resource Owner Password Grant
- Client Credentials Grant
- Refresh Grant
- Device Authorization Grant
Supported OIDC Authentication Flows
The following OpenID Connect Authentication Flows are supported- Authorization Code Flow (could also use Authorization Code Grant option and scope="openid profile")
- Implicit Flow (2 variants)
- Hybrid Flow (3 variants)
Privacy & Security
This project is built with your privacy and security as a priority. There is, however, one consequence of how the public site is hosted that you should be aware of.The public site (for example, the hosted idptools.com / test.idptools.com deployments) is served as static content — there is no application backend. Because of this, when your Identity Provider redirects back to the debugger, the authorization code (along with the other OAuth2/OIDC response parameters and the redirect URI) is delivered to the debugger page through the static hosting provider as part of the normal page request. This is an unavoidable property of a static, backend-less build of an OAuth2/OIDC client.
To be clear about what we do with that data:
- We do not log any of the query parameters, fragments, authorization codes, tokens, or redirect URIs that are sent to the site as part of normal debugger operations.
- All token handling, decoding, and validation happens client-side in your browser; your configuration is stored only in your browser's
localStorage.
./generate-tls-cert.sh # once per checkout: the TLS pair
sudo CONFIG_FILE=./env/local.js docker-compose up
Then use the debugger at https://localhost:3000, and register https://localhost:3000/callback as your redirect URI with your Identity Provider. The certificate is self-signed and generated per checkout by ./generate-tls-cert.sh, so your browser will ask you to accept it once; that script prints its SHA-256 fingerprint. In a local build, the authorization code is delivered to the debugger running on your own machine and never transits a third-party host.
AI Coding Tool Disclosure
As of Q1, 2026, Anthropic Claude was used to implement some new features of this project. All code is reviewed by a human before being merged into the main branch.Tested Platforms
So far, this tool has been tested with the following OAuth2 or OIDC implementations:- Red Hat SSO v7.1 (OAuth2 + OIDC)
- 3Scale SaaS with self-managed APICast Gateway (OAuth2 + OIDC)
- Azure Active Directory / EntraId (v1 endpoints, OIDC + OAuth2)
- Azure Active Directory / EntraId (v2 endpoints, OIDC + OAuth2)
- Apigee Edge (OAuth2, with caveats described here)
- Ping Federate (OAuth2 + OIDC)
- AWS Cognito (OAuth2 + OIDC)
- Facebook (OAuth2)
- Google STS (OAuth2 + OIDC)
- KeyCloak (recent versions used in our automated test suite, Red Hat SSO is KeyCloak under the Red Hat banner, so it should work)
- Okta (OIDC + OAuth2)
- Auth0 (OIDC + OAuth2)
- ForgeRock (OIDC + OAuth2 + SAML2)
- Walt.ID (VC Issuance + Presentation)
3Scale Usage Notes
The version of 3Scale SaaS + APICast only supports OAuth2; 3Scale can support the OIDC Authorization Code Flow since the response_type and grant_type values match OAuth2's Authorization Code Grant. The other OIDC Authentication Flows are not supported by 3Scale OAuth2. The latest version of 3Scale on-premise has OIDC support. As of 12/3/2017, I haven't been able to test this yet.Azure Active Directory Usage Notes
Azure Active Directory (v1 endpoints) support OIDC Authorization Code Flow, Implicit Flow, and the Hybrid Flow with response_type="code id_token".Apigee Edge Usage Notes
Apigee Edge supports OAuth2 by providing the building blogs of an OAuth2 Provider. The developer has much leeway in how the pieces are used. This debugger can only be used with Identity Providers that adhere to the spec.AWS Cognito Usage Notes
AWS Cognito has been tested with the OIDC Authorization Code Flow (with a user defined in a user pool and with a facebook federation relationship. Cognito supports federation, but has optional parameters that need to be passed in to tell Cognito which Userpool and Identity Provider to use (like Facebook). The debugger does not support passing in these parameters at this time (we'll call that a future enhancement). It appears to have worked because I tested in a browser session where I had previously authenticated against Cognito using Facebook. Also, when the refresh grant is used, it works without issue the first time; however, the refresh grant response does not include a new refresh token. So, the refresh_token is not prepopulated correctly. The original refresh token can be copied into the field and subsequent refresh token calls will work. I admit I didn't test all possible scenarios, but I imagine that once the refresh token expires, it will issue a new refresh token. Another interesting note that mattered a lot in a recent project, each refresh token grant response has a new ID Token--and, access token, of course.Facebook Usage Notes
Facebook OAuth2 was tested with the OAuth2 Authorization Code Grant. It worked, but there was no refresh token provided in the OAuth2 Token Endpoint response. So, the refresh grant is obviously not going to work. Configuration details can be found here.Google+ Usage Notes
Google+ login was tested with the Authorization Code3 Grant. It worked. No refresh token was provided. Configuration details can be found here.General Usage Notes
Note, that all configuration values except for the user password is written to local storage to prepopulate fields later. If this is not desired, clear your browser's local storage for the debugger when done using.
The debugger has been tested with recent versions of Chrome.
Getting Started
If you have docker / docker-compose installed already:git clone --recurse-submodules https://github.com/rcbj/id-proto-debugger.git
cd id-proto-debugger
sudo CONFIG_FILE=./env/local.js docker-compose build
./generate-tls-cert.sh # once per checkout: the TLS pair
sudo CONFIG_FILE=./env/local.js docker-compose up
Note, you will need at least 950MB of disk space ree in order to build this Docker image.
--recurse-submodules matters: the mock STS the test suite and the local stack
run against lives in its own repository, rcbj/iya-sts,
and is linked here as the sts/ submodule. Cloned without it, sts/ is an empty
directory and the build fails with failed to read dockerfile. In a checkout that
is already in that state:
git submodule update --init
--recursive, not plain --init. The mock STS has a submodule of its own —
node-ldapjs, which its package.json
takes as "ldapjs": "file:node-ldapjs" — and --init alone stops one level
short of it. An uninitialised submodule is an empty directory rather than a
missing one, so the image builds perfectly well, npm installs a package with no
main, and the sts container dies at startup with Cannot find module
'ldapjs': a message naming a package, not a submodule.
The test launchers do this for you — requireMockStsCheckout() in
common/common.sh, which checks the nested one too — but a
bare docker-compose build does not.
There is a second submodule, rcbj/node-ldapjs
at api/node-ldapjs/ (branch master): a fork of ldapjs for the LDAP support
of issue #257, pinned at the final commit its maintainer left behind when the
project was decommissioned on 2024-05-14 — which is why it is a submodule rather
than a dependency in api/package.json. Nothing builds from it yet, so an
uninitialised api/node-ldapjs/ does not break anything today; the bare
git submodule update --init above takes it along with sts/ regardless.
From a bash command prompt on Fedora or RHEL 7.x, run the following::
dnf install git # Or, yum install git
git clone --recurse-submodules https://github.com/rcbj/id-proto-debugger.git
dnf install docker
systemctl start docker
cd id-proto-debugger
sudo CONFIG_FILE=./env/local.js docker-compose build
./generate-tls-cert.sh # once per checkout: the TLS pair
sudo CONFIG_FILE=./env/local.js docker-compose up
Clean Up / Start Over
This is a nuclear option to cleanup docker. You may not want to do this if you have other important things running on your site. List all containers (only IDs) ``sudo docker ps -aq Stop all running containers:sudo docker stop $(docker ps -aq)
Remove all containers:sudo docker rm $(docker ps -aq) Remove all images:sudo docker rmi $(docker images -q) Prune volumes:sudo docker volume prune --all --force Remove all volumes:sudo docker volume rm $(sudo docker volume ls | awk '{ print $2 }')* Remmove all networks:sudo docker network prune -fOn other systems, the commands needed to start the debugger in a local docker container will be similar. The docker Sinatra/Ruby runtime will have to be able to establish connections to remote IdP endpoint (whether locally in other docker containers, on the host VM, or over the network/internet). On the test system, it was necessary to add "--net=host" to the "docker run" args. The network connectivity details for docker may vary from platform-to-platform.
Running
Step-by-step guides with screenshots for every OAuth2 grant and OIDC flow
are in docs/oauth2-oidc:
OAuth 2.0 authorization grants and
OpenID Connect authentication flows. The
summary below is the short version.
- Open your favorite browser and enter "https://localhost:3000" in the address bar.
- Choose the OAuth2 Grant or OIDC Flow that you want to test.
- Enter the Authorization Endpoint.
- Enter the Token Endpoint.
OAuth2 AUthorization Grant:
- Enter the client identifier.
- Enter the Redirect URI (use https://localhost:3000/callback by default)
- Enter the scope information.
- If you need to provide a resource parameter, click the radio button. Then, enter the desired resource parameter.
- Click the Authorize button.
- Authenticate the user.
- Scroll down to the "Exchange Authoriztaion Code for Access Token" Section.
- Verify that the Code field is filled in below in the Token Step section.
- Enter the client identifier
- Enter the client secret if this is a confidential client.
- Enter the scope information.
- If a resource is needed, click Yes. Enter the resource information in the Resource field.
- If the IdP is using a self-signed certificate or a cert issued from a non-public CA, click No next to the "Validate IdP Certificate?" question. Note, certificates signed by public CAs are validated against the trusted CAs included with the Ruby 2.4.0 docker image.
- Click the Get Token button.
- The standard tokens that are returned from the token endpoint are displayed at the bottom.
OAuth2 Implicit Grant:
- Enter the client identifier.
- Enter the Redirect URI (use https://localhost:3000/callback by default)
- Enter the scope information.
- If you need to provide a resource parameter, click the radio button. Then, enter the desired resource parameter.
- Click the Authorize button.
- Authenticate the user.
- The access_token will be listed at the bottom of the screen.
Refresh Token Grant
* In the configuration section, click the the "Yes" radio button next to "Use Refresh Token". This will make the Refresh Token Section appear.
* The refresh token is automatically populated from the Token Endpoint call response.
* Enter the client identifier.
* Enter the client secret.
* Enter the scope.
* Press Enter.
For the other grants and flows, similar steps to the above are used.
See the blog posts for more information.
Running tests
To run the docker-based tests locally, run the following commmand:sudo CONFIG_FILE=./env/local.js docker compose -f docker-compose-run-tests.yml up --abort-on-container-exitTo run tests locally, run:./local-run-tests.shIf you need to pop up the browser for troubleshooting, pass in the --browser option to the test scripts.
To generate a code coverage report, run
./run-coverage.sh`. The report will be under the coverage directory.
The mock STS is a submodule
Everything the suite needs on the other side of a protocol — the WS-Trust STS, the SAML 2.0 IdP endpoints, the OAuth 2.0 / OIDC authorization server, the OID4VCI Credential Issuer, the OID4VP Verifier and the did:web document — is one small Node service that used to live in sts/ in this repository and now lives in rcbj/iya-sts. This repository records a link to it (branch main) as the sts/ submodule; the code is fetched, not vendored.
Two things build from it, and both fail unhelpfully if the submodule was never initialised:
| What | Where | What it says when
sts/ is empty |
|---|---|---|
| the STS image | context: ./sts in all four compose files | failed to read dockerfile |
| the tests image | COPY sts/common/vendored/bbs2023.js in tests/Dockerfile, so tests/bbs2023_cryptosuite.js can check the wallet's cryptosuite against the issuer's | COPY sts/common/vendored/bbs2023.js: not found |
Neither message mentions a submodule, so every launcher (
./docker-run-tests.sh, ./local-run-tests.sh, ./remote-run-tests.sh, ./run-coverage.sh) calls requireMockStsCheckout() from common/common.sh before building: it initialises the submodule if it can and stops the run with the reason if it cannot. The CI workflows that build the stack check out with submodules: true for the same reason.
By default the build uses the commit this repository records, which is what makes a run repeatable. To build against the tip of
main instead, set MOCK_STS_TRACK_REMOTE=1 — that is git submodule update --remote, so the run is no longer repeatable and git status will show the submodule pointer as modified.
The SAML SP key pair
The SAML tests sign the AuthnRequest and LogoutRequest, and decrypt an encrypted assertion, with a Service Provider key pair. No key pair is stored in this repository. generateSpKeyPair() in common/common.sh creates a fresh self-signed RSA 2048 pair at the start of every run — in a temporary directory that is deleted as soon as the PEMs have been read — and exports it:
| Variable | What it is | Who uses it |
|---|---|---|
|
SAML_SP_PRIVATE_KEY | the private key, PEM | the tests, via common/sp_keypair.js — pasted into the debugger's SP key fields to sign and to decrypt |
| SAML_SP_CERT | the matching certificate, PEM | the tests, as the SP public key |
| SAML_SP_SIGNING_CERT | the same certificate as base64 DER | configureKeycloak, which registers it on the SAML client so Keycloak validates the request signature |
So the key exists only in the environment of the run that generated it: on the host for
./local-run-tests.sh and ./remote-run-tests.sh, and inside the tests container for the fully-containerized suite (tests/run-tests-in-container.sh generates its own, so nothing is baked into the image). The generator turns the shell's set -x trace off around the key material and logs only a SHA-256 fingerprint, so the private key never reaches a build log.
Running a single SAML test script by hand needs those variables in the environment — its certificate is what Keycloak was told to trust, so a self-generated pair would not validate. The scripts say so and stop if they are missing.
Prerequisites
To run this project you will need to install docker.
Running the published images
The api and the client are published on every build of
master and
develop, to the GitHub Container Registry and to Docker Hub, so they can be
run without building anything. Both registries get the same images with the
same tags from the same build — use whichever you prefer:
| GitHub Container Registry | Docker Hub |
|---|---|
|
ghcr.io/rcbj/id-proto-debugger-api | iyasec/id-proto-debugger-api |
| ghcr.io/rcbj/id-proto-debugger-client | iyasec/id-proto-debugger-client |
Tags:
latest (the newest master build), develop (the newest develop
build), and one M.N.O per build.
M.N.O (for example 0.9.20260928070000) is the build's version, and the git
tag of the same name marks the commit that image was built from — so
git checkout 0.9.20260928070000 is the source of
ghcr.io/rcbj/id-proto-debugger-api:0.9.20260928070000. The version is also in
each running client's footer and in /version.json.
No login is needed: both packages are public, so an anonymous
docker pull
works.
Pulling the images
# The newest master build
docker pull ghcr.io/rcbj/id-proto-debugger-api:latest
docker pull ghcr.io/rcbj/id-proto-debugger-client:latest
Or one specific build, pinned to the commit tagged with the same version
docker pull ghcr.io/rcbj/id-proto-debugger-api:0.9.20260928070000
docker pull ghcr.io/rcbj/id-proto-debugger-client:0.9.20260928070000
Or the newest develop build
docker pull ghcr.io/rcbj/id-proto-debugger-api:develop
docker pull ghcr.io/rcbj/id-proto-debugger-client:develop
The same images from Docker Hub
docker pull iyasec/id-proto-debugger-api:latest
docker pull iyasec/id-proto-debugger-client:latest
The tags are listed at
(and
.../id-proto-debugger-client) and at
(and
.../id-proto-debugger-client).
The TLS certificate
Both services serve TLS and need the certificate pair
./generate-tls-cert.sh
writes, so that script is still run once, from a checkout. It needs Node.js
and npm on the host (it installs the client's dependencies to issue the
certificate with this project's own X.509 code) and openssl:
git clone https://github.com/rcbj/id-proto-debugger.git
cd id-proto-debugger
./generate-tls-cert.sh # writes ./generated-tls; trust the root it prints
The script prints the root CA's fingerprint and writes it to
./generated-tls-ca/stack-tls-root.pem. Trust that root in your browser (or
accept the certificate on BOTH https://localhost:3000 and
https://localhost:4000 — the api is a separate origin, and a browser that has
not accepted it reports every call to it as a CORS error).
With docker compose
Save this as
docker-compose.yml beside the generated-tls directory the
script wrote:
# id-proto-debugger from its published images on ghcr.io.
DEBUGGER_TAG selects the build: latest (default), develop, or an M.N.O
version such as 0.9.20260928070000.
services:
api:
image: ghcr.io/rcbj/id-proto-debugger-api:${DEBUGGER_TAG:-latest}
container_name: api
environment:
- CONFIG_FILE=./env/local.js
- TLS_ENABLED=true
- TLS_CERT_FILE=/etc/idptools/tls/stack-tls-cert.pem
- TLS_KEY_FILE=/etc/idptools/tls/stack-tls-key.pem
volumes:
- ./generated-tls:/etc/idptools/tls:ro
ports:
- "4000:4000"
restart: unless-stopped
client:
image: ghcr.io/rcbj/id-proto-debugger-client:${DEBUGGER_TAG:-latest}
container_name: client
environment:
- CONFIG_FILE=./env/local.js
- TLS_ENABLED=true
- TLS_CERT_FILE=/etc/idptools/tls/stack-tls-cert.pem
- TLS_KEY_FILE=/etc/idptools/tls/stack-tls-key.pem
volumes:
- ./generated-tls:/etc/idptools/tls:ro
ports:
- "3000:3000"
depends_on:
- api
restart: unless-stopped
Then:
docker compose pull # fetch the images
docker compose up -d # start both services
DEBUGGER_TAG=0.9.20260928070000 docker compose up -d # or pin one build
docker compose logs -f # follow the logs
docker compose down # stop and remove them
To take the images from Docker Hub instead, replace
ghcr.io/rcbj/ with
iyasec/ in the two image: lines.
This is not the repository's own
docker-compose.yml, which BUILDS the images
from source and also starts the mock STS used by the test suite.
With docker run
The same two services without compose:
for svc in api:4000 client:3000; do
docker run -d --name "${svc%%:}" -p "${svc##:}:${svc##*:}" \
-e CONFIG_FILE=./env/local.js -e TLS_ENABLED=true \
-e TLS_CERT_FILE=/etc/idptools/tls/stack-tls-cert.pem \
-e TLS_KEY_FILE=/etc/idptools/tls/stack-tls-key.pem \
-v "$PWD/generated-tls:/etc/idptools/tls:ro" \
"ghcr.io/rcbj/id-proto-debugger-${svc%%:*}:latest"
done
Either way, open
https://localhost:3000. The images are built with the
local configuration, which is what makes the client call the api at
https://localhost:4000 — so the published images run on the machine whose
browser uses them, and register https://localhost:3000/callback as the
redirect URI with your identity provider.
Building the docker image
yum install git
git clone --recurse-submodules https://github.com/rcbj/id-proto-debugger.git
yum install docker
systemctl start docker
cd id-proto-debugger
sudo docker build -t rcbj/debugger-client --build-arg CONFIG_FILE=./env/local.js -f client/Dockerfile .
sudo docker run -p 3000:3000 -e CONFIG_FILE=./env/local.js -d rcbj/debugger-client
sudo docker build -t rcbj/debugger-api --build-arg CONFIG_FILE=./env/local.js -f api/Dockerfile .
sudo docker run -p 4000:4000 -e CONFIG_FILE=./env/local.js -d rcbj/debugger-api
On other systems, the commands needed to start the debugger in a local docker container will be similar. The docker Sinatra/Ruby runtime will have to be able to establish connections to remote IdP endpoint (whether locally in other docker containers, on the host VM, or over the network/internet). On the test system, it was necessary to add "--net=host" to the "docker run" args. The network connectivity details for docker may vary from platform-to-platform.
Embedded in the mock STS
The debugger has a third deployment shape besides the containers above and the
static idptools.com build: **embedded inside the
mock STS**, which serves the UI from a
listener of its own behind an OIDC sign-in and runs the api as a child process
on a unix socket behind its own proxy. This repository builds the tree that
service consumes:
git submodule update --init --recursive
embedded/build.sh --out /path/to/tree # without docker
docker build -f embedded/Dockerfile -t rcbj/id-proto-debugger-embedded:dev \
--build-arg GIT_COMMIT=$(git rev-parse --short=12 HEAD) .
Nothing about the standalone debugger changes. The contract — the tree, the
DEBUGGER_* environment the api child reads, and the address allow-list it
enforces — is in embedded/CLAUDE.md.
Additional Feature Information
RFC 9700 compliance mode
The Configuration Parameters pane of the OAuth2 / OIDC workflow carries a checkbox, Enforce RFC 9700 client requirements, at the top — above the grant selector, because it governs every control below it. It turns the client-side half of RFC 9700, Best Current Practice for OAuth 2.0 Security, from advice into enforcement on both pages of the workflow.
It ships OFF, and that is deliberate rather than cautious. A debugger exists to be pointed at identity providers that are wrong — that is most of what anybody uses one for. An authorization response with no
state on it is a finding worth seeing, and a client that refuses to send the request cannot show it to you; an OP that still only speaks the Implicit Grant is something somebody has to work with today, and a mode that removes it from the dropdown removes their reason for opening the page. With the box clear, this workflow behaves exactly as it did before the feature existed: no check runs, no report is drawn, nothing is refused.
With it ticked, in outline:
- the three Implicit variants, the two Hybrid flows that carry
token, and the Resource Owner Password Credentials grant are disabled in the selector, each with its own reason — and refused again at the request, because disabled is a property of a control while the rule is a property of the request. OIDC Hybrid (code id_token) survives: it returns no access token from the authorization endpoint;
PKCE is forced on with S256, certificate validation is forced on, client_secret_basic is selected, and DPoP is switched on together with the front-end token call (a proof cannot ride on the proxied one);
state, nonce and the PKCE verifier are regenerated per request and frozen onto a transaction record in sessionStorage — per-tab, dying with the browsing session, which is what "bound to the user-agent transaction" means;
every endpoint must be https (with RFC 8252's loopback exception, which is what lets the mode be used against a local stack at all), and the endpoints must have come from the server's metadata rather than been typed; response_mode=form_post is requested when the server advertises it and* this build has a /callback to receive the POST, so the response never reaches the address bar at all; whatever does reach it is removed with history.replaceState as soon as it has been read;
- on the way back,
state must match and is single-use, the RFC 9207 iss parameter must match the stored issuer (and its absence is fatal when the server advertises support for it), and the ID Token's iss must match too;
no token is used until the ID Token's nonce has been validated — not rendered, not written to Token History, not offered to the UserInfo or refresh panes. A mismatch discards the whole set;
an authorization code is presented once, and a rotated refresh token spends the one it replaced;
the audience, the granted scope, the token_type that came back, whether the refresh token was rotated, and whether the subject is confusable with the client_id are reported rather than enforced, because a client can only observe them.
A report appears beside each step naming the requirement, the level RFC 9700 states it at, and what was found.
Four of the fifteen sections are not behind the checkbox, because they are this deployment's own posture and are invisible to any identity provider:
/callback is not an open redirector and never takes its destination from the request, it redirects with 303 rather than 302 or 307, every response carries Referrer-Policy: no-referrer, X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none', and this workflow uses no browser messaging at all.
One thing is deliberately not implemented:
private_key_jwt and mutual-TLS client authentication. RFC 9700 recommends asymmetric client authentication and this workflow offers client_secret_basic and client_secret_post only; the mode reports the gap rather than pretending otherwise.
docs/rfc9700.md is the full account — every requirement, what is enforced and what is only detected, and how the two tests (tests/rfc9700_client.js, tests/rfc9700_flows.js) exercise it against the mock STS running in its RFC 9700 mode.
State Parameters
- A state parameter can be submitted as part of the authorization endpoint request. The state parameter will be validated when the redirect comes back to the registered callback endpoint. A UUID is used as the state value. This is an optional, but recommended parameter.
Custom Parameters
Various specs & RFCs that build on the OAuth2 & OIDC protocols add additional parameters that must be passed to the Authorization Endpoint and Token Endpoint. The debugger supports passing up to ten custom parameters.
Nonce Parameter
A nonce parameter can be included in the Authorization Endpoint call. A UUID is used as the nonce value.
Token Details
All tokens (Access, Refresh, ID) returned by the IdP can have their details viewed by clicking on the link next to the token on the Debugger2 page.
This feature currently only supports JWT tokens, but in the future will support other token types.
There are two views: raw-JSON or table view.
The table view will display a claim description for spec-defined claims.
Some caveats to keep in mind:
- If nothing is displayed, then the requested token retrieved from the endpoint is not a JWT or not a valid JWT.
- In the future, additional token formats may be added.
- Although, many leading IdPs use JWT as the format for OAuth2 access tokens and refresh tokens. The spec does not require this.
- Some IdPs intentionally use opaque tokens that have no deeper meaning than to be a randomly generated identifier that points back to session information stored on the IdP
OIDC RP-Initiated Logout Spec Support
If the Logout URL can be read from the OIDC Discovery Endpoint, it will be automatically populated; otherwise, you will need to manually copy in this URL.
The associated refresh token should be invalidated at this point. The corresponding JWT tokens could still be validated unless you compare them against the Introspection Endpoint.
JWT Validation
The detail view screen for tokens can validate a JWT token signature.
It can take a JWKS Endpoint or certificate directly.
JWT Tools
The JWT Tools page (/jwt_tools.html) is a standalone, browser-only workbench for building, signing, encrypting, verifying, and decrypting JSON Web Tokens. It implements JWS (RFC 7515), JWE (RFC 7516), JWT (RFC 7519), and the JOSE algorithms of JWA (RFC 7518) plus EdDSA (RFC 8037).
All cryptography runs in your browser via the Web Crypto API (
crypto.subtle). No key material — private keys, HMAC secrets, generated key pairs, or passwords — is ever written to localStorage or sent to a server. Because Web Crypto is only available in a secure context, use the page over https:// or http://localhost.
Reach it from the Tools pane on
oauth2_oidc_1.html or oauth2_oidc_2.html, or browse directly to /jwt_tools.html. The "← Return to debugger" link sends you back to whichever debugger page you came from. Every multi-line field has a Copy button, and hovering any field shows a tooltip describing it.
The page is three side-by-side panes that map to the lifecycle of a token:
| Pane | Title | What it does |
|---|---|---|
| #1 | Compose | Build the JWT Header and Payload; decode a pasted token |
| #2 | Sign (JWS) | Generate keys, sign the token, and validate a signature |
| #3 | Encrypt (JWE) | Generate keys, encrypt the token, and decrypt a JWE |
The typical order is Compose (#1) → Sign (#2) → Encrypt (#3), and the reverse for inspection: Decrypt (#3) → Validate signature (#2).
Pane #1 — Compose
Author the token. Three text areas plus helpers:
- JWT Header — the JOSE header, as JSON. Pre-populated with a sample (
alg, typ, kid).
JWT Payload — the claims set, as JSON. Pre-populated with RFC 7519 registered claims ( iss, sub, aud, exp, nbf, iat, jti) and placeholder values. Encoded JWT — the compact-serialized token. This field is two-way: editing the Header or Payload rebuilds it as BASE64URL(header).BASE64URL(payload). (an unsigned* token — note the trailing dot); pasting a token into it decodes the header and payload back into the fields on the left. If the pasted token carries a signature, the whole token is also copied into the Sign pane's JWT to Verify and Signed JWT fields.
A status line reports the sync state or any JSON parse error.
Add Custom Claim — insert a claim without hand-editing JSON: enter a name and value, pick a value type (
String, Number, Boolean, or JSON — the last parses arbitrary JSON), pick a target (Payload or Header), and click Add.
Buttons:
Generate RFC 9068 Token — overwrites the Header/Payload/Encoded JWT with a sample OAuth 2.0 JWT access token (RFC 9068): header
typ is at+jwt with the required access-token claims. The sample is unsigned* — sign it in Pane #2.
- JWT RFC Compliance — validates the current token against the JWT/JWS specs (RFC 7519 / RFC 7515) and writes a PASS/FAIL report to Compliance Output.
- RFC 9068 Compliance — validates the current token specifically as an OAuth 2.0 JWT access token.
Pane #2 — Sign (JWS)
Generate a signing key pair (or HMAC secret), sign the composed token, and validate signatures. The Signing Algorithm dropdown drives everything in this pane — it determines what key material Generate Keys produces and how the token is signed.
| Option | Family | Key generated |
|---|---|---|
|
RS256 / RS384 / RS512 | RSASSA-PKCS1-v1_5 | RSA key pair (size from the Key Size dropdown) |
| PS256 / PS384 / PS512 | RSASSA-PSS | RSA key pair (size from the Key Size dropdown) |
| ES256 / ES384 / ES512 | ECDSA | EC key pair on P-256 / P-384 / P-521 |
| EdDSA | EdDSA (RFC 8037) | OKP key pair on Ed25519 |
| HS256 / HS384 / HS512 | HMAC (symmetric) | a random shared secret (no key pair) |
The dropdown is read when you click a button and is not "sticky" to already-generated keys. Pick the algorithm first, then Generate Keys → Sign. If you switch to a different algorithm family after generating keys, regenerate them before signing.
Key generation & display:
- Generate Keys — creates key material for the selected algorithm and fills the key fields.
RSA Key Size — for the RSA families (RS/PS*), selects the modulus size: 2048 (default), 3072, 4096, or 1024 (insecure). Ignored for EC, EdDSA, and HMAC, whose sizes follow the chosen curve/algorithm.
- Private Key (PKCS#8) / HMAC Secret and Public Key (SPKI) — the key pair (HMAC shows only a base64url secret; the public field carries a notice). You may also paste your own PEM or JWK material.
Show keys as JWK (off = PEM) — a toggle that converts both* key fields between PEM (default) and JWK in place.
Downloading keys: choose a Keystore format (
PEM, JWK, DER, PKCS#12) and an optional Password (which encrypts the private key: PBES2-encrypted PEM/DER, a PBES2 JWE for JWK, and required for PKCS#12), then Download Keys. DER produces two files (private + public). PKCS#12 export is not available for EdDSA keys in-browser — use PEM, DER, or JWK.
Signing: Generate Signed JWT signs
BASE64URL(header).BASE64URL(payload) with the Private Key / HMAC Secret and forces the header alg to match your selection. The result is written to Signed JWT (read-only), the Encoded JWT box in Pane #1, the JWT to Verify box below, and the Payload to Encrypt box in Pane #3 (ready to nest inside a JWE).
Validate a Signature (sub-section): verify any JWS — the one you just produced or one you paste in.
- JWT to Verify — defaults to the just-signed token. Verification uses the
alg in the pasted token's header, not the dropdown above.
Verification Type: HMAC Secret — verify a symmetric (HS) signature with the secret.
* X.509 Certificate (PEM) — verify with a public key / certificate (auto-populated from the public key you generated).
JWKS (JSON) — paste a JWKS document; the key is chosen by the token's kid. (Currently supports RSA keys.)*
* JWKS (URL)` — same, but the JWKS is fetche
... (README truncated for length)