diff options
| author | Acid Chicken (硫酸鶏) <root@acid-chicken.com> | 2018-11-15 19:15:04 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2018-11-15 19:15:04 +0900 |
| commit | 9d8f7b081d8c47027583b3085923e2128f622b98 (patch) | |
| tree | badb8b660018fd4967b946cbe65d48650b88110f /src/server | |
| parent | 10.51.2 (diff) | |
| download | misskey-9d8f7b081d8c47027583b3085923e2128f622b98.tar.gz misskey-9d8f7b081d8c47027583b3085923e2128f622b98.tar.bz2 misskey-9d8f7b081d8c47027583b3085923e2128f622b98.zip | |
WIP: Add Discord auth (#3239)
* Add Discord auth
* Apply review 175263424
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/api/endpoints/admin/update-meta.ts | 35 | ||||
| -rw-r--r-- | src/server/api/endpoints/meta.ts | 4 | ||||
| -rw-r--r-- | src/server/api/index.ts | 1 | ||||
| -rw-r--r-- | src/server/api/service/discord.ts | 306 |
4 files changed, 345 insertions, 1 deletions
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 1e4ff959d9..bbae212bd7 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -177,9 +177,30 @@ export const meta = { githubClientSecret: { validator: $.str.optional.nullable, desc: { - 'ja-JP': 'GitHubアプリのClient secret' + 'ja-JP': 'GitHubアプリのClient Secret' } }, + + enableDiscordIntegration: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'Discord連携機能を有効にするか否か' + } + }, + + discordClientId: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'DiscordアプリのClient ID' + } + }, + + discordClientSecret: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'DiscordアプリのClient Secret' + } + } } }; @@ -282,6 +303,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { set.githubClientSecret = ps.githubClientSecret; } + if (ps.enableDiscordIntegration !== undefined) { + set.enableDiscordIntegration = ps.enableDiscordIntegration; + } + + if (ps.discordClientId !== undefined) { + set.discordClientId = ps.discordClientId; + } + + if (ps.discordClientSecret !== undefined) { + set.discordClientSecret = ps.discordClientSecret; + } + await Meta.update({}, { $set: set }, { upsert: true }); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index b324b113c8..56386cc1f5 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -79,6 +79,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { objectStorage: config.drive && config.drive.storage === 'minio', twitter: instance.enableTwitterIntegration, github: instance.enableGithubIntegration, + discord: instance.enableDiscordIntegration, serviceWorker: config.sw ? true : false, userRecommendation: config.user_recommendation ? config.user_recommendation : {} }; @@ -94,6 +95,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { response.enableGithubIntegration = instance.enableGithubIntegration; response.githubClientId = instance.githubClientId; response.githubClientSecret = instance.githubClientSecret; + response.enableDiscordIntegration = instance.enableDiscordIntegration; + response.discordClientId = instance.discordClientId; + response.discordClientSecret = instance.discordClientSecret; } res(response); diff --git a/src/server/api/index.ts b/src/server/api/index.ts index bb8bad8bbe..1cd0028574 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -43,6 +43,7 @@ endpoints.forEach(endpoint => endpoint.meta.requireFile router.post('/signup', require('./private/signup').default); router.post('/signin', require('./private/signin').default); +router.use(require('./service/discord').routes()); router.use(require('./service/github').routes()); router.use(require('./service/github-bot').routes()); router.use(require('./service/twitter').routes()); diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts new file mode 100644 index 0000000000..d90f39ffb3 --- /dev/null +++ b/src/server/api/service/discord.ts @@ -0,0 +1,306 @@ +import * as Koa from 'koa'; +import * as Router from 'koa-router'; +import * as request from 'request'; +import { OAuth2 } from 'oauth'; +import User, { pack, ILocalUser } from '../../../models/user'; +import config from '../../../config'; +import { publishMainStream } from '../../../stream'; +import redis from '../../../db/redis'; +import uuid = require('uuid'); +import signin from '../common/signin'; +import fetchMeta from '../../../misc/fetch-meta'; + +function getUserToken(ctx: Koa.Context) { + return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; +} + +function compareOrigin(ctx: Koa.Context) { + function normalizeUrl(url: string) { + return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; + } + + const referer = ctx.headers['referer']; + + return (normalizeUrl(referer) == normalizeUrl(config.url)); +} + +// Init router +const router = new Router(); + +router.get('/disconnect/discord', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = getUserToken(ctx); + if (!userToken) { + ctx.throw(400, 'signin required'); + return; + } + + const user = await User.findOneAndUpdate({ + host: null, + 'token': userToken + }, { + $set: { + 'discord': null + } + }); + + ctx.body = `Discordの連携を解除しました :v:`; + + // Publish i updated event + publishMainStream(user._id, 'meUpdated', await pack(user, user, { + detail: true, + includeSecrets: true + })); +}); + +async function getOAuth2() { + const meta = await fetchMeta(); + + if (meta.enableDiscordIntegration) { + return new OAuth2( + meta.discordClientId, + meta.discordClientSecret, + 'https://discordapp.com/', + 'api/oauth2/authorize', + 'api/oauth2/token'); + } else { + return null; + } +} + +router.get('/connect/discord', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = getUserToken(ctx); + if (!userToken) { + ctx.throw(400, 'signin required'); + return; + } + + const params = { + redirect_uri: `${config.url}/api/dc/cb`, + scope: ['identify'], + state: uuid(), + response_type: 'code' + }; + + redis.set(userToken, JSON.stringify(params)); + + const oauth2 = await getOAuth2(); + ctx.redirect(oauth2.getAuthorizeUrl(params)); +}); + +router.get('/signin/discord', async ctx => { + const sessid = uuid(); + + const params = { + redirect_uri: `${config.url}/api/dc/cb`, + scope: ['identify'], + state: uuid(), + response_type: 'code' + }; + + const expires = 1000 * 60 * 60; // 1h + ctx.cookies.set('signin_with_discord_session_id', sessid, { + path: '/', + domain: config.host, + secure: config.url.startsWith('https'), + httpOnly: true, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + redis.set(sessid, JSON.stringify(params)); + + const oauth2 = await getOAuth2(); + ctx.redirect(oauth2.getAuthorizeUrl(params)); +}); + +router.get('/dc/cb', async ctx => { + const userToken = getUserToken(ctx); + + const oauth2 = await getOAuth2(); + + if (!userToken) { + const sessid = ctx.cookies.get('signin_with_discord_session_id'); + + if (!sessid) { + ctx.throw(400, 'invalid session'); + return; + } + + const code = ctx.query.code; + + if (!code) { + ctx.throw(400, 'invalid session'); + return; + } + + const { redirect_uri, state } = await new Promise<any>((res, rej) => { + redis.get(sessid, async (_, state) => { + res(JSON.parse(state)); + }); + }); + + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } + + const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) => + oauth2.getOAuthAccessToken( + code, + { + grant_type: 'authorization_code', + redirect_uri + }, + (err, accessToken, refreshToken, result) => { + if (err) + rej(err); + else if (result.error) + rej(result.error); + else + res({ + accessToken, + refreshToken, + expiresDate: Date.now() + Number(result.expires_in) * 1000 + }); + })); + + const { id, username, discriminator } = await new Promise<any>((res, rej) => + request({ + url: 'https://discordapp.com/api/users/@me', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'User-Agent': config.user_agent + } + }, (err, response, body) => { + if (err) + rej(err); + else + res(JSON.parse(body)); + })); + + if (!id || !username || !discriminator) { + ctx.throw(400, 'invalid session'); + return; + } + + let user = await User.findOne({ + host: null, + 'discord.id': id + }) as ILocalUser; + + if (!user) { + ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + user = await User.findOneAndUpdate({ + host: null, + 'discord.id': id + }, { + $set: { + discord: { + accessToken, + refreshToken, + expiresDate, + username, + discriminator + } + } + }) as ILocalUser; + + signin(ctx, user, true); + } else { + const code = ctx.query.code; + + if (!code) { + ctx.throw(400, 'invalid session'); + return; + } + + const { redirect_uri, state } = await new Promise<any>((res, rej) => { + redis.get(userToken, async (_, state) => { + res(JSON.parse(state)); + }); + }); + + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } + + const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) => + oauth2.getOAuthAccessToken( + code, + { + grant_type: 'authorization_code', + redirect_uri + }, + (err, accessToken, refreshToken, result) => { + if (err) + rej(err); + else if (result.error) + rej(result.error); + else + res({ + accessToken, + refreshToken, + expiresDate: Date.now() + Number(result.expires_in) * 1000 + }); + })); + + const { id, username, discriminator } = await new Promise<any>((res, rej) => + request({ + url: 'https://discordapp.com/api/users/@me', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'User-Agent': config.user_agent + } + }, (err, response, body) => { + if (err) + rej(err); + else + res(JSON.parse(body)); + })); + + if (!id || !username || !discriminator) { + ctx.throw(400, 'invalid session'); + return; + } + + const user = await User.findOneAndUpdate({ + host: null, + token: userToken + }, { + $set: { + discord: { + accessToken, + refreshToken, + expiresDate, + id, + username, + discriminator + } + } + }); + + ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; + + // Publish i updated event + publishMainStream(user._id, 'meUpdated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + } +}); + +module.exports = router; |