OAuth: add a "nostr" grant_type

This commit is contained in:
Alex Gleason 2024-05-26 21:26:59 -05:00
parent c55cd2a977
commit 1aa2bafc44
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
3 changed files with 56 additions and 2 deletions

View File

@ -24,6 +24,7 @@
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
"@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0",
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.22.4", "@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", "@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/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0",
"@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0",

View File

@ -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 { encodeBase64 } from '@std/encoding/base64';
import { escape } from 'entities'; import { escape } from 'entities';
import { nip19 } from 'nostr-tools'; import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { DittoDB } from '@/db/DittoDB.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';
import { parseBody } from '@/utils/api.ts'; import { parseBody } from '@/utils/api.ts';
import { getClientConnectUri } from '@/utils/connect.ts'; import { getClientConnectUri } from '@/utils/connect.ts';
import { Storages } from '@/storages.ts';
const passwordGrantSchema = z.object({ const passwordGrantSchema = z.object({
grant_type: z.literal('password'), grant_type: z.literal('password'),
@ -22,10 +26,18 @@ const credentialsGrantSchema = z.object({
grant_type: z.literal('client_credentials'), 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', [ const createTokenSchema = z.discriminatedUnion('grant_type', [
passwordGrantSchema, passwordGrantSchema,
codeGrantSchema, codeGrantSchema,
credentialsGrantSchema, credentialsGrantSchema,
nostrGrantSchema,
]); ]);
const createTokenController: AppController = async (c) => { const createTokenController: AppController = async (c) => {
@ -37,6 +49,13 @@ const createTokenController: AppController = async (c) => {
} }
switch (result.data.grant_type) { 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': case 'password':
return c.json({ return c.json({
access_token: result.data.password, access_token: result.data.password,
@ -61,6 +80,40 @@ const createTokenController: AppController = async (c) => {
} }
}; };
async function getToken({ pubkey, secret, relays = [] }: z.infer<typeof nostrGrantSchema>): 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. */ /** Display the OAuth form. */
const oauthController: AppController = async (c) => { const oauthController: AppController = async (c) => {
const encodedUri = c.req.query('redirect_uri'); const encodedUri = c.req.query('redirect_uri');

View File

@ -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) // TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list)
relay: await Storages.pubsub(), relay: await Storages.pubsub(),
signer, signer,
timeout: 60000, timeout: 60_000,
}); });
} }