Skip to content

Commit

Permalink
[PROTO-1599] Add OTP support (#7193)
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondjacobson authored Jan 14, 2024
1 parent 39448b8 commit 8d031a4
Show file tree
Hide file tree
Showing 23 changed files with 711 additions and 94 deletions.
4 changes: 2 additions & 2 deletions packages/common/src/services/audius-backend/AudiusBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1591,9 +1591,9 @@ export const audiusBackend = ({
}
}

async function signIn(email: string, password: string) {
async function signIn(email: string, password: string, otp?: string) {
await waitForLibsInit()
return audiusLibs.Account.login(email, password)
return audiusLibs.Account.login(email, password, otp)
}

async function signOut() {
Expand Down
363 changes: 363 additions & 0 deletions packages/identity-service/src/notifications/emails/otp.js

Large diffs are not rendered by default.

48 changes: 33 additions & 15 deletions packages/identity-service/src/routes/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ const models = require('../models')
const {
handleResponse,
successResponse,
errorResponseBadRequest
errorResponseBadRequest,
errorResponseForbidden
} = require('../apiHelpers')
const { validateOtp, sendOtp } = require('../utils/otp')

module.exports = function (app) {
/**
Expand Down Expand Up @@ -73,24 +75,40 @@ module.exports = function (app) {
app.get(
'/authentication',
handleResponse(async (req, res, next) => {
const queryParams = req.query
const { lookupKey, username: email, otp } = req.query
if (!lookupKey) {
return errorResponseBadRequest('Missing lookupKey')
}

if (!email) {
return errorResponseBadRequest('Missing email')
}

if (queryParams && queryParams.lookupKey) {
const lookupKey = queryParams.lookupKey
const existingUser = await models.Authentication.findOne({
where: { lookupKey }
})
const existingUser = await models.Authentication.findOne({
where: { lookupKey }
})

const redis = req.app.get('redis')
const sendgrid = req.app.get('sendgrid')
if (!sendgrid) {
req.logger.error('Missing sendgrid api key')
}

if (existingUser) {
return successResponse(existingUser)
} else {
return errorResponseBadRequest(
'No auth record found for provided lookupKey.'
)
if (existingUser) {
if (!otp) {
await sendOtp({ email, redis })
return errorResponseForbidden('Missing otp')
}
} else {
return errorResponseBadRequest('Missing queryParam lookupKey.')

const isOtpValid = await validateOtp({ email, otp, redis })
if (!isOtpValid) {
return errorResponseBadRequest('Invalid credentials')
}

return successResponse(existingUser)
}

return errorResponseBadRequest('Invalid credentials')
})
)
}
57 changes: 57 additions & 0 deletions packages/identity-service/src/utils/otp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { getOtpEmail } = require('../notifications/emails/otp')

const OTP_CHARS = '0123456789'
const OTP_REDIS_PREFIX = 'otp'
const OTP_EXPIRATION_SECONDS = 600

const generateOtp = () => {
let OTP = ''
for (let i = 0; i < 6; i++) {
OTP += OTP_CHARS[Math.floor(Math.random() * OTP_CHARS.length)]
}
return OTP
}

const getEmail = ({ otp }) => {
const title = 'Your Audius Verification Code is:'
const expire = 'This code will expire in 10 minutes.'
const copyrightYear = new Date().getFullYear().toString()
const formattedOtp = `${otp.substring(0, 3)} ${otp.substring(3, 6)}`
return getOtpEmail({ title, otp: formattedOtp, expire, copyrightYear })
}

const validateOtp = async ({ email, otp, redis }) => {
const storedOtp = await redis.get(`${OTP_REDIS_PREFIX}:${email}`)
return otp === storedOtp
}

const sendOtp = async ({ email, redis, sendgrid }) => {
const otp = generateOtp()
const html = getEmail({
otp
})

const emailParams = {
from: 'The Audius Team <team@audius.co>',
to: email,
subject: 'Your Audius Verification Code',
html,
asm: {
groupId: 26666 // id of unsubscribe group at https://mc.sendgrid.com/unsubscribe-groups
}
}
await redis.set(
`${OTP_REDIS_PREFIX}:${email}`,
otp,
'EX',
OTP_EXPIRATION_SECONDS
)
if (sendgrid) {
await sendgrid.send(emailParams)
}
}

module.exports = {
validateOtp,
sendOtp
}
119 changes: 80 additions & 39 deletions packages/identity-service/test/authenticationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ describe('test authentication routes', function () {
request(app)
.post('/authentication')
.send({
'iv': 'a7407b91ccb1a09a270e79296c88a990',
'cipherText': '00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142'
iv: 'a7407b91ccb1a09a270e79296c88a990',
cipherText:
'00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142'
})
.expect(400, done)
})
Expand All @@ -28,9 +29,11 @@ describe('test authentication routes', function () {
request(app)
.post('/authentication')
.send({
'iv': 'a7407b91ccb1a09a270e79296c88a990',
'cipherText': '00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
iv: 'a7407b91ccb1a09a270e79296c88a990',
cipherText:
'00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
})
.expect(200, done)
})
Expand All @@ -39,87 +42,125 @@ describe('test authentication routes', function () {
await request(app)
.post('/authentication')
.send({
'iv': 'a7407b91ccb1a09a270e79296c88a990',
'cipherText': '00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
iv: 'a7407b91ccb1a09a270e79296c88a990',
cipherText:
'00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
})
.expect(200)

await request(app)
.post('/authentication')
.send({
'iv': 'b7407b91ccb1a09a270e79296c88a990',
'cipherText': '10b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
'lookupKey': '1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
'oldLookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
iv: 'b7407b91ccb1a09a270e79296c88a990',
cipherText:
'10b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
lookupKey:
'1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
oldLookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
})
.expect(200)

const redis = app.get('redis')
await redis.set('otp:dheeraj@audius.co', '123456')
// old lookup key doesn't work
await request(app)
.get('/authentication')
.query({ 'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77' })
.query({
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
username: 'dheeraj@audius.co',
otp: '123456'
})
.expect(400)

// New lookup key works
await request(app)
.get('/authentication')
.query({ 'lookupKey': '1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77' })
.query({
lookupKey:
'1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
username: 'dheeraj@audius.co',
otp: '123456'
})
.expect(200)

// Change back
await request(app)
.post('/authentication')
.send({
'iv': 'b7407b91ccb1a09a270e79296c88a990',
'cipherText': '10b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
'oldLookupKey': '1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
iv: 'b7407b91ccb1a09a270e79296c88a990',
cipherText:
'10b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
oldLookupKey:
'1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
})
.expect(200)

// old lookup key doesn't work
await request(app)
.get('/authentication')
.query({ 'lookupKey': '1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77' })
.query({
lookupKey:
'1bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
username: 'dheeraj@audius.co',
otp: '123456'
})
.expect(400)

// New lookup key works
await request(app)
.get('/authentication')
.query({ 'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77' })
.query({
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
username: 'dheeraj@audius.co',
otp: '123456'
})
.expect(200)
})

it('responds 400 for lookup authentication with invalid lookupKey', function (done) {
// Try getting data without the correct query params, should fail
request(app)
.get('/authentication')
.query({ 'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77' })
.query({
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
username: 'dheeraj@audius.co',
otp: '123456'
})
.expect(400, done)
})

it('responds 200 for lookup authentication with correct params', async function () {
// First, create some data in the db
await request(app)
.post('/authentication')
.send({
'iv': 'a7407b91ccb1a09a270e79296c88a990',
'cipherText': '00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
})

await request(app)
.post('/user')
.send({
'username': 'dheeraj@audius.co',
'walletAddress': '0xaaaaaaaaaaaaaaaaaaaaaaaaa'
})

await request(app).post('/authentication').send({
iv: 'a7407b91ccb1a09a270e79296c88a990',
cipherText:
'00b1684fe58f95ef7bca1442681a61b8aa817a136d3c932dcee2bdcb59454205b73174e71b39fa1d532ee915b6d4ba24e8487603fa63e738de35d3505085a142',
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77'
})

await request(app).post('/user').send({
username: 'dheeraj@audius.co',
walletAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaa'
})

const redis = app.get('redis')
await redis.set('otp:dheeraj@audius.co', '123456')
// Try getting data with the right params
let response = await request(app)
.get('/authentication')
.query({ 'lookupKey': '9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77' })
const response = await request(app).get('/authentication').query({
lookupKey:
'9bdc91e1bb7ef60177131690b18349625778c14656dc17814945b52a3f07ac77',
username: 'dheeraj@audius.co',
otp: '123456'
})

assert.deepStrictEqual(response.statusCode, 200)
})
Expand Down
5 changes: 3 additions & 2 deletions packages/libs/src/api/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class Account extends Base {
/**
* Logs a user into Audius
*/
async login(email: string, password: string) {
async login(email: string, password: string, otp?: string) {
const phases = {
FIND_WALLET: 'FIND_WALLET',
FIND_USER: 'FIND_USER'
Expand All @@ -68,7 +68,8 @@ export class Account extends Base {
this.REQUIRES(Services.HEDGEHOG)

try {
const ownerWallet = await this.hedgehog.login(email, password)
// @ts-ignore - hedgehog.login is overridden
const ownerWallet = await this.hedgehog.login(email, password, otp)
await this.web3Manager.setOwnerWallet(ownerWallet)
} catch (e) {
return { error: (e as Error).message, phase }
Expand Down
8 changes: 4 additions & 4 deletions packages/libs/src/services/hedgehog/Hedgehog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,17 @@ export class Hedgehog {
createKey
)

// we override the login function here because getFn needs both lookupKey and email
// in identity service, but hedgehog only sends lookupKey
hedgehog.login = async (email: string, password: string) => {
// we override the login function here because getFn needs lookupKey, email, and otp
// for identity service.
hedgehog.login = async (email: string, password: string, otp?: string) => {
const lookupKey = await WalletManager.createAuthLookupKey(
email,
password,
createKey
)

// hedgehog property is called username so being consistent instead of calling it email
const data = await this.getFn({ lookupKey, username: email })
const data = await this.getFn({ lookupKey, username: email, otp })

if (data?.iv && data.cipherText) {
const { walletObj, entropy } =
Expand Down
1 change: 1 addition & 0 deletions packages/libs/src/services/identity/IdentityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export class IdentityService {
async getFn(params: {
lookupKey: string
username: string
otp?: string
}): Promise<{ iv: string; cipherText: string }> {
return await this._makeRequest({
url: '/authentication',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ const TrackListItemComponent = (props: TrackListItemComponentProps) => {
isLongFormContent,
albumInfo,
playbackPositionInfo?.status,
showViewAlbum,
isContextPlaylistOwner,
dispatch,
track_id,
Expand Down
Loading

0 comments on commit 8d031a4

Please sign in to comment.