From 8c48b9f625b59739a45bbcf167fb01646192a546 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 29 Apr 2023 15:22:10 -0500 Subject: [PATCH] Add nip19 auth middleware --- src/api/accounts.ts | 22 +++++++++------------ src/api/home.ts | 14 +++++--------- src/api/statuses.ts | 12 ++++++------ src/app.ts | 26 +++++++++++++++++++------ src/deps.ts | 19 +++++++++++++++++-- src/middleware/auth.ts | 43 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 src/middleware/auth.ts diff --git a/src/api/accounts.ts b/src/api/accounts.ts index 458257a..2ad2465 100644 --- a/src/api/accounts.ts +++ b/src/api/accounts.ts @@ -1,21 +1,17 @@ +import { type AppController } from '@/app.ts'; + import { fetchUser } from '../client.ts'; import { toAccount } from '../transmute.ts'; -import { getKeys } from '../utils.ts'; -import type { Context } from '@/deps.ts'; +const credentialsController: AppController = async (c) => { + const pubkey = c.get('pubkey')!; -async function credentialsController(c: Context) { - const keys = getKeys(c); - - if (keys) { - const { pubkey } = keys; - const event = await fetchUser(pubkey); - if (event) { - return c.json(toAccount(event)); - } + const event = await fetchUser(pubkey); + if (event) { + return c.json(toAccount(event)); } - return c.json({ error: 'Invalid token' }, 400); -} + return c.json({ error: 'Could not find user.' }, 404); +}; export { credentialsController }; diff --git a/src/api/home.ts b/src/api/home.ts index 625a56f..fbf096a 100644 --- a/src/api/home.ts +++ b/src/api/home.ts @@ -1,22 +1,18 @@ +import { type AppController } from '@/app.ts'; import { z } from '@/deps.ts'; import { fetchFeed, fetchFollows } from '../client.ts'; import { toStatus } from '../transmute.ts'; -import { getKeys } from '../utils.ts'; -import type { Context } from '@/deps.ts'; import { LOCAL_DOMAIN } from '../config.ts'; -async function homeController(c: Context) { +const homeController: AppController = async (c) => { const since = paramSchema.parse(c.req.query('since')); const until = paramSchema.parse(c.req.query('until')); - const keys = getKeys(c); - if (!keys) { - return c.json({ error: 'Unauthorized' }, 401); - } + const pubkey = c.get('pubkey')!; - const follows = await fetchFollows(keys.pubkey); + const follows = await fetchFollows(pubkey); if (!follows) { return c.json([]); } @@ -30,7 +26,7 @@ async function homeController(c: Context) { return c.json(statuses, 200, { link: `<${next}>; rel="next", <${prev}>; rel="prev"`, }); -} +}; const paramSchema = z.coerce.number().optional().catch(undefined); diff --git a/src/api/statuses.ts b/src/api/statuses.ts index ad0c1bd..7ab44b7 100644 --- a/src/api/statuses.ts +++ b/src/api/statuses.ts @@ -1,21 +1,21 @@ +import { type AppContext } from '@/app.ts'; import { validator, z } from '@/deps.ts'; import { type Event } from '@/nostr/event.ts'; import publish from '../publisher.ts'; import { toStatus } from '../transmute.ts'; -import { getKeys } from '../utils.ts'; const createStatusSchema = z.object({ status: z.string(), }); -const createStatusController = validator('json', async (value, c) => { - const keys = getKeys(c); +const createStatusController = validator('json', async (value, c: AppContext) => { + const pubkey = c.get('pubkey')!; + const seckey = c.get('seckey'); const result = createStatusSchema.safeParse(value); - if (result.success && keys) { + if (result.success && seckey) { const { data } = result; - const { pubkey, privatekey } = keys; const event: Event<1> = { kind: 1, @@ -25,7 +25,7 @@ const createStatusController = validator('json', async (value, c) => { created_at: Math.floor(new Date().getTime() / 1000), }; - publish(event, privatekey); + publish(event, seckey); return c.json(await toStatus(event)); } else { diff --git a/src/app.ts b/src/app.ts index 58feba4..0491b3d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { cors, Hono } from '@/deps.ts'; +import { type Context, cors, type Handler, Hono, type HonoEnv, type MiddlewareHandler } from '@/deps.ts'; import { credentialsController } from './api/accounts.ts'; import { appCredentialsController, createAppController } from './api/apps.ts'; @@ -7,10 +7,22 @@ import homeController from './api/home.ts'; import instanceController from './api/instance.ts'; import { createTokenController } from './api/oauth.ts'; import { createStatusController } from './api/statuses.ts'; +import { requireAuth, setAuth } from './middleware/auth.ts'; -const app = new Hono(); +interface AppEnv extends HonoEnv { + Variables: { + pubkey?: string; + seckey?: string; + }; +} -app.use('/*', cors()); +type AppContext = Context; +type AppMiddleware = MiddlewareHandler; +type AppController = Handler; + +const app = new Hono(); + +app.use('/*', cors(), setAuth); app.get('/api/v1/instance', instanceController); @@ -20,11 +32,11 @@ app.post('/api/v1/apps', createAppController); app.post('/oauth/token', createTokenController); app.post('/oauth/revoke', emptyObjectController); -app.get('/api/v1/accounts/verify_credentials', credentialsController); +app.get('/api/v1/accounts/verify_credentials', requireAuth, credentialsController); -app.post('/api/v1/statuses', createStatusController); +app.post('/api/v1/statuses', requireAuth, createStatusController); -app.get('/api/v1/timelines/home', homeController); +app.get('/api/v1/timelines/home', requireAuth, homeController); // Not (yet) implemented. app.get('/api/v1/notifications', emptyArrayController); @@ -39,3 +51,5 @@ app.get('/api/v1/mutes', emptyArrayController); app.get('/api/v1/domain_blocks', emptyArrayController); export default app; + +export type { AppContext, AppController, AppMiddleware }; diff --git a/src/deps.ts b/src/deps.ts index 5ccdd04..27c051e 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,6 +1,21 @@ -export { type Context, Hono, validator } from 'https://deno.land/x/hono@v3.0.2/mod.ts'; +export { + type Context, + type Env as HonoEnv, + type Handler, + Hono, + type MiddlewareHandler, + validator, +} from 'https://deno.land/x/hono@v3.0.2/mod.ts'; +export { HTTPException } from 'https://deno.land/x/hono@v3.0.2/http-exception.ts'; export { cors } from 'https://deno.land/x/hono@v3.0.2/middleware.ts'; export { z } from 'https://deno.land/x/zod@v3.20.5/mod.ts'; export { Author, RelayPool } from 'https://dev.jspm.io/nostr-relaypool@0.5.3'; -export { type Filter, getEventHash, getPublicKey, nip19, signEvent as getSignature } from 'npm:nostr-tools@^1.10.1'; +export { + type Filter, + getEventHash, + getPublicKey, + nip19, + nip21, + signEvent as getSignature, +} from 'npm:nostr-tools@^1.10.1'; export { default as lmdb } from 'npm:lmdb'; diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..6b8e16e --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,43 @@ +import { AppMiddleware } from '@/app.ts'; +import { getPublicKey, HTTPException, nip19 } from '@/deps.ts'; + +/** NIP-19 auth middleware. */ +const setAuth: AppMiddleware = async (c, next) => { + const authHeader = c.req.headers.get('Authorization'); + + if (authHeader?.startsWith('Bearer ')) { + const bech32 = authHeader.replace(/^Bearer /, ''); + + try { + const decoded = nip19.decode(bech32!); + + switch (decoded.type) { + case 'npub': + c.set('pubkey', decoded.data); + break; + case 'nprofile': + c.set('pubkey', decoded.data.pubkey); + break; + case 'nsec': + c.set('pubkey', getPublicKey(decoded.data)); + c.set('seckey', decoded.data); + break; + } + } catch (_e) { + // + } + } + + await next(); +}; + +/** Throw a 401 if the pubkey isn't set. */ +const requireAuth: AppMiddleware = async (c, next) => { + if (!c.get('pubkey')) { + throw new HTTPException(401); + } + + await next(); +}; + +export { requireAuth, setAuth };