mikser-io-auth 0.5.0 → 0.5.2
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.
- package/index.js +17 -1
- package/lib/grants.js +15 -1
- package/lib/routes.js +28 -15
- package/package.json +1 -1
- package/test/authorize.test.js +28 -0
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { basic, jwt } from './lib/verifiers.js'
|
|
|
8
8
|
import { redirectUriAllowed, validateRedirectUri, registerDynamicClient } from './lib/clients.js'
|
|
9
9
|
import { challengeFromVerifier, verifyPkce, opaqueToken } from './lib/pkce.js'
|
|
10
10
|
import { loginPage } from './lib/login-page.js'
|
|
11
|
-
import { mountRoutes } from './lib/routes.js'
|
|
11
|
+
import { mountRoutes, metadataHandler } from './lib/routes.js'
|
|
12
12
|
import * as grants from './lib/grants.js'
|
|
13
13
|
|
|
14
14
|
export { parseHtpasswd, parseHtgroup, verifyPassword, createIdentityStore }
|
|
@@ -152,6 +152,22 @@ export function auth(options = {}) {
|
|
|
152
152
|
|
|
153
153
|
app.use(base, router)
|
|
154
154
|
|
|
155
|
+
// RFC 8414 §3.1: an issuer with no path component publishes its
|
|
156
|
+
// metadata at <origin>/.well-known/oauth-authorization-server.
|
|
157
|
+
// `issuerFor` is the origin, so that is where a conforming client
|
|
158
|
+
// looks — including every MCP client, which is sent here by the
|
|
159
|
+
// resource's RFC 9728 document naming this issuer. Mounted under
|
|
160
|
+
// `base` alone, the document exists but at an address nothing
|
|
161
|
+
// following the spec will ask for, and dynamic client
|
|
162
|
+
// registration fails with the metadata sitting right there.
|
|
163
|
+
//
|
|
164
|
+
// The copy under `base` stays: clients that append to the issuer
|
|
165
|
+
// rather than insert the well-known segment find it there.
|
|
166
|
+
if (base && base !== '/') {
|
|
167
|
+
app.get('/.well-known/oauth-authorization-server',
|
|
168
|
+
metadataHandler({ base, issuerFor: originOf }))
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
// Codes are 60s and refresh tokens 30d; without a sweep the rows
|
|
156
172
|
// accumulate for the life of the database. Once at boot is enough
|
|
157
173
|
// for a build tool — the checks that matter (expiry, single use)
|
package/lib/grants.js
CHANGED
|
@@ -49,7 +49,21 @@ registerSchema('auth', `
|
|
|
49
49
|
created_at INTEGER NOT NULL,
|
|
50
50
|
last_used_at INTEGER
|
|
51
51
|
);
|
|
52
|
-
|
|
52
|
+
`, {
|
|
53
|
+
// Durable: these tables are not derived from anything on disk.
|
|
54
|
+
//
|
|
55
|
+
// The engine wipes its database whenever the schema version or the
|
|
56
|
+
// config checksum changes — an upgrade, or any deploy that edits
|
|
57
|
+
// mikser.config.js. That is correct for a cache the files can rebuild,
|
|
58
|
+
// and wrong here: a registered OAuth client and its refresh token exist
|
|
59
|
+
// only because a human completed a sign-in once. Losing them logs every
|
|
60
|
+
// connected agent out, and the operator's first sign of it is being
|
|
61
|
+
// asked to authorize again after an unrelated deploy.
|
|
62
|
+
//
|
|
63
|
+
// Codes are 60s and swept anyway; they ride along because they share the
|
|
64
|
+
// schema, and a stale one is rejected on expiry rather than trusted.
|
|
65
|
+
durable: true,
|
|
66
|
+
})
|
|
53
67
|
|
|
54
68
|
const db = () => useDatabase().handle
|
|
55
69
|
|
package/lib/routes.js
CHANGED
|
@@ -19,6 +19,33 @@ function authParams(src) {
|
|
|
19
19
|
return { response_type, client_id, redirect_uri, code_challenge, code_challenge_method, scope, state }
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// The authorization server metadata document.
|
|
23
|
+
//
|
|
24
|
+
// Exported because WHERE it is served is part of the contract, and one mount
|
|
25
|
+
// cannot satisfy it. `issuerFor` returns the origin, and RFC 8414 §3.1 says
|
|
26
|
+
// an issuer with no path component publishes at
|
|
27
|
+
// <origin>/.well-known/oauth-authorization-server. This router is mounted
|
|
28
|
+
// under `base`, so its own copy answers one level down — where a client that
|
|
29
|
+
// appends to the issuer looks, and nowhere a client following RFC 8414 does.
|
|
30
|
+
// index.js mounts this at the root as well; both paths return this document.
|
|
31
|
+
export function metadataHandler({ base, issuerFor }) {
|
|
32
|
+
return (req, res) => {
|
|
33
|
+
const issuer = issuerFor(req)
|
|
34
|
+
res.json({
|
|
35
|
+
issuer,
|
|
36
|
+
authorization_endpoint: `${issuer}${base}/authorize`,
|
|
37
|
+
token_endpoint: `${issuer}${base}/token`,
|
|
38
|
+
jwks_uri: `${issuer}${base}/jwks.json`,
|
|
39
|
+
response_types_supported: ['code'],
|
|
40
|
+
grant_types_supported: ['authorization_code', 'refresh_token', 'password'],
|
|
41
|
+
code_challenge_methods_supported: ['S256'],
|
|
42
|
+
token_endpoint_auth_methods_supported: ['none'],
|
|
43
|
+
id_token_signing_alg_values_supported: [ALG],
|
|
44
|
+
registration_endpoint: `${issuer}${base}/register`,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
export function mountRoutes(router, ctx) {
|
|
23
50
|
const { base, nameOf, ready, logoUrl, ttl, issuerFor, audienceFor, dcr, logger } = ctx
|
|
24
51
|
|
|
@@ -41,21 +68,7 @@ export function mountRoutes(router, ctx) {
|
|
|
41
68
|
res.json(jwks({ publicJwk: ready().key.publicJwk }))
|
|
42
69
|
})
|
|
43
70
|
|
|
44
|
-
router.get('/.well-known/oauth-authorization-server', (
|
|
45
|
-
const issuer = issuerFor(req)
|
|
46
|
-
res.json({
|
|
47
|
-
issuer,
|
|
48
|
-
authorization_endpoint: `${issuer}${base}/authorize`,
|
|
49
|
-
token_endpoint: `${issuer}${base}/token`,
|
|
50
|
-
jwks_uri: `${issuer}${base}/jwks.json`,
|
|
51
|
-
response_types_supported: ['code'],
|
|
52
|
-
grant_types_supported: ['authorization_code', 'refresh_token', 'password'],
|
|
53
|
-
code_challenge_methods_supported: ['S256'],
|
|
54
|
-
token_endpoint_auth_methods_supported: ['none'],
|
|
55
|
-
id_token_signing_alg_values_supported: [ALG],
|
|
56
|
-
registration_endpoint: `${issuer}${base}/register`,
|
|
57
|
-
})
|
|
58
|
-
})
|
|
71
|
+
router.get('/.well-known/oauth-authorization-server', metadataHandler({ base, issuerFor }))
|
|
59
72
|
|
|
60
73
|
// ── /authorize ───────────────────────────────────────────────────────
|
|
61
74
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io-auth",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Authentication for mikser-io: an OAuth 2.1 authorization server (self-registering clients, authorization code + PKCE, refresh rotation) and HTTP Basic / JWT verifiers over Apache-format htpasswd and htgroup files in the working folder. Implements the ADR-0012 verifier contract, so it plugs in wherever a static token does — api, mcp, forms.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
package/test/authorize.test.js
CHANGED
|
@@ -340,6 +340,34 @@ describe('discovery', () => {
|
|
|
340
340
|
assert.ok(doc.grant_types_supported.includes('authorization_code'))
|
|
341
341
|
})
|
|
342
342
|
|
|
343
|
+
// RFC 8414 §3.1: the metadata for an issuer with no path component lives
|
|
344
|
+
// at <origin>/.well-known/oauth-authorization-server. The document names
|
|
345
|
+
// the origin as its issuer, so serving it only under /auth makes it
|
|
346
|
+
// unreachable at the one address a conforming client derives — and every
|
|
347
|
+
// MCP client is a conforming client, sent here by the resource's RFC 9728
|
|
348
|
+
// document. Dynamic registration then fails with the metadata present.
|
|
349
|
+
it('serves the metadata at the origin, where the issuer it declares says it is', async () => {
|
|
350
|
+
const res = await fetch(url('/.well-known/oauth-authorization-server'))
|
|
351
|
+
assert.equal(res.status, 200)
|
|
352
|
+
const doc = await res.json()
|
|
353
|
+
assert.match(doc.registration_endpoint, /\/auth\/register$/)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('is reachable by deriving the URL from the issuer it declares', async () => {
|
|
357
|
+
const doc = await (await fetch(url('/auth/.well-known/oauth-authorization-server'))).json()
|
|
358
|
+
// What a client actually does: take `issuer`, append the well-known
|
|
359
|
+
// path, fetch. If that 404s, discovery is broken however correct the
|
|
360
|
+
// document's contents are.
|
|
361
|
+
const derived = await fetch(`${doc.issuer}/.well-known/oauth-authorization-server`)
|
|
362
|
+
assert.equal(derived.status, 200)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('serves the same document at both mounts', async () => {
|
|
366
|
+
const atBase = await (await fetch(url('/auth/.well-known/oauth-authorization-server'))).json()
|
|
367
|
+
const atRoot = await (await fetch(url('/.well-known/oauth-authorization-server'))).json()
|
|
368
|
+
assert.deepEqual(atRoot, atBase)
|
|
369
|
+
})
|
|
370
|
+
|
|
343
371
|
it('publishes a JWKS with no private component', async () => {
|
|
344
372
|
const doc = await (await fetch(url('/auth/jwks.json'))).json()
|
|
345
373
|
assert.equal(doc.keys.length, 1)
|