From 1aa2bafc44bfd1c53755d899bb1eb3c4ab778831 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 26 May 2024 21:26:59 -0500 Subject: [PATCH] OAuth: add a "nostr" grant_type --- deno.json | 1 + src/controllers/api/oauth.ts | 55 +++++++++++++++++++++++++++++++++++- src/signers/ConnectSigner.ts | 2 +- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index ac13eb4..b925d20 100644 --- a/deno.json +++ b/deno.json @@ -24,6 +24,7 @@ "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.22.4", + "@scure/base": "npm:@scure/base@^1.1.6", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index c1407f4..b1309bc 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -1,12 +1,16 @@ +import { NConnectSigner, NSchema as n, NSecSigner } from '@nostrify/nostrify'; +import { bech32 } from '@scure/base'; import { encodeBase64 } from '@std/encoding/base64'; import { escape } from 'entities'; -import { nip19 } from 'nostr-tools'; +import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { nostrNow } from '@/utils.ts'; import { parseBody } from '@/utils/api.ts'; import { getClientConnectUri } from '@/utils/connect.ts'; +import { Storages } from '@/storages.ts'; const passwordGrantSchema = z.object({ grant_type: z.literal('password'), @@ -22,10 +26,18 @@ const credentialsGrantSchema = z.object({ grant_type: z.literal('client_credentials'), }); +const nostrGrantSchema = z.object({ + grant_type: z.literal('nostr'), + pubkey: n.id(), + relays: z.string().url().array().optional(), + secret: z.string().optional(), +}); + const createTokenSchema = z.discriminatedUnion('grant_type', [ passwordGrantSchema, codeGrantSchema, credentialsGrantSchema, + nostrGrantSchema, ]); const createTokenController: AppController = async (c) => { @@ -37,6 +49,13 @@ const createTokenController: AppController = async (c) => { } switch (result.data.grant_type) { + case 'nostr': + return c.json({ + access_token: await getToken(result.data), + token_type: 'Bearer', + scope: 'read write follow push', + created_at: nostrNow(), + }); case 'password': return c.json({ access_token: result.data.password, @@ -61,6 +80,40 @@ const createTokenController: AppController = async (c) => { } }; +async function getToken({ pubkey, secret, relays = [] }: z.infer): Promise<`token1${string}`> { + const kysely = await DittoDB.getInstance(); + const token = generateToken(); + + const serverSeckey = generateSecretKey(); + const serverPubkey = getPublicKey(serverSeckey); + + const signer = new NConnectSigner({ + pubkey, + signer: new NSecSigner(serverSeckey), + relay: await Storages.pubsub(), + timeout: 60_000, + }); + + await signer.connect(secret); + + await kysely.insertInto('connections').values({ + api_token: token, + user_pubkey: pubkey, + server_seckey: serverSeckey, + server_pubkey: serverPubkey, + relays: JSON.stringify(relays), + connected_at: new Date(), + }).execute(); + + return token; +} + +/** Generate a bech32 token for the API. */ +function generateToken(): `token1${string}` { + const words = bech32.toWords(generateSecretKey()); + return bech32.encode('token', words); +} + /** Display the OAuth form. */ const oauthController: AppController = async (c) => { const encodedUri = c.req.query('redirect_uri'); diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts index 35b68fb..d4cf603 100644 --- a/src/signers/ConnectSigner.ts +++ b/src/signers/ConnectSigner.ts @@ -21,7 +21,7 @@ export class ConnectSigner implements NostrSigner { // TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list) relay: await Storages.pubsub(), signer, - timeout: 60000, + timeout: 60_000, }); }