mcp-authlab 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. mcp_authlab-0.2.0/CHANGELOG.md +28 -0
  2. mcp_authlab-0.2.0/CONTRIBUTING.md +33 -0
  3. mcp_authlab-0.2.0/LICENSE +21 -0
  4. mcp_authlab-0.2.0/MANIFEST.in +5 -0
  5. mcp_authlab-0.2.0/PKG-INFO +246 -0
  6. mcp_authlab-0.2.0/README.md +217 -0
  7. mcp_authlab-0.2.0/SECURITY.md +26 -0
  8. mcp_authlab-0.2.0/docs/checks.md +45 -0
  9. mcp_authlab-0.2.0/docs/grading.md +100 -0
  10. mcp_authlab-0.2.0/docs/report.schema.json +119 -0
  11. mcp_authlab-0.2.0/examples/github-action.yml +41 -0
  12. mcp_authlab-0.2.0/examples/policy.toml +17 -0
  13. mcp_authlab-0.2.0/pyproject.toml +54 -0
  14. mcp_authlab-0.2.0/scripts/demo.sh +31 -0
  15. mcp_authlab-0.2.0/setup.cfg +4 -0
  16. mcp_authlab-0.2.0/src/mcp_authlab/__init__.py +5 -0
  17. mcp_authlab-0.2.0/src/mcp_authlab/__main__.py +3 -0
  18. mcp_authlab-0.2.0/src/mcp_authlab/catalog.py +436 -0
  19. mcp_authlab-0.2.0/src/mcp_authlab/checks.py +34 -0
  20. mcp_authlab-0.2.0/src/mcp_authlab/cli.py +270 -0
  21. mcp_authlab-0.2.0/src/mcp_authlab/discovery.py +65 -0
  22. mcp_authlab-0.2.0/src/mcp_authlab/fixture.py +688 -0
  23. mcp_authlab-0.2.0/src/mcp_authlab/grading.py +148 -0
  24. mcp_authlab-0.2.0/src/mcp_authlab/httpclient.py +152 -0
  25. mcp_authlab-0.2.0/src/mcp_authlab/jwtutil.py +76 -0
  26. mcp_authlab-0.2.0/src/mcp_authlab/oauthclient.py +722 -0
  27. mcp_authlab-0.2.0/src/mcp_authlab/policy.py +89 -0
  28. mcp_authlab-0.2.0/src/mcp_authlab/report.py +240 -0
  29. mcp_authlab-0.2.0/src/mcp_authlab/scanner.py +912 -0
  30. mcp_authlab-0.2.0/src/mcp_authlab/tokenstore.py +113 -0
  31. mcp_authlab-0.2.0/src/mcp_authlab/wwwauth.py +134 -0
  32. mcp_authlab-0.2.0/src/mcp_authlab.egg-info/PKG-INFO +246 -0
  33. mcp_authlab-0.2.0/src/mcp_authlab.egg-info/SOURCES.txt +40 -0
  34. mcp_authlab-0.2.0/src/mcp_authlab.egg-info/dependency_links.txt +1 -0
  35. mcp_authlab-0.2.0/src/mcp_authlab.egg-info/entry_points.txt +2 -0
  36. mcp_authlab-0.2.0/src/mcp_authlab.egg-info/top_level.txt +1 -0
  37. mcp_authlab-0.2.0/tests/support.py +69 -0
  38. mcp_authlab-0.2.0/tests/test_grading.py +174 -0
  39. mcp_authlab-0.2.0/tests/test_hardening.py +231 -0
  40. mcp_authlab-0.2.0/tests/test_scanner.py +223 -0
  41. mcp_authlab-0.2.0/tests/test_units.py +361 -0
  42. mcp_authlab-0.2.0/tests/test_wwwauth.py +174 -0
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (2026-10-19)
4
+
5
+ - `mcp-authlab login <url>` signs you in to a server's authorization server with the real authorization code flow (PKCE S256, dynamic registration or `--client-id`, loopback redirect, RFC 8707 `resource`, RFC 9207 `iss` checked) and stores the tokens in `~/.config/mcp-authlab/tokens.json` with mode 0600. `scan` picks them up on its own, so the critical token and session checks run for anyone who can log in.
6
+ - `login` also mints a second token for a different resource (the authorization server's issuer by default, `--foreign-resource` to choose) so the audience-binding check runs. When the authorization server ignores `resource=`, the report says so.
7
+ - Six new checks on the authorization server itself, ASF-01 to ASF-06: PKCE required, wrong verifier refused, wrong redirect_uri refused, tokens bound to the requested resource, refresh tokens rotate, HTTPS and `iss` everywhere. They run during `login` and show up in the scan report. A login is two sign-ins; a server that revokes codes after a failed exchange costs a third, and `login --thorough` spends a fourth on the redirect_uri probe.
8
+ - A scope step-up check, SCOPE-01: a tool call the token can't make should get a 403 with `error="insufficient_scope"`, a `scope` hint and `resource_metadata`. `--tool` chooses which tool to call; by default the scanner picks a read-only one.
9
+ - Every report opens with a coverage line: how many checks ran out of how many apply, and which critical checks didn't run.
10
+ - The headline is now a conformance level (Unsafe, Non-conformant, Conformant with warnings, Conformant) read straight from the spec's MUST and SHOULD language. The letter grade stays as the CI knob; `docs/grading.md` shows how to recompute both by hand.
11
+ - `--policy file.toml` lets a team set its own bar: `fail_under`, `ignore` and `downgrade`. Ignored checks still run and print. ORIG-01 and VER-01 now carry a note on why operators dispute them; they stay MUST.
12
+ - `--format sarif` for GitHub code scanning, and a JSON schema for the report at `docs/report.schema.json`.
13
+ - `registry/`: a list of public MCP endpoints and a script that scans them unauthenticated and writes the results and a summary table.
14
+ - The fixture grew a small authorization server with consent, PKCE, resource indicators and refresh tokens, plus eight new defect flags and a `stateless` flag. Several fixtures can run in one process.
15
+ - `WWW-Authenticate` parsing follows the RFC 9110 grammar: several challenges per header, token68, commas inside quoted strings, escaped quotes.
16
+ - `mcp-authlab logout <url>` forgets stored tokens.
17
+ - A critical check whose request times out or drops caps the grade at C and shows up in the coverage line, so a server that hangs up on a forged token can't grade A.
18
+ - `scan` refreshes an expired foreign-audience token and skips TOK-03 when it can't. An expired token gets a 401 from any server, which would otherwise read as a pass.
19
+ - SESS-01 replays the session ID on `tools/list` rather than `initialize`, which catches servers that check the token once and trust the session after that.
20
+ - ASM-02 and the callback `iss` check compare issuers as exact strings, as RFC 8414 and RFC 9207 require.
21
+ - `--requests` output hides tokens and authorization codes in URLs, along with `Set-Cookie` headers.
22
+ - The tokens file sits in a 0700 directory, writes are locked, and a damaged file is left in place for you to look at.
23
+ - The fixture's `opaque_tokens` and `issuer_trailing_slash` flags cover opaque access tokens and issuer mismatches.
24
+ - The test suite polls for shutdown every 50 ms instead of every 500, and ruff and mypy run in CI.
25
+
26
+ ## 0.1.0 (2026-10-19)
27
+
28
+ - First release: 26 black-box checks, the reference fixture with seven defect profiles, text, Markdown and JSON reports, `--fail-under` for CI.
@@ -0,0 +1,33 @@
1
+ # Contributing
2
+
3
+ Bug reports, false positives and new checks are all welcome. For anything bigger than a small fix, open an issue first so we can agree on the shape.
4
+
5
+ ## Setup
6
+
7
+ Python 3.11 or newer. The tool itself has no dependencies; the linters are only for development.
8
+
9
+ ```
10
+ pip install -e . ruff==0.15.22 mypy==2.1.0
11
+ python3 -m unittest discover -s tests
12
+ ruff check src tests && ruff format --check src tests && mypy src
13
+ ```
14
+
15
+ CI runs all three on Python 3.11, 3.12 and 3.13, plus `scripts/demo.sh` against every fixture profile.
16
+
17
+ ## Adding a check
18
+
19
+ 1. Add it to `CATALOG` in `src/mcp_authlab/catalog.py` with the spec text it enforces in `cite` and a link in `url`. Use MUST or SHOULD exactly as the spec does.
20
+ 2. Implement it in `scanner.Scanner`, or in `oauthclient.Login` if it probes the authorization server.
21
+ 3. Add a defect flag to `fixture.Profile` that breaks the rule, and put the check in `EXPECTED_FAILURES` for a profile that sets that flag. The profile tests fail until the scanner catches it.
22
+ 4. Regenerate the docs with `mcp-authlab checks --markdown > docs/checks.md`. A test fails while that file is stale.
23
+ 5. Add a line to `CHANGELOG.md` describing what a user sees.
24
+
25
+ A check has to be provable from outside the server. If the only way to know is to read the server's code, it belongs in the Limits section of the README, not the catalog.
26
+
27
+ ## Reporting a false positive
28
+
29
+ Run the scan with `--format json --requests` and attach the result. Tokens, codes and session IDs are already redacted, but read it over before posting. Say which check is wrong and which spec text you think applies.
30
+
31
+ ## Writing
32
+
33
+ Docs, messages and the changelog are written plainly, the way you'd explain the thing out loud: contractions, active voice, and what the user sees rather than what the code avoids.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tmtmiller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include CHANGELOG.md CONTRIBUTING.md SECURITY.md LICENSE README.md
2
+ recursive-include docs *.md *.json
3
+ recursive-include examples *.yml *.toml
4
+ recursive-include scripts *.sh
5
+ recursive-include tests *.py
@@ -0,0 +1,246 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-authlab
3
+ Version: 0.2.0
4
+ Summary: Conformance scanner for MCP server authorization (OAuth 2.1, RFC 9728, RFC 8414, RFC 8707). Logs in, scans a live server, grades it, fails CI.
5
+ Author: tmtmiller
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/tmtmiller1/mcp-authlab
8
+ Project-URL: Issues, https://github.com/tmtmiller1/mcp-authlab/issues
9
+ Project-URL: Changelog, https://github.com/tmtmiller1/mcp-authlab/blob/main/CHANGELOG.md
10
+ Project-URL: Source, https://github.com/tmtmiller1/mcp-authlab
11
+ Keywords: mcp,model-context-protocol,oauth,security,conformance,rfc9728,authorization
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Requires-Python: >=3.11
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # mcp-authlab
31
+
32
+ [![ci](https://github.com/tmtmiller1/mcp-authlab/actions/workflows/ci.yml/badge.svg)](https://github.com/tmtmiller1/mcp-authlab/actions/workflows/ci.yml)
33
+ [![python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](pyproject.toml)
34
+ [![dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](pyproject.toml)
35
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
36
+
37
+ Tests whether an [MCP](https://modelcontextprotocol.io) server refuses the tokens it should refuse. Most MCP auth scanners read the discovery documents and stop there. mcp-authlab logs in to the server's authorization server, mints real tokens, and then attacks the resource server with them: an unsigned token, a token issued for a different service, a session ID with no token at all. Every finding cites the line of the spec it enforces.
38
+
39
+ ```
40
+ $ scripts/demo.sh passthrough
41
+
42
+ mcp-authlab 0.2.0 · spec 2025-06-18
43
+ target http://127.0.0.1:8765/mcp
44
+ coverage 33/33
45
+ level Unsafe grade F score 81/100
46
+ critical failure: TOK-02, TOK-03
47
+ MUST failures: TOK-02, TOK-03, ORIG-01
48
+ checks 29 pass · 3 fail · 1 warn · 0 skipped
49
+
50
+ Token validation
51
+ ✓ TOK-01 MUST Malformed bearer token is refused with 401
52
+ ✗ TOK-02 MUST* Unsigned (alg=none) token with the right audience is refused
53
+ server accepted an unsigned JWT, so it isn't validating tokens at all
54
+ MCP Authorization, Token Handling; Security Best Practices, Token Passthrough
55
+ ✗ TOK-03 MUST* Valid token issued for a different resource is refused (audience binding)
56
+ server accepted a token issued for another resource, so it isn't checking the audience (token passthrough risk)
57
+ MCP Authorization, Access Token Privilege Restriction; RFC 8707 section 2
58
+ ✓ TOK-05 MUST A valid token in the query string is refused
59
+
60
+ Sessions
61
+ ✓ SESS-01 MUST* A session ID alone doesn't authenticate
62
+
63
+ Origin
64
+ ✗ ORIG-01 MUST Request with a hostile Origin header is refused
65
+ request from an unknown Origin was served; a DNS-rebinding page could drive this server
66
+ ```
67
+
68
+ That's a real run against the `passthrough` profile of the bundled reference server, trimmed to three sections.
69
+
70
+ ## What's different
71
+
72
+ Several tools already check MCP discovery: the 401 challenge, the RFC 9728 and RFC 8414 metadata, PKCE support in the metadata. mcp-authlab does that too, and it's the least interesting part. These are the parts that, as of 2026-10-19, no other public MCP scanner does (see [How it compares](#how-it-compares)):
73
+
74
+ - **Forged-token probe.** It sends an `alg=none` JWT with the right audience. A server that accepts it isn't checking signatures.
75
+ - **Foreign-audience probe.** `login` asks the authorization server for a second token bound to a different resource, then hands it to your server. A server that accepts it is open to token passthrough, the confused-deputy problem the MCP security guidance calls out by name. If the authorization server ignores `resource=` and can't mint such a token, the report says that instead of guessing.
76
+ - **Session replay.** It replays a live `Mcp-Session-Id` with the bearer token removed. A server that answers has made the session ID a password.
77
+ - **Authorization server enforcement, not just advertisement.** During `login` it checks that the authorization server actually refuses a request without `code_challenge`, a wrong `code_verifier` and a mismatched `redirect_uri`, that tokens are bound to the requested resource, and that refresh tokens rotate.
78
+ - **A reference server that proves the scanner.** The bundled fixture is a working MCP resource server and authorization server with switches that break it the ways real servers break. The test suite logs in to each broken profile and asserts the scanner flags exactly its defects.
79
+
80
+ It's pure Python standard library, so the only code you're trusting is in this repo.
81
+
82
+ ## Why these checks
83
+
84
+ The MCP authorization spec is a thin profile over OAuth 2.1, RFC 9728 (protected resource metadata), RFC 8414 (authorization server metadata) and RFC 8707 (resource indicators). You can read most of it off a 401. The failures that lead to account takeover only show up when you probe with real tokens:
85
+
86
+ - **Token passthrough.** A server that accepts any well-formed token and forwards it downstream looks identical to a correct one until you hand it a token minted for a different audience, or one without a signature.
87
+ - **Sessions as authentication.** A server that trusts `Mcp-Session-Id` after the first authenticated request turns every leaked session ID into a stolen account.
88
+
89
+ Those three checks (TOK-02, TOK-03 and SESS-01, marked `MUST*`) are critical. One failure makes the server **Unsafe** on its own, even if everything else passes. Getting the tokens to run them used to be the hard part; `login` does it for you.
90
+
91
+ ## Install
92
+
93
+ ```
94
+ pipx install git+https://github.com/tmtmiller1/mcp-authlab@v0.2.0
95
+ ```
96
+
97
+ or install the wheel from the [latest release](https://github.com/tmtmiller1/mcp-authlab/releases/latest), or `pip install -e .` from a checkout. Python 3.11 or newer is the whole requirement.
98
+
99
+ ## Use
100
+
101
+ ```
102
+ mcp-authlab scan https://mcp.example.com/mcp
103
+ ```
104
+
105
+ Without a login the scanner runs the 19 checks that need no credentials: the challenge, both metadata documents and the invalid-token probes. The first lines of the report say how much that covered and which critical checks it couldn't reach:
106
+
107
+ ```
108
+ coverage 19/33 · critical checks not run: TOK-03, SESS-01 (run `mcp-authlab login` to enable)
109
+ ```
110
+
111
+ ### Logging in
112
+
113
+ ```
114
+ mcp-authlab login https://mcp.example.com/mcp
115
+ ```
116
+
117
+ `login` finds the authorization server the same way a client does, registers itself by dynamic registration (RFC 7591) or uses `--client-id`, opens your browser for the authorization code flow with PKCE, and stores the tokens in `~/.config/mcp-authlab/tokens.json` (mode 0600, keyed by canonical resource). From then on `scan` uses them and refreshes them when they expire. `logout` forgets them.
118
+
119
+ While it has the authorization server's attention it does three more things:
120
+
121
+ - **Probes the authorization server** (the `ASF-*` checks): does it refuse a request without `code_challenge`, a wrong `code_verifier`, a mismatched `redirect_uri`? Are tokens bound to the resource you asked for? Do refresh tokens rotate? Were all URLs HTTPS and did the callback carry `iss`? You sign in twice, once per token. A strict server revokes an authorization code after the failed `code_verifier` probe, which costs one more sign-in. When that happens the `redirect_uri` probe is skipped and the report says so; `--thorough` runs it at the price of a fourth sign-in.
122
+ - **Mints a second token for a different resource** (the authorization server's own issuer URL by default, `--foreign-resource` to choose) so the scan can test audience binding. If the authorization server ignores `resource=` and hands back the same audience, the report says so.
123
+ - **Records the results** next to the tokens so the scan report carries them.
124
+
125
+ `--no-browser` prints the URL for you to open by hand. When the authorization server is on loopback, as the fixture's is, it fetches the URL itself. That's how the test suite and `scripts/demo.sh` run the whole flow hands-free.
126
+
127
+ You can also hand the scanner tokens directly with `--token` and `--foreign-token` (or `MCP_AUTHLAB_TOKEN` and `MCP_AUTHLAB_FOREIGN_TOKEN`). Explicit tokens win over stored ones, and `--no-login` ignores the store. Tokens only go to the server you name, and the scanner closes every session it opens with a `DELETE`.
128
+
129
+ ### Output and CI
130
+
131
+ `--format text` (default), `json` (schema in [docs/report.schema.json](docs/report.schema.json); add `--requests` for every HTTP exchange), `markdown`, or `sarif` for GitHub code scanning. `-o file` writes to a file. `--fail-under B` exits 2 when the grade is lower, and `--policy authlab.toml` lets a team write its bar down once (below). [examples/github-action.yml](examples/github-action.yml) shows a scheduled scan that gates on grade and uploads SARIF.
132
+
133
+ ## What it checks
134
+
135
+ 33 checks in ten groups. The full table with citations is in [docs/checks.md](docs/checks.md) and from `mcp-authlab checks`.
136
+
137
+ | Group | Covers |
138
+ |---|---|
139
+ | Transport | HTTPS on the MCP endpoint |
140
+ | Challenge | 401 on unauthenticated requests, `WWW-Authenticate: Bearer` with `resource_metadata`, `scope` hint |
141
+ | Protected Resource Metadata | published at the RFC 9728 well-known path, `resource` matches the canonical URI, authorization servers listed, challenge URL agrees, scopes are specific (wildcards and `offline_access` flagged), bearer tokens kept out of the query string |
142
+ | Authorization Server Metadata | discoverable via RFC 8414 or OpenID Connect at the right path for the issuer, required fields, issuer matches, HTTPS endpoints, PKCE S256, a client registration path, RFC 9207 `iss` |
143
+ | Authorization server flow (`login`) | PKCE required at the authorization endpoint and enforced at the token endpoint, `redirect_uri` checked, tokens bound to the requested resource, refresh token rotation, HTTPS and `iss` on the callback |
144
+ | Token validation | random string refused, `alg=none` refused, foreign-audience token refused, `error="invalid_token"` on 401, query-string token refused, valid token works |
145
+ | Scope step-up | a tool call the token can't make gets a 403 with `error="insufficient_scope"`, a `scope` hint and `resource_metadata` |
146
+ | Sessions | a session ID alone doesn't authenticate; IDs are long, random, distinct |
147
+ | Origin | hostile `Origin` refused |
148
+ | Protocol version | unsupported `MCP-Protocol-Version` gets 400 |
149
+
150
+ Checks cite the [2025-06-18 authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), the [security best practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) and [transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) pages, and the underlying RFCs. Items that only exist in the [draft spec](https://modelcontextprotocol.io/specification/draft/basic/authorization) (scope challenges, Client ID Metadata Documents, `iss`) are graded as SHOULD.
151
+
152
+ ### Grading
153
+
154
+ The headline is a conformance level read straight from the spec's language: **Unsafe** (a critical check failed), **Non-conformant** (a MUST failed), **Conformant with warnings** (only SHOULD items or soft MUST answers), or **Conformant**. A letter grade from a weighted score sits next to it for CI gates, and a MUST failure caps it at C. [docs/grading.md](docs/grading.md) has every weight and threshold and a worked example you can check by hand against the JSON report.
155
+
156
+ ### Policy files
157
+
158
+ Two MUST checks draw pushback from operators. ORIG-01 does because non-browser clients don't send `Origin`, and VER-01 because answering an old client with the newest version you support is friendlier than a 400. They stay MUST because the spec says MUST, and the catalog carries a `disputed` note explaining both sides. A policy file is where your organisation records its own call:
159
+
160
+ ```toml
161
+ fail_under = "B"
162
+ ignore = ["VER-01"] # still runs and prints, stops counting
163
+ downgrade = { "ORIG-01" = "SHOULD" } # counts as SHOULD and stops being critical
164
+ ```
165
+
166
+ Run it with `mcp-authlab scan --policy authlab.toml`. [examples/policy.toml](examples/policy.toml) explains each key.
167
+
168
+ ## The reference server
169
+
170
+ `mcp-authlab fixture` runs a small streamable HTTP MCP server plus a minimal authorization server, written against the spec, that grades A. The resource server serves protected resource metadata, HS256 tokens, sessions, Origin checks, a scope-gated tool and a plain one. The authorization server does dynamic registration, a consent page (`--auto-consent` approves on its own for tests), PKCE S256, resource indicators and rotating refresh tokens. Together they're about 600 lines, and they're a fair example of what a conformant pair has to do.
171
+
172
+ Profiles turn on defects:
173
+
174
+ | Profile | What's wrong | Level | Grade |
175
+ |---|---|---|---|
176
+ | `reference` | passes every check | Conformant with warnings (plain http on loopback) | A |
177
+ | `legacy` | resource metadata missing, bare 401, `invalid_token` error missing | Non-conformant | C |
178
+ | `weak-as` | authorization server metadata leaves out PKCE, registration, `iss` and scope hints; PKCE optional in practice; `resource=` ignored | Non-conformant | C |
179
+ | `passthrough` | accepts unsigned tokens and foreign audiences, ignores Origin | Unsafe | F |
180
+ | `sloppy` | sessions as auth, sequential session IDs, query-string tokens, wildcard scopes, redirect_uri unchecked, static refresh tokens, bare 403s | Unsafe | F |
181
+ | `open` | serves every request, token or not | Non-conformant | F |
182
+ | `mismatch` | metadata `resource` points elsewhere, 403 instead of 401, http:// authorization server | Non-conformant | F |
183
+
184
+ `--flag <defect>` turns on a single defect on top of any profile (`stateless` and `ignore_resource` are two useful ones). `scripts/demo.sh <profile>` starts one, logs in without a browser, scans with the tokens it minted, and stops it:
185
+
186
+ ```
187
+ scripts/demo.sh passthrough
188
+ ```
189
+
190
+ ## Field notes
191
+
192
+ Unauthenticated scans of public endpoints on 2026-10-19, so each line covers 19 of 33 checks and the critical checks `TOK-03` and `SESS-01` were skipped. `python3 registry/scan.py` reruns them and rewrites the [registry](registry/README.md) table.
193
+
194
+ - **Linear** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant, A. Every check that ran passed.
195
+ - **Notion** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant, A. Every check that ran passed.
196
+ - **Sentry** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant with warnings, A. The 401 doesn't say which scopes to request.
197
+ - **Asana** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant with warnings, A. The 401 doesn't say which scopes to request, and the authorization server doesn't advertise `iss`. Metadata binds tokens to the host rather than the `/sse` path, which RFC 9728 allows.
198
+ - **GitHub** `coverage 18/33 · critical checks not run: TOK-03, SESS-01`: Non-conformant, C. A malformed token gets 400 rather than the 401 the spec requires, so the `invalid_token` check couldn't run either. The challenge's `resource_metadata` URL returns a different document than the well-known path, and clients have to be registered by hand.
199
+
200
+ All five look clean from outside. The critical findings need a login to show up.
201
+
202
+ ## How it compares
203
+
204
+ Checked on 2026-10-19 against each project's README, and against the source for the official suite and mcp-authcheck.
205
+
206
+ | | scans a live server | runs the OAuth flow | probes token validation | CI gate | zero dependencies |
207
+ |---|---|---|---|---|---|
208
+ | **mcp-authlab** | yes | yes (`login`) | random, unsigned, foreign-audience and query-string tokens; session replay; scope step-up | `--fail-under`, `--policy`, SARIF | yes (stdlib) |
209
+ | [MCP conformance suite](https://github.com/modelcontextprotocol/conformance) (official) | yes (`server --url`) | for authorization servers: the happy-path code grant and DPoP | its server scenarios don't send a bearer token; it does test DNS rebinding | yes, GitHub Action with expected-failure baselines | no (Node) |
210
+ | [mcp-authcheck](https://github.com/tstanmay13/mcp-authcheck) | yes | no; it only sends credentials you give it | one bogus token; wrong-audience and scope checks are listed but not built yet | `--min-grade`, GitHub Action | yes (Node, no runtime packages) |
211
+ | [mcp-auth-doctor](https://github.com/Jah-yee/mcp-auth-doctor) | yes | opt-in `--login` with PKCE, `state` and `iss` checks | uses its own real token once on `tools/list` | exit codes 0 to 3 | one package (httpx) |
212
+ | [MCPJam OAuth conformance](https://docs.mcpjam.com/cli/oauth-conformance) | yes | interactive, headless or client credentials, with `redirect_uri` rejection checks | one invalid bearer token; an audience problem only shows up as a 401 on the real token | JUnit and JSON reporters | not stated (part of the MCPJam Inspector CLI) |
213
+ | [AuthPlane/conformance](https://github.com/AuthPlane/conformance) | a catalog of cases that SDKs run in-process against their own verifier | no | its example rejects a bad JWT header, against the SDK's verifier | through each SDK's report | n/a (catalog, no runner) |
214
+ | [mcp-auth-audit](https://github.com/sudo-ai-git/mcp-auth-audit) | reads a server's declared auth schema from supplied metadata ("No LLM, no network") | no | no | PASS/FAIL; exit codes undocumented | not stated (Python) |
215
+ | [Snyk Agent Scan](https://github.com/invariantlabs-ai/mcp-scan) (was invariantlabs mcp-scan) | connects to configured servers to read tool descriptions for prompt injection | uses OAuth tokens you already have | no | `--ci` | no (needs a Snyk API token) |
216
+ | [mcp-guardian](https://github.com/eqtylab/mcp-guardian) (policy proxy) | sits between client and server to log and approve tool calls | no | no | no | no (Rust) |
217
+
218
+ If you only need the discovery checks, mcp-authcheck and mcp-auth-doctor do them with less setup. MCPJam and the official suite are the better fit for testing a client or an SDK across the whole protocol. mcp-authlab is for the question they leave open: does this server refuse the tokens it should?
219
+
220
+ ## Limits
221
+
222
+ A scan sees what a client sees: the challenge, the metadata documents, and how the resource server answers tokens it should refuse. `login` adds the authorization server's behaviour for one public client. So the report covers both halves of the spec, but it can't tell you how a token was validated internally or what a confidential client would see. When tokens are opaque, the audience verdict rests on how the resource server treated the two tokens, and the report says so.
223
+
224
+ ## Roadmap
225
+
226
+ - Client ID Metadata Document checks: fetch the document, validate `redirect_uris`.
227
+ - Enterprise-managed authorization (ID-JAG) metadata checks from [ext-auth](https://github.com/modelcontextprotocol/ext-auth).
228
+ - Legacy HTTP+SSE transport.
229
+ - Token introspection (RFC 7662) for opaque-token audience checks when the authorization server offers it.
230
+
231
+ ## Development
232
+
233
+ ```
234
+ python3 -m unittest discover -s tests -v
235
+ ruff check src tests && ruff format --check src tests && mypy src
236
+ ```
237
+
238
+ The tests start each fixture profile on a random port, log in to it with `--no-browser`, scan it in-process, and assert the flagged set equals the profile's declared defects. The suite runs in about six seconds. Adding a check means adding it to `catalog.CATALOG`, implementing it in `scanner.Scanner` (or `oauthclient.Login` for the authorization server), breaking it in a fixture profile, declaring that in `EXPECTED_FAILURES`, and regenerating `docs/checks.md` with `mcp-authlab checks --markdown`. A test fails when that file is stale. [CHANGELOG.md](CHANGELOG.md) lists what changed in each release.
239
+
240
+ ## Contributing and security
241
+
242
+ [CONTRIBUTING.md](CONTRIBUTING.md) covers running the tests and adding a check. To report a vulnerability in mcp-authlab itself, see [SECURITY.md](SECURITY.md). To have a server taken off the registry or a finding corrected, [open a registry issue](https://github.com/tmtmiller1/mcp-authlab/issues/new?template=registry.yml).
243
+
244
+ ## License
245
+
246
+ MIT.
@@ -0,0 +1,217 @@
1
+ # mcp-authlab
2
+
3
+ [![ci](https://github.com/tmtmiller1/mcp-authlab/actions/workflows/ci.yml/badge.svg)](https://github.com/tmtmiller1/mcp-authlab/actions/workflows/ci.yml)
4
+ [![python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](pyproject.toml)
5
+ [![dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](pyproject.toml)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+
8
+ Tests whether an [MCP](https://modelcontextprotocol.io) server refuses the tokens it should refuse. Most MCP auth scanners read the discovery documents and stop there. mcp-authlab logs in to the server's authorization server, mints real tokens, and then attacks the resource server with them: an unsigned token, a token issued for a different service, a session ID with no token at all. Every finding cites the line of the spec it enforces.
9
+
10
+ ```
11
+ $ scripts/demo.sh passthrough
12
+
13
+ mcp-authlab 0.2.0 · spec 2025-06-18
14
+ target http://127.0.0.1:8765/mcp
15
+ coverage 33/33
16
+ level Unsafe grade F score 81/100
17
+ critical failure: TOK-02, TOK-03
18
+ MUST failures: TOK-02, TOK-03, ORIG-01
19
+ checks 29 pass · 3 fail · 1 warn · 0 skipped
20
+
21
+ Token validation
22
+ ✓ TOK-01 MUST Malformed bearer token is refused with 401
23
+ ✗ TOK-02 MUST* Unsigned (alg=none) token with the right audience is refused
24
+ server accepted an unsigned JWT, so it isn't validating tokens at all
25
+ MCP Authorization, Token Handling; Security Best Practices, Token Passthrough
26
+ ✗ TOK-03 MUST* Valid token issued for a different resource is refused (audience binding)
27
+ server accepted a token issued for another resource, so it isn't checking the audience (token passthrough risk)
28
+ MCP Authorization, Access Token Privilege Restriction; RFC 8707 section 2
29
+ ✓ TOK-05 MUST A valid token in the query string is refused
30
+
31
+ Sessions
32
+ ✓ SESS-01 MUST* A session ID alone doesn't authenticate
33
+
34
+ Origin
35
+ ✗ ORIG-01 MUST Request with a hostile Origin header is refused
36
+ request from an unknown Origin was served; a DNS-rebinding page could drive this server
37
+ ```
38
+
39
+ That's a real run against the `passthrough` profile of the bundled reference server, trimmed to three sections.
40
+
41
+ ## What's different
42
+
43
+ Several tools already check MCP discovery: the 401 challenge, the RFC 9728 and RFC 8414 metadata, PKCE support in the metadata. mcp-authlab does that too, and it's the least interesting part. These are the parts that, as of 2026-10-19, no other public MCP scanner does (see [How it compares](#how-it-compares)):
44
+
45
+ - **Forged-token probe.** It sends an `alg=none` JWT with the right audience. A server that accepts it isn't checking signatures.
46
+ - **Foreign-audience probe.** `login` asks the authorization server for a second token bound to a different resource, then hands it to your server. A server that accepts it is open to token passthrough, the confused-deputy problem the MCP security guidance calls out by name. If the authorization server ignores `resource=` and can't mint such a token, the report says that instead of guessing.
47
+ - **Session replay.** It replays a live `Mcp-Session-Id` with the bearer token removed. A server that answers has made the session ID a password.
48
+ - **Authorization server enforcement, not just advertisement.** During `login` it checks that the authorization server actually refuses a request without `code_challenge`, a wrong `code_verifier` and a mismatched `redirect_uri`, that tokens are bound to the requested resource, and that refresh tokens rotate.
49
+ - **A reference server that proves the scanner.** The bundled fixture is a working MCP resource server and authorization server with switches that break it the ways real servers break. The test suite logs in to each broken profile and asserts the scanner flags exactly its defects.
50
+
51
+ It's pure Python standard library, so the only code you're trusting is in this repo.
52
+
53
+ ## Why these checks
54
+
55
+ The MCP authorization spec is a thin profile over OAuth 2.1, RFC 9728 (protected resource metadata), RFC 8414 (authorization server metadata) and RFC 8707 (resource indicators). You can read most of it off a 401. The failures that lead to account takeover only show up when you probe with real tokens:
56
+
57
+ - **Token passthrough.** A server that accepts any well-formed token and forwards it downstream looks identical to a correct one until you hand it a token minted for a different audience, or one without a signature.
58
+ - **Sessions as authentication.** A server that trusts `Mcp-Session-Id` after the first authenticated request turns every leaked session ID into a stolen account.
59
+
60
+ Those three checks (TOK-02, TOK-03 and SESS-01, marked `MUST*`) are critical. One failure makes the server **Unsafe** on its own, even if everything else passes. Getting the tokens to run them used to be the hard part; `login` does it for you.
61
+
62
+ ## Install
63
+
64
+ ```
65
+ pipx install git+https://github.com/tmtmiller1/mcp-authlab@v0.2.0
66
+ ```
67
+
68
+ or install the wheel from the [latest release](https://github.com/tmtmiller1/mcp-authlab/releases/latest), or `pip install -e .` from a checkout. Python 3.11 or newer is the whole requirement.
69
+
70
+ ## Use
71
+
72
+ ```
73
+ mcp-authlab scan https://mcp.example.com/mcp
74
+ ```
75
+
76
+ Without a login the scanner runs the 19 checks that need no credentials: the challenge, both metadata documents and the invalid-token probes. The first lines of the report say how much that covered and which critical checks it couldn't reach:
77
+
78
+ ```
79
+ coverage 19/33 · critical checks not run: TOK-03, SESS-01 (run `mcp-authlab login` to enable)
80
+ ```
81
+
82
+ ### Logging in
83
+
84
+ ```
85
+ mcp-authlab login https://mcp.example.com/mcp
86
+ ```
87
+
88
+ `login` finds the authorization server the same way a client does, registers itself by dynamic registration (RFC 7591) or uses `--client-id`, opens your browser for the authorization code flow with PKCE, and stores the tokens in `~/.config/mcp-authlab/tokens.json` (mode 0600, keyed by canonical resource). From then on `scan` uses them and refreshes them when they expire. `logout` forgets them.
89
+
90
+ While it has the authorization server's attention it does three more things:
91
+
92
+ - **Probes the authorization server** (the `ASF-*` checks): does it refuse a request without `code_challenge`, a wrong `code_verifier`, a mismatched `redirect_uri`? Are tokens bound to the resource you asked for? Do refresh tokens rotate? Were all URLs HTTPS and did the callback carry `iss`? You sign in twice, once per token. A strict server revokes an authorization code after the failed `code_verifier` probe, which costs one more sign-in. When that happens the `redirect_uri` probe is skipped and the report says so; `--thorough` runs it at the price of a fourth sign-in.
93
+ - **Mints a second token for a different resource** (the authorization server's own issuer URL by default, `--foreign-resource` to choose) so the scan can test audience binding. If the authorization server ignores `resource=` and hands back the same audience, the report says so.
94
+ - **Records the results** next to the tokens so the scan report carries them.
95
+
96
+ `--no-browser` prints the URL for you to open by hand. When the authorization server is on loopback, as the fixture's is, it fetches the URL itself. That's how the test suite and `scripts/demo.sh` run the whole flow hands-free.
97
+
98
+ You can also hand the scanner tokens directly with `--token` and `--foreign-token` (or `MCP_AUTHLAB_TOKEN` and `MCP_AUTHLAB_FOREIGN_TOKEN`). Explicit tokens win over stored ones, and `--no-login` ignores the store. Tokens only go to the server you name, and the scanner closes every session it opens with a `DELETE`.
99
+
100
+ ### Output and CI
101
+
102
+ `--format text` (default), `json` (schema in [docs/report.schema.json](docs/report.schema.json); add `--requests` for every HTTP exchange), `markdown`, or `sarif` for GitHub code scanning. `-o file` writes to a file. `--fail-under B` exits 2 when the grade is lower, and `--policy authlab.toml` lets a team write its bar down once (below). [examples/github-action.yml](examples/github-action.yml) shows a scheduled scan that gates on grade and uploads SARIF.
103
+
104
+ ## What it checks
105
+
106
+ 33 checks in ten groups. The full table with citations is in [docs/checks.md](docs/checks.md) and from `mcp-authlab checks`.
107
+
108
+ | Group | Covers |
109
+ |---|---|
110
+ | Transport | HTTPS on the MCP endpoint |
111
+ | Challenge | 401 on unauthenticated requests, `WWW-Authenticate: Bearer` with `resource_metadata`, `scope` hint |
112
+ | Protected Resource Metadata | published at the RFC 9728 well-known path, `resource` matches the canonical URI, authorization servers listed, challenge URL agrees, scopes are specific (wildcards and `offline_access` flagged), bearer tokens kept out of the query string |
113
+ | Authorization Server Metadata | discoverable via RFC 8414 or OpenID Connect at the right path for the issuer, required fields, issuer matches, HTTPS endpoints, PKCE S256, a client registration path, RFC 9207 `iss` |
114
+ | Authorization server flow (`login`) | PKCE required at the authorization endpoint and enforced at the token endpoint, `redirect_uri` checked, tokens bound to the requested resource, refresh token rotation, HTTPS and `iss` on the callback |
115
+ | Token validation | random string refused, `alg=none` refused, foreign-audience token refused, `error="invalid_token"` on 401, query-string token refused, valid token works |
116
+ | Scope step-up | a tool call the token can't make gets a 403 with `error="insufficient_scope"`, a `scope` hint and `resource_metadata` |
117
+ | Sessions | a session ID alone doesn't authenticate; IDs are long, random, distinct |
118
+ | Origin | hostile `Origin` refused |
119
+ | Protocol version | unsupported `MCP-Protocol-Version` gets 400 |
120
+
121
+ Checks cite the [2025-06-18 authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), the [security best practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) and [transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) pages, and the underlying RFCs. Items that only exist in the [draft spec](https://modelcontextprotocol.io/specification/draft/basic/authorization) (scope challenges, Client ID Metadata Documents, `iss`) are graded as SHOULD.
122
+
123
+ ### Grading
124
+
125
+ The headline is a conformance level read straight from the spec's language: **Unsafe** (a critical check failed), **Non-conformant** (a MUST failed), **Conformant with warnings** (only SHOULD items or soft MUST answers), or **Conformant**. A letter grade from a weighted score sits next to it for CI gates, and a MUST failure caps it at C. [docs/grading.md](docs/grading.md) has every weight and threshold and a worked example you can check by hand against the JSON report.
126
+
127
+ ### Policy files
128
+
129
+ Two MUST checks draw pushback from operators. ORIG-01 does because non-browser clients don't send `Origin`, and VER-01 because answering an old client with the newest version you support is friendlier than a 400. They stay MUST because the spec says MUST, and the catalog carries a `disputed` note explaining both sides. A policy file is where your organisation records its own call:
130
+
131
+ ```toml
132
+ fail_under = "B"
133
+ ignore = ["VER-01"] # still runs and prints, stops counting
134
+ downgrade = { "ORIG-01" = "SHOULD" } # counts as SHOULD and stops being critical
135
+ ```
136
+
137
+ Run it with `mcp-authlab scan --policy authlab.toml`. [examples/policy.toml](examples/policy.toml) explains each key.
138
+
139
+ ## The reference server
140
+
141
+ `mcp-authlab fixture` runs a small streamable HTTP MCP server plus a minimal authorization server, written against the spec, that grades A. The resource server serves protected resource metadata, HS256 tokens, sessions, Origin checks, a scope-gated tool and a plain one. The authorization server does dynamic registration, a consent page (`--auto-consent` approves on its own for tests), PKCE S256, resource indicators and rotating refresh tokens. Together they're about 600 lines, and they're a fair example of what a conformant pair has to do.
142
+
143
+ Profiles turn on defects:
144
+
145
+ | Profile | What's wrong | Level | Grade |
146
+ |---|---|---|---|
147
+ | `reference` | passes every check | Conformant with warnings (plain http on loopback) | A |
148
+ | `legacy` | resource metadata missing, bare 401, `invalid_token` error missing | Non-conformant | C |
149
+ | `weak-as` | authorization server metadata leaves out PKCE, registration, `iss` and scope hints; PKCE optional in practice; `resource=` ignored | Non-conformant | C |
150
+ | `passthrough` | accepts unsigned tokens and foreign audiences, ignores Origin | Unsafe | F |
151
+ | `sloppy` | sessions as auth, sequential session IDs, query-string tokens, wildcard scopes, redirect_uri unchecked, static refresh tokens, bare 403s | Unsafe | F |
152
+ | `open` | serves every request, token or not | Non-conformant | F |
153
+ | `mismatch` | metadata `resource` points elsewhere, 403 instead of 401, http:// authorization server | Non-conformant | F |
154
+
155
+ `--flag <defect>` turns on a single defect on top of any profile (`stateless` and `ignore_resource` are two useful ones). `scripts/demo.sh <profile>` starts one, logs in without a browser, scans with the tokens it minted, and stops it:
156
+
157
+ ```
158
+ scripts/demo.sh passthrough
159
+ ```
160
+
161
+ ## Field notes
162
+
163
+ Unauthenticated scans of public endpoints on 2026-10-19, so each line covers 19 of 33 checks and the critical checks `TOK-03` and `SESS-01` were skipped. `python3 registry/scan.py` reruns them and rewrites the [registry](registry/README.md) table.
164
+
165
+ - **Linear** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant, A. Every check that ran passed.
166
+ - **Notion** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant, A. Every check that ran passed.
167
+ - **Sentry** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant with warnings, A. The 401 doesn't say which scopes to request.
168
+ - **Asana** `coverage 19/33 · critical checks not run: TOK-03, SESS-01`: Conformant with warnings, A. The 401 doesn't say which scopes to request, and the authorization server doesn't advertise `iss`. Metadata binds tokens to the host rather than the `/sse` path, which RFC 9728 allows.
169
+ - **GitHub** `coverage 18/33 · critical checks not run: TOK-03, SESS-01`: Non-conformant, C. A malformed token gets 400 rather than the 401 the spec requires, so the `invalid_token` check couldn't run either. The challenge's `resource_metadata` URL returns a different document than the well-known path, and clients have to be registered by hand.
170
+
171
+ All five look clean from outside. The critical findings need a login to show up.
172
+
173
+ ## How it compares
174
+
175
+ Checked on 2026-10-19 against each project's README, and against the source for the official suite and mcp-authcheck.
176
+
177
+ | | scans a live server | runs the OAuth flow | probes token validation | CI gate | zero dependencies |
178
+ |---|---|---|---|---|---|
179
+ | **mcp-authlab** | yes | yes (`login`) | random, unsigned, foreign-audience and query-string tokens; session replay; scope step-up | `--fail-under`, `--policy`, SARIF | yes (stdlib) |
180
+ | [MCP conformance suite](https://github.com/modelcontextprotocol/conformance) (official) | yes (`server --url`) | for authorization servers: the happy-path code grant and DPoP | its server scenarios don't send a bearer token; it does test DNS rebinding | yes, GitHub Action with expected-failure baselines | no (Node) |
181
+ | [mcp-authcheck](https://github.com/tstanmay13/mcp-authcheck) | yes | no; it only sends credentials you give it | one bogus token; wrong-audience and scope checks are listed but not built yet | `--min-grade`, GitHub Action | yes (Node, no runtime packages) |
182
+ | [mcp-auth-doctor](https://github.com/Jah-yee/mcp-auth-doctor) | yes | opt-in `--login` with PKCE, `state` and `iss` checks | uses its own real token once on `tools/list` | exit codes 0 to 3 | one package (httpx) |
183
+ | [MCPJam OAuth conformance](https://docs.mcpjam.com/cli/oauth-conformance) | yes | interactive, headless or client credentials, with `redirect_uri` rejection checks | one invalid bearer token; an audience problem only shows up as a 401 on the real token | JUnit and JSON reporters | not stated (part of the MCPJam Inspector CLI) |
184
+ | [AuthPlane/conformance](https://github.com/AuthPlane/conformance) | a catalog of cases that SDKs run in-process against their own verifier | no | its example rejects a bad JWT header, against the SDK's verifier | through each SDK's report | n/a (catalog, no runner) |
185
+ | [mcp-auth-audit](https://github.com/sudo-ai-git/mcp-auth-audit) | reads a server's declared auth schema from supplied metadata ("No LLM, no network") | no | no | PASS/FAIL; exit codes undocumented | not stated (Python) |
186
+ | [Snyk Agent Scan](https://github.com/invariantlabs-ai/mcp-scan) (was invariantlabs mcp-scan) | connects to configured servers to read tool descriptions for prompt injection | uses OAuth tokens you already have | no | `--ci` | no (needs a Snyk API token) |
187
+ | [mcp-guardian](https://github.com/eqtylab/mcp-guardian) (policy proxy) | sits between client and server to log and approve tool calls | no | no | no | no (Rust) |
188
+
189
+ If you only need the discovery checks, mcp-authcheck and mcp-auth-doctor do them with less setup. MCPJam and the official suite are the better fit for testing a client or an SDK across the whole protocol. mcp-authlab is for the question they leave open: does this server refuse the tokens it should?
190
+
191
+ ## Limits
192
+
193
+ A scan sees what a client sees: the challenge, the metadata documents, and how the resource server answers tokens it should refuse. `login` adds the authorization server's behaviour for one public client. So the report covers both halves of the spec, but it can't tell you how a token was validated internally or what a confidential client would see. When tokens are opaque, the audience verdict rests on how the resource server treated the two tokens, and the report says so.
194
+
195
+ ## Roadmap
196
+
197
+ - Client ID Metadata Document checks: fetch the document, validate `redirect_uris`.
198
+ - Enterprise-managed authorization (ID-JAG) metadata checks from [ext-auth](https://github.com/modelcontextprotocol/ext-auth).
199
+ - Legacy HTTP+SSE transport.
200
+ - Token introspection (RFC 7662) for opaque-token audience checks when the authorization server offers it.
201
+
202
+ ## Development
203
+
204
+ ```
205
+ python3 -m unittest discover -s tests -v
206
+ ruff check src tests && ruff format --check src tests && mypy src
207
+ ```
208
+
209
+ The tests start each fixture profile on a random port, log in to it with `--no-browser`, scan it in-process, and assert the flagged set equals the profile's declared defects. The suite runs in about six seconds. Adding a check means adding it to `catalog.CATALOG`, implementing it in `scanner.Scanner` (or `oauthclient.Login` for the authorization server), breaking it in a fixture profile, declaring that in `EXPECTED_FAILURES`, and regenerating `docs/checks.md` with `mcp-authlab checks --markdown`. A test fails when that file is stale. [CHANGELOG.md](CHANGELOG.md) lists what changed in each release.
210
+
211
+ ## Contributing and security
212
+
213
+ [CONTRIBUTING.md](CONTRIBUTING.md) covers running the tests and adding a check. To report a vulnerability in mcp-authlab itself, see [SECURITY.md](SECURITY.md). To have a server taken off the registry or a finding corrected, [open a registry issue](https://github.com/tmtmiller1/mcp-authlab/issues/new?template=registry.yml).
214
+
215
+ ## License
216
+
217
+ MIT.
@@ -0,0 +1,26 @@
1
+ # Security
2
+
3
+ ## Reporting a vulnerability in mcp-authlab
4
+
5
+ Use GitHub's private vulnerability reporting: the **Report a vulnerability** button under this repository's [Security tab](https://github.com/tmtmiller1/mcp-authlab/security/advisories/new). Please report it there rather than in a public issue.
6
+
7
+ Things that count as a vulnerability here:
8
+
9
+ - a token, authorization code or session ID ending up somewhere it shouldn't: a report, the request log, an error message, or a host other than the one you scanned
10
+ - the token store at `~/.config/mcp-authlab/tokens.json` readable by other users
11
+ - a check that reports a pass for a server that breaks the rule it tests, in a way an attacker could arrange
12
+
13
+ A wrong grade that isn't exploitable is an ordinary bug; open an issue for it.
14
+
15
+ ## What the tool sends
16
+
17
+ - `scan` sends a few dozen requests to the server you name and to the metadata and authorization server URLs that server advertises. It only fetches `http` and `https` URLs, and it never follows redirects.
18
+ - Bearer tokens only go to the MCP endpoint you name. The deliberately bad tokens it sends are random strings and an unsigned JWT that it makes up on the spot.
19
+ - Every session the scan opens, it closes with a `DELETE`. The only tool it calls is a read-only one (or the one you pick with `--tool`).
20
+ - `login` registers one client with the authorization server and signs in as you. Two of its probes deliberately send a wrong `code_verifier` and a wrong `redirect_uri` to the token endpoint.
21
+
22
+ Logging in and probing with tokens is a security test, so only do that against servers you run or have permission to test. The public [registry](registry/README.md) sticks to the unauthenticated checks, which send what any client sends while connecting, plus two invalid tokens.
23
+
24
+ ## Reporting a problem with a server in the registry
25
+
26
+ If a registry entry is about your server and you want it removed or a finding corrected, [open a registry issue](https://github.com/tmtmiller1/mcp-authlab/issues/new?template=registry.yml). If you've found a vulnerability in someone else's MCP server, report it to that vendor, not here.