Skip to content

Commit

Permalink
refactor proof verification and torusVerifier, add facebookVerifier, …
Browse files Browse the repository at this point in the history
…add email verification by accessToken
  • Loading branch information
serdiukov-o-nordwhale committed Jun 25, 2020
1 parent 3f6d72a commit 6defbdf
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 75 deletions.
1 change: 1 addition & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ TWILIO_PHONE_NUMBER=
NUMBER_OF_ADMIN_WALLET_ACCOUNTS=5
ADMIN_MIN_BALANCE=100000
MARKET_PASSWORD=markettokenmarkettokenmarkettoke
FACEBOOK_GRAPH_API_URL=https://graph.facebook.com/
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ MARKET_PASSWORD=
# You can use it to test Hanuka bonus. Required. Format: { start: 23/12/2019, end: 30/12/2019 }
HANUKA_START_DATE=
HANUKA_END_DATE=
# Facebook GraphAPI base url. You could adjust version to use by adding corresponding suffix (e.g /6.0 /7.0 etc)
FACEBOOK_GRAPH_API_URL=https://graph.facebook.com
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ ZOOM_MINIMAL_MATCHLEVEL=1
NUMBER_OF_ADMIN_WALLET_ACCOUNTS=5
CLAIM_QUEUE_ALLOWED=1
MAUTIC_BASIC_TOKEN=
FACEBOOK_GRAPH_API_URL=https://graph.facebook.com
63 changes: 63 additions & 0 deletions src/imports/facebookVerifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @flow

import Axios from 'axios'
import { isPlainObject } from 'lodash'

import Config from '../server/server.config'

class FacebookVerifier {
constructor(Config, httpFactory) {
const { facebookGraphApiUrl } = Config

this.http = httpFactory({
baseURL: facebookGraphApiUrl,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})

this._configureResponses()
}

async verifyEmail(userEmail, accessToken) {
const params = { fields: 'email', access_token: accessToken }
const userInfo = await this.http.get('/me', { params })

if (!('email' in userInfo)) {
throw new Error(
"Couldn't verify email: user hasn't confirmed it on Facebook " + 'or has used mobile phone number for sign in.'
)
}

return userEmail === userInfo.email
}

_configureResponses() {
const { response } = this.http.interceptors

response.use(response => this._transformResponse(response), exception => this._exceptionInterceptor(exception))
}

_transformResponse(response) {
return response.data
}

_exceptionInterceptor(exception) {
const { response, message } = exception

if (response && isPlainObject(response.data)) {
const graphResponse = this._transformResponse(response)
const { message: graphMessage } = graphResponse.error || {}

exception.message = graphMessage || message
exception.response = graphResponse
} else {
delete exception.response
}

throw exception
}
}

export default new FacebookVerifier(Config, Axios.create)
131 changes: 77 additions & 54 deletions src/imports/torusVerifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,61 @@ import TorusUtils from '@toruslabs/torus.js/dist/torusUtils-node.js'
import moment from 'moment'
import Config from '../server/server.config'
import { recoverPublickey } from '../server/utils/eth'

class GoogleLegacyStrategy {
getVerificationOptions(userRecord) {
return {
verifier: 'google-gooddollar',
identifier: userRecord.email,
emailVerified: true,
mobileVerified: false
}
}
}

class GoogleStrategy {
getVerificationOptions(userRecord) {
return {
verifier: 'google-auth0-gooddollar',
identifier: userRecord.email,
emailVerified: true,
mobileVerified: false
}
}
}

class PasswordlessEmailStrategy {
getVerificationOptions(userRecord) {
return {
verifier: 'google-auth0-gooddollar',
identifier: userRecord.email,
emailVerified: true,
mobileVerified: false
}
}
}

class PasswordlessSMSStrategy {
getVerificationOptions(userRecord) {
return {
verifier: 'google-auth0-gooddollar',
identifier: userRecord.mobile,
emailVerified: false,
mobileVerified: true
}
}
}

class TorusVerifier {
constructor(proxyContract, network) {
this.fetchNodeDetails = new FetchNodeDetails({ network, proxyAddress: proxyContract })
strategies = {}

constructor(proxyContract = null, network = null) {
this.torus = new TorusUtils()

this.fetchNodeDetails = new FetchNodeDetails({
network,
proxyAddress: proxyContract
})
}

async isIdentifierOwner(publicAddress, verifier, identifier) {
Expand All @@ -17,75 +68,47 @@ class TorusVerifier {
{ verifier, verifierId: identifier },
false
)

return publicAddress.toLowerCase() === response.toLowerCase()
}

getVerifierAndIdentifier(torusType, userRecord) {
switch (torusType) {
case 'google-old':
return {
verifier: 'google-gooddollar',
identifier: userRecord.email,
emailVerified: true,
mobileVerified: false
}
case 'google':
return {
verifier: 'google-auth0-gooddollar',
identifier: userRecord.email,
emailVerified: true,
mobileVerified: false
}
case 'facebook':
return {
verifier: 'facebook-gooddollar',
identifier: userRecord.email,
emailVerified: true,
mobileVerified: false
}
case 'auth0-pwdless-email':
return {
verifier: 'google-auth0-gooddollar',
identifier: userRecord.email,
emailVerified: true,
mobileVerified: false
}
case 'auth0-pwdless-sms':
return {
verifier: 'google-auth0-gooddollar',
identifier: userRecord.mobile,
emailVerified: false,
mobileVerified: true
}
default:
throw new Error('unknown torus login type: ' + torusType)
getVerificationOptions(torusType, userRecord) {
const { strategies } = this

if (!torusType || !(torusType in strategies)) {
throw new Error('unknown torus login type: ' + torusType)
}

return strategies[torusType].getVerificationOptions(userRecord)
}
async isFacebookEmail(token, email) {
return true
}

async verifyProof(signature, torusType, userRecord, nonce) {
if (moment().diff(moment(nonce), 'minutes') >= 1) {
throw new Error('torus proof nonce invalid:' + nonce)
}
//TODO: use auth token to verify facebook users
if (torusType === 'facebook') {
const isOwner = this.isFacebookEmail(signature, userRecord.email)
return { emailVerified: isOwner, mobileVerified: false }
}

const { verifier, identifier, emailVerified, mobileVerified } = this.getVerifierAndIdentifier(torusType, userRecord)
const { verifier, identifier, emailVerified, mobileVerified } = this.getVerificationOptions(torusType, userRecord)
const signedPublicKey = recoverPublickey(signature, identifier, nonce)
const isOwner = await this.isIdentifierOwner(signedPublicKey, verifier, identifier)

if (isOwner) {
return { emailVerified, mobileVerified }
}

return { emailVerified: false, mobileVerified: false }
}

addStrategy(torusType, strategyClass) {
this.strategies[torusType] = new strategyClass()
}
}

const verifier =
Config.env === 'production'
? new TorusVerifier()
: new TorusVerifier('0x4023d2a0D330bF11426B12C6144Cfb96B7fa6183', 'ropsten')
const verifierConfig = Config.env === 'production' ? [] : ['0x4023d2a0D330bF11426B12C6144Cfb96B7fa6183', 'ropsten'] // [contract, network]
const verifier = Reflect.construct(TorusVerifier, verifierConfig)

verifier.addStrategy('google', GoogleStrategy)
verifier.addStrategy('google-old', GoogleLegacyStrategy)
verifier.addStrategy('auth0-pwdless-sms', PasswordlessSMSStrategy)
verifier.addStrategy('auth0-pwdless-email', PasswordlessEmailStrategy)

export default verifier
6 changes: 6 additions & 0 deletions src/server/server.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,12 @@ const conf = convict({
format: Number,
env: 'CLAIM_QUEUE_ALLOWED',
default: 0
},
facebookGraphApiUrl: {
doc: 'Facebook GraphAPI base url',
format: '*',
env: 'FACEBOOK_GRAPH_API_URL',
default: 'https://graph.facebook.com'
}
})

Expand Down
49 changes: 28 additions & 21 deletions src/server/storage/storageAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { Mautic } from '../mautic/mauticAPI'
import conf from '../server.config'
import addUserSteps from './addUserSteps'
import { generateMarketToken } from '../utils/market'
import PropsModel from '../db/mongo/models/props'
import TorusVerifier from '../../imports/torusVerifier'
import createUserVerifier from './verifier'

const setup = (app: Router, gunPublic: StorageAPI, storage: StorageAPI) => {
/**
Expand All @@ -28,29 +27,37 @@ const setup = (app: Router, gunPublic: StorageAPI, storage: StorageAPI) => {
passport.authenticate('jwt', { session: false }),
wrapAsync(async (req, res) => {
const { body, user: userRecord } = req
const { user: userPayload } = body
const logger = req.log
logger.debug('new user request:', { data: body.user, userRecord })

//if torus, then we first verify the user mobile/email by verifying it matches the torus public key
//(torus maps identifier such as email and mobile to private/public key pairs)
const { torusProof, torusProvider, torusProofNonce, email, mobile, ...bodyUser } = body.user || {}
if (torusProof) {
const { emailVerified, mobileVerified } = await TorusVerifier.verifyProof(
torusProof,
torusProvider,
body.user,
torusProofNonce
).catch(e => {
logger.warn('TorusVerifier failed:', { e, msg: e.message })
return { emailVerified: false, mobileVerified: false }
})
logger.debug('new user request:', { data: userPayload, userRecord })

const {
torusProof,
torusProvider,
torusProofNonce,
torusAccessToken,
torusIdToken,
email,
mobile,
...payloadWithoutCreds
} = userPayload || {}

// if torus, then we first verify the user mobile/email by verifying it matches the torus public key
// (torus maps identifier such as email and mobile to private/public key pairs)
if (torusProof || torusAccessToken) {
const verifier = createUserVerifier(userRecord, userPayload, logger)

logger.info('TorusVerifier result:', { emailVerified, mobileVerified })
userRecord.smsValidated |= mobileVerified
userRecord.isEmailConfirmed |= emailVerified
if (torusProvider == 'facebook') {
if (torusAccessToken) {
await verifier.verifyEmail(email, torusAccessToken)
}
} else if (torusProof) {
await verifier.verifyProof(torusProof, torusProvider, torusProofNonce)
}
}

//check that user passed all min requirements
// check that user passed all min requirements
if (
['production', 'staging'].includes(conf.env) &&
(userRecord.smsValidated !== true ||
Expand All @@ -63,7 +70,7 @@ const setup = (app: Router, gunPublic: StorageAPI, storage: StorageAPI) => {
throw new Error('You cannot create more than 1 account with the same credentials')
}

const user: UserRecord = defaults(bodyUser, {
const user: UserRecord = defaults(payloadWithoutCreds, {
identifier: userRecord.loggedInAs,
email: get(userRecord, 'otp.email', email), //for development/test use email from body
mobile: get(userRecord, 'otp.mobile', mobile), //for development/test use mobile from body
Expand Down
47 changes: 47 additions & 0 deletions src/server/storage/verifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { assign } from 'lodash'

import TorusVerifier from '../../imports/torusVerifier'
import FacebookVerifier from '../../imports/facebookVerifier'

class UserVerifier {
constructor(userRecord, requestPayload, logger) {
assign(this, { userRecord, requestPayload, logger })
}

async verifyEmail(email, torusAccessToken) {
let emailVerified = false
const { userRecord, logger } = this

try {
emailVerified = await FacebookVerifier.verifyEmail(email, torusAccessToken)
} catch (exception) {
const { message: msg } = exception

logger.warn('FacebookVerifier failed:', { e: exception, msg })
}

logger.info('FacebookVerifier result:', { emailVerified })
userRecord.isEmailConfirmed |= emailVerified
}

async verifyProof(torusProof, torusProvider, torusProofNonce) {
const { userRecord, requestPayload, logger } = this
let verificationResult = { emailVerified: false, mobileVerified: false }

try {
verificationResult = await TorusVerifier.verifyProof(torusProof, torusProvider, requestPayload, torusProofNonce)
} catch (exception) {
const { message: msg } = exception

logger.warn('TorusVerifier failed:', { e: exception, msg })
}

const { emailVerified, mobileVerified } = verificationResult

logger.info('TorusVerifier result:', verificationResult)
userRecord.smsValidated |= mobileVerified
userRecord.isEmailConfirmed |= emailVerified
}
}

export default (userRecord, requestPayload, logger) => new UserVerifier((userRecord, requestPayload, logger))

0 comments on commit 6defbdf

Please sign in to comment.