# JWTForge Documentation > JWTForge: A JWT Vending Service for Testing, Fuzzing, and Security Research of OAuth2/OIDC Implementations. Complete agent-friendly JWTForge documentation bundle covering structured JSON requests, testing modes, attack scenarios, CLI usage, and API reference material. ## Overview # JWTForge JWTForge is a JWT Vending Service for Testing, Fuzzing, and Security Research of OAuth2/OIDC Implementations. It is a lightweight JWT token vending service for testing purposes, deployable on Cloudflare Workers. Generate JWT tokens with standard OIDC/OAuth2 and custom claims for development and testing. Use it for fuzzing, end-to-end testing, and penetration testing of OAuth2/OIDC applications and services. Use it to generate: - Normal signed JWTs with common OIDC/OAuth2 claims. - Structured JWTs with explicit `header`, `body`, and `signature` objects. - Unsigned or literal-signature tokens. - Fuzzed, malicious, and grammar-generated payloads. - Known JWT vulnerability scenarios. The interactive widget on the home page covers the common token-generation workflows. The Swagger page at `/api-reference` embeds the Worker-hosted Swagger UI and links to the raw `/openapi.json` contract. ## Request Models JWTForge supports both request styles: Structured JSON: ```json { "header": {}, "body": {}, "signature": false } ``` Legacy flat JSON: ```json { "sub": "user123", "scope": "openid" } ``` Structured JSON is recommended for new tests because it maps directly to JWT parts. --- ## Legacy Flat JSON Legacy requests put claims at the top level: ```json { "sub": "user123", "scope": "openid profile", "roles": ["admin"] } ``` Legacy header controls remain supported: ```json { "sub": "user123", "header_alg": "none", "header_kid": "../../../../../../dev/null", "sig": false } ``` Use the structured JSON model for new tests. Keep the legacy model for existing integrations and simple claim-only token generation. --- ## Signatures Omit `signature` to let JWTForge sign normally with its active key: ```json { "body": { "sub": "user123" } } ``` Set `signature` to `false` to generate an unsigned trailing-dot token: ```json { "header": { "alg": "none" }, "body": { "sub": "admin" }, "signature": false } ``` Pass a string to use a literal signature segment: ```json { "body": { "sub": "user123" }, "signature": "literal-signature" } ``` Private-key signing from request payloads is reserved for a future extension. --- ## Structured JSON Structured JSON is the recommended request model for new JWTForge tests. It maps the request to the three JWT parts directly: | Request field | JWT part | Purpose | | --- | --- | --- | | `header` | JWT header | Header parameters such as `alg`, `typ`, `kid`, `jku`, and `jwk` | | `body` | JWT payload | Standard, OIDC, OAuth2, and custom claims | | `signature` | JWT signature | Normal signing, unsigned token, or literal signature segment | JWTForge auto-detects structured JSON when the request contains any of these top-level fields: - `header` - `body` - `signature` If none of those fields are present, JWTForge treats the request as the legacy flat claim model. ## Minimal Request ```json { "header": { "alg": "RS256", "typ": "JWT", "kid": "rsa-key-1" }, "body": { "sub": "user123", "scope": "openid profile" } } ``` This generates a normally signed access token. JWTForge still adds default JWT claims such as `iss`, `aud`, `exp`, `nbf`, `iat`, and `jti` if they are not provided. ## Header Object The `header` object controls JWT header parameters. ```json { "header": { "alg": "RS256", "typ": "JWT", "cty": "application/json", "kid": "rsa-key-1", "jku": "https://example.com/.well-known/jwks.json", "jwk": { "kty": "RSA", "kid": "embedded-rsa-key", "use": "sig", "alg": "RS256", "n": "test", "e": "AQAB" } }, "body": { "sub": "user123" } } ``` ### Supported Header Fields | Field | Description | Default | Example | | --- | --- | --- | --- | | `alg` | JWT signing algorithm advertised in the header | Active key algorithm, usually `RS256` | `"RS256"`, `"ES256"`, `"none"`, `"nOne"` | | `typ` | Token type | `"JWT"` | `"JWT"`, `"at+jwt"` | | `cty` | Content type for nested or typed JWT payloads | none | `"JWT"`, `"application/jwt"` | | `kid` | Key identifier | Active signing key ID | `"rsa-key-1"` | | `jku` | JWK Set URL reference | none | `"https://example.com/.well-known/jwks.json"` | | `jwk` | Embedded public JWK object | none | `{"kty":"RSA","kid":"embedded-rsa-key"}` | | `crit` | Critical header parameter names | none | `["exp-ext","custom-policy-id"]` | Custom header parameters are allowed only when their names are listed in `crit`. JWTForge rejects a `crit` entry if the named header parameter is missing. ### Unsupported Header Fields | Field | Reason | Behavior | Example | | --- | --- | --- | --- | | `x5u` | X.509 certificate URL headers are intentionally out of scope | Request rejected with `400` | `"https://example.com/cert.pem"` | | `x5c` | X.509 certificate chain headers are intentionally out of scope | Request rejected with `400` | `["MIIB..."]` | | `x5t` | X.509 certificate thumbprint headers are intentionally out of scope | Request rejected with `400` | `"abc123"` | JWTForge rejects unsupported header fields with `400`. That includes the certificate-chain header fields above. ## Body Object The `body` object becomes the JWT payload. ```json { "body": { "iss": "https://issuer.example.com", "sub": "user123", "aud": "https://api.example.com", "scope": "openid profile email", "roles": ["admin", "user"], "tenant_id": "tenant-abc-123" } } ``` ### Supported Body Fields | Field | Description | Default | Example | | --- | --- | --- | --- | | `iss` | Issuer identifier | Worker URL | `"https://jwtforge.workers.dev"` | | `sub` | Subject identifier | `"user123"` | `"user123"` | | `aud` | Audience identifier | `"https://api.example.com"` | `"my-api"` | | `exp` | Expiration timestamp | Current time + 1 hour | `1735689600` | | `nbf` | Not-before timestamp | Current time | `1735686000` | | `iat` | Issued-at timestamp | Current time | `1735686000` | | `jti` | JWT ID | Random UUID | `"550e8400-e29b-41d4-a716-446655440000"` | | `scope` | OAuth2 scopes | none | `"openid profile email"` | | `client_id` | OAuth2 client identifier | Auto-generated when needed | `"test_app"` | | `roles` | Application roles | none | `["admin", "user"]` | | `groups` | User groups | none | `["engineering"]` | | `nonce` | OIDC nonce for replay protection | none | `"nonce-123"` | | `name` | Full name | Generated in `fake` mode with `profile` scope | `"Jane Doe"` | | `given_name` | First name | Generated in `fake` mode with `profile` scope | `"Jane"` | | `family_name` | Last name | Generated in `fake` mode with `profile` scope | `"Doe"` | | `preferred_username` | Preferred username | Generated in `fake` mode with `profile` scope | `"jane.doe"` | | `profile` | Profile URL | Generated in `fake` mode with `profile` scope | `"https://example.com/users/jane"` | | `picture` | Profile image URL | Generated in `fake` mode with `profile` scope | `"https://example.com/avatar.jpg"` | | `email` | Email address | Generated in `fake` mode with `email` scope | `"jane@example.com"` | | `email_verified` | Email verification state | Generated in `fake` mode with `email` scope | `true` | | `address` | OIDC address object | Generated in `fake` mode with `address` scope | `{"country":"US"}` | | `phone_number` | Phone number | Generated in `fake` mode with `phone` scope | `"+1-555-555-5555"` | | `phone_number_verified` | Phone verification state | Generated in `fake` mode with `phone` scope | `false` | | Custom fields | Any additional claim accepted by the API | none | `"tenant_id": "tenant-abc-123"` | Operational fields are not emitted as JWT claims. JWTForge removes fields such as: - `mode` - `exclude` - `kty` - `response_type` - `grammar_category` - `malicious_category` - `vulnerability` - `signature` For clarity, prefer putting operational fields at the top level and claims inside `body`. ## Signature Control Omit `signature` to sign normally: ```json { "body": { "sub": "user123" } } ``` Set `signature` to `false` to generate an unsigned token with a trailing dot: ```json { "header": { "alg": "none" }, "body": { "sub": "admin", "roles": ["admin"] }, "signature": false } ``` Pass a string to force a literal signature segment: ```json { "body": { "sub": "user123" }, "signature": "literal-signature" } ``` Private-key signing from request payloads is reserved for a future extension. ## Modes With Structured JSON Modes work with structured JSON. JWTForge applies body transformations to `body` claims and header transformations to supported `header` fields. ### Fake Mode ```json { "mode": "fake", "body": { "sub": "user123", "scope": "openid profile email" } } ``` ### Fuzz Mode Fuzz mode is selective. It randomly mutates 1-3 available `body` claims per token, while provided supported `header` fields are fuzzed unless excluded. The `signature` field is not fuzzed by `mode: "fuzz"`; use `signature: false`, a literal signature string, or a vulnerability preset for signature-specific tests. ```json { "mode": "fuzz", "header": { "alg": "RS256", "kid": "rsa-key-1" }, "body": { "sub": "user123", "email": "user@example.com" } } ``` Protect selected fields with `exclude`: ```json { "mode": "fuzz", "exclude": ["exp", "nbf", "iat", "header.alg"], "header": { "alg": "RS256" }, "body": { "sub": "user123" } } ``` ### Malicious Mode ```json { "mode": "malicious", "malicious_category": "sql_injection", "body": { "sub": "user123", "email": "user@example.com" } } ``` ### Grammar Mode ```json { "mode": "grammar", "grammar_category": "vulnerable", "header": { "alg": "trigger", "jku": "trigger", "jwk": {} }, "body": { "sub": "user123", "email": "user@example.com" } } ``` Grammar mode resolves literal values and semantic templates before generating the token. Faker templates become realistic values such as emails, names, booleans, UUIDs, avatars, and usernames. ## Vulnerability Presets Structured JSON supports known JWT vulnerability presets. ```json { "vulnerability": "rs_hs_confusion", "body": { "sub": "admin", "roles": ["admin"] } } ``` Supported presets: | Preset | Effect | | --- | --- | | `alg_none` | Sets `header.alg` to `none` and `signature` to `false`; use `alg_none_variant` for case variants such as `None`, `NONE`, or `nOne` | | `rs_hs_confusion` | Sets `header.alg` to `HS256` | | `kid_traversal` | Sets `header.kid` to a traversal-style value | | `jku_injection` | Sets `header.jku` to an attacker-style JWKS URL | | `embedded_jwk` | Embeds the current public JWK in `header.jwk` | Presets apply before mode transformations, so use `exclude` when a mode should not mutate a preset field. Example `alg_none` case variant: ```json { "vulnerability": "alg_none", "alg_none_variant": "nOne", "body": { "sub": "admin" } } ``` ## Response Types Use `response_type` at the top level to generate access tokens, ID tokens, or both: ```json { "response_type": "id_token token", "body": { "sub": "user123", "scope": "openid profile email", "nonce": "nonce-123" } } ``` Supported values: | Response type | Description | Returns | Use case | | --- | --- | --- | --- | | `token` | Access token only | `access_token` | API authorization and resource access | | `id_token` | ID token only | `id_token` | User authentication and identity verification | | `id_token token` | Access token and ID token | `access_token` + `id_token` | OIDC hybrid-style testing | | `token id_token` | Access token and ID token, alternative order | `access_token` + `id_token` | Compatibility testing for order-insensitive clients | ## Key Type Use `kty` at the top level to choose JWTForge's signing key type: ```json { "kty": "EC", "body": { "sub": "user123" } } ``` Supported values: | Key type | Signing algorithm | Description | Default | Example | | --- | --- | --- | --- | --- | | `RSA` | `RS256` | RSA-2048 with SHA-256 | Yes | `"kty": "RSA"` | | `EC` | `ES256` | ECDSA P-256 with SHA-256 | No | `"kty": "EC"` | ## Full Example ```bash curl -X POST https://your-worker.workers.dev/token \ -H "Content-Type: application/json" \ -d '{ "mode": "grammar", "grammar_category": "valid", "response_type": "id_token token", "kty": "RSA", "header": { "alg": "RS256", "typ": "JWT", "cty": "application/json", "kid": "rsa-key-1" }, "body": { "sub": "user123", "scope": "openid profile email", "aud": "my-client", "nonce": "nonce-123", "roles": ["admin"] } }' ``` ## Migration From Legacy JSON Legacy flat JSON: ```json { "sub": "user123", "scope": "openid profile", "header_alg": "none", "sig": false } ``` Structured JSON: ```json { "header": { "alg": "none" }, "body": { "sub": "user123", "scope": "openid profile" }, "signature": false } ``` Migration rules: - Move JWT payload claims into `body`. - Move `header_alg` to `header.alg`. - Move `header_kid` to `header.kid`. - Replace `sig: false` with `signature: false`. - Keep operational fields such as `mode`, `exclude`, `kty`, `response_type`, and `grammar_category` at the top level. --- ## Fake Mode `fake` is the default mode. It preserves provided claims and fills OIDC scope-derived claims with realistic Faker data. ```json { "mode": "fake", "body": { "sub": "user123", "scope": "openid profile email" } } ``` --- ## Fuzz Mode `fuzz` performs random selective mutation. It does not rewrite every field in the token. For body claims, JWTForge randomly mutates 1-3 available claims per generated token. It always protects `iss`, `jti`, `kty`, and `response_type`, and it also protects any fields listed in `exclude`. For header fields, JWTForge fuzzes each provided supported header field unless that field is excluded. Supported structured header fields are `alg`, `typ`, `cty`, `kid`, `jku`, and `jwk`. ```json { "mode": "fuzz", "header": { "alg": "RS256", "kid": "stable-key" }, "body": { "sub": "user123", "email": "user@example.com" } } ``` Use `exclude` to protect fields: ```json { "mode": "fuzz", "exclude": ["exp", "nbf", "iat", "header.alg"], "body": { "sub": "user123" } } ``` ## Mutation Scope | Token part | Fuzz behavior | Protected fields | | --- | --- | --- | | `body` | Randomly mutates 1-3 body claims from the available claim set | `iss`, `jti`, `kty`, `response_type`, and fields in `exclude` | | `header` | Mutates each provided supported header field | Fields in `exclude`, using either `header.alg` style or legacy names such as `header_alg` | | `signature` | Not fuzzed by `mode: "fuzz"` | Use `signature: false`, a literal signature string, or vulnerability presets for signature-specific testing | Use dotted field names to protect structured header fields: ```json { "mode": "fuzz", "exclude": ["header.alg", "exp", "nbf", "iat"], "header": { "alg": "RS256", "kid": "rsa-key-1" }, "body": { "sub": "user123", "email": "user@example.com", "roles": ["admin"] } } ``` In this example, `header.alg`, `exp`, `nbf`, and `iat` remain stable. `header.kid` can be fuzzed because it was provided and not excluded. One to three body claims from the remaining available body fields can be fuzzed. Fuzzed body values are selected from BLNS strings plus edge-case values such as booleans, large numbers, negative numbers, arrays, nested objects, `null`, `Infinity`, and `NaN`. Fuzzed `alg` values include algorithm-confusion candidates such as `none`, `None`, `NONE`, `nOnE`, `HS256`, `HS384`, and `HS512`. --- ## Grammar Mode `grammar` selects values from JWTForge's grammar catalog. ```json { "mode": "grammar", "grammar_category": "vulnerable", "header": { "alg": "trigger", "jku": "trigger" }, "body": { "sub": "user123", "email": "user@example.com" } } ``` Grammar entries can be direct literals or templates. Templates are resolved at generation time. Template types: - `timestamp` - `url` - `jwk` - `literal` - `attack_string` - `faker` Faker templates generate realistic values such as emails, names, usernames, phone numbers, avatars, booleans, locales, and UUIDs. --- ## Malicious Mode `malicious` injects attack strings into selected claims or header fields. ```json { "mode": "malicious", "malicious_category": "sql_injection", "body": { "sub": "user123", "email": "user@example.com" } } ``` Supported categories include SQL injection, XSS, path traversal, command injection, LDAP injection, NoSQL injection, XML injection, template injection, header injection, and buffer overflow. --- ## Security Testing Modes JWTForge uses the top-level `mode` field to control how request data is generated or mutated. | Mode | Purpose | Coverage | Use case | Example | | --- | --- | --- | --- | --- | | `fake` | Realistic test data | OIDC scope-derived claims with Faker values | Integration tests, demos, local development | `"mode": "fake"` | | `fuzz` | Random robustness testing | Mutates 1-3 body claims and mutates provided supported header fields unless excluded | Edge-case discovery and parser robustness | `"mode": "fuzz"` | | `malicious` | Security payload injection | Mutates 1-3 fields with attack strings | Penetration testing and sanitizer validation | `"mode": "malicious"` | | `grammar` | Systematic JWT grammar testing | Selects values from claim/header grammar rules | Spec coverage, boundary testing, repeatable security scans | `"mode": "grammar"` | ## Mode Options | Field | Applies to | Description | Default | Example | | --- | --- | --- | --- | --- | | `mode` | All modes | Selects generation behavior | `fake` | `"grammar"` | | `exclude` | `fuzz`, `malicious`, `grammar` | Protects fields from mutation | `[]` | `["exp", "nbf", "iat", "header.alg"]` | | `malicious_category` | `malicious` | Limits malicious values to one attack family | Mixed categories | `"sql_injection"` | | `grammar_category` | `grammar` | Selects grammar value family | `valid` or mixed rules depending path | `"vulnerable"` | ## Malicious Categories | Category | Description | Example payload | Use case | | --- | --- | --- | --- | | `sql_injection` | SQL query manipulation strings | `' OR '1'='1` | Test SQL sanitization | | `xss` | Browser script injection strings | `` | Test output encoding | | `path_traversal` | File path escape attempts | `../../../etc/passwd` | Test file path handling | | `command_injection` | Shell command separators/substitution | `; ls -la` | Test command execution defenses | | `ldap_injection` | LDAP filter manipulation | `*)(uid=*))(|(uid=*` | Test LDAP query construction | | `nosql_injection` | Document database operator strings | `{'$ne':null}` | Test NoSQL query handling | | `xml_injection` | XML/XXE style payloads | External entity payload | Test XML parsing defenses | | `template_injection` | Template expression payloads | `{{7*7}}` | Test template rendering boundaries | | `header_injection` | CRLF/header splitting strings | `test\r\nInjected-Header: malicious` | Test header parsing | | `buffer_overflow` | Very large payload | 1 million `A` characters | Test payload size limits | ## Grammar Categories | Category | Description | Example values | Use case | | --- | --- | --- | --- | | `valid` | Spec-aligned values | `RS256`, `user123`, `https://example.com` | Validate normal acceptance | | `edge_cases` | Boundary and unusual values | `null`, `0`, `-1`, `""` | Test boundary handling | | `type_variations` | Unexpected claim types | `["user123"]`, `"123"`, `"true"` | Test type validation | | `injection` | Security-relevant strings | SQL, XSS, traversal payloads | Test sanitizer behavior | | `invalid` | Malformed or invalid values | Empty or wrong-format values | Test rejection paths | | `vulnerable` | JWT-specific vulnerable patterns | `alg: "none"`, `alg: "NONE"`, `alg: "HS256"` | Test algorithm validation | Use the individual mode pages for executable examples. --- ## alg none Generate an unsigned token with `alg: none`: ```json { "vulnerability": "alg_none", "body": { "sub": "admin", "roles": ["admin"] } } ``` ## Case Variations Some JWT validation bugs only reject lowercase `none` and miss other case variants. Use `alg_none_variant` with the preset to generate a specific spelling: ```json { "vulnerability": "alg_none", "alg_none_variant": "nOne", "body": { "sub": "admin", "roles": ["admin"] } } ``` Supported values are any case variation of `none`, such as `none`, `None`, `NONE`, `nOne`, and `nOnE`. You can also set the header directly in structured JSON: ```json { "header": { "alg": "None" }, "body": { "sub": "admin" }, "signature": false } ``` --- ## Embedded JWK Generate a token with an embedded public JWK in the header: ```json { "vulnerability": "embedded_jwk", "body": { "sub": "user123" } } ``` --- ## jku Injection Generate a token with an attacker-controlled JWKS URL: ```json { "vulnerability": "jku_injection", "body": { "sub": "user123" } } ``` --- ## kid Injection Generate a token with a traversal-style `kid`: ```json { "vulnerability": "kid_traversal", "body": { "sub": "user123" } } ``` --- ## RS/HS Confusion Generate a token that advertises `HS256` while JWTForge uses its RSA key path: ```json { "vulnerability": "rs_hs_confusion", "body": { "sub": "admin" } } ``` --- ## Evaluation This section describes representative JWTForge testing scenarios across integration, robustness, algorithm-confusion, injection, grammar, OAuth2 client credentials, and token exchange workflows. Examples use `http://localhost:8787` as the local authorization server. To run them locally: ```bash git clone https://github.com/abhishektiwari/jwtforge cd jwtforge npm install npm run dev ``` ## Performance Benchmark JWTForge is architected for high-throughput token generation. Local performance testing on a Mac M2 Pro with 16 GB RAM demonstrated sustained throughput of 586 requests per second with 12 ms average response time and zero error rate across 74,315 requests. These results show that JWTForge can sustain hundreds of concurrent token generation requests with sub-20 ms latency, making it suitable for integration testing, CI/CD pipelines, and moderate-scale fuzzing campaigns. The consistent throughput and zero error rate indicate stable handling of sustained local load without observed degradation during the benchmark run. ![JWTForge performance benchmark showing 586 requests per second throughput and 12 ms average response time over 74,315 requests.](/img/performance-benchmark.png) ## Scenario Mapping | Scenario | JWTForge feature | Vulnerability or standard area | | --- | --- | --- | | Integration testing with compliant tokens | `fake` mode with OIDC scopes | OIDC claim parsing, scope enforcement, role-based authorization | | Claim and header fuzzing | `fuzz` mode | RFC 8725 robustness concerns, malformed input handling | | Algorithm confusion testing | Vulnerability presets and header overrides | RFC 8725 Section 2.1, CVE-2015-9235 class issues | | Injection discovery | `malicious` mode | OWASP Top 10 A03 Injection, A07 Identification and Authentication Failures | | Categorical grammar testing | `grammar` mode | RFC 7519, RFC 7518, OIDC Core value categories | | Client credentials testing | OAuth2 `client_credentials` grant | RFC 6749 client authentication | | Token exchange testing | RFC 8693 token exchange | Delegation, claim transformation, scope propagation | ## Integration Testing With Compliant Tokens **Objective**: remove external identity provider dependencies from automated integration tests by generating realistic OIDC-style tokens locally. **Scenario**: a development team building a single-page application with role-based access control needs tokens for multiple personas: standard users, administrators, and service accounts. **Method**: use `fake` mode with explicit scopes and role claims. ```json { "mode": "fake", "body": { "scope": "openid profile email", "sub": "test-user-1" } } ``` ```json { "mode": "fake", "body": { "scope": "openid profile email", "sub": "admin-user", "roles": ["admin"] } } ``` **Outcome**: generated tokens include realistic-looking Faker-backed names and email values when scopes request them. This supports local and CI tests for claim extraction, persona-based authorization, scope checks, and expiration handling without relying on an external IdP. Faker-backed values vary across requests; tests that require exact claim values should pass those values explicitly in `body`. ## Robustness Testing Via Claim and Header Fuzzing **Objective**: identify defects in JWT validation logic by injecting malformed, unexpected, and boundary-condition values. **Scenario**: a security team auditing a JWT-consuming API wants to observe behavior when claims contain unexpected types, deeply nested structures, unusual Unicode, large arrays, or extreme numeric values. **Method**: run `fuzz` mode repeatedly. Exclude fields that should remain stable for the target test. ```bash for i in $(seq 1 100); do TOKEN=$(curl -s -X POST http://localhost:8787/token \ -H "Content-Type: application/json" \ -d '{ "mode": "fuzz", "exclude": ["iss", "jti"], "body": { "sub": "user123" } }' | jq -r .access_token) curl -s -o /dev/null -w "%{http_code}\n" \ -H "Authorization: Bearer $TOKEN" \ https://target.example.com/protected done ``` **Outcome**: response patterns can reveal denial-of-service behavior from malformed numeric claims, type confusion when string fields receive arrays or objects, null dereferences, and latency anomalies from expensive parsing paths. Since fuzz mode is intentionally stochastic, evaluate distributions and classes of failures rather than expecting a single exact token value. ## Algorithm Confusion Attack Testing **Objective**: verify that a JWT-consuming server enforces an explicit algorithm allowlist and rejects manipulated `alg` values. ### Algorithm `none` Bypass ```json { "vulnerability": "alg_none", "alg_none_variant": "nOne", "body": { "sub": "user123" } } ``` ### Symmetric Key Confusion ```json { "vulnerability": "rs_hs_confusion", "body": { "sub": "user123" } } ``` ### Automated Algorithm Variant Fuzzing ```json { "mode": "fuzz", "header": { "alg": "trigger-fuzz" }, "body": { "sub": "user123" } } ``` Fuzz mode can generate algorithm values such as `none`, `None`, `NONE`, `nOnE`, `HS256`, `HS384`, `HS512`, `RS384`, `RS512`, `ES384`, `ES512`, `PS256`, empty string, and BLNS-derived values. **Outcome**: a correctly implemented server rejects every algorithm not explicitly configured for that key and issuer, including case variants of `none` and symmetric algorithm substitutions. Acceptance indicates a weakness in allowlist enforcement or key/algorithm binding. ## Injection Vulnerability Discovery Via Malicious Mode **Objective**: identify injection vulnerabilities caused by treating JWT claims as trusted input in downstream operations such as database queries, HTML rendering, filesystem access, or shell command construction. ### Claim Injection ```json { "mode": "malicious", "malicious_category": "sql_injection", "body": { "sub": "user123", "email": "test@example.com", "name": "Test User" } } ``` ### KID Header Injection ```json { "mode": "malicious", "malicious_category": "path_traversal", "header": { "kid": "trigger" }, "body": { "sub": "user123" } } ``` **Outcome**: target server responses can show whether claims are inserted into raw SQL, HTML rendering pipelines, filesystem-based key loading, LDAP filters, or template engines. These tests map directly to OWASP Top 10 injection classes and authentication/authorization failure classes. ## Systematic Categorical Testing Via Grammar Mode **Objective**: exercise categorized JWT claim and header value variations using grammar rules instead of purely random fuzzing. **Scenario**: a team developing a JWT validation library needs regression coverage across valid values, edge cases, type variations, injection strings, and known vulnerable patterns. ### Vulnerable Algorithm Patterns ```json { "mode": "grammar", "grammar_category": "vulnerable", "header": { "alg": "trigger" }, "body": { "sub": "user123" } } ``` ### Type Variations For `sub` ```json { "mode": "grammar", "grammar_category": "type_variations", "body": { "sub": "trigger" } } ``` ### Edge Cases For `exp` ```json { "mode": "grammar", "grammar_category": "edge_cases", "body": { "sub": "user123", "exp": "trigger" } } ``` ### Injection Patterns In `kid` ```json { "mode": "grammar", "grammar_category": "injection", "header": { "kid": "trigger" }, "body": { "sub": "user123" } } ``` **Outcome**: grammar mode supports repeatable category-level testing. A given `(field, grammar_category)` maps to the same category set, though individual values may be selected from that set. This complements fuzz mode: grammar mode verifies known specification categories, while fuzz mode explores broader unexpected inputs. ## OAuth2 Client Credentials Testing **Objective**: validate OAuth2 client credentials grant handling and client authentication. **Method**: request tokens with form-encoded `client_credentials` and HTTP Basic authentication. The Basic auth username and password must both be the client ID. ```bash curl -X POST http://localhost:8787/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Authorization: Basic Y2xpZW50MTIzOmNsaWVudDEyMw==" \ -d "grant_type=client_credentials&scope=openid%20profile" ``` Validate the issued token through introspection: ```bash TOKEN=$(curl -s -X POST http://localhost:8787/token \ -H "Authorization: Basic Y2xpZW50MTIzOmNsaWVudDEyMw==" \ -d "grant_type=client_credentials" | jq -r .access_token) curl -X POST http://localhost:8787/introspect \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Authorization: Basic Y2xpZW50MTIzOmNsaWVudDEyMw==" \ -d "token=$TOKEN" ``` **Outcome**: this verifies that authenticated clients can obtain tokens, requested scopes are honored, and introspection can validate and expose claims from issued client credentials tokens. ## Token Exchange And Claim Transformation **Objective**: test RFC 8693 token exchange flows and claim transformation logic in delegated authorization scenarios. **Method**: generate an initial JWT, exchange it with claim additions, then introspect the exchanged token. ```bash INITIAL_TOKEN=$(curl -s -X POST http://localhost:8787/token \ -H "Content-Type: application/json" \ -d '{ "body": { "sub": "user123", "scope": "read write" } }' | jq -r .access_token) EXCHANGED=$(curl -s -X POST http://localhost:8787/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=$INITIAL_TOKEN&subject_token_type=urn:ietf:params:oauth:token-type:jwt&add_claims=resource:shared-resource" | jq -r .access_token) curl -X POST http://localhost:8787/introspect \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Authorization: Basic Y2xpZW50MTIzOmNsaWVudDEyMw==" \ -d "token=$EXCHANGED" ``` **Outcome**: token exchange testing verifies that original claims such as `sub` and `scope` are preserved, requested claim additions are applied, and the exchanged token remains signed and introspectable. --- ## CLI The `jwtforge` CLI runs JWTForge locally and generates tokens from shell scripts, test suites, and CI/CD pipelines. ## Install Install globally from the GitHub repository: ```bash npm install -g abhishektiwari/jwtforge ``` Or install inside a project: ```bash npm install abhishektiwari/jwtforge npx jwtforge help ``` ## Commands | Command | Purpose | Example | | --- | --- | --- | | `jwtforge start` | Start the local JWTForge Worker server in the background | `jwtforge start` | | `jwtforge token [payload]` | Generate a token by posting JSON to `/token` | `jwtforge token '{"body":{"sub":"user123"}}'` | | `jwtforge status` | Check whether JWTForge is listening locally | `jwtforge status` | | `jwtforge stop` | Stop the local JWTForge server | `jwtforge stop` | | `jwtforge help` | Show CLI help | `jwtforge help` | The CLI uses port `8787` by default. Override the port with either: ```bash jwtforge token '{"body":{"sub":"alice"}}' --port=9000 ``` or: ```bash JWTFORGE_PORT=9000 jwtforge token '{"body":{"sub":"alice"}}' ``` When `jwtforge start` runs locally, it starts Wrangler with a local issuer by default: ```text http://localhost:8787 ``` If `JWTFORGE_PORT` or `--port` is set, the default local issuer follows that port. This avoids local CLI tokens accidentally using the production `ISSUER` from `wrangler.toml`. Set `ISSUER` explicitly only when you want production-like issuer values in local tokens: ```bash ISSUER=https://jwtforge.dev jwtforge start ``` ## Generate Tokens Default token: ```bash jwtforge token ``` Structured JSON token: ```bash jwtforge token '{ "header": { "alg": "RS256", "typ": "JWT" }, "body": { "sub": "alice", "scope": "openid profile email", "roles": ["admin"] } }' ``` Extract the access token for another command: ```bash TOKEN=$(jwtforge token '{"body":{"sub":"alice","scope":"openid profile"}}' | jq -r .access_token) curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/protected ``` Generate security testing tokens: ```bash jwtforge token '{"mode":"fuzz","body":{"sub":"user123","email":"user@example.com"}}' jwtforge token '{"mode":"malicious","malicious_category":"sql_injection","body":{"sub":"user123","email":"user@example.com"}}' jwtforge token '{"mode":"grammar","grammar_category":"vulnerable","header":{"alg":"trigger"},"body":{"sub":"user123"}}' jwtforge token '{"vulnerability":"alg_none","alg_none_variant":"nOne","body":{"sub":"admin"}}' ``` ## Local Workflow Use one terminal to start JWTForge: ```bash jwtforge start jwtforge status ``` Use another terminal or script to generate tokens: ```bash TOKEN=$(jwtforge token '{"body":{"sub":"user123"}}' | jq -r .access_token) echo "$TOKEN" ``` Stop the server when finished: ```bash jwtforge stop ``` ## CI/CD Pipeline Usage The CLI is useful in pipelines when tests need a local authorization server and fresh JWTs. ```yaml name: JWTForge CLI Example on: pull_request: push: branches: [main] jobs: jwtforge-cli: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - name: Install JWTForge CLI run: npm install -g abhishektiwari/jwtforge - name: Start JWTForge run: jwtforge start - name: Wait for JWTForge run: | for i in {1..30}; do jwtforge status && exit 0 sleep 1 done exit 1 - name: Generate token for tests run: | TOKEN=$(jwtforge token '{"body":{"sub":"ci-user","scope":"openid profile"}}' | jq -r .access_token) echo "::add-mask::$TOKEN" echo "TEST_TOKEN=$TOKEN" >> "$GITHUB_ENV" - name: Run API tests run: | curl -f -H "Authorization: Bearer $TEST_TOKEN" http://localhost:8000/protected - name: Stop JWTForge if: always() run: jwtforge stop || true ``` For this repository's existing GitHub Actions examples, see: - [Unit and Postman E2E workflow](https://github.com/abhishektiwari/jwtforge/blob/main/.github/workflows/test.yml) - [FastAPI integration workflow](https://github.com/abhishektiwari/jwtforge/blob/main/.github/workflows/integration.yml) ## Postman And Newman JWTForge includes Postman collections that can be run with Newman in local or CI environments: - [Basic JWTForge Postman collection](https://github.com/abhishektiwari/jwtforge/blob/main/tests/e2e/JWTForge-Collection.postman_collection.json) - [Advanced JWTForge Postman collection](https://github.com/abhishektiwari/jwtforge/blob/main/tests/e2e/JWTForge-Collection-Advanced.postman_collection.json) - [Development Postman environment](https://github.com/abhishektiwari/jwtforge/blob/main/tests/e2e/JWTForge-Environment-Dev.postman_environment.json) - [Production Postman environment](https://github.com/abhishektiwari/jwtforge/blob/main/tests/e2e/JWTForge-Environment-Prod.postman_environment.json) Run the basic collection against the local server: ```bash jwtforge start npx newman run tests/e2e/JWTForge-Collection.postman_collection.json \ -e tests/e2e/JWTForge-Environment-Dev.postman_environment.json \ --delay-request 5000 \ --reporters cli,json \ --reporter-json-export test-results-e2e-basic.json jwtforge stop ``` Run the advanced collection: ```bash jwtforge start npx newman run tests/e2e/JWTForge-Collection-Advanced.postman_collection.json \ -e tests/e2e/JWTForge-Environment-Dev.postman_environment.json \ --delay-request 5000 \ --reporters cli,json \ --reporter-json-export test-results-e2e-advanced.json jwtforge stop ``` The repository Makefile also exposes these as: ```bash make test-e2e-dev make test-e2e-adv ``` --- ## OpenAPI Pen Test Generator JWTForge can analyze an OpenAPI JSON or YAML document, infer JWT security requirements, export Postman collections, or execute the generated test plan directly. Only test systems you own or are explicitly authorized to assess. ## Validate security metadata ```bash jwtforge pentest validate --spec openapi.yaml ``` JWT-compatible schemes include HTTP Bearer, OAuth2, and OpenID Connect. OAuth2 scopes are read from operation security requirements. Roles and custom claims are read only from explicit vendor extensions or a JSON configuration overlay. Supported extensions include: ```yaml x-required-roles: [pet-editor] x-required-scopes: [pets:write] x-jwt-claims: tenant_id: example-tenant ``` ## Run without generating Postman files ```bash jwtforge pentest run \ --spec openapi.yaml \ --target-url http://localhost:8000 \ --issuer http://localhost:8787 \ --jwtforge-url http://localhost:8787 \ --audience http://localhost:8000 \ --report generated/pentest-report.json ``` The runner requests fresh JWTs from JWTForge and checks missing tokens, valid authorization, insufficient privileges, and known JWT vulnerability scenarios. Signature, time, issuer, audience, and key-ID bypass scenarios require HTTP 401/403. Signed injection and stochastic fuzz probes are reported as observations and fail only on server errors because acceptance alone does not prove that an untrusted JWT header or claim reached a vulnerable sink. POST, PUT, PATCH, and DELETE operations are skipped unless `--allow-write-methods` is provided. Review the plan without making requests: ```bash jwtforge pentest run --spec openapi.yaml --target-url http://localhost:8000 --dry-run ``` Production execution requires both an explicit environment and acknowledgement: ```bash jwtforge pentest run \ --spec openapi.yaml \ --environment prod \ --prod-base-url https://api.example.com \ --prod-issuer https://issuer.example.com \ --allow-prod ``` ## Generate Postman and Newman artifacts ```bash jwtforge pentest generate \ --spec openapi.yaml \ --out generated \ --test-base-url http://localhost:8000 \ --test-issuer http://localhost:8787 ``` Generated artifacts include authentication/authorization and JWT vulnerability collections, test and production environments, and an inference report. The complete runnable example is in `examples/petstore-service`. --- ## Header Fields Supported structured header fields: | Field | Description | Default | Example | | --- | --- | --- | --- | | `alg` | JWT signing algorithm advertised in the header | Active key algorithm, usually `RS256` | `"RS256"`, `"ES256"`, `"none"`, `"nOne"` | | `typ` | Token type | `"JWT"` | `"JWT"`, `"at+jwt"` | | `cty` | Content type for nested or typed JWT payloads | none | `"JWT"`, `"application/jwt"` | | `kid` | Key identifier | Active signing key ID | `"rsa-key-1"` | | `jku` | JWK Set URL reference | none | `"https://example.com/.well-known/jwks.json"` | | `jwk` | Embedded public JWK object | none | `{"kty":"RSA","kid":"embedded-rsa-key"}` | | `crit` | Critical header parameter names | none | `["exp-ext","custom-policy-id"]` | Custom header parameters are allowed only when their names are listed in `crit`. Each name in `crit` must also exist as a header parameter: ```json { "header": { "alg": "RS256", "typ": "JWT", "crit": ["exp-ext", "custom-policy-id"], "exp-ext": "2026-12-31T23:59:59Z", "custom-policy-id": "policy_99ab" }, "body": { "sub": "user123" } } ``` Unsupported and rejected fields: | Field | Reason | Behavior | Example | | --- | --- | --- | --- | | `x5u` | X.509 certificate URL headers are intentionally out of scope | Request rejected with `400` | `"https://example.com/cert.pem"` | | `x5c` | X.509 certificate chain headers are intentionally out of scope | Request rejected with `400` | `["MIIB..."]` | | `x5t` | X.509 certificate thumbprint headers are intentionally out of scope | Request rejected with `400` | `"abc123"` | Any other header field is rejected unless it is explicitly listed in `crit`. Supported fields are limited to `alg`, `typ`, `cty`, `kid`, `jku`, `jwk`, and `crit`. ## Key Injection Surfaces JWTForge supports three key-injection test surfaces: | Surface | Header field | Preset | What it tests | | --- | --- | --- | --- | | Key ID injection | `kid` | `kid_traversal` | Unsafe local key lookup, path traversal, SQL-style lookup injection, or untrusted key ID handling | | JWKS URL injection | `jku` | `jku_injection` | Whether a verifier fetches token-supplied JWKS URLs without enforcing a trusted allowlist | | Embedded key injection | `jwk` | `embedded_jwk` | Whether a verifier trusts a public key embedded directly in the token header | --- ## OIDC/OAuth2 Claims JWTForge supports standard JWT, OIDC, and OAuth2 claims, plus arbitrary custom claims. In the structured JSON model, put claims under `body`: ```json { "body": { "sub": "user123", "scope": "openid profile email", "roles": ["admin", "user"] } } ``` In the legacy flat model, claims can still be passed at the top level: ```json { "sub": "user123", "scope": "openid profile email", "roles": ["admin", "user"] } ``` JWTForge intentionally does not strictly validate standard claims. It is a testing tool, so malformed, unexpected, or custom values are allowed where possible. ## Claim Reference | Claim | Description | Default | Example | | --- | --- | --- | --- | | `iss` | Issuer | Worker URL | `"https://jwtforge.workers.dev"` | | `sub` | Subject | `"user123"` | `"user123"`, `"auth0\|507f1f77bcf86cd799439011"` | | `aud` | Audience | `"https://api.example.com"` | `"https://api.example.com"`, `"my-resource-id"` | | `exp` | Expiration time | Current time + 1 hour | `1735689600` | | `nbf` | Not before | Current time | `1735686000` | | `iat` | Issued at | Current time | `1735686000` | | `jti` | JWT ID | Random UUID | `"550e8400-e29b-41d4-a716-446655440000"` | | `client_id` | OAuth2 client identifier | Auto-generated or user-provided | `"test_app"`, `"client_a1b2c3d4"` | | `name` | Full name | none | `"John Doe"` | | `given_name` | First name | none | `"John"` | | `family_name` | Last name | none | `"Doe"` | | `middle_name` | Middle name | none | `"Michael"` | | `nickname` | Nickname | none | `"Johnny"` | | `preferred_username` | Preferred username | none | `"johndoe"` | | `profile` | Profile page URL | none | `"https://example.com/users/johndoe"` | | `picture` | Picture URL | none | `"https://example.com/avatar.jpg"` | | `website` | Website URL | none | `"https://johndoe.com"` | | `email` | Email address | none | `"john@example.com"` | | `email_verified` | Email verification status | none | `true`, `false` | | `gender` | Gender | none | `"male"`, `"female"`, `"other"` | | `birthdate` | Birthdate | none | `"1990-01-15"` | | `zoneinfo` | Time zone | none | `"America/New_York"` | | `locale` | Locale | none | `"en-US"`, `"fr-CA"` | | `phone_number` | Phone number | none | `"+1-555-555-5555"` | | `phone_number_verified` | Phone verification status | none | `true`, `false` | | `address` | Address object | none | `{"street_address":"123 Main St","locality":"City","region":"State","postal_code":"12345","country":"US"}` | | `updated_at` | Last update timestamp | none | `1735686000` | | `scope` | OAuth2 scopes | none | `"openid profile email"`, `"read write"` | | `roles` | User roles | none | `["admin", "user"]` | | `groups` | User groups | none | `["engineering", "management"]` | | `nonce` | Nonce for ID tokens | none | `"random-nonce-12345"` | You can override any default claim or add custom claims in the request body. ## Custom Claims Custom claims are preserved in the JWT payload: ```json { "body": { "sub": "user123", "tenant_id": "tenant-456", "permissions": ["read", "write", "delete"], "metadata": { "department": "Engineering", "level": "senior" } } } ``` ## Operational Fields Are Not Claims These fields configure token generation and are not emitted as payload claims: - `mode` - `exclude` - `kty` - `response_type` - `grammar_category` - `malicious_category` - `vulnerability` - `header` - `signature` Keep operational fields at the top level and payload claims under `body` when using structured JSON. --- ## OIDC Scopes JWTForge can populate common OIDC claims from the `scope` value. This is mainly useful in `fake` mode, where Faker generates realistic-looking user data. ```json { "mode": "fake", "body": { "sub": "user123", "scope": "openid profile email address phone" } } ``` ## Scope Mapping | Scope | Claims Included | Example Data | | --- | --- | --- | | `openid` | Base JWT claims such as `sub`, `iss`, `aud`, `exp`, `iat`, `nbf`, `jti` | Base claims are always present | | `profile` | `name`, `given_name`, `family_name`, `middle_name`, `nickname`, `preferred_username`, `profile`, `picture`, `website`, `gender`, `birthdate`, `zoneinfo`, `locale`, `updated_at` | `Jane Smith`, `jane.smith`, `https://example.com/avatar.jpg` | | `email` | `email`, `email_verified` | `jane.smith@example.com`, `true` | | `address` | `address` object with `street_address`, `locality`, `region`, `postal_code`, `country` | `{"street_address":"123 Main St","locality":"Anytown","region":"CA","postal_code":"12345","country":"US"}` | | `phone` | `phone_number`, `phone_number_verified` | `+1-555-555-5555`, `true` | ## Structured Request Example ```json { "mode": "fake", "response_type": "id_token token", "body": { "sub": "user123", "scope": "openid profile email", "nonce": "nonce-123" } } ``` ## Notes - Scope-derived claims are generated only where JWTForge has scope logic for that claim family. - Explicit claims in `body` override generated defaults. - In non-`fake` modes, claims may be fuzzed, replaced with malicious values, or selected from grammar rules. --- ## OpenAPI # OpenAPI and Swagger JWTForge keeps the OpenAPI contract available at: ```text /openapi.json ``` The Worker serves Swagger UI for that contract at: ```text /swagger ``` ## Trying Requests Swagger UI provides editable request forms and `Try it out`. To use it locally: 1. Run `npm run dev`. 2. Open `http://localhost:8787/swagger`. 3. Select an operation such as `POST /token`. 4. Click `Try it out`. 5. Edit the request body, headers, auth, and content type. 6. Execute the request. The Docusaurus `/api-reference` page embeds the Worker-hosted Swagger UI from `/swagger` in an iframe and also links to the raw OpenAPI JSON. For guided token generation, use the JWTForge widget on the Docusaurus landing page. ## Local Development During local development, Docusaurus and the Worker run on different ports: | Service | URL | | --- | --- | | Docusaurus docs | `http://localhost:3000` | | JWTForge Worker API | `http://localhost:8787` | | Swagger UI | `http://localhost:8787/swagger` | | OpenAPI JSON | `http://localhost:8787/openapi.json` | ## Production JWTForge can use one Cloudflare Worker domain for both the Docusaurus static site and the API. The hosted site uses: ```text https://jwtforge.dev ``` Wrangler static assets serve the documentation pages, while Worker routes handle API paths such as `/token`, `/introspect`, `/.well-known/*`, `/openapi.json`, and `/swagger`. The OpenAPI `servers` value is generated from the Worker `ISSUER` environment variable when it is configured. In this project: ```toml [vars] ISSUER = "https://jwtforge.dev" ``` The Docusaurus widget uses the same origin in production. Set `JWTFORGE_API_BASE_URL` only if the docs are built for a separate API host. Keep the OpenAPI JSON available at: ```text https://jwtforge.dev/openapi.json ``` ## Troubleshooting If Swagger UI is empty or cannot send requests: - Confirm the Worker is running. - Open `http://localhost:8787/openapi.json`. - Confirm `/openapi.json` returns valid JSON. - Open `http://localhost:8787/swagger`. - Confirm the browser can load `swagger-ui-dist` from the CDN. Keep `/openapi.json` as the machine-readable contract even when Docusaurus is the primary documentation experience. --- ## Local Development Run the Worker API and Docusaurus docs in separate terminal tabs. ## Terminal 1: JWTForge API ```bash npm run dev ``` By default, Wrangler serves the Worker at: ```text http://localhost:8787 ``` Because Wrangler static assets are enabled, this local Worker can also serve the built Docusaurus site from `build/`. That means `http://localhost:8787` and `http://localhost:3000` can both show the JWTForge docs UI: - `http://localhost:8787` is the Worker preview with static assets plus API routes. - `http://localhost:3000` is the live Docusaurus development server. API routes still run through the Worker on port `8787`, including `/token`, `/introspect`, `/.well-known/*`, `/openapi.json`, and `/swagger`. The Docusaurus widget calls the `/token` endpoint on the same origin in production. For local docs running on port `3000`, it falls back to the local Worker API at `http://localhost:8787`. Set `JWTFORGE_API_BASE_URL` only when building or starting docs against a separate API host: ```bash JWTFORGE_API_BASE_URL=https://your-worker.workers.dev npm run docs:start ``` For the same-domain production site, no API base override is required: ```bash npm run docs:build ``` If it is not set, local Docusaurus uses `http://localhost:8787` and production uses the docs site's origin. ## Terminal 2: Docusaurus ```bash npm run docs:start ``` Docusaurus serves the docs site at: ```text http://localhost:3000 ``` The landing page includes the custom token-generation widget. The embedded Swagger page is available at: ```text http://localhost:3000/api-reference ``` ## Production Build Build the static documentation site: ```bash npm run docs:build ``` Preview the built site: ```bash npm run docs:serve ``` ## Cloudflare Deployment `wrangler.toml` uses Wrangler static assets to deploy the Docusaurus build with the Worker: ```toml [build] command = "npm run docs:build" [assets] directory = "./build" binding = "ASSETS" ``` Wrangler runs the build command before packaging static assets. This prevents deploys from failing when `./build` does not exist yet. The Worker handles API paths such as `/token`, `/introspect`, `/.well-known/jwks.json`, `/.well-known/openid-configuration`, and `/openapi.json` first. Other paths fall back to the static Docusaurus assets when `ASSETS` is available. The same deployment uses the `ISSUER` variable for token `iss` claims, OIDC discovery URLs, and the OpenAPI server URL: ```toml [vars] ISSUER = "https://jwtforge.dev" ``` Deploy the Worker and static docs together: ```bash npm run deploy ``` For Cloudflare dashboard deployments, use these build settings: | Field | Value | | --- | --- | | Build command | None | | Deploy command | `npx wrangler versions upload` | | Version command | `npx wrangler versions upload` | | Root directory | `/` | | Build token | `jwtforge-dev build token` | | Build variables | None | The dashboard Build command can stay empty because Wrangler reads `[build] command = "npm run docs:build"` from `wrangler.toml`. In the build log this appears as `[custom build] Running: npm run docs:build` before Wrangler uploads static assets from `./build`. ## Swagger UI The Worker API serves Swagger UI at: ```text http://localhost:8787/swagger ``` The OpenAPI JSON is available at: ```text http://localhost:8787/openapi.json ``` The Docusaurus `/api-reference` page embeds Swagger UI from the Worker `/swagger` route and links to the raw OpenAPI JSON. For the hosted same-domain site, the production API URLs are: ```text https://jwtforge.dev/token https://jwtforge.dev/swagger https://jwtforge.dev/openapi.json ``` ## Troubleshooting If Docusaurus starts but token generation fails, confirm: - Wrangler is running in the API terminal. - The widget API base URL resolves to the Wrangler URL. - The Worker allows CORS for `Content-Type` and returns `Access-Control-Allow-Origin` on the actual `/token` response, not only the `OPTIONS` preflight response. - `http://localhost:8787/swagger` is reachable if you are using Swagger UI locally.