diff --git a/src/api/oauth.ts b/src/api/oauth.ts index fa3dbac..cbad45b 100644 --- a/src/api/oauth.ts +++ b/src/api/oauth.ts @@ -1,22 +1,98 @@ import { validator, z } from '@/deps.ts'; +import { AppController } from '@/app.ts'; -const createTokenSchema = z.object({ +const passwordGrantSchema = z.object({ + grant_type: z.literal('password'), password: z.string(), }); +const codeGrantSchema = z.object({ + grant_type: z.literal('authorization_code'), + code: z.string(), +}); + +const createTokenSchema = z.discriminatedUnion('grant_type', [ + passwordGrantSchema, + codeGrantSchema, +]); + const createTokenController = validator('json', (value, c) => { const result = createTokenSchema.safeParse(value); if (result.success) { - return c.json({ - access_token: result.data.password, - token_type: 'Bearer', - scope: 'read write follow push', - created_at: Math.floor(new Date().getTime() / 1000), - }); - } else { - return c.json({ error: 'Invalid request' }, 400); + switch (result.data.grant_type) { + case 'password': + return c.json({ + access_token: result.data.password, + token_type: 'Bearer', + scope: 'read write follow push', + created_at: Math.floor(new Date().getTime() / 1000), + }); + case 'authorization_code': + return c.json({ + access_token: result.data.code, + token_type: 'Bearer', + scope: 'read write follow push', + created_at: Math.floor(new Date().getTime() / 1000), + }); + } } + + return c.json({ error: 'Invalid request' }, 400); }); -export { createTokenController }; +/** Display the OAuth form. */ +const oauthController: AppController = (c) => { + const encodedUri = c.req.query('redirect_uri'); + if (!encodedUri) { + return c.text('Missing `redirect_uri` query param.', 422); + } + + const redirectUri = decodeURIComponent(encodedUri); + + // Poor man's XSS check. + // TODO: Render form with JSX. + try { + new URL(redirectUri); + } catch (_e) { + return c.text('Invalid `redirect_uri`.', 422); + } + + c.res.headers.set('content-security-policy', 'default-src \'self\''); + + // TODO: Login with `window.nostr` (NIP-07). + return c.html(` + + + Log in with Ditto + + +
+ + + +
+ + + `); +}; + +const oauthAuthorizeController: AppController = async (c) => { + const formData = await c.req.formData(); + const nostrId = formData.get('nostr_id'); + const redirectUri = formData.get('redirect_uri'); + + if (nostrId && redirectUri) { + const url = new URL(redirectUri.toString()); + const q = new URLSearchParams(); + + q.set('code', nostrId.toString()); + url.search = q.toString(); + + return c.redirect(url.toString()); + } + + return c.text('Missing `redirect_uri` or `nostr_id`.', 422); +}; + +export { createTokenController, oauthAuthorizeController, oauthController }; diff --git a/src/app.ts b/src/app.ts index 9bf9115..5782f26 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,7 @@ import { appCredentialsController, createAppController } from './api/apps.ts'; import { emptyArrayController, emptyObjectController } from './api/fallback.ts'; import homeController from './api/home.ts'; import instanceController from './api/instance.ts'; -import { createTokenController } from './api/oauth.ts'; +import { createTokenController, oauthAuthorizeController, oauthController } from './api/oauth.ts'; import { contextController, createStatusController, statusController } from './api/statuses.ts'; import { requireAuth, setAuth } from './middleware/auth.ts'; @@ -37,6 +37,8 @@ app.post('/api/v1/apps', createAppController); app.post('/oauth/token', createTokenController); app.post('/oauth/revoke', emptyObjectController); +app.post('/oauth/authorize', oauthAuthorizeController); +app.get('/oauth/authorize', oauthController); app.get('/api/v1/accounts/verify_credentials', requireAuth, credentialsController); app.get('/api/v1/accounts/search', accountSearchController);