Add a UUID to auth tokens for private websocket event signing
This commit is contained in:
parent
9500ceee7c
commit
f3e42cc6a7
|
@ -27,7 +27,9 @@ import { requireAuth, setAuth } from './middleware/auth.ts';
|
|||
|
||||
interface AppEnv extends HonoEnv {
|
||||
Variables: {
|
||||
/** Hex pubkey for the current user. If provided, the user is considered "logged in." */
|
||||
pubkey?: string;
|
||||
/** Hex secret key for the current user. Optional, but easiest way to use legacy Mastodon apps. */
|
||||
seckey?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { lodash, nip19, z } from '@/deps.ts';
|
||||
import { lodash, nip19, uuid62, z } from '@/deps.ts';
|
||||
import { AppController } from '@/app.ts';
|
||||
import { parseBody } from '@/utils.ts';
|
||||
|
||||
|
@ -94,6 +94,7 @@ function maybeDecodeUri(uri: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
/** Schema for FormData POSTed to the OAuthController. */
|
||||
const oauthAuthorizeSchema = z.object({
|
||||
pubkey: z.string().regex(/^[0-9a-f]{64}$/).optional().catch(undefined),
|
||||
nip19: z.string().regex(new RegExp(`^${nip19.BECH32_REGEX.source}$`)).optional().catch(undefined),
|
||||
|
@ -107,21 +108,31 @@ const oauthAuthorizeSchema = z.object({
|
|||
}
|
||||
});
|
||||
|
||||
/** Controller the OAuth form is POSTed to. */
|
||||
const oauthAuthorizeController: AppController = async (c) => {
|
||||
/** FormData results in JSON. */
|
||||
const result = oauthAuthorizeSchema.safeParse(await parseBody(c.req.raw));
|
||||
|
||||
if (!result.success) {
|
||||
return c.json(result.error, 422);
|
||||
}
|
||||
|
||||
// Parsed FormData values.
|
||||
const { pubkey, nip19: nip19id, redirect_uri: redirectUri } = result.data;
|
||||
|
||||
/**
|
||||
* Normally the auth token is just an npub, which is public information.
|
||||
* The sessionId helps us know that Request "B" and Request "A" came from the same person.
|
||||
* Useful for sending websocket events to the correct client.
|
||||
*/
|
||||
const sessionId: string = uuid62.v4();
|
||||
|
||||
if (pubkey) {
|
||||
const encoded = nip19.npubEncode(pubkey!);
|
||||
const url = addCodeToRedirectUri(redirectUri, encoded);
|
||||
const url = addCodeToRedirectUri(redirectUri, `${encoded}_${sessionId}`);
|
||||
return c.redirect(url);
|
||||
} else if (nip19id) {
|
||||
const url = addCodeToRedirectUri(redirectUri, nip19id);
|
||||
const url = addCodeToRedirectUri(redirectUri, `${nip19id}_${sessionId}`);
|
||||
return c.redirect(url);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { AppController } from '@/app.ts';
|
||||
import { nip19 } from '@/deps.ts';
|
||||
import { TOKEN_REGEX } from '@/middleware/auth.ts';
|
||||
import { signStreams } from '@/sign.ts';
|
||||
|
||||
const streamingController: AppController = (c) => {
|
||||
|
@ -17,19 +18,26 @@ const streamingController: AppController = (c) => {
|
|||
return c.json({ error: 'Missing access token' }, 401);
|
||||
}
|
||||
|
||||
if (!nip19.BECH32_REGEX.test(token)) {
|
||||
if (!(new RegExp(`^${TOKEN_REGEX.source}$`)).test(token)) {
|
||||
return c.json({ error: 'Invalid access token' }, 401);
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token });
|
||||
|
||||
socket.addEventListener('open', () => console.log('websocket: connection opened'));
|
||||
socket.addEventListener('close', () => console.log('websocket: connection closed'));
|
||||
socket.addEventListener('message', (e) => console.log('websocket message: ', e.data));
|
||||
|
||||
if (stream === 'user' && nostr === 'true') {
|
||||
socket.addEventListener('open', () => {
|
||||
console.log('websocket: connection opened');
|
||||
// Only send signing events if the user has a session ID.
|
||||
if (stream === 'user' && nostr === 'true' && new RegExp(`^${nip19.BECH32_REGEX.source}_\\w+$`).test(token)) {
|
||||
signStreams.set(token, socket);
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener('message', (e) => console.log('websocket message: ', e.data));
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
signStreams.delete(token);
|
||||
console.log('websocket: connection closed');
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -32,3 +32,4 @@ import 'npm:linkify-plugin-hashtag@^4.1.0';
|
|||
export { default as mime } from 'npm:mime@^3.0.0';
|
||||
export { unfurl } from 'npm:unfurl.js@^6.3.2';
|
||||
export { default as TTLCache } from 'npm:@isaacs/ttlcache@^1.4.0';
|
||||
export { default as uuid62 } from 'npm:uuid62@^1.0.2';
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { AppMiddleware } from '@/app.ts';
|
||||
import { getPublicKey, HTTPException, nip19 } from '@/deps.ts';
|
||||
|
||||
/** The token includes a Bech32 Nostr ID (npub, nsec, etc) and an optional session ID. */
|
||||
const TOKEN_REGEX = new RegExp(`(${nip19.BECH32_REGEX.source})(?:_(\\w+))?`);
|
||||
/** We only accept "Bearer" type. */
|
||||
const BEARER_REGEX = new RegExp(`^Bearer (${TOKEN_REGEX.source})$`);
|
||||
|
||||
/** NIP-19 auth middleware. */
|
||||
const setAuth: AppMiddleware = async (c, next) => {
|
||||
const authHeader = c.req.headers.get('authorization');
|
||||
const match = authHeader?.match(BEARER_REGEX);
|
||||
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const bech32 = authHeader.replace(/^Bearer /, '');
|
||||
if (match) {
|
||||
const [_, _token, bech32, _sessionId] = match;
|
||||
|
||||
try {
|
||||
const decoded = nip19.decode(bech32!);
|
||||
|
@ -40,4 +46,4 @@ const requireAuth: AppMiddleware = async (c, next) => {
|
|||
await next();
|
||||
};
|
||||
|
||||
export { requireAuth, setAuth };
|
||||
export { requireAuth, setAuth, TOKEN_REGEX };
|
||||
|
|
39
src/utils.ts
39
src/utils.ts
|
@ -1,5 +1,5 @@
|
|||
import { getAuthor } from '@/client.ts';
|
||||
import { Context, getPublicKey, nip19, parseFormData } from '@/deps.ts';
|
||||
import { nip19, parseFormData } from '@/deps.ts';
|
||||
import { type Event } from '@/event.ts';
|
||||
import { lookupNip05Cached } from '@/nip05.ts';
|
||||
|
||||
|
@ -9,30 +9,6 @@ const nostrNow = () => Math.floor(new Date().getTime() / 1000);
|
|||
/** Pass to sort() to sort events by date. */
|
||||
const eventDateComparator = (a: Event, b: Event) => b.created_at - a.created_at;
|
||||
|
||||
function getKeys(c: Context) {
|
||||
const auth = c.req.headers.get('Authorization') || '';
|
||||
|
||||
if (auth.startsWith('Bearer ')) {
|
||||
const privatekey = auth.split('Bearer ')[1];
|
||||
const pubkey = getPublicKey(privatekey);
|
||||
|
||||
return {
|
||||
privatekey,
|
||||
pubkey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Return true if the value is a bech32 string, eg for use with NIP-19. */
|
||||
function isBech32(value: unknown): value is string {
|
||||
return typeof value === 'string' && nip19.BECH32_REGEX.test(value);
|
||||
}
|
||||
|
||||
/** Return true if the value is a Nostr pubkey, private key, or event ID. */
|
||||
function isNostrId(value: unknown): value is string {
|
||||
return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|
||||
}
|
||||
|
||||
/** Get pubkey from bech32 string, if applicable. */
|
||||
function bech32ToPubkey(bech32: string): string | undefined {
|
||||
try {
|
||||
|
@ -99,15 +75,4 @@ async function parseBody(req: Request): Promise<unknown> {
|
|||
}
|
||||
}
|
||||
|
||||
export {
|
||||
bech32ToPubkey,
|
||||
eventDateComparator,
|
||||
getKeys,
|
||||
isBech32,
|
||||
isNostrId,
|
||||
lookupAccount,
|
||||
type Nip05,
|
||||
nostrNow,
|
||||
parseBody,
|
||||
parseNip05,
|
||||
};
|
||||
export { bech32ToPubkey, eventDateComparator, lookupAccount, type Nip05, nostrNow, parseBody, parseNip05 };
|
||||
|
|
Loading…
Reference in New Issue