@boxxy/auth-oauth 0.2.6 → 0.2.7
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/README.md +12 -13
- package/dist/auth/create-auth-admin.d.ts +25 -12
- package/dist/auth/create-security-event-log.d.ts +1 -1
- package/dist/auth/format-security-event.d.ts +10 -2
- package/dist/auth/open-auth-database.d.ts +2 -2
- package/dist/http/create-public-door.d.ts +5 -1
- package/dist/http/{answer-mcp-request.d.ts → types.d.ts} +0 -2
- package/dist/index.d.ts +2 -5
- package/dist/migrations/grant-rows-migration.d.ts +2 -0
- package/dist/migrations/run-auth-migrations.d.ts +2 -0
- package/dist/test-utils/authorize-test-client.d.ts +1 -13
- package/dist/test-utils/pre-package-schema.d.ts +1 -0
- package/dist/test-utils/seed-pre-package-auth.d.ts +21 -0
- package/package.json +7 -13
- package/src/auth/create-auth-admin.ts +109 -41
- package/src/auth/create-security-event-log.ts +1 -3
- package/src/auth/format-security-event.ts +12 -11
- package/src/auth/normalize-client-name.ts +0 -2
- package/src/auth/open-auth-database.ts +8 -13
- package/src/http/create-public-door.ts +113 -29
- package/src/http/is-loopback-host.ts +0 -2
- package/src/http/normalize-public-url.ts +0 -2
- package/src/http/types.ts +11 -0
- package/src/index.ts +9 -5
- package/src/migrations/grant-rows-migration.ts +108 -0
- package/src/migrations/run-auth-migrations.ts +53 -0
- package/src/test-utils/authorize-test-client.ts +2 -194
- package/src/test-utils/build-stub-clock.ts +0 -2
- package/src/test-utils/pre-package-schema.ts +36 -0
- package/src/test-utils/register-test-cleanup.ts +0 -2
- package/src/test-utils/seed-pre-package-auth.ts +162 -0
- package/src/test-utils/setup-temp-dir.ts +0 -2
- package/src/utils/types.ts +0 -2
- package/dist/auth/answer-authorize-request.d.ts +0 -2
- package/dist/auth/answer-consent-request.d.ts +0 -10
- package/dist/auth/answer-enroll-request.d.ts +0 -9
- package/dist/auth/answer-login-request.d.ts +0 -9
- package/dist/auth/approval-window-seconds.d.ts +0 -1
- package/dist/auth/build-consent-binding.d.ts +0 -1
- package/dist/auth/create-approval-state.d.ts +0 -41
- package/dist/auth/create-enrollment-state.d.ts +0 -6
- package/dist/auth/create-enrollment-token.d.ts +0 -6
- package/dist/auth/create-owner-plugin.d.ts +0 -38
- package/dist/auth/create-passkey.d.ts +0 -11
- package/dist/auth/derive-token-hash.d.ts +0 -1
- package/dist/auth/is-allowed-redirect-uri.d.ts +0 -1
- package/dist/auth/load-web-authn.d.ts +0 -6
- package/dist/auth/mcp-auth-database.d.ts +0 -5
- package/dist/auth/mcp-auth-schema.d.ts +0 -54
- package/dist/auth/open-mcp-auth.d.ts +0 -80
- package/dist/auth/passkey-script.d.ts +0 -4
- package/dist/auth/read-client-name.d.ts +0 -3
- package/dist/auth/read-client-views.d.ts +0 -9
- package/dist/auth/read-enrollment-token.d.ts +0 -3
- package/dist/auth/read-grant-views.d.ts +0 -12
- package/dist/auth/read-owner-session-id.d.ts +0 -2
- package/dist/auth/record-refused-exchange.d.ts +0 -10
- package/dist/auth/remove-client.d.ts +0 -3
- package/dist/auth/remove-owner-session.d.ts +0 -3
- package/dist/auth/render-consent-page.d.ts +0 -9
- package/dist/auth/render-enroll-page.d.ts +0 -10
- package/dist/auth/render-login-page.d.ts +0 -9
- package/dist/auth/revoke-grant.d.ts +0 -3
- package/dist/auth/to-html-text.d.ts +0 -1
- package/dist/auth/to-script-json.d.ts +0 -1
- package/dist/auth/types.d.ts +0 -79
- package/dist/auth/update-passkey-use.d.ts +0 -10
- package/dist/auth/verify-oauth-query.d.ts +0 -1
- package/dist/auth/verify-passkey-assertion.d.ts +0 -11
- package/dist/auth/verify-passkey-registration.d.ts +0 -14
- package/dist/auth/verify-session-passkey.d.ts +0 -8
- package/dist/auth/write-grant-use.d.ts +0 -3
- package/dist/http/build-page-response.d.ts +0 -5
- package/dist/http/create-mcp-http-app.d.ts +0 -92
- package/dist/http/pick-error-message.d.ts +0 -1
- package/dist/http/public-door-paths.d.ts +0 -17
- package/dist/http/read-token-access.d.ts +0 -8
- package/dist/http/verify-access-token.d.ts +0 -2
- package/dist/test-utils/build-mock-new-passkey.d.ts +0 -2
- package/dist/test-utils/build-passkey-seeder.d.ts +0 -5
- package/dist/test-utils/build-seeder.d.ts +0 -5
- package/dist/test-utils/build-stub-authenticator.d.ts +0 -48
- package/dist/test-utils/load-web-authn-early.d.ts +0 -1
- package/dist/test-utils/seed-passkey.d.ts +0 -4
- package/dist/test-utils/start-stub-telemetry-collector.d.ts +0 -5
- package/dist/test-utils/types.d.ts +0 -7
- package/dist/utils/is-record.d.ts +0 -1
- package/dist/utils/parse-json-strings.d.ts +0 -1
- package/src/auth/answer-authorize-request.ts +0 -137
- package/src/auth/answer-consent-request.ts +0 -189
- package/src/auth/answer-enroll-request.ts +0 -190
- package/src/auth/answer-login-request.ts +0 -256
- package/src/auth/approval-window-seconds.ts +0 -5
- package/src/auth/build-consent-binding.ts +0 -13
- package/src/auth/create-approval-state.ts +0 -221
- package/src/auth/create-enrollment-state.ts +0 -56
- package/src/auth/create-enrollment-token.ts +0 -28
- package/src/auth/create-owner-plugin.ts +0 -113
- package/src/auth/create-passkey.ts +0 -48
- package/src/auth/derive-token-hash.ts +0 -7
- package/src/auth/is-allowed-redirect-uri.ts +0 -19
- package/src/auth/load-web-authn.ts +0 -19
- package/src/auth/mcp-auth-database.ts +0 -10
- package/src/auth/mcp-auth-schema.ts +0 -56
- package/src/auth/open-mcp-auth.ts +0 -138
- package/src/auth/passkey-script.ts +0 -50
- package/src/auth/read-client-name.ts +0 -17
- package/src/auth/read-client-views.ts +0 -27
- package/src/auth/read-enrollment-token.ts +0 -20
- package/src/auth/read-grant-views.ts +0 -53
- package/src/auth/read-owner-session-id.ts +0 -16
- package/src/auth/record-refused-exchange.ts +0 -74
- package/src/auth/remove-client.ts +0 -33
- package/src/auth/remove-owner-session.ts +0 -11
- package/src/auth/render-consent-page.ts +0 -61
- package/src/auth/render-enroll-page.ts +0 -59
- package/src/auth/render-login-page.ts +0 -58
- package/src/auth/revoke-grant.ts +0 -25
- package/src/auth/to-html-text.ts +0 -10
- package/src/auth/to-script-json.ts +0 -8
- package/src/auth/types.ts +0 -115
- package/src/auth/update-passkey-use.ts +0 -31
- package/src/auth/verify-oauth-query.ts +0 -41
- package/src/auth/verify-passkey-assertion.ts +0 -124
- package/src/auth/verify-passkey-registration.ts +0 -118
- package/src/auth/verify-session-passkey.ts +0 -47
- package/src/auth/write-grant-use.ts +0 -18
- package/src/http/answer-mcp-request.ts +0 -77
- package/src/http/build-page-response.ts +0 -26
- package/src/http/create-mcp-http-app.ts +0 -340
- package/src/http/pick-error-message.ts +0 -21
- package/src/http/public-door-paths.ts +0 -21
- package/src/http/read-token-access.ts +0 -44
- package/src/http/verify-access-token.ts +0 -46
- package/src/test-utils/build-mock-new-passkey.ts +0 -15
- package/src/test-utils/build-passkey-seeder.ts +0 -11
- package/src/test-utils/build-seeder.ts +0 -10
- package/src/test-utils/build-stub-authenticator.ts +0 -244
- package/src/test-utils/load-web-authn-early.ts +0 -8
- package/src/test-utils/seed-passkey.ts +0 -32
- package/src/test-utils/start-stub-telemetry-collector.ts +0 -27
- package/src/test-utils/types.ts +0 -11
- package/src/utils/is-record.ts +0 -5
- package/src/utils/parse-json-strings.ts +0 -15
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/to-script-json.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
export function toScriptJSON(value: unknown): string {
|
|
4
|
-
return JSON.stringify(value)
|
|
5
|
-
.replaceAll('<', String.raw`\u003c`)
|
|
6
|
-
.replaceAll('>', String.raw`\u003e`)
|
|
7
|
-
.replaceAll('&', String.raw`\u0026`);
|
|
8
|
-
}
|
package/src/auth/types.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/types.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import type { GrantScope } from '@boxxy/contract';
|
|
4
|
-
import type { AuthClock } from '../utils/types.ts';
|
|
5
|
-
import type { ApprovalState } from './create-approval-state.ts';
|
|
6
|
-
import type { EnrollmentState } from './create-enrollment-state.ts';
|
|
7
|
-
import type { openMCPAuth } from './open-mcp-auth.ts';
|
|
8
|
-
|
|
9
|
-
export interface Requester {
|
|
10
|
-
readonly socketAddress: string | null;
|
|
11
|
-
|
|
12
|
-
// the `CF-Connecting-IP` header. Anyone can send it, so it is `trusted`
|
|
13
|
-
// only when the socket's peer is a proxy the owner listed.
|
|
14
|
-
readonly connectingIP: {
|
|
15
|
-
readonly address: string;
|
|
16
|
-
readonly provenance: 'trusted' | 'unverified';
|
|
17
|
-
} | null;
|
|
18
|
-
readonly userAgent: string | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type SecurityOutcome =
|
|
22
|
-
| { readonly kind: 'authorize_started'; readonly outcome: 'accepted' }
|
|
23
|
-
| {
|
|
24
|
-
readonly kind: 'authorize_started';
|
|
25
|
-
readonly outcome: 'rejected';
|
|
26
|
-
readonly reason: 'rate_limited';
|
|
27
|
-
}
|
|
28
|
-
| {
|
|
29
|
-
readonly kind: 'authorize_invalid';
|
|
30
|
-
readonly outcome: 'rejected';
|
|
31
|
-
readonly reason: AuthorizeInvalidReason;
|
|
32
|
-
}
|
|
33
|
-
| { readonly kind: 'owner_auth'; readonly outcome: 'succeeded' }
|
|
34
|
-
| {
|
|
35
|
-
readonly kind: 'owner_auth';
|
|
36
|
-
readonly outcome: 'failed';
|
|
37
|
-
readonly reason?: OwnerAuthFailure;
|
|
38
|
-
}
|
|
39
|
-
| { readonly kind: 'consent'; readonly outcome: 'allowed' }
|
|
40
|
-
| { readonly kind: 'consent'; readonly outcome: 'denied'; readonly reason?: 'none_ticked' }
|
|
41
|
-
| {
|
|
42
|
-
readonly kind: 'token_exchange';
|
|
43
|
-
readonly outcome: 'rejected';
|
|
44
|
-
readonly reason: TokenExchangeReason;
|
|
45
|
-
}
|
|
46
|
-
| { readonly kind: 'bearer'; readonly outcome: 'rejected'; readonly reason: BearerReason };
|
|
47
|
-
|
|
48
|
-
export type OwnerAuthFailure =
|
|
49
|
-
| 'assertion_failed'
|
|
50
|
-
| 'locked'
|
|
51
|
-
| 'expired'
|
|
52
|
-
| 'not_enrolled'
|
|
53
|
-
| 'invalid_token';
|
|
54
|
-
|
|
55
|
-
export type AuthorizeInvalidReason =
|
|
56
|
-
| 'invalid_client'
|
|
57
|
-
| 'client_disabled'
|
|
58
|
-
| 'invalid_redirect'
|
|
59
|
-
| 'unauthorized_client'
|
|
60
|
-
| 'unsupported_response_type'
|
|
61
|
-
| 'invalid_request'
|
|
62
|
-
| 'invalid_scope'
|
|
63
|
-
| 'invalid_target'
|
|
64
|
-
| 'bad_host'
|
|
65
|
-
| 'bad_origin'
|
|
66
|
-
| 'other';
|
|
67
|
-
|
|
68
|
-
export type TokenExchangeReason =
|
|
69
|
-
| 'invalid_request'
|
|
70
|
-
| 'invalid_client'
|
|
71
|
-
| 'invalid_grant'
|
|
72
|
-
| 'unauthorized_client'
|
|
73
|
-
| 'unsupported_grant_type'
|
|
74
|
-
| 'invalid_scope'
|
|
75
|
-
| 'invalid_target'
|
|
76
|
-
| 'other';
|
|
77
|
-
|
|
78
|
-
type BearerReason = 'malformed' | 'invalid_token';
|
|
79
|
-
|
|
80
|
-
export type SecurityEvent = SecurityOutcome & {
|
|
81
|
-
readonly path: string;
|
|
82
|
-
readonly requester: Requester;
|
|
83
|
-
readonly client?: { readonly id: string; readonly name: string };
|
|
84
|
-
readonly redirectHost?: string;
|
|
85
|
-
readonly correlationID?: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// What a valid bearer token grants: the client it was issued to, and the
|
|
89
|
-
// scopes it holds.
|
|
90
|
-
export interface VerifiedAccess {
|
|
91
|
-
readonly clientID: string;
|
|
92
|
-
readonly clientName: string;
|
|
93
|
-
readonly scopes: readonly GrantScope[];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
type MCPAuth = Awaited<ReturnType<typeof openMCPAuth>>;
|
|
97
|
-
|
|
98
|
-
// What every HTTP route reads: the authorization server, the approvals waiting for the owner, and
|
|
99
|
-
// the passkey enrollments in progress.
|
|
100
|
-
export interface HTTPServerContext {
|
|
101
|
-
// The public origin: the OAuth issuer.
|
|
102
|
-
readonly origin: string;
|
|
103
|
-
|
|
104
|
-
// `<origin>/mcp`: the one resource every token is bound to.
|
|
105
|
-
readonly resource: string;
|
|
106
|
-
readonly store: MCPAuth;
|
|
107
|
-
readonly approvals: ApprovalState;
|
|
108
|
-
readonly enrollments: EnrollmentState;
|
|
109
|
-
|
|
110
|
-
// Receives each sign-in outcome and each refused token as a security event.
|
|
111
|
-
readonly security: { readonly record: (event: SecurityEvent) => void };
|
|
112
|
-
|
|
113
|
-
// The time the routes stamp and check expiries against.
|
|
114
|
-
readonly clock: AuthClock;
|
|
115
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/update-passkey-use.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import type { Kysely } from 'kysely';
|
|
4
|
-
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
5
|
-
|
|
6
|
-
interface PasskeyUse {
|
|
7
|
-
readonly id: string;
|
|
8
|
-
|
|
9
|
-
// The counter the verification read, which the passkey must still hold,
|
|
10
|
-
// so a passkey removed during the verification, or a concurrent assertion
|
|
11
|
-
// that stored a newer counter, makes this use fail.
|
|
12
|
-
readonly counter: number;
|
|
13
|
-
readonly newCounter: number;
|
|
14
|
-
|
|
15
|
-
// Epoch milliseconds, stamped as the passkey's last use.
|
|
16
|
-
readonly now: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function updatePasskeyUse(
|
|
20
|
-
db: Kysely<MCPAuthSchema>,
|
|
21
|
-
use: PasskeyUse,
|
|
22
|
-
): Promise<boolean> {
|
|
23
|
-
const stored = await db
|
|
24
|
-
.updateTable('boxxy_passkey')
|
|
25
|
-
.set({ counter: use.newCounter, last_used_at: new Date(use.now).toISOString() })
|
|
26
|
-
.where('id', '=', use.id)
|
|
27
|
-
.where('counter', '=', use.counter)
|
|
28
|
-
.executeTakeFirst();
|
|
29
|
-
|
|
30
|
-
return stored.numUpdatedRows > 0n;
|
|
31
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/verify-oauth-query.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import { constantTimeEqual, makeSignature } from 'better-auth/crypto';
|
|
4
|
-
|
|
5
|
-
export async function verifyOAuthQuery(
|
|
6
|
-
oauthQuery: string,
|
|
7
|
-
secret: string,
|
|
8
|
-
now: number,
|
|
9
|
-
): Promise<boolean> {
|
|
10
|
-
const params = new URLSearchParams(oauthQuery);
|
|
11
|
-
|
|
12
|
-
const signatures = params.getAll('sig');
|
|
13
|
-
const [signature] = signatures;
|
|
14
|
-
const expiresAt = Number(params.get('exp')) * 1000;
|
|
15
|
-
|
|
16
|
-
if (signatures.length !== 1 || signature === undefined || signature === '') {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
params.delete('sig');
|
|
21
|
-
|
|
22
|
-
// Ordered by name, then by value, comparing code units as better-auth does
|
|
23
|
-
// when it signs; the NUL separator sorts a name before any longer name it
|
|
24
|
-
// prefixes.
|
|
25
|
-
const canonical = new URLSearchParams(
|
|
26
|
-
[...params].toSorted(([keyA, valueA], [keyB, valueB]) => {
|
|
27
|
-
const pairA = `${keyA}\u0000${valueA}`;
|
|
28
|
-
const pairB = `${keyB}\u0000${valueB}`;
|
|
29
|
-
|
|
30
|
-
if (pairA === pairB) {
|
|
31
|
-
return 0;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return pairA < pairB ? -1 : 1;
|
|
35
|
-
}),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const expected = await makeSignature(canonical.toString(), secret);
|
|
39
|
-
|
|
40
|
-
return constantTimeEqual(signature, expected) && Number.isFinite(expiresAt) && expiresAt >= now;
|
|
41
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/verify-passkey-assertion.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
AuthenticationResponseJSON,
|
|
5
|
-
VerifiedAuthenticationResponse,
|
|
6
|
-
} from '@simplewebauthn/server';
|
|
7
|
-
import type { Kysely } from 'kysely';
|
|
8
|
-
import { z } from 'zod';
|
|
9
|
-
import { parseJSONStrings } from '../utils/parse-json-strings.ts';
|
|
10
|
-
import { loadWebAuthn } from './load-web-authn.ts';
|
|
11
|
-
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
12
|
-
import { updatePasskeyUse } from './update-passkey-use.ts';
|
|
13
|
-
|
|
14
|
-
interface AssertionCheck {
|
|
15
|
-
// The credential JSON the sign-in page posted.
|
|
16
|
-
readonly credential: string;
|
|
17
|
-
|
|
18
|
-
// The challenge the page was given, which the caller has already taken.
|
|
19
|
-
readonly challenge: string;
|
|
20
|
-
readonly origin: string;
|
|
21
|
-
readonly rpID: string;
|
|
22
|
-
|
|
23
|
-
// Epoch milliseconds, stamped as the passkey's last use.
|
|
24
|
-
readonly now: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function verifyPasskeyAssertion(
|
|
28
|
-
db: Kysely<MCPAuthSchema>,
|
|
29
|
-
check: AssertionCheck,
|
|
30
|
-
): Promise<string | null> {
|
|
31
|
-
const response = parseAssertion(check.credential);
|
|
32
|
-
|
|
33
|
-
if (response === null) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const passkey = await db
|
|
38
|
-
.selectFrom('boxxy_passkey')
|
|
39
|
-
.select(['id', 'public_key', 'counter', 'transports'])
|
|
40
|
-
.where('id', '=', response.id)
|
|
41
|
-
.executeTakeFirst();
|
|
42
|
-
|
|
43
|
-
if (passkey === undefined) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const webAuthn = await loadWebAuthn();
|
|
48
|
-
|
|
49
|
-
let verified: VerifiedAuthenticationResponse;
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
verified = await webAuthn.verifyAuthenticationResponse({
|
|
53
|
-
response,
|
|
54
|
-
expectedChallenge: check.challenge,
|
|
55
|
-
expectedOrigin: check.origin,
|
|
56
|
-
expectedRPID: check.rpID,
|
|
57
|
-
expectedType: 'webauthn.get',
|
|
58
|
-
requireUserVerification: true,
|
|
59
|
-
credential: {
|
|
60
|
-
id: passkey.id,
|
|
61
|
-
publicKey: new Uint8Array(Buffer.from(passkey.public_key, 'base64url')),
|
|
62
|
-
counter: passkey.counter,
|
|
63
|
-
transports: parseJSONStrings(passkey.transports) ?? [],
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!verified.verified || !verified.authenticationInfo.userVerified) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const stored = await updatePasskeyUse(db, {
|
|
75
|
-
id: passkey.id,
|
|
76
|
-
counter: passkey.counter,
|
|
77
|
-
newCounter: verified.authenticationInfo.newCounter,
|
|
78
|
-
now: check.now,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return stored ? passkey.id : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const ASSERTION_REPLY = z.object({
|
|
85
|
-
clientDataJSON: z.string(),
|
|
86
|
-
authenticatorData: z.string(),
|
|
87
|
-
signature: z.string(),
|
|
88
|
-
userHandle: z.string().optional(),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const ASSERTION = z.object({
|
|
92
|
-
id: z.string().min(1),
|
|
93
|
-
rawId: z.string().min(1),
|
|
94
|
-
type: z.literal('public-key'),
|
|
95
|
-
response: ASSERTION_REPLY,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// The posted JSON when it has the shape of an authentication response,
|
|
99
|
-
// else null; the verifier checks every value.
|
|
100
|
-
function parseAssertion(credential: string): AuthenticationResponseJSON | null {
|
|
101
|
-
let parsed: unknown;
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
parsed = JSON.parse(credential);
|
|
105
|
-
} catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const result = ASSERTION.safeParse(parsed);
|
|
110
|
-
|
|
111
|
-
if (!result.success) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const { userHandle, ...response } = result.data.response;
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
id: result.data.id,
|
|
119
|
-
rawId: result.data.rawId,
|
|
120
|
-
type: result.data.type,
|
|
121
|
-
response: userHandle === undefined ? response : { ...response, userHandle },
|
|
122
|
-
clientExtensionResults: {},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/verify-passkey-registration.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import type { RegistrationResponseJSON } from '@simplewebauthn/server';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { loadWebAuthn } from './load-web-authn.ts';
|
|
6
|
-
|
|
7
|
-
interface RegistrationCheck {
|
|
8
|
-
// The credential JSON the enrollment page posted.
|
|
9
|
-
readonly credential: string;
|
|
10
|
-
|
|
11
|
-
// The challenge the page was given, which the caller has already taken.
|
|
12
|
-
readonly challenge: string;
|
|
13
|
-
readonly origin: string;
|
|
14
|
-
readonly rpID: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface RegisteredPasskey {
|
|
18
|
-
readonly id: string;
|
|
19
|
-
readonly publicKey: Uint8Array;
|
|
20
|
-
readonly counter: number;
|
|
21
|
-
readonly transports: readonly string[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function verifyPasskeyRegistration(
|
|
25
|
-
check: RegistrationCheck,
|
|
26
|
-
): Promise<RegisteredPasskey | null> {
|
|
27
|
-
const response = parseRegistration(check.credential);
|
|
28
|
-
|
|
29
|
-
if (response === null) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const webAuthn = await loadWebAuthn();
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const verified = await webAuthn.verifyRegistrationResponse({
|
|
37
|
-
response,
|
|
38
|
-
expectedChallenge: check.challenge,
|
|
39
|
-
expectedOrigin: check.origin,
|
|
40
|
-
expectedRPID: check.rpID,
|
|
41
|
-
expectedType: 'webauthn.create',
|
|
42
|
-
requireUserVerification: true,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (!verified.verified || !verified.registrationInfo.userVerified) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const credential = verified.registrationInfo.credential;
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
id: credential.id,
|
|
53
|
-
publicKey: credential.publicKey,
|
|
54
|
-
counter: credential.counter,
|
|
55
|
-
transports: response.response.transports ?? [],
|
|
56
|
-
};
|
|
57
|
-
} catch {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const TRANSPORTS: ReadonlySet<string> = new Set([
|
|
63
|
-
'ble',
|
|
64
|
-
'cable',
|
|
65
|
-
'hybrid',
|
|
66
|
-
'internal',
|
|
67
|
-
'nfc',
|
|
68
|
-
'smart-card',
|
|
69
|
-
'usb',
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
const REGISTRATION_REPLY = z.object({
|
|
73
|
-
clientDataJSON: z.string(),
|
|
74
|
-
attestationObject: z.string(),
|
|
75
|
-
transports: z.string().array().optional(),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const REGISTRATION = z.object({
|
|
79
|
-
id: z.string().min(1),
|
|
80
|
-
rawId: z.string().min(1),
|
|
81
|
-
type: z.literal('public-key'),
|
|
82
|
-
response: REGISTRATION_REPLY,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// The posted JSON when it has the shape of a registration response, keeping
|
|
86
|
-
// only the transports WebAuthn defines, else null; the verifier checks
|
|
87
|
-
// every value.
|
|
88
|
-
function parseRegistration(credential: string): RegistrationResponseJSON | null {
|
|
89
|
-
let parsed: unknown;
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
parsed = JSON.parse(credential);
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const result = REGISTRATION.safeParse(parsed);
|
|
98
|
-
|
|
99
|
-
if (!result.success) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const transports = (result.data.response.transports ?? []).filter((transport) =>
|
|
104
|
-
TRANSPORTS.has(transport),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
id: result.data.id,
|
|
109
|
-
rawId: result.data.rawId,
|
|
110
|
-
type: result.data.type,
|
|
111
|
-
response: {
|
|
112
|
-
clientDataJSON: result.data.response.clientDataJSON,
|
|
113
|
-
attestationObject: result.data.response.attestationObject,
|
|
114
|
-
transports,
|
|
115
|
-
},
|
|
116
|
-
clientExtensionResults: {},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/verify-session-passkey.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import type { Kysely } from 'kysely';
|
|
4
|
-
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
5
|
-
|
|
6
|
-
interface SessionPasskey {
|
|
7
|
-
// The owner session a passkey sign-in just created.
|
|
8
|
-
readonly sessionID: string;
|
|
9
|
-
|
|
10
|
-
// The passkey whose assertion signed the owner in.
|
|
11
|
-
readonly passkeyID: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function verifySessionPasskey(
|
|
15
|
-
db: Kysely<MCPAuthSchema>,
|
|
16
|
-
check: SessionPasskey,
|
|
17
|
-
): Promise<boolean> {
|
|
18
|
-
try {
|
|
19
|
-
return await db.transaction().execute(async (trx) => {
|
|
20
|
-
const passkey = await trx
|
|
21
|
-
.selectFrom('boxxy_passkey')
|
|
22
|
-
.select('id')
|
|
23
|
-
.where('id', '=', check.passkeyID)
|
|
24
|
-
.executeTakeFirst();
|
|
25
|
-
|
|
26
|
-
const session = await trx
|
|
27
|
-
.selectFrom('session')
|
|
28
|
-
.select('id')
|
|
29
|
-
.where('id', '=', check.sessionID)
|
|
30
|
-
.executeTakeFirst();
|
|
31
|
-
|
|
32
|
-
if (passkey !== undefined && session !== undefined) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
await trx.deleteFrom('session').where('id', '=', check.sessionID).execute();
|
|
37
|
-
|
|
38
|
-
return false;
|
|
39
|
-
});
|
|
40
|
-
} catch {
|
|
41
|
-
// a transaction that fails, such as one a concurrent write keeps busy, still
|
|
42
|
-
// removes the session.
|
|
43
|
-
await db.deleteFrom('session').where('id', '=', check.sessionID).execute();
|
|
44
|
-
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/auth/write-grant-use.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import type { Kysely } from 'kysely';
|
|
4
|
-
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
5
|
-
|
|
6
|
-
export async function writeGrantUse(
|
|
7
|
-
db: Kysely<MCPAuthSchema>,
|
|
8
|
-
grantID: string,
|
|
9
|
-
now: number,
|
|
10
|
-
): Promise<void> {
|
|
11
|
-
const lastUsedAt = new Date(now).toISOString();
|
|
12
|
-
|
|
13
|
-
await db
|
|
14
|
-
.insertInto('boxxy_grant_use')
|
|
15
|
-
.values({ grant_id: grantID, last_used_at: lastUsedAt })
|
|
16
|
-
.onConflict((conflict) => conflict.column('grant_id').doUpdateSet({ last_used_at: lastUsedAt }))
|
|
17
|
-
.execute();
|
|
18
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/http/answer-mcp-request.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import type { HTTPServerContext, Requester } from '../auth/types.ts';
|
|
4
|
-
import { PUBLIC_DOOR_PATHS } from './public-door-paths.ts';
|
|
5
|
-
import { verifyAccessToken } from './verify-access-token.ts';
|
|
6
|
-
|
|
7
|
-
// What the MCP transport reads of a verified token: the scopes it holds, and the resource metadata
|
|
8
|
-
// that a scope challenge points at.
|
|
9
|
-
export interface McpAuthInfo {
|
|
10
|
-
readonly token: string;
|
|
11
|
-
readonly clientId: string;
|
|
12
|
-
readonly scopes: string[];
|
|
13
|
-
readonly resourceMetadataUrl: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Serves one MCP request for a token that verified.
|
|
17
|
-
export type McpServe = (request: Request, authInfo: McpAuthInfo) => Promise<Response>;
|
|
18
|
-
|
|
19
|
-
// `requester` is where the request came from, for its security events.
|
|
20
|
-
export async function answerMCPRequest(
|
|
21
|
-
ctx: HTTPServerContext,
|
|
22
|
-
request: Request,
|
|
23
|
-
requester: Requester,
|
|
24
|
-
serve: McpServe,
|
|
25
|
-
): Promise<Response> {
|
|
26
|
-
const path = new URL(request.url).pathname;
|
|
27
|
-
|
|
28
|
-
const metadataURL = `${ctx.origin}${PUBLIC_DOOR_PATHS.machine.mcpResourceMetadata}`;
|
|
29
|
-
const authorization = request.headers.get('authorization');
|
|
30
|
-
const token = /^Bearer (?<token>\S+)$/iu.exec(authorization ?? '')?.groups?.['token'];
|
|
31
|
-
|
|
32
|
-
if (token === undefined) {
|
|
33
|
-
// a request with no header at all is a client discovering the
|
|
34
|
-
// authorization server, so only a header that holds no bearer token is
|
|
35
|
-
// recorded.
|
|
36
|
-
if (authorization !== null) {
|
|
37
|
-
ctx.security.record({
|
|
38
|
-
kind: 'bearer',
|
|
39
|
-
outcome: 'rejected',
|
|
40
|
-
reason: 'malformed',
|
|
41
|
-
path,
|
|
42
|
-
requester,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return new Response(null, {
|
|
47
|
-
status: 401,
|
|
48
|
-
headers: { 'www-authenticate': `Bearer resource_metadata="${metadataURL}"` },
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const access = await verifyAccessToken(ctx, token);
|
|
53
|
-
|
|
54
|
-
if (access === null) {
|
|
55
|
-
ctx.security.record({
|
|
56
|
-
kind: 'bearer',
|
|
57
|
-
outcome: 'rejected',
|
|
58
|
-
reason: 'invalid_token',
|
|
59
|
-
path,
|
|
60
|
-
requester,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return new Response(null, {
|
|
64
|
-
status: 401,
|
|
65
|
-
headers: {
|
|
66
|
-
'www-authenticate': `Bearer error="invalid_token", resource_metadata="${metadataURL}"`,
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return serve(request, {
|
|
72
|
-
token,
|
|
73
|
-
clientId: access.clientID,
|
|
74
|
-
scopes: [...access.scopes],
|
|
75
|
-
resourceMetadataUrl: metadataURL,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// Copied from atc: packages/mcp/src/http/build-page-response.ts at commit 4fe647cf.
|
|
2
|
-
|
|
3
|
-
import { toHTMLText } from '../auth/to-html-text.ts';
|
|
4
|
-
|
|
5
|
-
export function buildPageResponse(
|
|
6
|
-
status: number,
|
|
7
|
-
body: { readonly page: string } | { readonly message: string },
|
|
8
|
-
formTargets: readonly string[] = [],
|
|
9
|
-
scriptSources: readonly string[] = [],
|
|
10
|
-
): Response {
|
|
11
|
-
const html =
|
|
12
|
-
'page' in body
|
|
13
|
-
? body.page
|
|
14
|
-
: `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>boxxy</title></head><body><p>${toHTMLText(body.message)}</p></body></html>`;
|
|
15
|
-
|
|
16
|
-
return new Response(html, {
|
|
17
|
-
status,
|
|
18
|
-
headers: {
|
|
19
|
-
'content-type': 'text/html; charset=utf-8',
|
|
20
|
-
'content-security-policy': `default-src 'none'; style-src 'unsafe-inline';${scriptSources.length === 0 ? '' : ` script-src ${scriptSources.join(' ')};`} form-action 'self'${formTargets.map((target) => ` ${target}`).join('')}; frame-ancestors 'none'; base-uri 'none'`,
|
|
21
|
-
'x-frame-options': 'DENY',
|
|
22
|
-
'cache-control': 'no-store',
|
|
23
|
-
'referrer-policy': 'same-origin',
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|