From adeff1cae519b552a62ba9fac518c91cfab05cfd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 21 Feb 2025 15:53:29 -0600 Subject: [PATCH] tokenMiddleware: support nip98 auth --- packages/ditto/app.ts | 3 +- packages/mastoapi/middleware/User.ts | 1 + .../mastoapi/middleware/tokenMiddleware.ts | 90 ++++++++++++------- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/packages/ditto/app.ts b/packages/ditto/app.ts index 459baf6b..3333b9eb 100644 --- a/packages/ditto/app.ts +++ b/packages/ditto/app.ts @@ -138,7 +138,7 @@ import { metricsController } from '@/controllers/metrics.ts'; import { manifestController } from '@/controllers/manifest.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; -import { auth98Middleware, requireProof as _requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; +import { requireProof as _requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cacheControlMiddleware } from '@/middleware/cacheControlMiddleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts'; @@ -216,7 +216,6 @@ app.use( cors({ origin: '*', exposeHeaders: ['link'] }), tokenMiddleware(), uploaderMiddleware, - auth98Middleware(), ); app.get('/metrics', metricsController); diff --git a/packages/mastoapi/middleware/User.ts b/packages/mastoapi/middleware/User.ts index ac38b8de..8fd61f96 100644 --- a/packages/mastoapi/middleware/User.ts +++ b/packages/mastoapi/middleware/User.ts @@ -3,4 +3,5 @@ import type { NostrSigner, NRelay } from '@nostrify/nostrify'; export interface User { signer: S; relay: R; + verified?: boolean; } diff --git a/packages/mastoapi/middleware/tokenMiddleware.ts b/packages/mastoapi/middleware/tokenMiddleware.ts index 407548ed..4796b5d4 100644 --- a/packages/mastoapi/middleware/tokenMiddleware.ts +++ b/packages/mastoapi/middleware/tokenMiddleware.ts @@ -1,5 +1,6 @@ +import { parseAuthRequest } from '@ditto/nip98'; import { HTTPException } from '@hono/hono/http-exception'; -import { type NostrSigner, type NRelay, NSecSigner } from '@nostrify/nostrify'; +import { type NostrSigner, NSecSigner } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import { aesDecrypt } from '../auth/aes.ts'; @@ -8,14 +9,10 @@ import { ConnectSigner } from '../signers/ConnectSigner.ts'; import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts'; import { UserStore } from '../storages/UserStore.ts'; -import type { DittoConf } from '@ditto/conf'; -import type { DittoDB } from '@ditto/db'; -import type { DittoMiddleware } from '@ditto/router'; +import type { DittoEnv, DittoMiddleware } from '@ditto/router'; +import type { Context } from '@hono/hono'; import type { User } from './User.ts'; -/** We only accept "Bearer" type. */ -const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); - export function tokenMiddleware(): DittoMiddleware<{ user?: User }> { return async (c, next) => { const header = c.req.header('authorization'); @@ -23,13 +20,15 @@ export function tokenMiddleware(): DittoMiddleware<{ user?: User }> { if (header) { const { relay, conf } = c.var; - const signer = await getSigner(header, c.var); + const auth = parseAuthorization(header); + const signer = await getSigner(c, auth); const userPubkey = await signer.getPublicKey(); const adminPubkey = await conf.signer.getPublicKey(); const user: User = { signer, relay: new UserStore({ relay, userPubkey, adminPubkey }), + verified: auth.realm === 'Nostr', }; c.set('user', user); @@ -39,34 +38,26 @@ export function tokenMiddleware(): DittoMiddleware<{ user?: User }> { }; } -interface GetSignerOpts { - db: DittoDB; - conf: DittoConf; - relay: NRelay; -} - -function getSigner(header: string, opts: GetSignerOpts): NostrSigner | Promise { - const match = header.match(BEARER_REGEX); - - if (!match) { - throw new HTTPException(400, { message: 'Invalid Authorization header.' }); - } - - const [_, bech32] = match; - - if (isToken(bech32)) { - return getSignerFromToken(bech32, opts); - } else { - return getSignerFromNip19(bech32); +function getSigner(c: Context, auth: Authorization): NostrSigner | Promise { + switch (auth.realm) { + case 'Bearer': { + if (isToken(auth.token)) { + return getSignerFromToken(c, auth.token); + } else { + return getSignerFromNip19(auth.token); + } + } + case 'Nostr': { + return getSignerFromNip98(c); + } + default: { + throw new HTTPException(400, { message: 'Unsupported Authorization realm.' }); + } } } -function isToken(value: string): value is `token1${string}` { - return value.startsWith('token1'); -} - -async function getSignerFromToken(token: `token1${string}`, opts: GetSignerOpts): Promise { - const { conf, db, relay } = opts; +async function getSignerFromToken(c: Context, token: `token1${string}`): Promise { + const { conf, db, relay } = c.var; try { const tokenHash = await getTokenHash(token); @@ -109,3 +100,36 @@ function getSignerFromNip19(bech32: string): NostrSigner { throw new HTTPException(401, { message: 'Invalid NIP-19 identifier in Authorization header.' }); } + +async function getSignerFromNip98(c: Context): Promise { + const { conf } = c.var; + + const req = Object.create(c.req.raw, { + url: { value: conf.local(c.req.url) }, + }); + + const result = await parseAuthRequest(req); + + if (result.success) { + return new ReadOnlySigner(result.data.pubkey); + } else { + throw new HTTPException(401, { message: 'Invalid NIP-98 event in Authorization header.' }); + } +} + +interface Authorization { + realm: string; + token: string; +} + +function parseAuthorization(header: string): Authorization { + const [realm, ...parts] = header.split(' '); + return { + realm, + token: parts.join(' '), + }; +} + +function isToken(value: string): value is `token1${string}` { + return value.startsWith('token1'); +}