OAuth: add a "nostr" grant_type
This commit is contained in:
parent
c55cd2a977
commit
1aa2bafc44
|
@ -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",
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue