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 { 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);

View file

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

View file

@ -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<NostrSigner> {
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);
function getSigner(c: Context<DittoEnv>, auth: Authorization): NostrSigner | Promise<NostrSigner> {
switch (auth.realm) {
case 'Bearer': {
if (isToken(auth.token)) {
return getSignerFromToken(c, auth.token);
} else {
return getSignerFromNip19(bech32);
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<NostrSigner> {
const { conf, db, relay } = opts;
async function getSignerFromToken(c: Context<DittoEnv>, token: `token1${string}`): Promise<NostrSigner> {
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<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');
}