diff --git a/src/app.ts b/src/app.ts index 24fdc98..7d9750a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -122,8 +122,8 @@ app.get('/api/v1/statuses/:id{[0-9a-f]{64}}', statusController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/favourite', favouriteController); app.post('/api/v1/statuses', requirePubkey, createStatusController); -app.post('/api/v1/media', requireRole('user'), mediaController); -app.post('/api/v2/media', requireRole('user'), mediaController); +app.post('/api/v1/media', requireRole('user', { validatePayload: false }), mediaController); +app.post('/api/v2/media', requireRole('user', { validatePayload: false }), mediaController); app.get('/api/v1/timelines/home', requirePubkey, homeTimelineController); app.get('/api/v1/timelines/public', publicTimelineController); diff --git a/src/middleware/auth98.ts b/src/middleware/auth98.ts index a64370a..128d351 100644 --- a/src/middleware/auth98.ts +++ b/src/middleware/auth98.ts @@ -1,6 +1,11 @@ import { type AppContext, type AppMiddleware } from '@/app.ts'; import { HTTPException } from '@/deps.ts'; -import { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts } from '@/utils/nip98.ts'; +import { + buildAuthEventTemplate, + parseAuthRequest, + type ParseAuthRequestOpts, + validateAuthEvent, +} from '@/utils/nip98.ts'; import { localRequest } from '@/utils/web.ts'; import { signNostrConnect } from '@/sign.ts'; import { findUser, User } from '@/db/users.ts'; @@ -26,10 +31,10 @@ function auth98(opts: ParseAuthRequestOpts = {}): AppMiddleware { type UserRole = 'user' | 'admin'; /** Require the user to prove their role before invoking the controller. */ -function requireRole(role: UserRole): AppMiddleware { +function requireRole(role: UserRole, opts?: ParseAuthRequestOpts): AppMiddleware { return async (c, next) => { const header = c.req.headers.get('x-nostr-sign'); - const proof = c.get('proof') || header ? await obtainProof(c) : undefined; + const proof = c.get('proof') || header ? await obtainProof(c, opts) : undefined; const user = proof ? await findUser({ pubkey: proof.pubkey }) : undefined; if (proof && user && matchesRole(user, role)) { @@ -55,10 +60,15 @@ function matchesRole(user: User, role: UserRole): boolean { } /** Get the proof over Nostr Connect. */ -async function obtainProof(c: AppContext) { +async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) { const req = localRequest(c); - const event = await buildAuthEventTemplate(req); - return signNostrConnect(event, c); + const reqEvent = await buildAuthEventTemplate(req, opts); + const resEvent = await signNostrConnect(reqEvent, c); + const result = await validateAuthEvent(req, resEvent, opts); + + if (result.success) { + return result.data; + } } export { auth98, requireRole }; diff --git a/src/utils/nip98.ts b/src/utils/nip98.ts index e1cae5d..5606d8b 100644 --- a/src/utils/nip98.ts +++ b/src/utils/nip98.ts @@ -15,13 +15,21 @@ interface ParseAuthRequestOpts { } /** Parse the auth event from a Request, returning a zod SafeParse type. */ -function parseAuthRequest(req: Request, opts: ParseAuthRequestOpts = {}) { - const { maxAge = Time.minutes(1), validatePayload = true } = opts; - +// deno-lint-ignore require-await +async function parseAuthRequest(req: Request, opts: ParseAuthRequestOpts = {}) { const header = req.headers.get('authorization'); const base64 = header?.match(/^Nostr (.+)$/)?.[1]; + const result = decode64EventSchema.safeParse(base64); - const schema = decode64EventSchema + if (!result.success) return result; + return validateAuthEvent(req, result.data, opts); +} + +/** Compare the auth event with the request, returning a zod SafeParse type. */ +function validateAuthEvent(req: Request, event: Event, opts: ParseAuthRequestOpts = {}) { + const { maxAge = Time.minutes(1), validatePayload = true } = opts; + + const schema = signedEventSchema .refine((event): event is Event<27235> => event.kind === 27235, 'Event must be kind 27235') .refine((event) => eventAge(event) < maxAge, 'Event expired') .refine((event) => tagValue(event, 'method') === req.method, 'Event method does not match HTTP request method') @@ -35,22 +43,28 @@ function parseAuthRequest(req: Request, opts: ParseAuthRequestOpts = {}) { .then((hash) => hash === tagValue(event, 'payload')); } - return schema.safeParseAsync(base64); + return schema.safeParseAsync(event); } /** Create an auth EventTemplate from a Request. */ -async function buildAuthEventTemplate(req: Request): Promise> { +async function buildAuthEventTemplate(req: Request, opts: ParseAuthRequestOpts = {}): Promise> { + const { validatePayload = true } = opts; const { method, url } = req; - const payload = await req.clone().text().then(sha256); + + const tags = [ + ['method', method], + ['u', url], + ]; + + if (validatePayload) { + const payload = await req.clone().text().then(sha256); + tags.push(['payload', payload]); + } return { kind: 27235, content: '', - tags: [ - ['method', method], - ['u', url], - ['payload', payload], - ], + tags, created_at: nostrNow(), }; } @@ -60,4 +74,4 @@ function tagValue(event: Event, tagName: string): string | undefined { return findTag(event.tags, tagName)?.[1]; } -export { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts }; +export { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent };