|
import * as base64url from 'base64-url' |
|
import crypto from 'crypto' |
|
import config from 'config' |
|
|
|
import { getLogger } from '../logger.mjs' |
|
import { getOrThrowClientError } from '../util.mjs' |
|
import ResponseError from '../ResponseError.mjs' |
|
|
|
const logger = getLogger('parseSignedRequest') |
|
|
|
export default function parseSignedRequest (req, res, next) { |
|
const signedRequest = getSignedRequest(req) |
|
logger.debug('Validating signature', { |
|
signedRequest |
|
}) |
|
const [signature, payload] = getSignatureAndPayloadFromSignedRequest(signedRequest) |
|
validateSignature(signature, payload) |
|
replaceBodyWithDecodedPayload(req, payload) |
|
next() |
|
} |
|
|
|
function getSignedRequest (req) { |
|
return getOrThrowClientError(req, 'body', 'signed_request') |
|
} |
|
|
|
function getSignatureAndPayloadFromSignedRequest (signedRequest) { |
|
const [encodedSignature, payload] = signedRequest.split('.', 2) |
|
if (encodedSignature == null || payload == null) { |
|
throw new ResponseError(400, 'Signed request has invalid format') |
|
} |
|
const signature = decodeSignature(encodedSignature) |
|
return [signature, payload] |
|
} |
|
|
|
function replaceBodyWithDecodedPayload (req, payload) { |
|
const body = decodePayload(payload) |
|
req.body = body |
|
} |
|
|
|
function validateSignature (actualSignature, payload) { |
|
const expectedSignature = getExpectedSignature(payload) |
|
// For some reason, the actual signature always has a '=' appended |
|
const actualSignatureWithEqualsSign = actualSignature + '=' |
|
if (actualSignatureWithEqualsSign !== expectedSignature) { |
|
throw new ResponseError(401, 'Invalid signature') |
|
} |
|
} |
|
|
|
function decodeSignature (encodedSignature) { |
|
return urlDecode(encodedSignature) |
|
} |
|
|
|
function decodePayload (payload) { |
|
const bodyJson = base64url.decode(urlDecode(payload)) |
|
try { |
|
return JSON.parse(bodyJson) |
|
} catch (error) { |
|
// If the JSON is invalid, this is the client's fault |
|
error.code = 400 |
|
throw error |
|
} |
|
} |
|
|
|
function urlDecode (string) { |
|
return string.replace(/-/g, '+').replace(/_/g, '/') |
|
} |
|
|
|
function getExpectedSignature (payload) { |
|
const secret = config.get('services.facebook.appSecret') |
|
const hmac = crypto.createHmac('sha256', secret) |
|
hmac.update(payload) |
|
return hmac.digest('base64') |
|
} |
I left those out intentionally because the gist is just supposed to show how the data deletion request works.
If you implement this yourself, you can just leave out the calls to the log functions.
If you want to know how to get the full URI of a request, check out this Stackoverflow post.
ResponseError is simply a plain JavaScript error with a status property, and causes Node to respond with the corresponding HTTP error code.