From a1823a750e157a13a71dc7d0144578177ce9db2a Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 6 May 2022 21:17:53 +0100 Subject: [PATCH] Add authentication --- README.md | 4 +- package.json | 4 ++ src/http.js | 52 +++++++++++++++++++- src/lib/http.js | 1 + src/routes/api/admin/guilds/[guild]/index.js | 7 +++ src/routes/api/admin/guilds/index.js | 9 ++++ src/routes/auth/callback.js | 29 +++++++++++ src/routes/index.js | 4 +- 8 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/lib/http.js create mode 100644 src/routes/auth/callback.js diff --git a/README.md b/README.md index 1cb051d..5d6f6e5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ CONFIG_PATH=./user/config.yml +DISCORD_SECRET= DISCORD_TOKEN= DB_ENCRYPTION_KEY= DB_CONNECTION_URL="" -HTTP_BIND=3000 \ No newline at end of file +HTTP_BIND=3000 +HTTP_EXTERNAL= \ No newline at end of file diff --git a/package.json b/package.json index c9013cb..faf7d7a 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,9 @@ "dependencies": { "@eartharoid/dbf": "^0.0.1", "@eartharoid/dtf": "^2.0.1", + "@fastify/cookie": "^6.0.0", + "@fastify/jwt": "^5.0.1", + "@fastify/oauth2": "^5.0.0", "@prisma/client": "^3.13.0", "discord.js": "^13.6.0", "dotenv": "^16.0.0", @@ -40,6 +43,7 @@ "leeks.js": "^0.2.4", "leekslazylogger": "^4.1.7", "node-dir": "^0.1.17", + "node-fetch": "2", "semver": "^7.3.7", "terminal-link": "^2.1.1", "yaml": "^1.10.2" diff --git a/src/http.js b/src/http.js index 4102191..5e7ccbd 100644 --- a/src/http.js +++ b/src/http.js @@ -1,9 +1,50 @@ const fastify = require('fastify')(); +const oauth = require('@fastify/oauth2'); +const { randomBytes } = require('crypto'); const { short } = require('leeks.js'); const { join } = require('path'); const { readFiles } = require('node-dir'); module.exports = client => { + + // oauth2 plugin + fastify.register(oauth, { + callbackUri: `${process.env.HTTP_EXTERNAL}/auth/callback`, + credentials: { + auth: oauth.DISCORD_CONFIGURATION, + client: { + id: client.user.id, + secret: process.env.DISCORD_SECRET, + }, + }, + name: 'discord', + scope: ['identify'], + startRedirectPath: '/auth/login', + }); + + // cookies plugin + fastify.register(require('@fastify/cookie')); + + // jwt plugin + fastify.register(require('@fastify/jwt'), { + cookie: { + cookieName: 'token', + signed: false, + }, + secret: randomBytes(16).toString('hex'), + }); + + // auth + fastify.decorate('authenticate', async (req, res) => { + try { + const data = await req.jwtVerify(); + if (data.payload.expiresAt < Date.now()) res.redirect('/auth/login'); + } catch (err) { + res.send(err); + } + }); + + // logging fastify.addHook('onResponse', (req, res, done) => { done(); const status = (res.statusCode >= 500 @@ -22,8 +63,12 @@ module.exports = client => { ? '&e' : '&a') + response_time + 'ms'; client.log.info.http(short(`${req.ip} ${req.method} ${req.routerPath ?? '*'} &m-+>&r ${status}&b in ${response_time}`)); + done(); }); + fastify.addHook('onError', async (req, res, err) => client.log.error.http(err)); + + // route loading const dir = join(__dirname, '/routes'); readFiles(dir, @@ -43,12 +88,15 @@ module.exports = client => { .replace('/index', '') || '/'; // remove index const route = require(file); - Object.keys(route).forEach(method => fastify[method](path, { + Object.keys(route).forEach(method => fastify.route({ config: { client }, - ...route[method], + method: method.toUpperCase(), + path, + ...route[method](fastify), })); // register route } + // start server fastify.listen(process.env.HTTP_BIND, (err, addr) => { if (err) client.log.error.http(err); else client.log.success.http(`Listening at ${addr}`); diff --git a/src/lib/http.js b/src/lib/http.js new file mode 100644 index 0000000..9dbcdf2 --- /dev/null +++ b/src/lib/http.js @@ -0,0 +1 @@ +module.exports.domain = process.env.HTTP_EXTERNAL.match(/http(s?):\/\/(?[a-zA-Z0-9\-_.]+)(:\d+)?/).groups.domain; \ No newline at end of file diff --git a/src/routes/api/admin/guilds/[guild]/index.js b/src/routes/api/admin/guilds/[guild]/index.js index e69de29..6dff4d6 100644 --- a/src/routes/api/admin/guilds/[guild]/index.js +++ b/src/routes/api/admin/guilds/[guild]/index.js @@ -0,0 +1,7 @@ +module.exports.get = fastify => ({ + handler: (req, res) => { + const { client } = res.context.config; + return client.guilds.cache.get(req.params.guild); + }, + onRequest: [fastify.authenticate], +}); \ No newline at end of file diff --git a/src/routes/api/admin/guilds/index.js b/src/routes/api/admin/guilds/index.js index e69de29..c3e1d0a 100644 --- a/src/routes/api/admin/guilds/index.js +++ b/src/routes/api/admin/guilds/index.js @@ -0,0 +1,9 @@ +module.exports.get = fastify => ({ + handler: async (req, res) => { + const { client } = res.context.config; + const user = await client.users.fetch(req.user.payload.id); + console.log(req.user.payload.username, user?.tag); + res.send(client.guilds.cache); + }, + onRequest: [fastify.authenticate], +}); \ No newline at end of file diff --git a/src/routes/auth/callback.js b/src/routes/auth/callback.js new file mode 100644 index 0000000..5afa6b1 --- /dev/null +++ b/src/routes/auth/callback.js @@ -0,0 +1,29 @@ +const fetch = require('node-fetch'); +const { domain } = require('../../lib/http'); + +module.exports.get = () => ({ + handler: async function (req, res) { // must NOT use arrow function syntax + const { + access_token, expires_in, + } = await this.discord.getAccessTokenFromAuthorizationCodeFlow(req); + const user = await (await fetch('https://discordapp.com/api/users/@me', { headers: { 'Authorization': `Bearer ${access_token}` } })).json(); + const payload = { + avatar: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`, + discriminator: user.discriminator, + expiresAt: Date.now() + (expires_in * 1000), + id: user.id, + username: user.username, + + }; + const token = this.jwt.sign({ payload }); + res + .setCookie('token', token, { + domain: domain, + httpOnly: true, + path: '/', + sameSite: true, + secure: false, + }) + .redirect('/'); + }, +}); \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index 64cdf38..0a10ed6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,6 +1,6 @@ -module.exports.get = { +module.exports.get = () => ({ handler: (req, res) => { const { client } = res.context.config; return `Hello, I am ${client.user.username}!`; }, -}; \ No newline at end of file +}); \ No newline at end of file