caccl-lti
Version:
LTI launch validator for IMS-LTI standard launches.
130 lines (115 loc) • 3.38 kB
text/typescript
// Import libraries
import express from 'express';
import oauth from 'oauth-signature';
import clone from 'fast-clone';
// Import caccl libs
import CACCLStore from 'caccl-memory-store/lib/CACCLStore';
import NONCE_CACHE_LIFESPAN_SEC from './shared/constants/NONCE_LIFESPAN_SEC';
/**
* Checks if an LTI launch request is valid. Throws an error if invalid
* @author Gabe Abrams
* @param opts object containing all arguments
* @param opts.req Express request object to verify
* @param opts.consumerSecret an LTI consumer secret to use for signature
* signing
* @param opts.store a nonce store to use for keeping track of used nonces
* @param opts.startTimestamp time when store was created
*/
const validateLaunch = async (
opts: {
req: express.Request,
consumerSecret: string,
store: CACCLStore,
startTimestamp: number,
},
) => {
const {
req,
consumerSecret,
store,
startTimestamp,
} = opts;
/*----------------------------------------*/
/* Parse request */
/*----------------------------------------*/
// Nonce
if (!req.body?.oauth_nonce) {
throw new Error('no nonce included');
}
const nonce = String(req.body.oauth_nonce);
// Timestamp
if (
!req.body?.oauth_timestamp
|| Number.isNaN(Number.parseFloat(req.body.oauth_timestamp))
) {
throw new Error('no valid timestamp included');
}
const timestamp = (
Number.parseFloat(req.body.oauth_timestamp)
* 1000
);
// Signature
if (!req.body?.oauth_signature) {
throw new Error('no signature included');
}
/*----------------------------------------*/
/* Check Nonce */
/*----------------------------------------*/
// Check if from before start
if (timestamp <= startTimestamp) {
throw new Error('nonce too old');
}
// Check if expired
const secDiff = Math.abs(Date.now() - timestamp) / 1000;
if (secDiff > (NONCE_CACHE_LIFESPAN_SEC - 5)) {
// Expired!
throw new Error('nonce expired');
}
// Add/replace in store
const prevNonceOccurrence = await store.set(
nonce,
{ timestamp },
);
if (prevNonceOccurrence) {
// Nonce was already used
throw new Error('nonce already used');
}
/*----------------------------------------*/
/* Check Signature */
/*----------------------------------------*/
// Generate signature for verification
// > Build URL
const originalURL = (req.originalUrl || req.url);
if (!originalURL) {
// No url: cannot sign the request
throw new Error('no URL to use in signature');
}
// > Get path from original URL
const urlNoQuery = originalURL.split('?')[0].split('#')[0];
const path = (
urlNoQuery
// Remove protocol
.replace(`${req.protocol}://`, '')
// Remove host
.replace(req.hostname, '')
);
// Build another url
const url = `https://${req.headers.host}${path}`;
// > Remove oauth signature from body
const body = clone(req.body);
delete body.oauth_signature;
// > Create signature
const generatedSignature = decodeURIComponent(
oauth.generate(
req.method,
url,
body,
consumerSecret,
)
);
// Check the signature
if (generatedSignature !== req.body.oauth_signature) {
throw new Error('invalid signature');
}
};
export default validateLaunch;