From 876b5da30095bfe202d30e20b55bdbad4ee2de7e Mon Sep 17 00:00:00 2001 From: Yukai Huang Date: Sat, 2 Mar 2019 19:28:59 +0800 Subject: [PATCH] Implement web3 frontend eauth Signed-off-by: Yukai Huang --- app.js | 1 + lib/auth/eauth/index.js | 115 +++++++++++++++++++++++++++ lib/auth/index.js | 1 + lib/config/default.js | 7 ++ lib/config/environment.js | 7 ++ lib/config/index.js | 1 + public/js/cover.js | 2 + public/js/lib/eauth-helper.js | 75 +++++++++++++++++ public/views/shared/signin-modal.ejs | 11 ++- 9 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 lib/auth/eauth/index.js create mode 100644 public/js/lib/eauth-helper.js diff --git a/app.js b/app.js index 4980d45d02..835817886c 100644 --- a/app.js +++ b/app.js @@ -213,6 +213,7 @@ app.locals.authProviders = { oauth2: config.isOAuth2Enable, oauth2ProviderName: config.oauth2.providerName, openID: config.isOpenIDEnable, + eauth: config.isEAuthEnable, email: config.isEmailEnable, allowEmailRegister: config.allowEmailRegister } diff --git a/lib/auth/eauth/index.js b/lib/auth/eauth/index.js new file mode 100644 index 0000000000..b9850c7e5a --- /dev/null +++ b/lib/auth/eauth/index.js @@ -0,0 +1,115 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const Eauth = require('express-eauth') + +const config = require('../../config') +const models = require('../../models') +const logger = require('../../logger') +const { + setReturnToFromReferer +} = require('../utils') + +const eauth = module.exports = Router() + +class EAuthStreategy extends passport.Strategy { + constructor (options, verify) { + if (typeof options === 'function') { + verify = options + options = undefined + } + + options = options || {} + + super(options) + + this.name = options.name || 'eauth' + this._verify = verify + this._passReqToCallback = options.passReqToCallback || false + } + + authenticate (req) { + const verified = (error, user, info) => { + if (error) { + return this.error(error) + } + + if (!user) { + return this.fail(info) + } + + this.success(user, info) + } + + try { + if (this._passReqToCallback && req) { + this._verify(req, verified) + } else { + this._verify(verified) + } + } catch (e) { + return this.error(e) + } + } +} + +passport.use(new EAuthStreategy({ + passReqToCallback: true +}, function (req, done) { + const address = req.eauth.recoveredAddress + if (!address) { + return done(new Error('EAuth failed'), null) + } + + // construct profile + const profile = { + provider: 'eauth', + id: `eauth-${address}`, + emails: [] + } + + const stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ + where: { + profileid: address + }, + defaults: { + profile: stringifiedProfile + } + }).spread(function (user, created) { + if (user) { + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.debug('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.debug('user login: ' + user.id) } + return done(null, user) + } + } + }).catch(function (err) { + logger.error('eth auth failed: ' + err) + return done(err, null) + }) +})) + +const { signature, message, address, banner } = config.eauth +const eauthMiddleware = new Eauth({ signature, message, address, banner }) + +eauth.get('/auth/eauth/:Address', eauthMiddleware, function (req, res) { + return req.eauth.message ? res.send(req.eauth.message) : res.status(400).send() +}) + +eauth.post('/auth/eauth/:Message/:Signature', eauthMiddleware, function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('eauth', { + successReturnToOrRedirect: true + })(req, res, next) +}) diff --git a/lib/auth/index.js b/lib/auth/index.js index fcc39a2a49..6359cd47f9 100644 --- a/lib/auth/index.js +++ b/lib/auth/index.js @@ -47,6 +47,7 @@ if (config.isSAMLEnable) authRouter.use(require('./saml')) if (config.isOAuth2Enable) authRouter.use(require('./oauth2')) if (config.isEmailEnable) authRouter.use(require('./email')) if (config.isOpenIDEnable) authRouter.use(require('./openid')) +if (config.isEAuthEnable) authRouter.use(require('./eauth')) // logout authRouter.get('/logout', function (req, res) { diff --git a/lib/config/default.js b/lib/config/default.js index 95ee1940fe..f959a9d55e 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -165,6 +165,13 @@ module.exports = { email: undefined } }, + eauth: { + enable: false, + signature: 'Signature', + message: 'Message', + address: 'Address', + banner: 'codimd-eauth' + }, plantuml: { server: 'https://www.plantuml.com/plantuml' }, diff --git a/lib/config/environment.js b/lib/config/environment.js index 0867aecf54..d7eb3a4a1a 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -137,6 +137,13 @@ module.exports = { email: process.env.CMD_SAML_ATTRIBUTE_EMAIL } }, + eauth: { + enable: toBooleanConfig(process.env.CMD_EAUTH_ENABLE), + signature: process.env.CMD_EAUTH_SIGNATURE, + message: process.env.CMD_EAUTH_MESSAGE, + address: process.env.CMD_EAUTH_ADDRESS, + banner: process.env.CMD_EAUTH_BANNER + }, plantuml: { server: process.env.CMD_PLANTUML_SERVER }, diff --git a/lib/config/index.js b/lib/config/index.js index 150116f628..7c42d04373 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -124,6 +124,7 @@ config.isMattermostEnable = config.mattermost.clientID && config.mattermost.clie config.isLDAPEnable = config.ldap.url config.isSAMLEnable = config.saml.idpSsoUrl config.isOAuth2Enable = config.oauth2.clientID && config.oauth2.clientSecret +config.isEAuthEnable = config.eauth.enable config.isPDFExportEnable = config.allowPDFExport // Check gitlab api version diff --git a/public/js/cover.js b/public/js/cover.js index 10111bfa08..eae43685a5 100644 --- a/public/js/cover.js +++ b/public/js/cover.js @@ -1,6 +1,8 @@ /* eslint-env browser, jquery */ /* global moment, serverurl */ +import './lib/eauth-helper' + import { checkIfAuth, clearLoginState, diff --git a/public/js/lib/eauth-helper.js b/public/js/lib/eauth-helper.js new file mode 100644 index 0000000000..45720e5a9f --- /dev/null +++ b/public/js/lib/eauth-helper.js @@ -0,0 +1,75 @@ +/* global web3, $ */ + +const { serverurl } = require('./config') + +let data +let message +let signature + +$('#eth-auth-login').on('click', function () { + // #region Detect metamask + if (typeof web3 !== 'undefined') { + console.log('web3 is detected.') + if (web3.currentProvider.isMetaMask === true) { + if (web3.eth.accounts[0] === undefined && !web3.currentProvider.enable) { + return window.alert('Please login metamask first.') + } + } + } else { + return window.alert('No web3 detected. Please install metamask') + } + // #endregion + + function authStart () { + return $.get(`${serverurl}/auth/eauth/${web3.eth.accounts[0]}`, res => { + data = '' + message = '' + const method = 'eth_signTypedData' // $('#method')[0].value + if (method === 'personal_sign') { + data = '0x' + Array.from(res).map(x => x.charCodeAt(0).toString(16)).join('') + message = res + } else if (method === 'eth_signTypedData') { + data = res + message = res[1].value + } + + // Call metamask to sign + const from = web3.eth.accounts[0] + const params = [data, from] + web3.currentProvider.sendAsync({ + method, + params, + from + }, async (err, result) => { + if (err) { + return console.error(err) + } + + if (result.error) { + return console.error(result.error) + } + + signature = result.result + + if (message !== null && signature !== null) { + const form = document.createElement('form') + document.body.appendChild(form) + form.method = 'post' + form.action = `${serverurl}/auth/eauth/${message}/${signature}` + form.submit() + } + }) + }).fail(function () { + // TODO: flash error + }) + } + + if (web3.currentProvider.enable) { + web3.currentProvider.enable() + .then(function () { + authStart() + }) + } else if (web3.eth.accounts[0]) { + authStart() + } +}) diff --git a/public/views/shared/signin-modal.ejs b/public/views/shared/signin-modal.ejs index c209ef4372..286755f2e7 100644 --- a/public/views/shared/signin-modal.ejs +++ b/public/views/shared/signin-modal.ejs @@ -58,7 +58,12 @@ <%= __('Sign in via %s', authProviders.oauth2ProviderName || 'OAuth2') %> <% } %> - <% if ((authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.bitbucket || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.saml || authProviders.oauth2) && authProviders.ldap) { %> + <% if (authProviders.eauth) { %> + + <% } %> + <% if ((authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.bitbucket || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.saml || authProviders.oauth2 || authProviders.eauth) && authProviders.ldap) { %>
<% }%> <% if (authProviders.ldap) { %> @@ -83,7 +88,7 @@ <% } %> - <% if ((authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.bitbucket || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.oauth2) && authProviders.openID) { %> + <% if ((authProviders.facebook || authProviders.twitter || authProviders.github || authProviders.bitbucket || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.oauth2 || authProviders.eauth) && authProviders.openID) { %>
<% }%> <% if (authProviders.openID) { %> @@ -102,7 +107,7 @@ <% } %> - <% if ((authProviders.facebook || authProviders.twitter || authProviders.github|| authProviders.bitbucket || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.oauth2 || authProviders.openID) && authProviders.email) { %> + <% if ((authProviders.facebook || authProviders.twitter || authProviders.github|| authProviders.bitbucket || authProviders.gitlab || authProviders.mattermost || authProviders.dropbox || authProviders.google || authProviders.ldap || authProviders.oauth2 || authProviders.openID || authProviders.eauth) && authProviders.email) { %>
<% }%> <% if (authProviders.email) { %>