Fix nip98 signing (validate proof), skip validating payload for media requests

This commit is contained in:
Alex Gleason 2023-09-08 18:22:38 -05:00
parent 969d8bfe7f
commit 527e276340
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
3 changed files with 45 additions and 21 deletions

View File

@ -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/:id{[0-9a-f]{64}}/favourite', favouriteController);
app.post('/api/v1/statuses', requirePubkey, createStatusController); app.post('/api/v1/statuses', requirePubkey, createStatusController);
app.post('/api/v1/media', requireRole('user'), mediaController); app.post('/api/v1/media', requireRole('user', { validatePayload: false }), mediaController);
app.post('/api/v2/media', requireRole('user'), mediaController); app.post('/api/v2/media', requireRole('user', { validatePayload: false }), mediaController);
app.get('/api/v1/timelines/home', requirePubkey, homeTimelineController); app.get('/api/v1/timelines/home', requirePubkey, homeTimelineController);
app.get('/api/v1/timelines/public', publicTimelineController); app.get('/api/v1/timelines/public', publicTimelineController);

View File

@ -1,6 +1,11 @@
import { type AppContext, type AppMiddleware } from '@/app.ts'; import { type AppContext, type AppMiddleware } from '@/app.ts';
import { HTTPException } from '@/deps.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 { localRequest } from '@/utils/web.ts';
import { signNostrConnect } from '@/sign.ts'; import { signNostrConnect } from '@/sign.ts';
import { findUser, User } from '@/db/users.ts'; import { findUser, User } from '@/db/users.ts';
@ -26,10 +31,10 @@ function auth98(opts: ParseAuthRequestOpts = {}): AppMiddleware {
type UserRole = 'user' | 'admin'; type UserRole = 'user' | 'admin';
/** Require the user to prove their role before invoking the controller. */ /** 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) => { return async (c, next) => {
const header = c.req.headers.get('x-nostr-sign'); 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; const user = proof ? await findUser({ pubkey: proof.pubkey }) : undefined;
if (proof && user && matchesRole(user, role)) { if (proof && user && matchesRole(user, role)) {
@ -55,10 +60,15 @@ function matchesRole(user: User, role: UserRole): boolean {
} }
/** Get the proof over Nostr Connect. */ /** Get the proof over Nostr Connect. */
async function obtainProof(c: AppContext) { async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
const req = localRequest(c); const req = localRequest(c);
const event = await buildAuthEventTemplate(req); const reqEvent = await buildAuthEventTemplate(req, opts);
return signNostrConnect(event, c); const resEvent = await signNostrConnect(reqEvent, c);
const result = await validateAuthEvent(req, resEvent, opts);
if (result.success) {
return result.data;
}
} }
export { auth98, requireRole }; export { auth98, requireRole };

View File

@ -15,13 +15,21 @@ interface ParseAuthRequestOpts {
} }
/** Parse the auth event from a Request, returning a zod SafeParse type. */ /** Parse the auth event from a Request, returning a zod SafeParse type. */
function parseAuthRequest(req: Request, opts: ParseAuthRequestOpts = {}) { // deno-lint-ignore require-await
const { maxAge = Time.minutes(1), validatePayload = true } = opts; async function parseAuthRequest(req: Request, opts: ParseAuthRequestOpts = {}) {
const header = req.headers.get('authorization'); const header = req.headers.get('authorization');
const base64 = header?.match(/^Nostr (.+)$/)?.[1]; 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): event is Event<27235> => event.kind === 27235, 'Event must be kind 27235')
.refine((event) => eventAge(event) < maxAge, 'Event expired') .refine((event) => eventAge(event) < maxAge, 'Event expired')
.refine((event) => tagValue(event, 'method') === req.method, 'Event method does not match HTTP request method') .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')); .then((hash) => hash === tagValue(event, 'payload'));
} }
return schema.safeParseAsync(base64); return schema.safeParseAsync(event);
} }
/** Create an auth EventTemplate from a Request. */ /** Create an auth EventTemplate from a Request. */
async function buildAuthEventTemplate(req: Request): Promise<EventTemplate<27235>> { async function buildAuthEventTemplate(req: Request, opts: ParseAuthRequestOpts = {}): Promise<EventTemplate<27235>> {
const { validatePayload = true } = opts;
const { method, url } = req; 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 { return {
kind: 27235, kind: 27235,
content: '', content: '',
tags: [ tags,
['method', method],
['u', url],
['payload', payload],
],
created_at: nostrNow(), created_at: nostrNow(),
}; };
} }
@ -60,4 +74,4 @@ function tagValue(event: Event, tagName: string): string | undefined {
return findTag(event.tags, tagName)?.[1]; return findTag(event.tags, tagName)?.[1];
} }
export { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts }; export { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent };