From 9bd2b30eb35c4f6955d4d6cb4c022279142e1723 Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Thu, 11 Mar 2021 14:08:48 +0200 Subject: [PATCH 1/9] [WIP] Instagram verification Add instagram routes & register in api handler Add dedicated mgr Add analytics functions for Instagram verification Add overall logic for instagram-request Add working instagram-verify Init tests with error testing --- packages/server/.template.env | 4 + packages/server/serverless.yml | 42 ++++++ .../api/__tests__/instagram-request.test.js | 66 +++++++++ .../api/__tests__/instagram-verify.test.js | 84 ++++++++++++ packages/server/src/api/instagram-request.js | 47 +++++++ packages/server/src/api/instagram-verify.js | 72 ++++++++++ packages/server/src/api_handler.js | 30 ++++ packages/server/src/lib/analytics.js | 14 ++ packages/server/src/lib/instagramMgr.js | 129 ++++++++++++++++++ 9 files changed, 488 insertions(+) create mode 100644 packages/server/src/api/__tests__/instagram-request.test.js create mode 100644 packages/server/src/api/__tests__/instagram-verify.test.js create mode 100644 packages/server/src/api/instagram-request.js create mode 100644 packages/server/src/api/instagram-verify.js create mode 100644 packages/server/src/lib/instagramMgr.js diff --git a/packages/server/.template.env b/packages/server/.template.env index 8ee5ef3..3f79eb6 100644 --- a/packages/server/.template.env +++ b/packages/server/.template.env @@ -10,3 +10,7 @@ TWITTER_CONSUMER_SECRET= TWITTER_ACCESS_TOKEN= TWITTER_ACCESS_TOKEN_SECRET= SEGMENT_WRITE_KEY= +INSTAGRAM_CLIENT_ID= +INSTAGRAM_CLIENT_SECRET= +# Redirect URI should match with the registered one in Instagram App on Facebook Developers portal +INSTAGRAM_REDIRECT_URI= diff --git a/packages/server/serverless.yml b/packages/server/serverless.yml index cc5b05b..ed7c64e 100644 --- a/packages/server/serverless.yml +++ b/packages/server/serverless.yml @@ -25,6 +25,9 @@ provider: TWITTER_CONSUMER_SECRET: ${self:custom.secrets.TWITTER_CONSUMER_SECRET} TWITTER_ACCESS_TOKEN: ${self:custom.secrets.TWITTER_ACCESS_TOKEN} TWITTER_ACCESS_TOKEN_SECRET: ${self:custom.secrets.TWITTER_ACCESS_TOKEN_SECRET} + INSTAGRAM_CLIENT_ID: ${self:custom.secrets.INSTAGRAM_CLIENT_ID} + INSTAGRAM_CLIENT_SECRET: ${self:custom.secrets.INSTAGRAM_CLIENT_SECRET} + INSTAGRAM_REDIRECT_URI: ${self:custom.secrets.INSTAGRAM_REDIRECT_URI} # Enable auto-packing of external modules custom: @@ -116,6 +119,24 @@ functions: method: post cors: true path: /api/v0/request-twitter + request-instagram: + handler: src/api_handler.request_instagram + timeout: 30 + vpc: + securityGroupIds: + - Fn::GetAtt: [ ServerlessSecurityGroup, GroupId ] + subnetIds: + - Ref: PrivateSubnetA + events: + - http: + method: get + cors: true + path: /api/v0/request-instagram + request: + parameters: + querystrings: + did: true + username: true verify-twitter: handler: src/api_handler.verify_twitter timeout: 30 @@ -142,6 +163,27 @@ functions: method: post cors: true path: /api/v0/confirm-discord + verify-instagram: + handler: src/api_handler.verify_instagram + timeout: 30 + vpc: + securityGroupIds: + - Fn::GetAtt: [ ServerlessSecurityGroup, GroupId ] + subnetIds: + - Ref: PrivateSubnetA + events: + - http: + method: get + cors: true + path: /api/v0/confirm-instagram + request: + parameters: + querystrings: + code: true + state: true + error: true + error_reason: true + error_description: true resources: Resources: ${file(cf-resources.yml)} diff --git a/packages/server/src/api/__tests__/instagram-request.test.js b/packages/server/src/api/__tests__/instagram-request.test.js new file mode 100644 index 0000000..4ba6428 --- /dev/null +++ b/packages/server/src/api/__tests__/instagram-request.test.js @@ -0,0 +1,66 @@ +const InstagramRequestHandler = require('../instagram-request') + +describe('InstagramRequestHandler', () => { + let sut + let instagramMgrMock = { validateProfileFromAccount: jest.fn() } + let claimMgrMock = { issue: jest.fn() } + let analyticsMock = { trackRequestInstagram: jest.fn() } + + beforeAll(() => { + sut = new InstagramRequestHandler( + instagramMgrMock, + claimMgrMock, + analyticsMock + ) + }) + + test('empty constructor', () => { + expect(sut).not.toBeUndefined() + }) + + test('no did', done => { + sut.handle( + { queryStringParameters: { username: 'anthony' } }, + {}, + (err, res) => { + expect(err).not.toBeNull() + expect(err.code).toEqual(400) + expect(err.message).toEqual('no did') + done() + } + ) + }) + + test('no username', done => { + sut.handle( + { + queryStringParameters: { did: 'did:123' } + }, + {}, + (err, res) => { + expect(err).not.toBeNull() + expect(err.code).toEqual(400) + expect(err.message).toEqual('no username') + done() + } + ) + }) + + test('happy path', done => { + // instagramMgrMock.findDidInGists.mockReturnValue('http://some.valid.url') + // claimMgrMock.issueInstagram.mockReturnValue('somejwttoken') + // + // sut.handle( + // { + // headers: { origin: 'https://subdomain.3box.io' }, + // body: JSON.stringify({ did: 'did:https:test', instagram_handle: 'test' }) + // }, + // {}, + // (err, res) => { + // expect(err).toBeNull() + // expect(res).toEqual({ verification: 'somejwttoken' }) + done() + // } + // ) + }) +}) diff --git a/packages/server/src/api/__tests__/instagram-verify.test.js b/packages/server/src/api/__tests__/instagram-verify.test.js new file mode 100644 index 0000000..eab9d04 --- /dev/null +++ b/packages/server/src/api/__tests__/instagram-verify.test.js @@ -0,0 +1,84 @@ +const InstagramVerifyHandler = require('../instagram-verify') + +describe('InstagramVerifyHandler', () => { + let sut + let instagramMgrMock = { validateProfileFromAccount: jest.fn() } + let claimMgrMock = { issue: jest.fn(), verifyJWS: jest.fn() } + let analyticsMock = { trackVerifyInstagram: jest.fn() } + + beforeAll(() => { + sut = new InstagramVerifyHandler( + instagramMgrMock, + claimMgrMock, + analyticsMock + ) + }) + + test('empty constructor', () => { + expect(sut).not.toBeUndefined() + }) + + test('error', done => { + sut.handle( + { + queryStringParameters: { + error: 'access_denied', + error_reason: 'user_denied', + error_description: 'The+user+denied+your+request' + } + }, + {}, + (err, res) => { + expect(err).not.toBeNull() + expect(err.code).toEqual(400) + expect(err.message).toEqual('error') + done() + } + ) + }) + + test('no code', done => { + sut.handle( + { + queryStringParameters: { state: 'did:123,challenge' } + }, + {}, + (err, res) => { + expect(err).not.toBeNull() + expect(err.code).toEqual(400) + expect(err.message).toEqual('no code in query param.') + done() + } + ) + }) + + test('no did', done => { + sut.handle( + { + queryStringParameters: { code: '123', state: ',challenge' } + }, + {}, + (err, res) => { + expect(err).not.toBeNull() + expect(err.code).toEqual(400) + expect(err.message).toEqual('no did in query param.') + done() + } + ) + }) + + test('no challengeCode', done => { + sut.handle( + { + queryStringParameters: { code: '123', state: 'did:123,' } + }, + {}, + (err, res) => { + expect(err).not.toBeNull() + expect(err.code).toEqual(400) + expect(err.message).toEqual('no challengeCode in query param.') + done() + } + ) + }) +}) diff --git a/packages/server/src/api/instagram-request.js b/packages/server/src/api/instagram-request.js new file mode 100644 index 0000000..d2096ff --- /dev/null +++ b/packages/server/src/api/instagram-request.js @@ -0,0 +1,47 @@ +class InstagramRequestHandler { + constructor(instagramMgr, claimMgr, analytics) { + this.name = 'InstagramRequestHandler' + this.instagramMgr = instagramMgr + this.claimMgr = claimMgr + this.analytics = analytics + } + + async handle(event, context, cb) { + + let did = event.queryStringParameters.did; + let username = event.queryStringParameters.username; + + if (!did) { + cb({ code: 403, message: 'no did' }) + this.analytics.trackRequestInstagram(did, 403) + return + } + if (!username) { + cb({ code: 400, message: 'no username' }) + this.analytics.trackRequestInstagram(did, 400) + return + } + + let challengeCode = '' + try { + challengeCode = await this.instagramMgr.saveRequest(username, did) + } catch (e) { + console.error(e) + cb({ code: 500, message: `Error while trying save to Redis`}) + this.analytics.trackRequestInstagram(did, 500) + return + } + + const response = { + statusCode: 307, + headers: { + Location: this.instagramMgr.generateRedirectionUrl(did,challengeCode), + }, + body: '', + }; + + cb(null, response) + this.analytics.trackRequestInstagram(did, 307) + } +} +module.exports = InstagramRequestHandler diff --git a/packages/server/src/api/instagram-verify.js b/packages/server/src/api/instagram-verify.js new file mode 100644 index 0000000..cc1da0d --- /dev/null +++ b/packages/server/src/api/instagram-verify.js @@ -0,0 +1,72 @@ +class InstagramVerifyHandler { + constructor(instagramMgr, claimMgr, analytics) { + this.name = 'InstagramVerifyHandler' + this.instagramMgr = instagramMgr + this.claimMgr = claimMgr + this.analytics = analytics + } + + async handle(event, context, cb) { + let error = event.queryStringParameters.error + let errorReason = event.queryStringParameters.error_reason + let errorDescription = event.queryStringParameters.error_description + + if (error) { + cb({ + code: 400, + message: `user cancelled login flow (${error}: ${errorReason} , ${errorDescription} .` + }) + return + } + + let code = event.queryStringParameters.code + let [did, challengeCode] = event.queryStringParameters.state.split(',') + + if (!code) { + cb({ code: 400, message: 'no code in query param.' }) + return + } + if (!did) { + cb({ code: 400, message: 'no did in query param.' }) + return + } + if (!challengeCode) { + cb({ code: 400, message: 'no challengeCode in query param.' }) + return + } + + let userId + let username = '' + try { + const me = this.instagramMgr.validateProfileFromAccount(did, challengeCode, code) + username = me.username + userId = me.id + } catch (e) { + cb({ + code: 500, + message: 'error while trying verify Instagram. ' + e + }) + this.analytics.trackVerifyInstagram(did, 500) + return + } + + let attestation = '' + + try { + attestation = await this.claimMgr.issue({ + did, + username, + userId, + type: 'Instagram' + }) + } catch (e) { + cb({ code: 500, message: 'could not issue a verification claim' + e }) + this.analytics.trackVerifyInstagram(did, 500) + return + } + + cb(null, { attestation }) + this.analytics.trackVerifyInstagram(did, 200) + } +} +module.exports = InstagramVerifyHandler diff --git a/packages/server/src/api_handler.js b/packages/server/src/api_handler.js index b48e1ad..46d9812 100644 --- a/packages/server/src/api_handler.js +++ b/packages/server/src/api_handler.js @@ -6,12 +6,15 @@ const TwitterVerifyHandler = require('./api/twitter-verify') const DiscordVerifyHandler = require('./api/discord-verify') const DiscourseRequestHandler = require('./api/discourse-request') const DiscourseVerifyHandler = require('./api/discourse-verify') +const InstagramRequestHandler = require('./api/instagram-request') +const InstagramVerifyHandler = require('./api/instagram-verify') const DidDocumentHandler = require('./api/diddoc') const GithubMgr = require('./lib/githubMgr') const TwitterMgr = require('./lib/twitterMgr') const DiscordMgr = require('./lib/discordMgr') const DiscourseMgr = require('./lib/discourseMgr') +const InstagramMgr = require('./lib/instagramMgr') const ClaimMgr = require('./lib/claimMgr') const Analytics = require('./lib/analytics') @@ -19,6 +22,7 @@ let githubMgr = new GithubMgr() let twitterMgr = new TwitterMgr() let discordMgr = new DiscordMgr() let discourseMgr = new DiscourseMgr() +let instagramMgr = new InstagramMgr() let claimMgr = new ClaimMgr() const analytics = new Analytics() @@ -89,6 +93,9 @@ const preHandler = (handler, event, context, callback) => { TWITTER_CONSUMER_SECRET: process.env.TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN: process.env.TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.TWITTER_ACCESS_TOKEN_SECRET, + INSTAGRAM_CLIENT_ID: process.env.INSTAGRAM_CLIENT_ID, + INSTAGRAM_CLIENT_SECRET: process.env.INSTAGRAM_CLIENT_SECRET, + INSTAGRAM_REDIRECT_URI: process.env.INSTAGRAM_REDIRECT_URI, SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY } const config = { ...secretsFromEnv, ...envConfig } @@ -98,6 +105,7 @@ const preHandler = (handler, event, context, callback) => { twitterMgr.setSecrets(config) discordMgr.setSecrets(config) discourseMgr.setSecrets(config) + instagramMgr.setSecrets(config) doHandler(handler, event, context, callback) } else { doHandler(handler, event, context, callback) @@ -185,3 +193,25 @@ let discourseVerifyHandler = new DiscourseVerifyHandler( module.exports.verify_discourse = (event, context, callback) => { preHandler(discourseVerifyHandler, event, context, callback) } + +/// ///////////////////// +// Instagram +/// //////////////////// +let instagramRequestHandler = new InstagramRequestHandler( + instagramMgr, + claimMgr, + analytics +) + +module.exports.request_instagram = (event, context, callback) => { + preHandler(instagramRequestHandler, event, context, callback) +} + +let instagramVerifyHandler = new InstagramVerifyHandler( + instagramMgr, + claimMgr, + analytics +) +module.exports.verify_instagram = (event, context, callback) => { + preHandler(instagramVerifyHandler, event, context, callback) +} diff --git a/packages/server/src/lib/analytics.js b/packages/server/src/lib/analytics.js index a38b750..3d2e750 100644 --- a/packages/server/src/lib/analytics.js +++ b/packages/server/src/lib/analytics.js @@ -93,6 +93,20 @@ class Analytics { data.properties = { did_hash: hash(did), status } this._track(data) } + + trackRequestInstagram(did, status) { + let data = {} + data.event = 'request_service_instagram' + data.properties = { did_hash: hash(did), status } + this._track(data) + } + + trackVerifyInstagram(did, status) { + let data = {} + data.event = 'verify_service_instagram' + data.properties = { did_hash: hash(did), status } + this._track(data) + } } module.exports = Analytics diff --git a/packages/server/src/lib/instagramMgr.js b/packages/server/src/lib/instagramMgr.js new file mode 100644 index 0000000..faf242c --- /dev/null +++ b/packages/server/src/lib/instagramMgr.js @@ -0,0 +1,129 @@ +import { randomString } from '@stablelib/random' +import fetch from 'node-fetch' + +const { RedisStore } = require('./store') + +const challengeKey = did => `${did}:instagram` + +class InstagramMgr { + constructor() { + this.client = null + this.client_id = null + this.client_secret = null + this.redirect_uri = null + this.store = {} + } + + isSecretsSet() { + return ( + this.client_id !== null || + this.client_secret !== null || + this.redirect_uri !== null + ) + } + + setSecrets(secrets) { + this.client = fetch + + this.client_id = secrets.INSTAGRAM_CLIENT_ID + this.client_secret = secrets.INSTAGRAM_CLIENT_SECRET + this.redirect_uri = secrets.INSTAGRAM_REDIRECT_URI + + if (secrets.REDIS_URL) + this.store = new RedisStore({ + host: secrets.REDIS_URL + }) + } + + async saveRequest(username, did) { + const challengeCode = randomString(32) + const data = { + did, + username, + timestamp: Date.now(), + challengeCode + } + try { + await this.store.write(challengeKey(did), data) + // console.log('Saved: ' + data) + } catch (e) { + throw new Error(`issue writing to the database for ${did}. ${e}`) + } + // await this.store.quit() + return challengeCode + } + + // Returns verification url if sucessful + generateRedirectionUrl(did, challengeCode) { + return `https://api.instagram.com/oauth/authorize/?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=user_profile&response_type=code&state=${did},${challengeCode}` + } + + async validateProfileFromAccount(did, challengeCode, code) { + if (!did) throw new Error('no did provided') + if (!challengeCode) throw new Error('no challengeCode provided') + + let details + try { + details = await this.store.read(challengeKey(did)) + } catch (e) { + throw new Error( + `Error fetching from the database for user ${did}. Error: ${e}` + ) + } + // console.log('Fetched: ' + JSON.stringify(details)) + if (!details) throw new Error(`No database entry for ${did}.`) + + console.log(details) + + // await this.store.quit() + const { username, timestamp, challengeCode: _challengeCode } = details + + if (challengeCode !== _challengeCode) + throw new Error(`Challenge Code is incorrect`) + + const startTime = new Date(timestamp) + if (new Date() - startTime > 30 * 60 * 1000) + throw new Error( + 'The challenge must have been generated within the last 30 minutes' + ) + + // Convert the Instagram code to an Oauth Access token and query /me to verify the user + const params = new URLSearchParams() + params.append('client_id', this.client_id) + params.append('client_secret', this.client_secret) + params.append('grant_type', 'authorization_code') + params.append('redirect_uri', this.redirect_uri) + params.append('code', code) + + try { + // TODO improve response handling + const response = await this.client( + 'https://api.instagram.com/oauth/access_token', + { + method: 'post', + body: params, + } + ) + const data = await response.json() + console.log(data) + + const me = await this.client( + `https://graph.instagram.com/me?fields=id,username,account_type&access_token=${data.access_token}` + ) + const meData = await me.json() + console.log(meData) + + if (username !== meData.username) { + throw new Error( + `Verification made for the wrong username (${username} != ${meData.username})` + ) + } + + return meData + } catch (e) { + throw new Error('Could not validate user from Instagram') + } + } +} + +module.exports = InstagramMgr From b613207461b9839b174316c656d45f5305ec922f Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Thu, 11 Mar 2021 23:59:31 +0200 Subject: [PATCH 2/9] Add hex lib to generateKeyPair in utils/scripts --- packages/utils/package.json | 1 + packages/utils/scripts/generateKeyPair.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index 88fdef4..e017b59 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -26,6 +26,7 @@ "@babel/core": "^7.12.7", "@babel/preset-env": "^7.12.7", "@stablelib/ed25519": "^1.0.1", + "@stablelib/hex": "^1.0.0", "@stablelib/random": "^1.0.0", "dotenv": "^8.2.0", "eslint": "^7.12.1", diff --git a/packages/utils/scripts/generateKeyPair.js b/packages/utils/scripts/generateKeyPair.js index 08f10c5..736aa7b 100644 --- a/packages/utils/scripts/generateKeyPair.js +++ b/packages/utils/scripts/generateKeyPair.js @@ -1,7 +1,8 @@ const { generateKeyPairFromSeed } = require("@stablelib/ed25519"); const { randomBytes } = require("@stablelib/random"); +const { encode } = require("@stablelib/hex"); const { secretKey, publicKey } = generateKeyPairFromSeed(randomBytes(32)); -console.log(secret); -console.log(public); +console.log(encode(secretKey)); +console.log(encode(publicKey)); From 2763c8852481e51b943db652e957ce65cd0b651a Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Fri, 12 Mar 2021 02:10:27 +0200 Subject: [PATCH 3/9] Update verify method to POST Update verify method from GET to POST Update function & tests Fix tests for instagram-request Remove tmp logs in instagramMgr Add check on username in instagramMgr read from redis Add tests for instagramMgr --- packages/server/serverless.yml | 10 +- .../server/src/__tests__/api_handler.test.js | 16 ++ .../api/__tests__/instagram-request.test.js | 31 +-- .../api/__tests__/instagram-verify.test.js | 47 ++-- packages/server/src/api/instagram-request.js | 18 +- packages/server/src/api/instagram-verify.js | 47 ++-- .../src/lib/__tests__/instagramMgr.test.js | 254 ++++++++++++++++++ packages/server/src/lib/instagramMgr.js | 12 +- 8 files changed, 346 insertions(+), 89 deletions(-) create mode 100644 packages/server/src/lib/__tests__/instagramMgr.test.js diff --git a/packages/server/serverless.yml b/packages/server/serverless.yml index ed7c64e..697e92b 100644 --- a/packages/server/serverless.yml +++ b/packages/server/serverless.yml @@ -173,17 +173,9 @@ functions: - Ref: PrivateSubnetA events: - http: - method: get + method: post cors: true path: /api/v0/confirm-instagram - request: - parameters: - querystrings: - code: true - state: true - error: true - error_reason: true - error_description: true resources: Resources: ${file(cf-resources.yml)} diff --git a/packages/server/src/__tests__/api_handler.test.js b/packages/server/src/__tests__/api_handler.test.js index 8c7e5e8..06760a7 100644 --- a/packages/server/src/__tests__/api_handler.test.js +++ b/packages/server/src/__tests__/api_handler.test.js @@ -76,4 +76,20 @@ describe('apiHandler', () => { done() }) }) + + test('request instagram', done => { + apiHandler.request_instagram({}, {}, (err, res) => { + expect(err).toBeNull() + expect(res).not.toBeNull() + done() + }) + }) + + test('verify instagram', done => { + apiHandler.verify_instagram({}, {}, (err, res) => { + expect(err).toBeNull() + expect(res).not.toBeNull() + done() + }) + }) }) diff --git a/packages/server/src/api/__tests__/instagram-request.test.js b/packages/server/src/api/__tests__/instagram-request.test.js index 4ba6428..a4aef8b 100644 --- a/packages/server/src/api/__tests__/instagram-request.test.js +++ b/packages/server/src/api/__tests__/instagram-request.test.js @@ -2,7 +2,7 @@ const InstagramRequestHandler = require('../instagram-request') describe('InstagramRequestHandler', () => { let sut - let instagramMgrMock = { validateProfileFromAccount: jest.fn() } + let instagramMgrMock = { generateRedirectionUrl: jest.fn() } let claimMgrMock = { issue: jest.fn() } let analyticsMock = { trackRequestInstagram: jest.fn() } @@ -24,7 +24,7 @@ describe('InstagramRequestHandler', () => { {}, (err, res) => { expect(err).not.toBeNull() - expect(err.code).toEqual(400) + expect(err.code).toEqual(403) expect(err.message).toEqual('no did') done() } @@ -47,20 +47,17 @@ describe('InstagramRequestHandler', () => { }) test('happy path', done => { - // instagramMgrMock.findDidInGists.mockReturnValue('http://some.valid.url') - // claimMgrMock.issueInstagram.mockReturnValue('somejwttoken') - // - // sut.handle( - // { - // headers: { origin: 'https://subdomain.3box.io' }, - // body: JSON.stringify({ did: 'did:https:test', instagram_handle: 'test' }) - // }, - // {}, - // (err, res) => { - // expect(err).toBeNull() - // expect(res).toEqual({ verification: 'somejwttoken' }) - done() - // } - // ) + sut.handle( + { + queryStringParameters: { username: 'wallkanda', did: 'did:123' } + }, + {}, + (_err, res) => { + expect(res).not.toBeNull() + // expect(res.status).toEqual(307) + // expect(res.headers.get('Location')).not.toBeNull() + done() + } + ) }) }) diff --git a/packages/server/src/api/__tests__/instagram-verify.test.js b/packages/server/src/api/__tests__/instagram-verify.test.js index eab9d04..c6b09fb 100644 --- a/packages/server/src/api/__tests__/instagram-verify.test.js +++ b/packages/server/src/api/__tests__/instagram-verify.test.js @@ -18,20 +18,16 @@ describe('InstagramVerifyHandler', () => { expect(sut).not.toBeUndefined() }) - test('error', done => { + test('no jws', done => { sut.handle( { - queryStringParameters: { - error: 'access_denied', - error_reason: 'user_denied', - error_description: 'The+user+denied+your+request' - } + body: JSON.stringify({ code: '123' }) }, {}, (err, res) => { expect(err).not.toBeNull() expect(err.code).toEqual(400) - expect(err.message).toEqual('error') + expect(err.message).toEqual('no jws') done() } ) @@ -40,43 +36,36 @@ describe('InstagramVerifyHandler', () => { test('no code', done => { sut.handle( { - queryStringParameters: { state: 'did:123,challenge' } + body: JSON.stringify({ jws: 'abc123' }) }, {}, (err, res) => { expect(err).not.toBeNull() expect(err.code).toEqual(400) - expect(err.message).toEqual('no code in query param.') + expect(err.message).toEqual('no code') done() } ) }) - test('no did', done => { + test('happy path', done => { + instagramMgrMock.validateProfileFromAccount.mockReturnValue({ + id: '123', + username: 'onetwothree' + }) + claimMgrMock.verifyJWS.mockReturnValue({ + payload: { challengeCode: '123' }, + did: 'did:123' + }) + claimMgrMock.issue.mockReturnValue('somejwttoken') sut.handle( { - queryStringParameters: { code: '123', state: ',challenge' } + body: JSON.stringify({ code: 'Azerty123', jws: 'abc123' }) }, {}, (err, res) => { - expect(err).not.toBeNull() - expect(err.code).toEqual(400) - expect(err.message).toEqual('no did in query param.') - done() - } - ) - }) - - test('no challengeCode', done => { - sut.handle( - { - queryStringParameters: { code: '123', state: 'did:123,' } - }, - {}, - (err, res) => { - expect(err).not.toBeNull() - expect(err.code).toEqual(400) - expect(err.message).toEqual('no challengeCode in query param.') + expect(err).toBeNull() + expect(res).toEqual({ attestation: 'somejwttoken' }) done() } ) diff --git a/packages/server/src/api/instagram-request.js b/packages/server/src/api/instagram-request.js index d2096ff..c82655d 100644 --- a/packages/server/src/api/instagram-request.js +++ b/packages/server/src/api/instagram-request.js @@ -7,9 +7,11 @@ class InstagramRequestHandler { } async handle(event, context, cb) { - - let did = event.queryStringParameters.did; - let username = event.queryStringParameters.username; + let did, username + if (event.queryStringParameters) { + did = event.queryStringParameters.did + username = event.queryStringParameters.username + } if (!did) { cb({ code: 403, message: 'no did' }) @@ -27,7 +29,7 @@ class InstagramRequestHandler { challengeCode = await this.instagramMgr.saveRequest(username, did) } catch (e) { console.error(e) - cb({ code: 500, message: `Error while trying save to Redis`}) + cb({ code: 500, message: `Error while trying save to Redis` }) this.analytics.trackRequestInstagram(did, 500) return } @@ -35,11 +37,11 @@ class InstagramRequestHandler { const response = { statusCode: 307, headers: { - Location: this.instagramMgr.generateRedirectionUrl(did,challengeCode), + Location: this.instagramMgr.generateRedirectionUrl(did, challengeCode) }, - body: '', - }; - + body: '' + } + cb(null, response) this.analytics.trackRequestInstagram(did, 307) } diff --git a/packages/server/src/api/instagram-verify.js b/packages/server/src/api/instagram-verify.js index cc1da0d..53654b9 100644 --- a/packages/server/src/api/instagram-verify.js +++ b/packages/server/src/api/instagram-verify.js @@ -7,38 +7,47 @@ class InstagramVerifyHandler { } async handle(event, context, cb) { - let error = event.queryStringParameters.error - let errorReason = event.queryStringParameters.error_reason - let errorDescription = event.queryStringParameters.error_description - - if (error) { - cb({ - code: 400, - message: `user cancelled login flow (${error}: ${errorReason} , ${errorDescription} .` - }) + let body + try { + body = JSON.parse(event.body) + } catch (e) { + cb({ code: 400, message: 'no json body: ' + e.toString() }) return } - let code = event.queryStringParameters.code - let [did, challengeCode] = event.queryStringParameters.state.split(',') - - if (!code) { - cb({ code: 400, message: 'no code in query param.' }) + if (!body.jws) { + cb({ code: 400, message: 'no jws' }) + this.analytics.trackVerifyInstagram(body.jws, 400) return } - if (!did) { - cb({ code: 400, message: 'no did in query param.' }) + if (!body.code) { + cb({ code: 400, message: 'no code' }) + this.analytics.trackVerifyInstagram(body.jws, 400) return } - if (!challengeCode) { - cb({ code: 400, message: 'no challengeCode in query param.' }) + + let did = '' + let challengeCode = '' + const code = body.code + + try { + const unwrappped = await this.claimMgr.verifyJWS(body.jws) + challengeCode = unwrappped.payload.challengeCode + did = unwrappped.did + } catch (e) { + cb({ code: 500, message: 'error while trying to verify the JWS' }) + this.analytics.trackVerifyInstagram(body.jws, 500) return } let userId let username = '' try { - const me = this.instagramMgr.validateProfileFromAccount(did, challengeCode, code) + const me = await this.instagramMgr.validateProfileFromAccount( + did, + challengeCode, + code + ) username = me.username userId = me.id } catch (e) { diff --git a/packages/server/src/lib/__tests__/instagramMgr.test.js b/packages/server/src/lib/__tests__/instagramMgr.test.js new file mode 100644 index 0000000..febe14b --- /dev/null +++ b/packages/server/src/lib/__tests__/instagramMgr.test.js @@ -0,0 +1,254 @@ +const InstagramMgr = require('../instagramMgr') + +describe('InstagramMgr', () => { + let sut + let USERNAME = 'wallkanda' + const CHALLENGE_CODE = '123' + const FAKE_DID = 'did:key:z6MkkyAkqY9bPr8gyQGuJTwQvzk8nsfywHCH4jyM1CgTq4KA' + const USER_ID = '1337' + const FAKE_CODE = '1337' + + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 + sut = new InstagramMgr() + }) + + test('empty constructor', () => { + expect(sut).not.toBeUndefined() + }) + + test('setSecrets', () => { + expect(sut.isSecretsSet()).toEqual(false) + sut.setSecrets({ + REDIS_URL: '123', + REDIS_PASSWORD: 'abc', + INSTAGRAM_CLIENT_ID: '123', + INSTAGRAM_CLIENT_SECRET: 'secret', + INSTAGRAM_REDIRECT_URI: 'https://example.com/auth/' + }) + expect(sut.isSecretsSet()).toEqual(true) + expect(sut.store).not.toBeUndefined() + }) + + test('saveRequest() happy case', done => { + sut.store.write = jest.fn() + sut.store.quit = jest.fn() + sut + .saveRequest(USERNAME, FAKE_DID) + .then(resp => { + expect(/[a-zA-Z0-9]{32}/.test(resp)).toBe(true) + done() + }) + .catch(err => { + fail(err) + done() + }) + }) + + test('generateRedirectionUrl()', done => { + const result = sut.generateRedirectionUrl( + 1337, + 'did:key:z6MkkyAkqY9bPr8gyQGuJTwQvzk8nsfywHCH4jyM1CgTq4KA' + ) + expect(result).toEqual( + 'https://api.instagram.com/oauth/authorize/?client_id=123&redirect_uri=https://example.com/auth/&scope=user_profile&response_type=code&state=1337,did:key:z6MkkyAkqY9bPr8gyQGuJTwQvzk8nsfywHCH4jyM1CgTq4KA' + ) + done() + }) + + test('validateProfileFromAccount() no did', done => { + sut + .validateProfileFromAccount(null, CHALLENGE_CODE, FAKE_CODE) + .then(resp => { + fail("shouldn't return") + }) + .catch(err => { + expect(err.message).toEqual('no did provided') + done() + }) + }) + + test('validateProfileFromAccount() no challengeCode', done => { + sut + .validateProfileFromAccount(FAKE_DID, null, FAKE_CODE) + .then(resp => { + fail("shouldn't return") + }) + .catch(err => { + expect(err.message).toEqual('no challengeCode provided') + done() + }) + }) + + test('validateProfileFromAccount() no authorization code', done => { + sut + .validateProfileFromAccount(FAKE_DID, CHALLENGE_CODE, null) + .then(resp => { + fail("shouldn't return") + }) + .catch(err => { + expect(err.message).toEqual('no authorization code provided') + done() + }) + }) + + test('validateProfileFromAccount() database entry not found', done => { + sut.store.quit = jest.fn() + sut.store.read = jest.fn(() => ({})) + sut + .validateProfileFromAccount(FAKE_DID, CHALLENGE_CODE, FAKE_CODE) + .then(resp => { + fail(`shouldn't return`) + }) + .catch(err => { + expect(err.message).toEqual(`No database entry for ${FAKE_DID}`) + done() + }) + }) + + test('validateProfileFromAccount() incorrect challenge code', done => { + sut.store.quit = jest.fn() + sut.store.read = jest.fn(() => ({ + username: USERNAME, + timestamp: Date.now(), + challengeCode: CHALLENGE_CODE, + userId: USER_ID + })) + sut + .validateProfileFromAccount( + FAKE_DID, + 'incorect challenge code', + FAKE_CODE + ) + .then(resp => { + fail(`shouldn't return`) + }) + .catch(err => { + expect(err.message).toEqual('Challenge Code is incorrect') + done() + }) + }) + + test('validateProfileFromAccount() Challenge created over 30min ago', done => { + sut.store.quit = jest.fn() + sut.store.read = jest.fn(() => ({ + username: USERNAME, + timestamp: Date.now() - 31 * 60 * 1000, + challengeCode: CHALLENGE_CODE, + userId: USER_ID + })) + sut + .validateProfileFromAccount(FAKE_DID, CHALLENGE_CODE, FAKE_CODE) + .then(resp => { + fail("shouldn't return") + }) + .catch(err => { + expect(err.message).toEqual( + 'The challenge must have been generated within the last 30 minutes' + ) + done() + }) + }) + + test('validateProfileFromAccount() bad Authorization code', done => { + sut.store.quit = jest.fn() + sut.store.read = jest.fn(() => ({ + username: USERNAME, + timestamp: Date.now(), + challengeCode: CHALLENGE_CODE, + userId: USER_ID + })) + sut.client = jest.fn().mockRejectedValue(new Error('Async error')) + sut + .validateProfileFromAccount(FAKE_DID, CHALLENGE_CODE, FAKE_CODE) + .then(resp => { + fail("shouldn't return") + }) + .catch(err => { + expect(err.message).toEqual( + 'Could not validate user from Instagram. Error: Async error' + ) + done() + }) + }) + + test('validateProfileFromAccount() bad username returned', done => { + sut.store.quit = jest.fn() + sut.store.read = jest.fn(() => ({ + username: USERNAME, + timestamp: Date.now(), + challengeCode: CHALLENGE_CODE, + userId: USER_ID + })) + sut.client = jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve({ + json: async function () { + return Promise.resolve({ access_token: '123' }) + } + }) + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + json: async function () { + return Promise.resolve({ + username: 'thisisnottheexpectedusername', + id: '123' + }) + } + }) + }) + + sut + .validateProfileFromAccount(FAKE_DID, CHALLENGE_CODE, FAKE_CODE) + .then(resp => { + fail("shouldn't return") + }) + .catch(err => { + expect(err.message).toEqual('Could not validate user from Instagram. Error: Verification made for the wrong username (wallkanda != thisisnottheexpectedusername)') + done() + }) + }) + + test('validateProfileFromAccount()', done => { + sut.store.quit = jest.fn() + sut.store.read = jest.fn(() => ({ + username: USERNAME, + timestamp: Date.now(), + challengeCode: CHALLENGE_CODE, + userId: USER_ID + })) + sut.client = jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve({ + json: async function () { + return Promise.resolve({ access_token: '123' }) + } + }) + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + json: async function () { + return Promise.resolve({ + username: USERNAME, + id: '123' + }) + } + }) + }) + sut + .validateProfileFromAccount(FAKE_DID, CHALLENGE_CODE, FAKE_CODE) + .then(resp => { + // console.log(resp) + expect(resp.username).toEqual(USERNAME) + expect(resp.id).toEqual('123') + done() + }) + .catch(err => { + fail(err) + done() + }) + }) +}) diff --git a/packages/server/src/lib/instagramMgr.js b/packages/server/src/lib/instagramMgr.js index faf242c..b4fbba5 100644 --- a/packages/server/src/lib/instagramMgr.js +++ b/packages/server/src/lib/instagramMgr.js @@ -61,6 +61,7 @@ class InstagramMgr { async validateProfileFromAccount(did, challengeCode, code) { if (!did) throw new Error('no did provided') if (!challengeCode) throw new Error('no challengeCode provided') + if (!code) throw new Error('no authorization code provided') let details try { @@ -71,9 +72,8 @@ class InstagramMgr { ) } // console.log('Fetched: ' + JSON.stringify(details)) - if (!details) throw new Error(`No database entry for ${did}.`) - - console.log(details) + if (!details || !details.username) + throw new Error(`No database entry for ${did}.`) // await this.store.quit() const { username, timestamp, challengeCode: _challengeCode } = details @@ -101,17 +101,15 @@ class InstagramMgr { 'https://api.instagram.com/oauth/access_token', { method: 'post', - body: params, + body: params } ) const data = await response.json() - console.log(data) const me = await this.client( `https://graph.instagram.com/me?fields=id,username,account_type&access_token=${data.access_token}` ) const meData = await me.json() - console.log(meData) if (username !== meData.username) { throw new Error( @@ -121,7 +119,7 @@ class InstagramMgr { return meData } catch (e) { - throw new Error('Could not validate user from Instagram') + throw new Error(`Could not validate user from Instagram. ${e}`) } } } From 3a6cf0f8891b772cb5a8fd52f09baa9a52ae66cd Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Wed, 17 Mar 2021 08:36:46 +0200 Subject: [PATCH 4/9] Add documentation about Instagram verification --- API.md | 104 ++++++++++++++++++++++++++++++++++++++ README.md | 2 +- packages/server/README.md | 3 +- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index 44ac90c..de0c3c4 100644 --- a/API.md +++ b/API.md @@ -350,6 +350,98 @@ When this request is recieved the service does the following: } ``` + +## Request Instagram Verification + +When this request is made the service stores the `did`, `username`, and a *`timestamp`* in it's database of requested instagram verifications. + +Note: due to the OAuth Authorization code flow, the service can provide a convenient HTTP redirection 307 to user by setting the env variable `INSTAGRAM_HTTP_REDIRECT=true` + +**Endpoint:** `GET /api/v0/request-instagram` + +**Query params:** + +```jsx + - did: + - username: +``` + +Example `/api/v0/request-instagram?username=&did=` + +**Response:** + +With `INSTAGRAM_HTTP_REDIRECT=true`, a redirection to [Instagram Authorization Window](https://developers.facebook.com/docs/instagram-basic-display-api/overview/#authorization-window). + +Otherwise: + +```jsx +{ + "status": "success", + "data": { + "statusCode": 307, + "headers": { + "Location": "https://api.instagram.com/oauth/authorize/?client_id=&redirect_uri=&scope=user_profile&response_type=code&state=" + }, + "body": "" + } +} +``` + +## Confirm Instagram Verification + +When this request is received the service does the following: + +1. Validate that the JWS has a correct signature (is signed by the DID in the `kid` property of the JWS) +2. Retrieve the stored request from the database using the DID part of the `kid` if present, otherwise respond with an error +3. Verify that the JWS has content equal to the `challenge-code`, otherwise return error +4. Verify that the *`timestamp`* is from less than 30 minutes ago +5. Call Instagram OAuth API to convert Authorization code to Oauth access token +6. Get Instagram User profile from the Graph API (`/me`) with the previous access token and +7. Verify that the username from Instagram authenticated response is equal to the stored one. +8. Create a Verifiable Credential with the content described below, sign it with the service key (web-did), and send this as the response + +**Endpoint:** `POST /api/v0/confirm-instagram` + +**Body:** + +```jsx +{ + code: + jws: +} +``` + +**Response:** + +```jsx +{ + status: 'success', + data: { + attestation: + } +} +``` + +**Verifiable Credential content:** + +```jsx +{ + sub: , + nbf: 1562950282, // Time jwt was issued + vc: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential'], + credentialSubject: { + account: { + type: 'Instagram', + username: , + userId: + } + } + } +} +``` + # User flows These user flows describe high level user interactions needed to facilitate the verifications. They are mainly meant to illustrate the rough flow so that individual steps that happen in the background can be more easily understood, which is useful if you just want to write a simple test that validates that the services work. The actual user facing implementation can have more optimized UX (e.g. automatically populating a tweet). @@ -395,6 +487,18 @@ These user flows describe high level user interactions needed to facilitate the 1. A JWS containing the *challenge code* is created using the js-did library 2. The JWS is sent to the *confirm discourse* endpoint and the Verifiable Credential is returned +## Instagram verification + +1. User inputs their instagram username and clicks verify + 1. A request with users DID, instagram username is made to the *request instagram* endpoint + 2. The returned *challenge code* is temporarily stored +2. User is redirected to [Instagram Authorization window](https://developers.facebook.com/docs/instagram-basic-display-api/overview/#authorization-window) on Instagram's website to login (if not already logged-in) and approve the request of information access. +This can be done by an HTTP redirect if `INSTAGRAM_HTTP_REDIRECT` env var is set or by the verification website. +3. Instagram Auth API redirects to the `INSTAGRAM_REDIRECT_URI` specified in .env and the Instagram Client App settings. The redirection is done with an OAuth2 authorization `code` in query param and `state` containing the *challenge code*. +4. User clicks verify and they now get the Verifiable credential back from the service + 1. A JWS containing the *challenge code* is created using the js-did library + 2. The JWS and the OAuth2 authorization code are sent to the *confirm isntagram* endpoint and the Verifiable Credential is returned + # Implementation details Here is a few details that will help with the implementation and in particular related to the DID and JWS stuff. Note that some of these are new libraries so definitely reach out if something seems off! diff --git a/README.md b/README.md index 9f9fb5e..0ea2b15 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ -> Services for issuing verifiable credentials that link a decentralized identifier (DID) to various social accounts including Twitter, Github, and Discord. Additional account types can be added in the future. +> Services for issuing verifiable credentials that link a decentralized identifier (DID) to various social accounts including Twitter, Github, Instagram and Discord. Additional account types can be added in the future. ## What's included diff --git a/packages/server/README.md b/packages/server/README.md index 8181f33..7d73b99 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -12,7 +12,7 @@ -> A decentralized identifier (DID) verification service for Ceramic. Available methods include Twitter, Discord, and Github. +> A decentralized identifier (DID) verification service for Ceramic. Available methods include Twitter, Discord, Instagram and Github. ## Install @@ -27,6 +27,7 @@ Copy `.template.env` to `.env` and update the variables. You'll need the followi - Ceramic client url to resolve `@ceramicnetwork/3id-did-resolver` - Twitter developer tokens (you need all 4 items) - Github account username & API token. "Account Settings" > "Developer settings" > "Personal access tokens" +- Instagram app id & secret from [Facebook Apps](https://developers.facebook.com/apps/) and a registered redirect URI - Redis database URL & password - (optional) Segment token From bd1a349a7c52528aa393f3fa04da5326cf3a4a49 Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Wed, 17 Mar 2021 08:57:46 +0200 Subject: [PATCH 5/9] Add HTTP redirection for request-insstagram Add env var INSTAGRAM_HTTP_REDIRECT to enable 307 redir or not Remove useless did in Instagram query param --- packages/server/.template.env | 2 ++ packages/server/src/api/instagram-request.js | 4 ++-- packages/server/src/api_handler.js | 10 +++++++++- packages/server/src/lib/instagramMgr.js | 6 ++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/server/.template.env b/packages/server/.template.env index 3f79eb6..2aedcde 100644 --- a/packages/server/.template.env +++ b/packages/server/.template.env @@ -14,3 +14,5 @@ INSTAGRAM_CLIENT_ID= INSTAGRAM_CLIENT_SECRET= # Redirect URI should match with the registered one in Instagram App on Facebook Developers portal INSTAGRAM_REDIRECT_URI= +# Choose if you want to be redirected to Instagram Auth on calling GET /api/v0/request-instagram, or if you want a JSON +INSTAGRAM_HTTP_REDIRECT=false \ No newline at end of file diff --git a/packages/server/src/api/instagram-request.js b/packages/server/src/api/instagram-request.js index c82655d..6e81824 100644 --- a/packages/server/src/api/instagram-request.js +++ b/packages/server/src/api/instagram-request.js @@ -37,13 +37,13 @@ class InstagramRequestHandler { const response = { statusCode: 307, headers: { - Location: this.instagramMgr.generateRedirectionUrl(did, challengeCode) + Location: this.instagramMgr.generateRedirectionUrl(challengeCode) }, body: '' } - cb(null, response) this.analytics.trackRequestInstagram(did, 307) + cb(null, response) } } module.exports = InstagramRequestHandler diff --git a/packages/server/src/api_handler.js b/packages/server/src/api_handler.js index 46d9812..22d8c10 100644 --- a/packages/server/src/api_handler.js +++ b/packages/server/src/api_handler.js @@ -32,6 +32,13 @@ const doHandler = (handler, event, context, callback) => { let body = JSON.stringify({}) if (handler.name === 'DidDocumentHandler') { body = JSON.stringify(resp) + // Enable GET redirection for Instagram Oauth2 Authorization code flow + } else if ( + handler.name === 'InstagramRequestHandler' && + process.env.INSTAGRAM_HTTP_REDIRECT + ) { + callback(null, resp) + return } else { body = JSON.stringify({ status: 'success', @@ -79,7 +86,8 @@ const preHandler = (handler, event, context, callback) => { !twitterMgr.isSecretsSet() || !claimMgr.isSecretsSet() || !githubMgr.isSecretsSet() || - !discordMgr.isSecretsSet() + !discordMgr.isSecretsSet() || + !instagramMgr.isSecretsSet() ) { const secretsFromEnv = { VERIFICATION_ISSUER_DOMAIN: process.env.VERIFICATION_ISSUER_DOMAIN, diff --git a/packages/server/src/lib/instagramMgr.js b/packages/server/src/lib/instagramMgr.js index b4fbba5..16cdc9c 100644 --- a/packages/server/src/lib/instagramMgr.js +++ b/packages/server/src/lib/instagramMgr.js @@ -11,6 +11,7 @@ class InstagramMgr { this.client_id = null this.client_secret = null this.redirect_uri = null + this.http_redirect = null this.store = {} } @@ -28,6 +29,7 @@ class InstagramMgr { this.client_id = secrets.INSTAGRAM_CLIENT_ID this.client_secret = secrets.INSTAGRAM_CLIENT_SECRET this.redirect_uri = secrets.INSTAGRAM_REDIRECT_URI + this.http_redirect = secrets.INSTAGRAM_HTTP_REDIRECT if (secrets.REDIS_URL) this.store = new RedisStore({ @@ -54,8 +56,8 @@ class InstagramMgr { } // Returns verification url if sucessful - generateRedirectionUrl(did, challengeCode) { - return `https://api.instagram.com/oauth/authorize/?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=user_profile&response_type=code&state=${did},${challengeCode}` + generateRedirectionUrl(challengeCode) { + return `https://api.instagram.com/oauth/authorize/?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=user_profile&response_type=code&state=${challengeCode}` } async validateProfileFromAccount(did, challengeCode, code) { From e74ce63fe28767db248a426a592976e6b090e57a Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Wed, 17 Mar 2021 09:28:59 +0200 Subject: [PATCH 6/9] Fix test for instagram verification --- .../server/src/lib/__tests__/instagramMgr.test.js | 11 +++++------ packages/server/src/lib/instagramMgr.js | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/server/src/lib/__tests__/instagramMgr.test.js b/packages/server/src/lib/__tests__/instagramMgr.test.js index febe14b..dce6c17 100644 --- a/packages/server/src/lib/__tests__/instagramMgr.test.js +++ b/packages/server/src/lib/__tests__/instagramMgr.test.js @@ -46,12 +46,9 @@ describe('InstagramMgr', () => { }) test('generateRedirectionUrl()', done => { - const result = sut.generateRedirectionUrl( - 1337, - 'did:key:z6MkkyAkqY9bPr8gyQGuJTwQvzk8nsfywHCH4jyM1CgTq4KA' - ) + const result = sut.generateRedirectionUrl(1337) expect(result).toEqual( - 'https://api.instagram.com/oauth/authorize/?client_id=123&redirect_uri=https://example.com/auth/&scope=user_profile&response_type=code&state=1337,did:key:z6MkkyAkqY9bPr8gyQGuJTwQvzk8nsfywHCH4jyM1CgTq4KA' + 'https://api.instagram.com/oauth/authorize/?client_id=123&redirect_uri=https://example.com/auth/&scope=user_profile&response_type=code&state=1337' ) done() }) @@ -206,7 +203,9 @@ describe('InstagramMgr', () => { fail("shouldn't return") }) .catch(err => { - expect(err.message).toEqual('Could not validate user from Instagram. Error: Verification made for the wrong username (wallkanda != thisisnottheexpectedusername)') + expect(err.message).toEqual( + 'Could not validate user from Instagram. Error: Verification made for the wrong username (wallkanda != thisisnottheexpectedusername)' + ) done() }) }) diff --git a/packages/server/src/lib/instagramMgr.js b/packages/server/src/lib/instagramMgr.js index 16cdc9c..f8e70d3 100644 --- a/packages/server/src/lib/instagramMgr.js +++ b/packages/server/src/lib/instagramMgr.js @@ -75,7 +75,7 @@ class InstagramMgr { } // console.log('Fetched: ' + JSON.stringify(details)) if (!details || !details.username) - throw new Error(`No database entry for ${did}.`) + throw new Error(`No database entry for ${did}`) // await this.store.quit() const { username, timestamp, challengeCode: _challengeCode } = details From 9e1f94892c2af9d3730282266960e0e48cc9091f Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Wed, 17 Mar 2021 12:25:45 +0200 Subject: [PATCH 7/9] Minor fixes Add missing env var INSTAGRAM_HTTP_REDIRECT in serverless.yml Fix failing tests due to previous update Add new line at the end of .template.env --- packages/server/.template.env | 2 +- packages/server/serverless.yml | 1 + .../api/__tests__/instagram-request.test.js | 37 +++++++++++++++++-- packages/server/src/api/instagram-request.js | 4 ++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/server/.template.env b/packages/server/.template.env index 2aedcde..dcc36c1 100644 --- a/packages/server/.template.env +++ b/packages/server/.template.env @@ -15,4 +15,4 @@ INSTAGRAM_CLIENT_SECRET= # Redirect URI should match with the registered one in Instagram App on Facebook Developers portal INSTAGRAM_REDIRECT_URI= # Choose if you want to be redirected to Instagram Auth on calling GET /api/v0/request-instagram, or if you want a JSON -INSTAGRAM_HTTP_REDIRECT=false \ No newline at end of file +INSTAGRAM_HTTP_REDIRECT=false diff --git a/packages/server/serverless.yml b/packages/server/serverless.yml index 697e92b..c5089c2 100644 --- a/packages/server/serverless.yml +++ b/packages/server/serverless.yml @@ -28,6 +28,7 @@ provider: INSTAGRAM_CLIENT_ID: ${self:custom.secrets.INSTAGRAM_CLIENT_ID} INSTAGRAM_CLIENT_SECRET: ${self:custom.secrets.INSTAGRAM_CLIENT_SECRET} INSTAGRAM_REDIRECT_URI: ${self:custom.secrets.INSTAGRAM_REDIRECT_URI} + INSTAGRAM_HTTP_REDIRECT: ${self:custom.secrets.INSTAGRAM_HTTP_REDIRECT} # Enable auto-packing of external modules custom: diff --git a/packages/server/src/api/__tests__/instagram-request.test.js b/packages/server/src/api/__tests__/instagram-request.test.js index a4aef8b..7169f61 100644 --- a/packages/server/src/api/__tests__/instagram-request.test.js +++ b/packages/server/src/api/__tests__/instagram-request.test.js @@ -2,7 +2,10 @@ const InstagramRequestHandler = require('../instagram-request') describe('InstagramRequestHandler', () => { let sut - let instagramMgrMock = { generateRedirectionUrl: jest.fn() } + let instagramMgrMock = { + generateRedirectionUrl: jest.fn(), + saveRequest: jest.fn() + } let claimMgrMock = { issue: jest.fn() } let analyticsMock = { trackRequestInstagram: jest.fn() } @@ -18,6 +21,15 @@ describe('InstagramRequestHandler', () => { expect(sut).not.toBeUndefined() }) + test('no did nor username', done => { + sut.handle({}, {}, (err, res) => { + expect(err).not.toBeNull() + expect(err.code).toEqual(400) + expect(err.message).toEqual('no did nor username') + done() + }) + }) + test('no did', done => { sut.handle( { queryStringParameters: { username: 'anthony' } }, @@ -47,6 +59,9 @@ describe('InstagramRequestHandler', () => { }) test('happy path', done => { + instagramMgrMock.generateRedirectionUrl.mockReturnValue( + 'http://some.valid.url' + ) sut.handle( { queryStringParameters: { username: 'wallkanda', did: 'did:123' } @@ -54,10 +69,26 @@ describe('InstagramRequestHandler', () => { {}, (_err, res) => { expect(res).not.toBeNull() - // expect(res.status).toEqual(307) - // expect(res.headers.get('Location')).not.toBeNull() + // console.log(res) + expect(res.statusCode).toEqual(307) + expect(res.headers.Location).not.toBeNull() done() } ) }) + + // test('happy path with HTTP redirect', done => { + // sut.handle( + // { + // queryStringParameters: { username: 'wallkanda', did: 'did:123' } + // }, + // {}, + // (_err, res) => { + // expect(res).not.toBeNull() + // expect(res.status).toEqual(307) + // expect(res.headers.get('Location')).not.toBeNull() + // done() + // } + // ) + // }) }) diff --git a/packages/server/src/api/instagram-request.js b/packages/server/src/api/instagram-request.js index 6e81824..aa1fb7a 100644 --- a/packages/server/src/api/instagram-request.js +++ b/packages/server/src/api/instagram-request.js @@ -11,6 +11,10 @@ class InstagramRequestHandler { if (event.queryStringParameters) { did = event.queryStringParameters.did username = event.queryStringParameters.username + } else { + cb({ code: 400, message: 'no did nor username' }) + this.analytics.trackRequestInstagram(did, 400) + return } if (!did) { From 29ca6d53ba08b1c708fcd9b10d06778efe355b81 Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Thu, 18 Mar 2021 08:31:23 +0200 Subject: [PATCH 8/9] Skip test in api_handler for request_instagram (HTTP GET) --- .../server/src/__tests__/api_handler.test.js | 22 +++++++++++++------ packages/server/src/lib/instagramMgr.js | 1 - 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/server/src/__tests__/api_handler.test.js b/packages/server/src/__tests__/api_handler.test.js index 06760a7..dee1b28 100644 --- a/packages/server/src/__tests__/api_handler.test.js +++ b/packages/server/src/__tests__/api_handler.test.js @@ -8,7 +8,10 @@ describe('apiHandler', () => { TWITTER_CONSUMER_SECRET: 'FAKE', KEYPAIR_PRIVATE_KEY: '4baba8f4a', KEYPAIR_PUBLIC_KEY: '04fff936f805ee2', - GITHUB_PERSONAL_ACCESS_TOKEN: 'FAKE' + GITHUB_PERSONAL_ACCESS_TOKEN: 'FAKE', + INSTAGRAM_CLIENT_ID: '123', + INSTAGRAM_CLIENT_SECRET: 'secret', + INSTAGRAM_REDIRECT_URI: 'http://my.url' } process.env.SECRETS = secrets }) @@ -77,12 +80,17 @@ describe('apiHandler', () => { }) }) - test('request instagram', done => { - apiHandler.request_instagram({}, {}, (err, res) => { - expect(err).toBeNull() - expect(res).not.toBeNull() - done() - }) + // FIXME fix the "Error: input is invalid type" + test.skip('request instagram', done => { + apiHandler.request_instagram( + { queryStringParameters: {} }, + {}, + (err, res) => { + expect(err).toBeNull() + expect(res).not.toBeNull() + done() + } + ) }) test('verify instagram', done => { diff --git a/packages/server/src/lib/instagramMgr.js b/packages/server/src/lib/instagramMgr.js index f8e70d3..bd7cdcd 100644 --- a/packages/server/src/lib/instagramMgr.js +++ b/packages/server/src/lib/instagramMgr.js @@ -98,7 +98,6 @@ class InstagramMgr { params.append('code', code) try { - // TODO improve response handling const response = await this.client( 'https://api.instagram.com/oauth/access_token', { From de018cc11d0ff21274a16904131db0a34a6f858e Mon Sep 17 00:00:00 2001 From: Anthony Graignic Date: Thu, 21 Oct 2021 22:15:15 +0200 Subject: [PATCH 9/9] Fix request instagram test & add useful commands in package.json --- packages/server/package.json | 6 ++++-- .../server/src/__tests__/api_handler.test.js | 17 ++++++----------- packages/server/src/api/instagram-request.js | 3 +-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 580b67b..9123041 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -6,7 +6,9 @@ "scripts": { "lint": "eslint ./src ", "test": "jest --testPathPattern=src/ --detectOpenHandles --setupFiles dotenv/config", - "coverage": "jest --coverage" + "coverage": "jest --coverage", + "redis:docker": "docker run -d -h redis -e REDIS_PASSWORD=redis -v redis-data:/data -p 6379:6379 --name redis redis /bin/sh -c 'redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}'", + "start": "sls offline --host 0.0.0.0 --port 3080 --trace-warnings" }, "repository": "git@github.com:ceramicstudio/identitylink-services.git", "author": "Patrick Gallagher ", @@ -49,4 +51,4 @@ "serverless-offline": "^5.10.1", "standard": "^14.0.0" } -} +} \ No newline at end of file diff --git a/packages/server/src/__tests__/api_handler.test.js b/packages/server/src/__tests__/api_handler.test.js index dee1b28..4e76c3f 100644 --- a/packages/server/src/__tests__/api_handler.test.js +++ b/packages/server/src/__tests__/api_handler.test.js @@ -80,17 +80,12 @@ describe('apiHandler', () => { }) }) - // FIXME fix the "Error: input is invalid type" - test.skip('request instagram', done => { - apiHandler.request_instagram( - { queryStringParameters: {} }, - {}, - (err, res) => { - expect(err).toBeNull() - expect(res).not.toBeNull() - done() - } - ) + test('request instagram', done => { + apiHandler.request_instagram({}, {}, (err, res) => { + expect(err).toBeNull() + expect(res).not.toBeNull() + done() + }) }) test('verify instagram', done => { diff --git a/packages/server/src/api/instagram-request.js b/packages/server/src/api/instagram-request.js index aa1fb7a..4dbb705 100644 --- a/packages/server/src/api/instagram-request.js +++ b/packages/server/src/api/instagram-request.js @@ -8,12 +8,11 @@ class InstagramRequestHandler { async handle(event, context, cb) { let did, username - if (event.queryStringParameters) { + if (event && event.queryStringParameters) { did = event.queryStringParameters.did username = event.queryStringParameters.username } else { cb({ code: 400, message: 'no did nor username' }) - this.analytics.trackRequestInstagram(did, 400) return }