forked from canada-ca/tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauthenticate.js
More file actions
234 lines (214 loc) · 7.27 KB
/
Copy pathauthenticate.js
File metadata and controls
234 lines (214 loc) · 7.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import { GraphQLNonNull, GraphQLString, GraphQLInt } from 'graphql'
import { mutationWithClientMutationId } from 'graphql-relay'
import { t } from '@lingui/macro'
import { authenticateUnion } from '../unions'
import { TfaSendMethodEnum } from '../../enums'
import ms from 'ms'
const { REFRESH_KEY, REFRESH_TOKEN_EXPIRY, AUTHENTICATED_KEY, SIGN_IN_KEY, AUTH_TOKEN_EXPIRY } = process.env
export const authenticate = new mutationWithClientMutationId({
name: 'Authenticate',
description:
'This mutation allows users to give their credentials and retrieve a token that gives them access to restricted content.',
inputFields: () => ({
sendMethod: {
type: new GraphQLNonNull(TfaSendMethodEnum),
description: 'The method that the user wants to receive their authentication code by.',
},
authenticationCode: {
type: new GraphQLNonNull(GraphQLInt),
description: 'Security code found in text msg, or email inbox.',
},
authenticateToken: {
type: new GraphQLNonNull(GraphQLString),
description: 'The JWT that is retrieved from the sign in mutation.',
},
}),
outputFields: () => ({
result: {
type: authenticateUnion,
description: 'Authenticate union returning either a `authResult` or `authenticateError` object.',
resolve: (payload) => payload,
},
}),
mutateAndGetPayload: async (
args,
{
i18n,
response,
query,
collections,
transaction,
uuidv4,
jwt,
auth: { tokenize, verifyToken },
loaders: { loadUserByKey },
validators: { cleanseInput },
},
) => {
// Cleanse Inputs
const authenticationCode = args.authenticationCode
const authenticationToken = cleanseInput(args.authenticateToken)
const sendMethod = cleanseInput(args.sendMethod)
// Gather token parameters
const tokenParameters = verifyToken({
token: authenticationToken,
secret: String(SIGN_IN_KEY),
})
if (tokenParameters.userKey === 'undefined' || typeof tokenParameters.userKey === 'undefined') {
console.warn(`Authentication token does not contain the userKey`)
return {
_type: 'error',
code: 400,
description: i18n._(t`Token value incorrect, please sign in again.`),
}
}
// Gather sign in user
const user = await loadUserByKey.load(tokenParameters.userKey)
// Replace with userRequired()
if (typeof user === 'undefined') {
console.warn(`User: ${tokenParameters.userKey} attempted to authenticate, no account is associated with this id.`)
return {
_type: 'error',
code: 400,
description: i18n._(t`Unable to authenticate. Please try again.`),
}
}
// Check to see if security token matches the user submitted one
if (authenticationCode === user.tfaCode) {
const refreshId = uuidv4()
const loginDate = new Date().toISOString()
const refreshInfo = {
refreshId,
rememberMe: user.refreshInfo.rememberMe,
expiresAt: new Date(new Date().getTime() + ms(REFRESH_TOKEN_EXPIRY)),
}
// Setup Transaction
const trx = await transaction(collections)
// Reset tfa code attempts, and set refresh code
try {
await trx.step(
() => query`
WITH users
UPSERT { _key: ${user._key} }
INSERT {
tfaCode: null,
refreshInfo: ${refreshInfo},
lastLogin: ${loginDate}
}
UPDATE {
tfaCode: null,
refreshInfo: ${refreshInfo},
lastLogin: ${loginDate}
}
IN users
`,
)
} catch (err) {
console.error(
`Trx step error occurred when clearing tfa code and setting refresh id for user: ${user._key} during authentication: ${err}`,
)
await trx.abort()
throw new Error(i18n._(t`Unable to authenticate. Please try again.`))
}
// verify user email
if (sendMethod === 'email' && !user.emailValidated) {
try {
await trx.step(
() => query`
WITH users
UPSERT { _key: ${user._key} }
INSERT {
emailValidated: true,
}
UPDATE {
emailValidated: true,
}
IN users
`,
)
user.emailValidated = true
} catch (err) {
console.error(
`Trx step error occurred when setting email validated to true for user: ${user._key} during authentication: ${err}`,
)
await trx.abort()
throw new Error(i18n._(t`Unable to authenticate. Please try again.`))
}
}
try {
await trx.commit()
} catch (err) {
console.error(`Trx commit error occurred while user: ${user._key} attempted to authenticate: ${err}`)
await trx.abort()
throw new Error(i18n._(t`Unable to authenticate. Please try again.`))
}
const token = tokenize({
expiresIn: AUTH_TOKEN_EXPIRY,
parameters: { userKey: user._key },
secret: String(AUTHENTICATED_KEY),
})
const refreshToken = tokenize({
expiresIn: REFRESH_TOKEN_EXPIRY,
parameters: { userKey: user._key, uuid: refreshId },
secret: String(REFRESH_KEY),
})
// if the user does not want to stay logged in, create http session cookie
let cookieData = {
httpOnly: true,
secure: true,
sameSite: true,
expires: 0,
}
// if user wants to stay logged in create normal http cookie
if (user.refreshInfo.rememberMe) {
const tokenMaxAgeSeconds = jwt.decode(refreshToken).exp - jwt.decode(refreshToken).iat
cookieData = {
maxAge: tokenMaxAgeSeconds * 1000,
httpOnly: true,
secure: true,
sameSite: true,
}
}
response.cookie('refresh_token', refreshToken, cookieData)
console.info(`User: ${user._key} successfully authenticated their account.`)
return {
_type: 'authResult',
token,
user,
}
} else {
console.warn(`User: ${user._key} attempted to authenticate their account, however the tfaCodes did not match.`)
// reset tfa code
const trx = await transaction(collections)
try {
await trx.step(
() => query`
WITH users
UPSERT { _key: ${user._key} }
INSERT {
tfaCode: null,
}
UPDATE {
tfaCode: null,
}
IN users
`,
)
} catch (err) {
console.error(
`Trx step error occurred when clearing tfa code on attempt timeout for user: ${user._key} during authentication: ${err}`,
)
await trx.abort()
throw new Error(i18n._(t`Incorrect TFA code. Please sign in again.`))
}
try {
await trx.commit()
} catch (err) {
console.error(`Trx commit error occurred while user: ${user._key} attempted to authenticate: ${err}`)
await trx.abort()
throw new Error(i18n._(t`Incorrect TFA code. Please sign in again.`))
}
throw new Error(i18n._(t`Incorrect TFA code. Please sign in again.`))
}
},
})