How to detect and redact jwt tokens in markdown
- Step 1Open the redactor — Go to /markdown-tools/md-secret-redactor and paste the doc or drop the single
.mdfile containing example responses. - Step 2Keep JWTs on one line — The JWT pattern needs
eyJ...with three dotted segments on a single line. If your example wraps the token, join it onto one line first so the pattern can match. - Step 3Choose scope — Leave
scanAlloff for example responses inside fenced ``` blocks. Enable it if a JWT appears inline in prose or in a heading. - Step 4Run and verify the placeholder — A standalone JWT becomes
[REDACTED_JWT]. A JWT inside anAuthorization: Bearer ...header becomesBearer [REDACTED]because the Bearer pattern runs first. - Step 5Check for two-segment or wrapped tokens — Unsigned (two-segment) JWTs and line-wrapped tokens are not matched. Reformat to a signed, single-line token or redact those by hand.
- Step 6Confirm the JSON still parses — Because only the token value is swapped, the response body keeps its shape. Spot-check that the example is still valid-looking before publishing.
JWT shapes and whether the tool catches them
The JWT pattern is eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,} — three dotted segments on one line. The Bearer pattern catches any 8+ char token after Bearer .
| JWT shape | Detected? | Becomes | Why |
|---|---|---|---|
Standalone signed JWT eyJh....eyJz....SflK... | Yes | [REDACTED_JWT] | Three segments, single line. |
JWT in Authorization: Bearer <jwt> | Yes | Bearer [REDACTED] | Bearer pattern fires first (earlier in order). |
Unsigned JWT (two segments, eyJ...eyJ...) | No | (unchanged) | Pattern requires three segments. |
| Line-wrapped JWT across two lines | No | (unchanged) | Pattern requires one line. |
| JWT with very short middle segment (<10 chars) | No | (unchanged) | Each segment needs 10+ chars. |
| JWT in prose, scanAll off | No | (unchanged) | Default scope is fenced code blocks only. |
What the redactor actually detects
The five regex patterns the redactor applies, in the exact order it applies them, taken from lib/markdown/markdown-engine.ts. There are no other patterns — anything not matched here is left untouched.
| Pattern (what it matches) | Example that matches | Replaced with | Order |
|---|---|---|---|
AWS access key id: AKIA + 16 uppercase letters/digits (case-sensitive) | AKIAIOSFODNN7EXAMPLE | [REDACTED_AWS_KEY] | 1 |
Keyword assignment: api_key, api-key, apikey, token, secret, password, passwd, pwd, authorization followed by =/:/space, then an 8+ char value | api_key = abcd12345678 | <keyword>=[REDACTED] (separator normalized to =) | 2 |
Bearer + an 8+ char token | Bearer eyJhbGci... | Bearer [REDACTED] | 3 |
Three-segment JWT: eyJ + 10+ chars, dot, 10+ chars, dot, 10+ chars | eyJhbGci....eyJzdWIi....SflKxw... | [REDACTED_JWT] | 4 |
PEM private-key block: -----BEGIN ... KEY----- ... -----END ... KEY----- | an RSA/EC/OPENSSH key block | [REDACTED_PRIVATE_KEY] | 5 |
Scope: which parts of the document are scanned
The single scanAll option controls scope. Default (off) restricts redaction to fenced `` code blocks only; on scans the entire document. Inline backtick` spans and 4-space indented code are treated as prose, not code blocks.
| Document region | scanAll: false (default) | scanAll: true |
|---|---|---|
| Fenced ``` code block | Scanned | Scanned |
| Prose / paragraph text | Left untouched | Scanned |
Inline backtick code span | Left untouched (counts as prose) | Scanned |
| 4-space indented code block | Left untouched (only ``` fences count) | Scanned |
| Headings, tables, blockquotes | Left untouched | Scanned |
Cookbook
Example-response scenarios from real API docs, run against the engine. Tokens shortened for readability but shaped to match (or deliberately not match) the pattern.
Token field in an example response
The standalone JWT pattern matches the value and swaps it for [REDACTED_JWT], leaving the JSON structure intact.
Input:
```json
{
"access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpM",
"expires_in": 3600
}
```
Output:
```json
{
"access_token": "[REDACTED_JWT]",
"expires_in": 3600
}
```Same token in a request header
When the JWT sits behind Bearer , the Bearer pattern fires first (it is earlier in the order), so the result is Bearer [REDACTED] rather than [REDACTED_JWT].
Input: ```http GET /me HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMe ``` Output: ```http GET /me HTTP/1.1 Authorization: Bearer [REDACTED] ```
Unsigned (two-segment) JWT slips through
An unsigned token has only two dotted segments. The pattern requires three, so it is not detected. Redact it manually or sign it.
Input: ```text eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjMifQ. ``` Output (unchanged — only two segments before the trailing dot): ```text eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjMifQ. ```
Wrapped token is missed — join it first
A pretty-printed doc may wrap a long token across lines. The pattern needs one line, so join it before running.
Input: ```text eyJhbGciOiJIUzI1NiJ9. eyJzdWIiOiIxMjMifQ. SflKxwRJSMeKKF2QT4fwpM ``` Output (unchanged — token spans 3 lines) Fix: join onto one line, then re-run → ```text [REDACTED_JWT] ```
JWT shown inline in prose needs scanAll
A sentence like "send eyJ... as the bearer token" lives in prose, so default scope skips it. Enable scanAll.
Input: Use `eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4` for testing. scanAll: false → unchanged scanAll: true → Use `[REDACTED_JWT]` for testing.
Edge cases and what actually happens
Unsigned JWT (two segments)
Not detectedThe pattern requires three dotted segments. A two-segment unsigned token is missed; redact it manually.
Line-wrapped / multi-line JWT
Not detectedThe pattern matches within a single line. Join the token onto one line before running.
Segment shorter than 10 chars
Not detectedEach of the three segments must be 10+ chars. A toy token with a tiny payload will not match.
JWT behind `Bearer `
Redacted as BearerExpected — the Bearer pattern runs before the JWT pattern, so you get Bearer [REDACTED], not [REDACTED_JWT]. Both remove the token.
JWT in prose, scanAll off
PreservedBy design — default scope is fenced code blocks. Enable scanAll for inline/prose tokens.
Base64 string that isn't a JWT but starts with eyJ
ExpectedAny eyJ... value with three dotted 10+ char segments matches, even if it is not a real token. Cosmetic over-redaction.
Refresh token / opaque token (not a JWT)
Not detectedOpaque tokens have no eyJ prefix or dotted structure. Caught only if keyword-prefixed (token=...).
Doc over Free tier limits
RejectedFree is 1 MB / 500,000 chars / 1 file. Split with md-splitter or upgrade.
Token already expired but still shown
Action recommendedEven expired example tokens can leak tenant ids/claims. Redact them anyway and prefer synthetic tokens in docs.
Frequently asked questions
What JWT shape does it match?
A three-segment token: eyJ followed by 10+ chars, a dot, 10+ chars, a dot, 10+ chars — all on one line. That produces [REDACTED_JWT].
Why did my JWT become `Bearer [REDACTED]` instead of `[REDACTED_JWT]`?
Because it sat behind Bearer , and the Bearer pattern runs before the JWT pattern in the engine's fixed order. The token is still removed.
Are unsigned (two-segment) JWTs detected?
No. The pattern requires three dotted segments. Sign the token or redact it manually.
What if the token wraps across lines?
It will not be detected. Join it onto a single line first, then re-run.
Are opaque or refresh tokens detected?
Not unless a keyword precedes them. Without eyJ structure they match no JWT pattern; token=<opaque> would be caught by the keyword pattern.
Does it preserve my example JSON?
Yes. Only the token value is swapped, so the response body keeps its shape and stays readable.
Does it scan inline JWTs in prose?
Only with scanAll on. By default it scans fenced ``` code blocks, where example responses usually sit.
Is the doc uploaded?
No. Redaction runs in your browser, so tokens minted against real tenants never leave the machine.
Can I redact several docs at once?
No. acceptsMultiple is false — one document per run.
Will it over-redact non-JWT base64?
Possibly. Any eyJ... value matching the three-segment shape is redacted. Spot-check the output.
Can I add a custom token pattern?
No. The only option is scanAll. There is no custom-pattern field.
What other doc cleanup pairs well?
Tag fenced blocks with md-code-block-tagger, validate links with md-link-validator, and lint with md-lint.
Privacy first
All Markdown processing runs locally in your browser using JavaScript. No file is ever uploaded to JAD Apps servers — only metadata counters are saved for signed-in dashboard stats.