Skip to content

Commit 5985d2c

Browse files
committed
style: apply prettier 3.9.3 formatting
prettier 3.9.3 changed two formatting behaviors (long string property values no longer wrap to their own line; some double-quoted strings normalize to single quotes). Reapplied across the tree so committed code stays in sync with the formatter. Cosmetic only, no logic changes.
1 parent 822a175 commit 5985d2c

31 files changed

Lines changed: 1174 additions & 580 deletions

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ labels: bug
88
A clear description of the bug.
99

1010
**Input type**
11+
1112
- [ ] SAMLRequest
1213
- [ ] SAMLResponse
1314
- [ ] JWT
1415
- [ ] Other
1516

1617
**Steps to reproduce**
18+
1719
1. Paste ...
1820
2. See ...
1921

@@ -24,6 +26,7 @@ What did you expect to see?
2426
What did you see instead?
2527

2628
**Environment**
29+
2730
- Browser:
2831
- OS:
2932

CHANGELOG.md

Lines changed: 72 additions & 102 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The core UX premise: paste whatever messy thing you grabbed from a log file, a n
2828
A single `<textarea>` input. The `detect()` function determines whether the input is a JWT (three-part dot-separated base64url, or starts with `Authorization: Bearer`) or SAML (everything else). A debounced `$effect` drives decoding on a 300ms timer.
2929

3030
The page handles:
31+
3132
- Multiple SAML messages in one paste (`decodeAllSaml` scans for all `SAMLRequest`/`SAMLResponse` params)
3233
- Generic fallback decoding (`decodeAllGeneric`) for non-SAML base64/XML/JSON blobs
3334
- Lazy-loaded cert decoding via dynamic `import('$lib/cert')` to avoid blocking first render
@@ -60,6 +61,7 @@ The page handles:
6061
### JWT decode library (`src/lib/jwt.ts`)
6162

6263
`decodeJwt(input)` — strips `Bearer` prefix, splits on `.`, base64url-decodes header and payload, returns typed `JwtDecodeResult` with:
64+
6365
- `isAlgNone` / `isWeakAlg` flags
6466
- `timestamps` object (`iat`, `exp`, `nbf` as `JwtTimestamp` with `date`, `label`, `expired`)
6567
- `scopes` array (handles both `scope` string and `scp` array claim variants)
@@ -105,6 +107,7 @@ All tooltip text externalized as `FIELD_EXPLANATIONS: Record<string, string>` wi
105107
## Test coverage
106108

107109
100% statements, branches, functions, and lines across all `src/lib/` modules. Achieved via:
110+
108111
- `/* v8 ignore next */` directives on defensive null-guards that are unreachable in valid DER/XML (never in `parseName`, `parseKeyAlg`, etc.)
109112
- Carefully crafted DER certificate fixtures (single-line base64, length divisible by 4 — jsdom's `atob()` is strict, unlike Node's `Buffer.from()`)
110113
- Test cases for every edge path: UTCTime year ≥ 50, EC with no curve OID, RSA modulus without leading zero, unknown DN attribute OIDs, all SAML timestamp variants, JWT non-string alg, etc.
@@ -135,10 +138,12 @@ Run with: `npm run coverage`
135138
## Planned features
136139

137140
### Medium priority
141+
138142
- **SAML signature validation** — fetch the IdP's SAML metadata (by EntityID or URL), extract the signing cert, and verify `<ds:Signature>` using the Web Crypto API. Display verified/unverified status prominently.
139143
- **JWT JWKS validation** — after OIDC discovery, fetch `jwks_uri` and attempt to verify the JWT signature against the matching key.
140144

141145
### Lower priority / ideas
146+
142147
- **MDQ discovery** — fetch an entity's metadata from InCommon's MDQ service by EntityID and feed it into the metadata view (`src/lib/metadata.ts`)
143148
- **REFEDS entity category checker** — cross-check an SP's `RequestedAttribute`s against entity categories already parsed by the metadata view
144149
- **i18n**`explanations.ts` is already structured for this; add a locale switcher and alternate record implementations
@@ -157,15 +162,15 @@ Optional body explaining why, not what.
157162

158163
### Types and their effects
159164

160-
| Type | Version bump | Changelog section |
161-
|------|-------------|-------------------|
162-
| `feat` | minor (1.x.0) | Features |
163-
| `fix` | patch (1.0.x) | Bug Fixes & Dependencies |
164-
| `perf` | patch | Performance |
165+
| Type | Version bump | Changelog section |
166+
| ---------- | ------------------- | ---------------------------------------- |
167+
| `feat` | minor (1.x.0) | Features |
168+
| `fix` | patch (1.0.x) | Bug Fixes & Dependencies |
169+
| `perf` | patch | Performance |
165170
| `security` | none (display only) | Security — use `fix` if a bump is needed |
166-
| `docs` | none | Documentation (visible) |
167-
| `chore` | none | Miscellaneous (hidden) |
168-
| `ci` | none | CI/CD (hidden) |
171+
| `docs` | none | Documentation (visible) |
172+
| `chore` | none | Miscellaneous (hidden) |
173+
| `ci` | none | CI/CD (hidden) |
169174

170175
Breaking changes: add `!` after the type (`feat!:` or `fix!:`) **or** include a `BREAKING CHANGE:` footer. Either triggers a major bump (x.0.0).
171176

@@ -184,9 +189,11 @@ Commits that don't match any of the above types (e.g. `Add something` with no ty
184189
Before any structural refactor on a file >300 LOC, remove dead props, unused exports, and debug logs. Commit separately.
185190

186191
After any significant change, run:
192+
187193
```bash
188194
npx tsc --noEmit && npx eslint . --quiet && npm run coverage
189195
```
196+
190197
All three must pass before reporting work complete.
191198

192199
ESLint config note: `svelte/no-navigation-without-resolve` is configured with `ignoreLinks: true` because samlguy.com deploys at the root with no base path, making `resolve()` unnecessary for plain `href="/"` links.

README.md

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<div align="center">
22

33
# `<saml:Guy/>`
4-
**[*samlguy.com*](https://samlguy.com) — A SAML assertion and JWT decoder for the IAM community.**
4+
5+
**[_samlguy.com_](https://samlguy.com) — A SAML assertion and JWT decoder for the IAM community.**
56

67
[![CI](https://github.com/kellenmurphy/samlguy/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/kellenmurphy/samlguy/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/kellenmurphy/samlguy/graph/badge.svg)](https://codecov.io/gh/kellenmurphy/samlguy) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12859/badge)](https://www.bestpractices.dev/projects/12859)
78

@@ -13,10 +14,9 @@ SAML assertions get a full summary panel with binding type, message type, status
1314

1415
Think [jwt.io](https://jwt.io), but for the full modern IAM stack — built by someone who actually works in identity, for everyone else who does too.
1516

16-
1717
## Who is it for?
1818

19-
[samlguy.com](https://samlguy.com) is built by an IAM practitioner, for the IAM practitioner community: identity architects and engineers at universities, research institutions, and other organizations participating in federated identity like **[InCommon](https://incommon.org)** and **[eduGAIN](https://edugain.org/)**, and the broader R&E Trust & Identity community.
19+
[samlguy.com](https://samlguy.com) is built by an IAM practitioner, for the IAM practitioner community: identity architects and engineers at universities, research institutions, and other organizations participating in federated identity like **[InCommon](https://incommon.org)** and **[eduGAIN](https://edugain.org/)**, and the broader R&E Trust & Identity community.
2020

2121
If you run a Shibboleth IdP, debug SAML flows for a living, or spend time staring at raw base64 blobs in Fiddler or a browser DevTools network tab — this tool belongs in your bookmarks.
2222

@@ -34,14 +34,12 @@ But it's not just SAML! It's equally useful for OAuth 2.0 / OIDC work. Access to
3434

3535
**Everything else** — contextual `?` tooltips on every field covering the SAML spec, JWT/OIDC standards, and trust fabric conventions. Shareable links base64url-encode the input into the URL fragment — never sent to the server. Dark mode default with `localStorage` persistence.
3636

37-
3837
## Privacy & Security
3938

4039
**All SAML and JWT payloads are decoded entirely in your browser.** Nothing you paste is ever transmitted to or stored on any server. The only network request the tool makes on your behalf is the OIDC discovery proxy described above, which receives only an issuer URL, not the token itself.
4140

4241
See [SECURITY.md](SECURITY.md) for the full vulnerability disclosure policy and a description of the security controls in this project.
4342

44-
4543
## Local development
4644

4745
```bash
@@ -63,7 +61,6 @@ npm run build # production build
6361
npm run preview # preview production build locally
6462
```
6563

66-
6764
## Project structure
6865

6966
```
@@ -96,12 +93,10 @@ src/
9693
CODEOWNERS
9794
```
9895

99-
10096
## Deployment
10197

10298
[samlguy.com](https://samlguy.com) is deployed using Cloudflare Pages. Every merge to `main` triggers an automatic build and deploy via GitHub Actions — but only after the test and coverage jobs pass. Build command: `npm run build`, output directory: `.svelte-kit/cloudflare`.
10399

104-
105100
## What's Planned
106101

107102
- **Diff view** — paste two assertions side-by-side and highlight what changed; most useful for attribute table and timestamp diffs when debugging why a second login attempt looks different
@@ -111,12 +106,10 @@ src/
111106
- **JWT JWKS validation** — after OIDC discovery, fetch `jwks_uri` and verify the JWT signature against the matching key
112107
- **REFEDS entity category checker** — given an EntityID (via MDQ), check whether the IdP's attribute release policy would likely cover the SP's requested attributes; builds on the entity-category and requested-attribute parsing already in the metadata view
113108

114-
115109
## Contributing
116110

117111
This started as a personal passion project for [The SAML Guy](https://kellenmurphy.com) to mess with a nifty tech stack (Svelte). Bug reports and suggestions are welcome via [GitHub Issues](https://github.com/kellenmurphy/samlguy/issues). See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution guidelines. For security vulnerabilities, please follow the [responsible disclosure process](SECURITY.md) rather than opening a public issue.
118112

119-
120113
## License
121114

122-
[MIT](LICENSE) — free to use, modify, and distribute. Attribution appreciated but not required.
115+
[MIT](LICENSE) — free to use, modify, and distribute. Attribution appreciated but not required.

SECURITY.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ You can expect an acknowledgement within 48 hours and a resolution or status upd
3131

3232
### Adversaries and assets
3333

34-
| Adversary | Goal | Primary controls |
35-
|---|---|---|
36-
| Malicious paste / shared link | Trigger XSS via crafted SAML or JWT content rendered in the DOM | Svelte's default text escaping, `esc()` in the XML highlighter, CSP (`default-src 'self'`, no `unsafe-inline`, hash-pinned inline scripts) |
37-
| Compromised npm package | Inject malicious code into the build or CI environment | GuardDog, `npm audit`, Grype, Dependency Review, SHA-pinned actions, `npm ci` lockfile enforcement |
38-
| Compromised upstream GitHub Action | Substitute malicious CI code via a tampered version tag | All actions pinned to commit SHA, Dependabot rotates pins daily |
39-
| SSRF via OIDC proxy | Use the discovery Worker to reach internal infrastructure | `redirect: 'error'`, 5-second timeout, 100 KB cap, response validation |
40-
| Compromised maintainer account | Push unsigned or unreviewed code to `main` | Required commit signatures, branch protection, CODEOWNERS review, scoped API tokens |
41-
| Information leakage | Exfiltrate SAML assertions or JWT payloads | All decoding is client-side; no server ever receives token content; Worker code logs nothing; `Referrer-Policy: no-referrer` prevents issuer URLs from leaking via outbound navigation |
34+
| Adversary | Goal | Primary controls |
35+
| ---------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36+
| Malicious paste / shared link | Trigger XSS via crafted SAML or JWT content rendered in the DOM | Svelte's default text escaping, `esc()` in the XML highlighter, CSP (`default-src 'self'`, no `unsafe-inline`, hash-pinned inline scripts) |
37+
| Compromised npm package | Inject malicious code into the build or CI environment | GuardDog, `npm audit`, Grype, Dependency Review, SHA-pinned actions, `npm ci` lockfile enforcement |
38+
| Compromised upstream GitHub Action | Substitute malicious CI code via a tampered version tag | All actions pinned to commit SHA, Dependabot rotates pins daily |
39+
| SSRF via OIDC proxy | Use the discovery Worker to reach internal infrastructure | `redirect: 'error'`, 5-second timeout, 100 KB cap, response validation |
40+
| Compromised maintainer account | Push unsigned or unreviewed code to `main` | Required commit signatures, branch protection, CODEOWNERS review, scoped API tokens |
41+
| Information leakage | Exfiltrate SAML assertions or JWT payloads | All decoding is client-side; no server ever receives token content; Worker code logs nothing; `Referrer-Policy: no-referrer` prevents issuer URLs from leaking via outbound navigation |
4242

4343
### Out of scope
4444

@@ -130,6 +130,7 @@ The adapter additionally appends `Cache-Control: public, immutable, max-age=3153
130130
All GitHub Actions are pinned to a full commit SHA, not a mutable version tag. Version tags can be silently redirected by a compromised upstream repository, substituting malicious code into the pipeline. Pinning by SHA means the exact code reviewed at setup time is the code that runs. Human-readable version numbers are preserved as inline comments.
131131

132132
Current pins in `.github/workflows/ci.yml`:
133+
133134
- `actions/checkout` — SHA-pinned
134135
- `actions/setup-node` — SHA-pinned
135136
- `astral-sh/setup-uv` — SHA-pinned (v8.1.0)
@@ -142,15 +143,18 @@ Current pins in `.github/workflows/ci.yml`:
142143
- `actions/attest-build-provenance` — SHA-pinned (v4.1.0)
143144

144145
Current pins in `.github/workflows/scorecard.yml`:
146+
145147
- `actions/checkout` — SHA-pinned
146148
- `ossf/scorecard-action` — SHA-pinned (v2.4.3)
147149
- `actions/upload-artifact` — SHA-pinned (v7.0.1)
148150
- `github/codeql-action/upload-sarif` — SHA-pinned
149151

150152
Current pins in `.github/workflows/release-please.yml`:
153+
151154
- `googleapis/release-please-action` — SHA-pinned (v4)
152155

153156
Current pins in `.github/workflows/codeql.yml`:
157+
154158
- `actions/checkout` — SHA-pinned
155159
- `github/codeql-action/init` — SHA-pinned (v3.28.13)
156160
- `github/codeql-action/analyze` — SHA-pinned (v3.28.13)
@@ -186,6 +190,7 @@ GitHub secret scanning and push protection are both enabled on this repository.
186190
### Dependabot
187191

188192
Dependabot runs daily for:
193+
189194
- **npm packages** — grouped (Svelte ecosystem together, Cloudflare tools together) to reduce PR noise while keeping everything current. Commits use the `chore(deps)` prefix so routine dependency bumps do not trigger unnecessary patch releases via release-please.
190195
- **GitHub Actions** — separate ecosystem entry, because action dependencies are a supply chain vector that is easy to neglect
191196

@@ -215,7 +220,7 @@ Alongside the `Dependency Audit` job (push, weekly schedule, and manual dispatch
215220

216221
### Dependency Review
217222

218-
The [dependency-review-action](https://github.com/actions/dependency-review-action) runs on pull requests only. It compares the dependency diff introduced by the PR against GitHub's vulnerability database and fails the check if any newly added package carries a moderate or higher CVE. This is the pull-request-time vulnerability gate: it catches vulnerable dependencies a PR would *introduce*, before they land in `main`, while the full-tree `npm audit` and Grype scans run on push and the weekly schedule. A PR is therefore never blocked by a pre-existing, tree-wide advisory in a dependency the PR did not touch.
223+
The [dependency-review-action](https://github.com/actions/dependency-review-action) runs on pull requests only. It compares the dependency diff introduced by the PR against GitHub's vulnerability database and fails the check if any newly added package carries a moderate or higher CVE. This is the pull-request-time vulnerability gate: it catches vulnerable dependencies a PR would _introduce_, before they land in `main`, while the full-tree `npm audit` and Grype scans run on push and the weekly schedule. A PR is therefore never blocked by a pre-existing, tree-wide advisory in a dependency the PR did not touch.
219224

220225
### GitHub Code Scanning
221226

@@ -266,4 +271,3 @@ Since samlguy.com deploys continuously from `main`, the production deployment al
266271
## License
267272

268273
This project is released under the [MIT License](LICENSE). You are free to use, fork, modify, and distribute the code. The only requirement is that the copyright notice is preserved in copies or substantial portions of the software.
269-

src/lib/InfoTip.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
onmouseenter={show}
3030
onfocus={show}
3131
onmouseleave={hide}
32-
onblur={hide}
33-
>?</button>
32+
onblur={hide}>?</button
33+
>
3434
{#if visible}
3535
<div
3636
role="tooltip"

src/lib/attributes.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import {
3-
getAttributeInfo,
4-
eppnScopedStatus,
5-
EPPN_OID,
6-
ATTRIBUTE_REGISTRY,
7-
} from './attributes';
2+
import { getAttributeInfo, eppnScopedStatus, EPPN_OID, ATTRIBUTE_REGISTRY } from './attributes';
83

94
describe('ATTRIBUTE_REGISTRY', () => {
105
it('EPPN_OID constant matches the registry entry', () => {

0 commit comments

Comments
 (0)