add AuthService as a DM authentication handler. (NEEDS SECURITY PASS)

This commit is contained in:
41666 2019-03-10 15:22:05 -05:00
parent 02a66ee7b0
commit f30ca78e40
6 changed files with 154 additions and 4 deletions

View file

@ -8,6 +8,7 @@ import logger from './logger'
import ServerService from './services/server' import ServerService from './services/server'
import DiscordService from './services/discord' import DiscordService from './services/discord'
import SessionService from './services/sessions' import SessionService from './services/sessions'
import AuthService from './services/auth'
import PresentationService from './services/presentation' import PresentationService from './services/presentation'
import RPCServer from './rpc' import RPCServer from './rpc'
import fetchModels, { type Models } from './models' import fetchModels, { type Models } from './models'
@ -43,7 +44,8 @@ export type AppContext = {
P: PresentationService, P: PresentationService,
RPC: RPCServer, RPC: RPCServer,
M: Models, M: Models,
sql: Sequelize sql: Sequelize,
auth: AuthService
} }
class Roleypoly { class Roleypoly {
@ -112,6 +114,7 @@ class Roleypoly {
this.ctx.server = new ServerService(this.ctx) this.ctx.server = new ServerService(this.ctx)
this.ctx.discord = new DiscordService(this.ctx) this.ctx.discord = new DiscordService(this.ctx)
this.ctx.sessions = new SessionService(this.ctx) this.ctx.sessions = new SessionService(this.ctx)
this.ctx.auth = new AuthService(this.ctx)
this.ctx.P = new PresentationService(this.ctx) this.ctx.P = new PresentationService(this.ctx)
this.ctx.RPC = new RPCServer(this) this.ctx.RPC = new RPCServer(this)
} }

View file

@ -2,6 +2,8 @@
import { type Context } from 'koa' import { type Context } from 'koa'
import { type AppContext, type Router } from '../Roleypoly' import { type AppContext, type Router } from '../Roleypoly'
import ksuid from 'ksuid' import ksuid from 'ksuid'
import logger from '../logger'
const log = logger(__filename)
export default (R: Router, $: AppContext) => { export default (R: Router, $: AppContext) => {
R.post('/api/auth/token', async (ctx: Context) => { R.post('/api/auth/token', async (ctx: Context) => {
@ -35,7 +37,7 @@ export default (R: Router, $: AppContext) => {
}) })
R.get('/api/auth/user', async (ctx: Context) => { R.get('/api/auth/user', async (ctx: Context) => {
const { accessToken } = (ctx.session: { accessToken?: string }) const { accessToken } = ((ctx.session: any): { accessToken?: string })
if (accessToken === undefined) { if (accessToken === undefined) {
ctx.body = { err: 'not_logged_in' } ctx.body = { err: 'not_logged_in' }
ctx.status = 401 ctx.status = 401
@ -81,4 +83,24 @@ export default (R: Router, $: AppContext) => {
R.get('/api/oauth/bot/callback', async (ctx: Context) => { R.get('/api/oauth/bot/callback', async (ctx: Context) => {
// console.log(ctx.request) // console.log(ctx.request)
}) })
R.get('/magic/:challenge', async (ctx: Context) => {
const { challenge } = ((ctx.params: any): { challenge: string })
const chall = await $.auth.fetchDMChallenge({ magic: challenge })
if (chall == null) {
log.warn('bad magic', challenge)
ctx.status = 404
return
}
ctx.session = {
userId: chall.userId,
authType: 'dm',
expiresAt: Date.now() + 1000 * 60 * 60 * 24
}
log.info('logged in via magic', chall)
return ctx.redirect('/')
})
} }

View file

@ -4,10 +4,12 @@ import chalk from 'chalk'
export class Logger { export class Logger {
debugOn: boolean debugOn: boolean
name: string name: string
quietSql: boolean
constructor (name: string, debugOverride: boolean = false) { constructor (name: string, debugOverride: boolean = false) {
this.name = name this.name = name
this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride
this.quietSql = (process.env.DEBUG_SQL !== 'true')
} }
fatal (text: string, ...data: any) { fatal (text: string, ...data: any) {
@ -52,7 +54,7 @@ export class Logger {
} }
sql (logger: Logger, ...data: any) { sql (logger: Logger, ...data: any) {
if (logger.debugOn) { if (logger.debugOn && !logger.quietSql) {
console.log(chalk.bold('DEBUG SQL:\n '), data) console.log(chalk.bold('DEBUG SQL:\n '), data)
} }
} }

14
models/AuthChallenge.js Normal file
View file

@ -0,0 +1,14 @@
// @flow
import type Sequelize, { DataTypes as DT } from 'sequelize'
export default (sql: Sequelize, DataTypes: DT) => {
return sql.define('auth_challenge', {
userId: DataTypes.TEXT,
issuedAt: DataTypes.DATE,
type: DataTypes.ENUM('dm', 'other'),
human: { type: DataTypes.TEXT, unique: true },
magic: { type: DataTypes.TEXT, unique: true }
}, {
indexes: [ { fields: ['userId'] } ]
})
}

51
services/auth.js Normal file
View file

@ -0,0 +1,51 @@
// @flow
import Service from './Service'
import nanoid from 'nanoid'
import moniker from 'moniker'
import type { AppContext } from '../Roleypoly'
// import type { Models } from '../models'
export type DMChallenge = {
userId: string, // snowflake of the user
human: string, // humanized string for input elsewhere, adjective adjective noun
magic: string, // magic URL, a nanoid
issuedAt: Date
}
export default class AuthService extends Service {
M: { AuthChallenge: any }
monikerGen = moniker.generator([ moniker.adjective, moniker.adjective, moniker.noun ], { glue: ' ' })
constructor (ctx: AppContext) {
super(ctx)
this.M = ctx.M
}
async createDMChallenge (userId: string): Promise<DMChallenge> {
const out: DMChallenge = {
userId,
human: this.monikerGen.choose(),
magic: nanoid(10),
issuedAt: new Date()
}
await this.M.AuthChallenge.build({ ...out, type: 'dm' }).save()
this.log.debug('created DM auth challenge', out)
return out
}
async fetchDMChallenge (input: { human: string } | { magic: string }): Promise<?DMChallenge> {
const challenge: ?DMChallenge = this.M.AuthChallenge.findOne({ where: input })
if (challenge == null) {
this.log.debug('challenge not found', challenge)
return null
}
// if issued more than 1 hour ago, it doesn't matter.
if (+challenge.issuedAt + 3.6e5 < new Date()) {
this.log.debug('challenge expired', challenge)
return null
}
return challenge
}
}

View file

@ -9,6 +9,7 @@ import {
type Role, type Role,
type GuildMember, type GuildMember,
type Collection, type Collection,
type User,
Client Client
} from 'discord.js' } from 'discord.js'
@ -172,6 +173,20 @@ class DiscordService extends Service {
} }
} }
getUserPartial (userId: string): ?UserPartial {
const U = this.client.users.get(userId)
if (U == null) {
return null
}
return {
username: U.username,
discriminator: U.discriminator,
avatar: U.displayAvatarURL,
id: U.id
}
}
// on sign out, we revoke the token we had. // on sign out, we revoke the token we had.
// async revokeAuthToken (code, state) { // async revokeAuthToken (code, state) {
// const url = 'https://discordapp.com/api/oauth2/revoke' // const url = 'https://discordapp.com/api/oauth2/revoke'
@ -269,11 +284,54 @@ class DiscordService extends Service {
this.mentionResponse(message) this.mentionResponse(message)
} }
async issueChallenge (message: Message) {
// Create a challenge
const chall = await this.ctx.auth.createDMChallenge(message.author.id)
const randomLines = [
'🐄 A yellow cow is only as bright as it lets itself be. ✨',
'‼ **Did you know?** On this day, at least one second ago, you were right here!',
'<:AkkoC8:437428070849314816> *Reticulating splines...*',
'Also, you look great today <:YumekoWink:439519270376964107>',
'btw, ur bright like a <:diamond:544665968631087125>',
`🌈 psst! pssssst! I'm an expensive bot, would you please spare some change? <https://patreon.com/kata>`,
'📣 have suggestions? wanna help out? join my discord! <https://discord.gg/PWQUVsd>\n*(we\'re nice people, i swear!)*',
`🤖 this bot is at least ${Math.random() * 100}% LIT 🔥`,
'💖 wanna contribute to these witty lines? <https://discord.gg/PWQUVsd> suggest them on our discord!',
'🛠 I am completely open source, check me out!~ <https://github.com/kayteh/roleypoly>'
]
message.channel.send([
'**Hey there!** <a:StockKyaa:435353158462603266>',
'',
`Use this secret code: **${chall.human}**`,
`Or, click here: ${this.ctx.config.appUrl}/magic/${chall.magic}`,
'',
'This code will self-destruct in 1 hour.',
'---',
randomLines[Math.floor(Math.random() * randomLines.length)]
].join('\n'))
}
handleDM (message: Message) {
switch (message.content.toLowerCase()) {
case 'login':
case 'auth':
case 'log in':
this.issueChallenge(message)
}
}
handleMessage (message: Message) { handleMessage (message: Message) {
if (message.author.bot || message.channel.type !== 'text') { // drop bot messages and dms if (message.author.bot) { // drop bot messages
return return
} }
if (message.channel.type === 'dm') {
// handle dm
return this.handleDM(message)
}
if (message.mentions.users.has(this.client.user.id)) { if (message.mentions.users.has(this.client.user.id)) {
if (this.rootUsers.has(message.author.id)) { if (this.rootUsers.has(message.author.id)) {
this.handleCommand(message) this.handleCommand(message)