فهرست منبع

companion: add deauthorization callback endpoint (#2470)

* companion: add deauthorization callback endpoint

* companion: remove custom option

* companion: fix test
Ifedapo .A. Olarewaju 4 سال پیش
والد
کامیت
f311e34bfe

+ 1 - 0
packages/@uppy/companion/env.test.sh

@@ -18,3 +18,4 @@ export COMPANION_INSTAGRAM_SECRET="instagram_secret"
 
 export COMPANION_ZOOM_KEY="zoom_key"
 export COMPANION_ZOOM_SECRET="zoom_secret"
+export COMPANION_ZOOM_VERIFICATION_TOKEN="zoom_verfication_token"

+ 2 - 1
packages/@uppy/companion/src/companion.js

@@ -112,9 +112,10 @@ module.exports.app = (options = {}) => {
   app.use('/s3', s3(options.providerOptions.s3))
   app.use('/url', url())
 
-  app.get('/:providerName/callback', middlewares.hasSessionAndProvider, controllers.callback)
   app.get('/:providerName/connect', middlewares.hasSessionAndProvider, controllers.connect)
   app.get('/:providerName/redirect', middlewares.hasSessionAndProvider, controllers.redirect)
+  app.get('/:providerName/callback', middlewares.hasSessionAndProvider, controllers.callback)
+  app.post('/:providerName/deauthorization/callback', middlewares.hasSessionAndProvider, controllers.deauthorizationCallback)
   app.get('/:providerName/logout', middlewares.hasSessionAndProvider, middlewares.gentleVerifyToken, controllers.logout)
   app.get('/:providerName/send-token', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.sendToken)
   app.get('/:providerName/list/:id?', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)

+ 20 - 0
packages/@uppy/companion/src/server/controllers/deauth-callback.js

@@ -0,0 +1,20 @@
+const { errorToResponse } = require('../provider/error')
+
+function deauthCallback ({ body, companion, headers }, res, next) {
+  // we need the provider instance to decide status codes because
+  // this endpoint does not cater to a uniform client.
+  // It doesn't respond to Uppy client like other endpoints.
+  // Instead it responds to the providers themselves.
+  companion.provider.deauthorizationCallback({ companion, body, headers }, (err, data, status) => {
+    if (err) {
+      const errResp = errorToResponse(err)
+      if (errResp) {
+        return res.status(errResp.code).json({ message: errResp.message })
+      }
+      return next(err)
+    }
+    return res.status(status || 200).json(data)
+  })
+}
+
+module.exports = deauthCallback

+ 1 - 0
packages/@uppy/companion/src/server/controllers/index.js

@@ -1,5 +1,6 @@
 module.exports = {
   callback: require('./callback'),
+  deauthorizationCallback: require('./deauth-callback'),
   sendToken: require('./send-token'),
   get: require('./get'),
   thumbnail: require('./thumbnail'),

+ 10 - 0
packages/@uppy/companion/src/server/provider/Provider.js

@@ -54,6 +54,16 @@ class Provider {
     throw new Error('method not implemented')
   }
 
+  /**
+   * handle deauthorization notification from oauth providers
+   * @param {object} options
+   * @param {function} cb
+   */
+  deauthorizationCallback (options, cb) {
+    // @todo consider doing something like cb(new NotImplementedError()) instead
+    throw new Error('method not implemented')
+  }
+
   /**
    * @returns {string}
    */

+ 40 - 2
packages/@uppy/companion/src/server/provider/zoom/index.js

@@ -12,6 +12,7 @@ const GET_LIST_PATH = '/users/me/recordings'
 const GET_USER_PATH = '/users/me'
 const PAGE_SIZE = 300
 const DEFAULT_RANGE_MOS = 23
+const DEAUTH_EVENT_NAME = 'app_deauthorized'
 
 /**
  * Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api
@@ -242,8 +243,7 @@ class Zoom extends Provider {
   }
 
   logout ({ companion, token }, done) {
-    const key = companion.options.providerOptions.zoom.key
-    const secret = companion.options.providerOptions.zoom.secret
+    const { key, secret } = companion.options.providerOptions.zoom
     const encodedAuth = Buffer.from(`${key}:${secret}`, 'binary').toString('base64')
 
     return this.client
@@ -264,6 +264,44 @@ class Zoom extends Provider {
       })
   }
 
+  deauthorizationCallback ({ companion, body, headers }, done) {
+    if (!body || body.event !== DEAUTH_EVENT_NAME) {
+      return done(null, {}, 400)
+    }
+
+    const { verificationToken } = companion.options.providerOptions.zoom
+    const tokenSupplied = headers.authorization
+    if (!tokenSupplied || verificationToken !== tokenSupplied) {
+      return done(null, {}, 400)
+    }
+
+    const { key, secret } = companion.options.providerOptions.zoom
+    const encodedAuth = Buffer.from(`${key}:${secret}`, 'binary').toString('base64')
+
+    this.client
+      .post('https://api.zoom.us/oauth/data/compliance')
+      .options({
+        headers: {
+          Authorization: `Basic ${encodedAuth}`
+        }
+      })
+      .json({
+        client_id: key,
+        user_id: body.payload.user_id,
+        account_id: body.payload.account_id,
+        deauthorization_event_received: body.payload,
+        compliance_completed: true
+      })
+      .request((err, resp) => {
+        if (err || resp.statusCode !== 200) {
+          logger.error(err, 'provider.zoom.deauth.error')
+          done(this._error(err, resp))
+          return
+        }
+        done(null, {})
+      })
+  }
+
   _error (err, resp) {
     const authErrorCodes = [
       124, // expired token

+ 2 - 1
packages/@uppy/companion/src/standalone/helper.js

@@ -51,7 +51,8 @@ const getConfigFromEnv = () => {
       },
       zoom: {
         key: process.env.COMPANION_ZOOM_KEY,
-        secret: getSecret('COMPANION_ZOOM_SECRET')
+        secret: getSecret('COMPANION_ZOOM_SECRET'),
+        verificationToken: getSecret('COMPANION_ZOOM_VERIFICATION_TOKEN')
       },
       s3: {
         key: process.env.COMPANION_AWS_KEY,

+ 3 - 0
packages/@uppy/companion/test/__mocks__/purest.js

@@ -19,6 +19,9 @@ class MockPurest {
   request (done) {
     if (typeof done === 'function') {
       const responses = {
+        zoom: {
+          default: {}
+        },
         dropbox: {
           default: {
             hash: '0a9f95a989dd4b1851f0103c31e304ce',

+ 102 - 0
packages/@uppy/companion/test/__tests__/companion.js

@@ -318,3 +318,105 @@ describe('handle master oauth redirect', () => {
       .expect(400)
   })
 })
+
+// @todo consider moving this to a separate test file (zoom.js maybe)
+// in general, we should consider testing all providers endpoints separately
+describe('handle deauthorization callback', () => {
+  test('providers without support for callback endpoint', () => {
+    return request(authServer)
+      .post('/dropbox/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .send({
+        foo: 'bar'
+      })
+    // @todo consider receiving 501 instead
+      .expect(500)
+  })
+
+  test('validate that request credentials match', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'wrong-verfication-token')
+      .send({
+        event: 'app_deauthorized',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(400)
+  })
+
+  test('validate request credentials is present', () => {
+    // Authorization header is absent
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .send({
+        event: 'app_deauthorized',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(400)
+  })
+
+  test('validate request content', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'zoom_verfication_token')
+      .send({
+        invalid: 'content'
+      })
+      .expect(400)
+  })
+
+  test('validate request content (event name)', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'zoom_verfication_token')
+      .send({
+        event: 'wrong_event_name',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(400)
+  })
+
+  test('allow valid request', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'zoom_verfication_token')
+      .send({
+        event: 'app_deauthorized',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(200)
+  })
+})

+ 2 - 0
packages/@uppy/companion/test/__tests__/provider-manager.js

@@ -74,6 +74,7 @@ describe('Test Provider options', () => {
     process.env.COMPANION_GOOGLE_SECRET_FILE = process.env.PWD + '/test/resources/google_secret_file'
     process.env.COMPANION_INSTAGRAM_SECRET_FILE = process.env.PWD + '/test/resources/instagram_secret_file'
     process.env.COMPANION_ZOOM_SECRET_FILE = process.env.PWD + '/test/resources/zoom_secret_file'
+    process.env.COMPANION_ZOOM_VERIFICATION_TOKEN_FILE = process.env.PWD + '/test/resources/zoom_verification_token_file'
 
     companionOptions = getCompanionOptions()
 
@@ -83,6 +84,7 @@ describe('Test Provider options', () => {
     expect(grantConfig.google.secret).toBe('elgoog')
     expect(grantConfig.instagram.secret).toBe('margatsni')
     expect(grantConfig.zoom.secret).toBe('u8Z5ceq')
+    expect(companionOptions.providerOptions.zoom.verificationToken).toBe('o0u8Z5c')
   })
 
   test('does not add provider options if protocol and host are not set', () => {

+ 1 - 0
packages/@uppy/companion/test/resources/zoom_verification_token_file

@@ -0,0 +1 @@
+o0u8Z5c