tokenMiddleware: support nip98 auth

This commit is contained in:
Alex Gleason 2025-02-21 15:53:29 -06:00
parent f0add87c6d
commit adeff1cae5
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
3 changed files with 59 additions and 35 deletions

View file

@ -138,7 +138,7 @@ import { metricsController } from '@/controllers/metrics.ts';
import { manifestController } from '@/controllers/manifest.ts'; import { manifestController } from '@/controllers/manifest.ts';
import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts';
import { nostrController } from '@/controllers/well-known/nostr.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 { cacheControlMiddleware } from '@/middleware/cacheControlMiddleware.ts';
import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts';
import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts'; import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts';
@ -216,7 +216,6 @@ app.use(
cors({ origin: '*', exposeHeaders: ['link'] }), cors({ origin: '*', exposeHeaders: ['link'] }),
tokenMiddleware(), tokenMiddleware(),
uploaderMiddleware, uploaderMiddleware,
auth98Middleware(),
); );
app.get('/metrics', metricsController); app.get('/metrics', metricsController);

View file

@ -3,4 +3,5 @@ import type { NostrSigner, NRelay } from '@nostrify/nostrify';
export interface User<S extends NostrSigner = NostrSigner, R extends NRelay = NRelay> { export interface User<S extends NostrSigner = NostrSigner, R extends NRelay = NRelay> {
signer: S; signer: S;
relay: R; relay: R;
verified?: boolean;
} }

View file

@ -1,5 +1,6 @@
import { parseAuthRequest } from '@ditto/nip98';
import { HTTPException } from '@hono/hono/http-exception'; 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 { nip19 } from 'nostr-tools';
import { aesDecrypt } from '../auth/aes.ts'; import { aesDecrypt } from '../auth/aes.ts';
@ -8,14 +9,10 @@ import { ConnectSigner } from '../signers/ConnectSigner.ts';
import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts'; import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts';
import { UserStore } from '../storages/UserStore.ts'; import { UserStore } from '../storages/UserStore.ts';
import type { DittoConf } from '@ditto/conf'; import type { DittoEnv, DittoMiddleware } from '@ditto/router';
import type { DittoDB } from '@ditto/db'; import type { Context } from '@hono/hono';
import type { DittoMiddleware } from '@ditto/router';
import type { User } from './User.ts'; 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 }> { export function tokenMiddleware(): DittoMiddleware<{ user?: User }> {
return async (c, next) => { return async (c, next) => {
const header = c.req.header('authorization'); const header = c.req.header('authorization');
@ -23,13 +20,15 @@ export function tokenMiddleware(): DittoMiddleware<{ user?: User }> {
if (header) { if (header) {
const { relay, conf } = c.var; 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 userPubkey = await signer.getPublicKey();
const adminPubkey = await conf.signer.getPublicKey(); const adminPubkey = await conf.signer.getPublicKey();
const user: User = { const user: User = {
signer, signer,
relay: new UserStore({ relay, userPubkey, adminPubkey }), relay: new UserStore({ relay, userPubkey, adminPubkey }),
verified: auth.realm === 'Nostr',
}; };
c.set('user', user); c.set('user', user);
@ -39,34 +38,26 @@ export function tokenMiddleware(): DittoMiddleware<{ user?: User }> {
}; };
} }
interface GetSignerOpts { function getSigner(c: Context<DittoEnv>, auth: Authorization): NostrSigner | Promise<NostrSigner> {
db: DittoDB; switch (auth.realm) {
conf: DittoConf; case 'Bearer': {
relay: NRelay; if (isToken(auth.token)) {
} return getSignerFromToken(c, auth.token);
} else {
function getSigner(header: string, opts: GetSignerOpts): NostrSigner | Promise<NostrSigner> { return getSignerFromNip19(auth.token);
const match = header.match(BEARER_REGEX); }
}
if (!match) { case 'Nostr': {
throw new HTTPException(400, { message: 'Invalid Authorization header.' }); return getSignerFromNip98(c);
} }
default: {
const [_, bech32] = match; throw new HTTPException(400, { message: 'Unsupported Authorization realm.' });
}
if (isToken(bech32)) {
return getSignerFromToken(bech32, opts);
} else {
return getSignerFromNip19(bech32);
} }
} }
function isToken(value: string): value is `token1${string}` { async function getSignerFromToken(c: Context<DittoEnv>, token: `token1${string}`): Promise<NostrSigner> {
return value.startsWith('token1'); const { conf, db, relay } = c.var;
}
async function getSignerFromToken(token: `token1${string}`, opts: GetSignerOpts): Promise<NostrSigner> {
const { conf, db, relay } = opts;
try { try {
const tokenHash = await getTokenHash(token); 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.' }); throw new HTTPException(401, { message: 'Invalid NIP-19 identifier in Authorization header.' });
} }
async function getSignerFromNip98(c: Context<DittoEnv>): Promise<NostrSigner> {
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');
}