-
Notifications
You must be signed in to change notification settings - Fork 4
/
oauth.js
348 lines (326 loc) · 14.6 KB
/
oauth.js
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
// This module demonstrates how your Checkr integration can authorize itself
// with OAuth. OAuth authorization will enable your integration to make
// requests against Checkr on your user's behalf. It will provide a working
// code example for the following Checkr docs:
// - [How to retrieve an OAuth access
// token from Checkr](https://docs.checkr.com/partners/#section/Getting-Started/Connect-your-customers-to-Checkr)
// - [How to validate and respond to Checkr
// webhooks](https://docs.checkr.com/partners/#section/Webhooks/Responding-to-and-securing-webhooks)
// - [How to receive the account.credentialed
// webhook](https://docs.checkr.com/partners/#section/Getting-Started/Customer-account-credentialing)
// - [How to deauthorize your OAuth
// token](https://docs.checkr.com/partners/#section/Getting-Started/Display-customers'-connected-state-and-deauthorization)
import express from 'express'
import database from '../db.js'
import fetch from 'node-fetch'
import chalk from 'chalk'
import {parseJSON, findAccountWithMatchingToken} from '../helpers/index.js'
import {encrypt, decrypt} from '../encryption.js'
const {createHmac, timingSafeEqual} = await import('node:crypto')
const oauthRouter = express.Router()
// OAuth Redirect URL: Create your customer account-level access token
// ---------------
// When your user wishes to sign-up or connect their existing account with
// Checkr. They will click your
// [CheckrConnectLink](https://github.com/checkr/embeds-reference-integration/blob/main/client/src/components/account/CheckrConnectLink.js#L18)
// and follow the sign-up flow instructions. At the end of this flow, Checkr
// will redirect your user to this endpoint on the backend. We chose to handle
// the redirect on the backend instead of the frontend in this reference
// implementation to simplify communication between the backend and frontend.
//
// This function covers:
oauthRouter.get('/api/checkr/oauth', async (req, res) => {
// <details>
// <summary><b>Requesting an OAuth access token from Checkr</b></summary>
//
// Checkr's redirect request will contain the following query parameters:
//
// - a ```code``` variable which is the OAuth authorization code generated by
// Checkr. This is subsequently used to acquire an access token for the
// user's Checkr account.
//
// - a ```state``` variable which you set when creating the
// [CheckrConnectLink](https://github.com/checkr/embeds-reference-integration/blob/main/client/src/components/account/CheckrConnectLink.js#L18).
// We recommend that you set the ```state``` to a value from your product
// that you will associate this Checkr access token with. For example, in
// this scenario each customer account will have a Checkr access token associated with
// them. So our ```state``` value is the ID of the customer account.
//
// Next, you will request an OAuth access token from Checkr. This token will
// be used for all your requests to Checkr on behalf of this user. This
// request will require the following variables:
// - ```CHECKR_API_URL``` which is ```https://api.checkr-staging.com``` in the staging environment and ```https://api.checkr.com``` in production
// - ```REACT_APP_CHECKR_OAUTH_CLIENT_ID``` which is the OAuth Client ID from your [partner application](https://dashboard.checkrhq-staging.net/account/applications). This variable is prefaced with "REACT_APP" because it is also used in our UI.
// - ```CHECKR_OAUTH_CLIENT_SECRET``` which is the OAuth Client Secret from your [partner application](https://dashboard.checkrhq-staging.net/account/applications)
// - ```oauthAuthorizationCode``` from the request query parameters sent by Checkr
//
// The ```CHECKR_API_URL```, ```REACT_APP_CHECKR_OAUTH_CLIENT_ID```, and ```CHECKR_OAUTH_CLIENT_SECRET```
// variables are taken from the app environment (via process.env) because
// these values are different depending on whether you are using the Checkr
// production environment or the Checkr staging environment.
// </details>
const oauthAuthorizationCode = req.query.code
const customerAccountId = req.query.state
const response = await fetch(`${process.env.CHECKR_API_URL}/oauth/tokens`, {
method: 'POST',
body: JSON.stringify({
client_id: process.env.REACT_APP_CHECKR_OAUTH_CLIENT_ID,
client_secret: process.env.CHECKR_OAUTH_CLIENT_SECRET,
code: oauthAuthorizationCode,
}),
headers: {'Content-Type': 'application/json'},
})
const jsonBody = await parseJSON(response)
if (!response.ok) {
res.status(422).send({
errors: {
checkrApiErrors: jsonBody.errors,
},
})
return
}
// <details>
// <summary><b>Storing the token and the Checkr account state</b></summary>
//
// A successful ```HTTP POST``` to
// ```${process.env.CHECKR_API_URL}/oauth/tokens``` will have the following
// response body:
//
// {
// "access_token": "the customer account-level access token",
// "checkr_account_id": "the Checkr customer account ID",
// }
//
// Save this information along with your user's information so that you
// can use their access token to make Checkr requests on the user's behalf. The
// ```access_token``` is a secret, and we encrypt it here to emphasize that
// it should not be stored in plaintext.
//
// The ```checkr_account_id``` will be used later to record that this
// user's account has been credentialed by Checkr.
// </details>
const checkrAccount = {
accessToken: await encrypt(jsonBody.access_token),
id: jsonBody.checkr_account_id,
state: 'uncredentialed',
}
const db = await database()
const account = db.data.accounts.find(a => a.id === customerAccountId)
account.checkrAccount = checkrAccount
await db.write()
// <details>
// <summary><b>Redirecting the user back to their session</b></summary>
//
// After saving this information, we redirect the user to a page that
// shows them that we are waiting for Checkr to credential their account.
// Their account must be credentialed before they can make any background
// check requests with the stored OAuth access token.
//
// Be sure to register this endpoint as the OAuth Redirect URL in your
// [partner application
// configuration](https://dashboard.checkrhq-staging.net/account/applications).
//
// In localhost development environments, our
// [Developing.md](https://github.com/checkr/embeds-reference-integration/blob/main/docs/Developing.md)
// instructions will show you how to use [ngrok](https://ngrok.com/) to
// create a URL for this configuration.
// </details>
if (process.env.NODE_ENV === 'production') {
res.status(200).redirect('/')
} else {
res.status(200).redirect('http://localhost:3000/')
}
})
// OAuth Webhook URL: Responding to Checkr Requests
// ---------------
// This endpoint handles webhook requests sent by Checkr with information about
// events that have occured. This function covers:
oauthRouter.post('/api/checkr/webhooks', async (req, res) => {
// <details>
// <summary><b>Validating Checkr's webhook requests</b></summary>
//
// To prove the integrity of each webhook request, Checkr will create a
// signature with the request and provide it in the ```X-Checkr-Signature```
// header. Before processing the webhook request, it's important to check the
// validity of the header. Refer to Checkr's [webhook
// docs](https://docs.checkr.com/partners/#section/Webhooks/Responding-to-and-securing-webhooks)
// for more information.
// </details>
const validCheckrSignature = (signature, payload) => {
const expectedMac = createHmac(
'sha256',
process.env.CHECKR_OAUTH_CLIENT_SECRET,
)
.update(JSON.stringify(payload))
.digest('hex')
return timingSafeEqual(Buffer.from(signature), Buffer.from(expectedMac))
}
if (!validCheckrSignature(req.headers['x-checkr-signature'], req.body)) {
res.status(400).send({errors: ['invalid x-checkr-signature']})
return
}
const db = await database()
console.log(
chalk.black.bgGreen(' Handling webhook '),
chalk.black.bgYellow(` ${req.body.type} `),
)
switch (req.body.type) {
// <details>
// <summary><b>Receiving the <code>account.credentialed</code> webhook</b></summary>
//
// The ```account.credentialed``` webhook is sent by Checkr to notify you
// that the user's Checkr account has been credentialed. Only credentialed
// accounts can order background checks.
// The ```account.credentialed``` payload will look like this:
//
// {
// "id": "1002d6bca6acdfcbb8442178",
// "object": "event",
// "type": "account.credentialed",
// "created_at": "2018-08-17T01:12:43Z",
// "webhook_url": "https://notify.company.com/checkr",
// "data": {
// "object": {
// "id": "a13f4827d8711ddc75abc56ct",
// "object": "account",
// "uri": "/v1/accounts/a13f4827d8711ddc75abc56ct",
// "created_at": "2018-08-17T01:10:21Z",
// "completed_at": "2018-08-17T01:12:26Z"
// }
// },
// "account_id": "61a01b40fb6dc8305c648784"
// }
//
// The ```account.credentialed``` webhook payload will have an ```account_id```
// that you can use to identify which Checkr Account has been
// credentialed. This ```account_id``` will match the ```checkr_account_id``` from
// the ```${process.env.CHECKR_API_URL}/oauth/tokens``` request above.
// Depending on your partner application's `Pre-Credentialed accounts`
// setting, you may receive the `account.credentialed` webhook too early
// and you may not be in the right state to receive this webhook.
// Whenever you are not in the right state to receive a webhook, respond
// back with the appropriate error status (in this case a 404 Checkr
// Account Not Found). This error status will tell Checkr to retry this
// webhook later.
//
// Once you record that this Checkr account is credentialed, you can make
// background check requests with the access token associated with this
// account.
//
// Successful ```HTTP Status Code 200``` responses to Checkr's webhook
// requests will tell Checkr not to retry this webhook request.
// </details>
case 'account.credentialed':
const checkrAccountId = req.body.account_id
const accountToCredential = db.data.accounts.find(
a => a.checkrAccount && a.checkrAccount.id === checkrAccountId,
)
if (!accountToCredential) {
res.status(404).send({
errors: [
`cannot find account with checkr account ID ${checkrAccountId}`,
],
})
return
}
accountToCredential.checkrAccount.state = 'credentialed'
await db.write()
res.status(200).end()
break
// <details>
// <summary><b>Receiving the <code>token.deauthorized</code> webhook</b></summary>
//
// The ```token.deauthorized``` webhook is sent by Checkr to notify you that
// the user's access token is no longer valid. This can happen when your
// user wishes to stop you from ordering background checks on your behalf.
// <mark>All Checkr integrations are required to support this webhook.</mark>
// The ```token.deauthorized``` webhook payload will look like this:
//
// {
// "id": "627d901159cacb00016149b2",
// "object": "event",
// "type": "token.deauthorized",
// "created_at": "2022-05-12T22:54:09Z",
// "data": {
// "object": {
// "access_code": "your user's access token",
// }
// },
// "account_id": "74376c45cf77c9567565233c"
// }
//
// Use the ```data.object.access_code``` value to identify which of your
// user's access tokens match. <span style="color:red; font-weight:
// bold;">Do not use the account_id in the ```token.deauthorized```
// webhook to do this. The account_id is your partner account ID, not
// your customer's.</span>
//
// Here, we mark the Checkr account as ```disconnected```. If your user
// decides to reconnect your integration with their Checkr account, the
// ```checkr_account_id``` and ```access_token``` will be regenerated.
case 'token.deauthorized':
const checkrAccessToken = req.body.data.object.access_code
const accountToDisconnect = await findAccountWithMatchingToken(
db.data.accounts,
checkrAccessToken,
)
accountToDisconnect.checkrAccount = {state: 'disconnected'}
await db.write()
res.status(204).end()
break
default:
console.warn(`[ WARNING ] Unhandled webhook for type: ${req.body.type}`)
// For more information about Checkr's
// webhooks, please visit the docs
// [here](https://docs.checkr.com/partners/#section/Webhooks/Responding-to-and-securing-webhooks).
}
})
// Deauthorize an access token
// ---------------
// <mark>Checkr integrations are highly encouraged to support
// deauthorizing an user's OAuth access token.</mark> This would disable your
// integration from ordering background checks on the user's behalf. This function
// covers:
oauthRouter.post('/api/checkr/deauthorize', async (req, res) => {
// <details>
// <summary><b>Requesting the deauthorization of a customer's token</b></summary>
//
// The deauthorize request to Checkr is an ```HTTP POST``` that uses basic
// authentication. The basic auth username is the user's access token and
// the password is blank.
// </details>
const plaintextToken = await decrypt(req.body.encryptedToken)
const credentials = `${Buffer.from(`${plaintextToken}:`, 'utf-8').toString(
'base64',
)}`
const response = await fetch(
`${process.env.CHECKR_API_URL}/oauth/deauthorize`,
{
method: 'POST',
headers: {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/json',
},
},
)
if (!response.ok) {
const jsonBody = parseJSON(response)
res.status(422).send({
errors: {
checkrApiErrors: jsonBody.errors,
},
})
return
}
// <details>
// <summary><b>Waiting for the <code>token.deauthorized</code> webhook before storing the account state change</b></summary>
//
// Instead of deleting the user's access token immediately after a
// successful deauthorization request, we wait for the subsequent
// ```token.deauthorized``` webhook to be received. If we delete the access
// token now, our user's Checkr account will be in a bad state to receive
// this webhook.
// </details>
res.status(204).end()
})
export default oauthRouter