caccl-authorizer
Advanced tools
Comparing version 1.0.4 to 1.0.5
86
index.js
@@ -5,4 +5,6 @@ const fs = require('fs'); | ||
const API = require('caccl-api'); | ||
const CACCLError = require('caccl-error'); | ||
const sendRequest = require('caccl-send-request'); | ||
const parseLaunch = require('caccl-lti/parseLaunch'); | ||
@@ -68,4 +70,7 @@ const MemoryTokenStore = require('./MemoryTokenStore.js'); | ||
* both functions return promises | ||
* @param {function} [onManualLogin] - a function to call with params (req, res) | ||
* @param {function} [onLogin] - a function to call with params (req, res) | ||
* after req.logInManually is called and finishes manually logging in | ||
* @param {boolean} [allowAuthorizationWithoutLaunch] - if true, allows user to | ||
* be authorized even without a launch (when no LTI launch occurred and | ||
* simulateLaunchOnAuthorize is false) | ||
* @param {boolean} [simulateLaunchOnAuthorize] - if truthy, simulates an LTI | ||
@@ -100,3 +105,7 @@ * launch upon successful authorization (if the user hasn't already launched | ||
// Initialize autoRefreshRoutes | ||
const autoRefreshRoutes = config.autoRefreshRoutes || ['*']; | ||
const autoRefreshRoutes = ( | ||
config.autoRefreshRoutes === null | ||
? [] | ||
: config.autoRefreshRoutes || ['*'] | ||
); | ||
@@ -152,3 +161,4 @@ // Initialize the default authorized redirect path | ||
const accessToken = body.access_token; | ||
const accessTokenExpiry = new Date().getTime() + 3540000; | ||
const expiresIn = (body.expires_in * 1000); | ||
const accessTokenExpiry = new Date().getTime() + expiresIn; | ||
// Save credentials | ||
@@ -175,4 +185,4 @@ return req.logInManually(accessToken, refreshToken, accessTokenExpiry); | ||
// Send callback | ||
if (config.onManualLogin) { | ||
config.onManualLogin(req, res); | ||
if (config.onLogin) { | ||
config.onLogin(req, res); | ||
} | ||
@@ -208,3 +218,2 @@ | ||
|| !req.session.accessToken | ||
|| !req.session.accessTokenExpiry | ||
|| !req.session.refreshToken | ||
@@ -217,3 +226,6 @@ ) { | ||
// Check if token has expired | ||
if (new Date().getTime() < req.session.accessTokenExpiry) { | ||
if ( | ||
req.session.accessTokenExpiry | ||
&& new Date().getTime() < req.session.accessTokenExpiry | ||
) { | ||
// Not expired yet. Don't need to refresh | ||
@@ -243,2 +255,7 @@ return next(); | ||
config.app.get(launchPath, (req, res, next) => { | ||
if (!req.session) { | ||
// No session! Cannot authorize without session | ||
return res.status(403).send('Internal error: cannot authorize without express-session initialized on the app.'); | ||
} | ||
// Skip if not step 1 | ||
@@ -256,5 +273,14 @@ if (req.query.code && req.query.state) { | ||
// LTI launches | ||
if (!req.session.launchInfo && !config.simulateLaunchOnAuthorize) { | ||
const launchOccurred = ( | ||
req.session | ||
&& req.session.launchInfo | ||
&& Object.keys(req.session.launchInfo).length > 0 | ||
); | ||
if ( | ||
!launchOccurred | ||
&& !config.simulateLaunchOnAuthorize | ||
&& !config.allowAuthorizationWithoutLaunch | ||
) { | ||
// Cannot authorize | ||
return res.status(403).send(`Please launch ${appName} again via Canvas.`); | ||
return res.status(403).send(`Please launch ${appName} via Canvas.`); | ||
} | ||
@@ -345,2 +371,3 @@ | ||
let launchUserId; | ||
let accessToken; | ||
sendRequest({ | ||
@@ -362,4 +389,10 @@ host: canvasHost, | ||
// Detect invalid client_secret error | ||
if (body.error && body.error === 'invalid_client') { | ||
res.redirect(nextPath + '?success=false&reason=invalid_client'); | ||
throw new Error('break'); | ||
} | ||
// Extract token | ||
const accessToken = body.access_token; | ||
accessToken = body.access_token; | ||
const refreshToken = body.refresh_token; | ||
@@ -439,4 +472,11 @@ const expiresInMs = (body.expires_in * 0.99 * 1000); | ||
if (config.simulateLaunchOnAuthorize && !req.session.launchInfo) { | ||
// Get API | ||
const api = new API({ | ||
accessToken, | ||
canvasHost, | ||
cacheType: null, | ||
}); | ||
// Pull list of courses, ask user to choose a course | ||
return req.api.user.self.listCourses({ includeTerm: true }) | ||
return api.user.self.listCourses({ includeTerm: true }) | ||
.then((courses) => { | ||
@@ -455,3 +495,6 @@ return renderCourseChooser({ | ||
}) | ||
.catch(() => { | ||
.catch((err) => { | ||
if (err.message === 'break') { | ||
return; | ||
} | ||
return res.redirect(nextPath + '?success=false&reason=error'); | ||
@@ -463,3 +506,7 @@ }); | ||
config.app.get(launchPath, (req, res, next) => { | ||
if (!req.query.course || !req.api) { | ||
if ( | ||
!req.query.course | ||
|| !req.session | ||
|| !req.session.accessToken | ||
) { | ||
return next(); | ||
@@ -471,2 +518,9 @@ } | ||
// Create API | ||
const api = new API({ | ||
canvasHost, | ||
accessToken: req.session.accessToken, | ||
cacheType: null, | ||
}); | ||
// Simulate the launch | ||
@@ -476,4 +530,4 @@ | ||
Promise.all([ | ||
req.api.course.get({ courseId }), | ||
req.api.user.self.getProfile(), | ||
api.course.get({ courseId }), | ||
api.user.self.getProfile(), | ||
]) | ||
@@ -490,3 +544,3 @@ .then(([course, profile]) => { | ||
// Parse and save the simulated launch | ||
return req._parseLaunch(simulatedLTILaunchBody); | ||
return parseLaunch(simulatedLTILaunchBody, req); | ||
}) | ||
@@ -493,0 +547,0 @@ .then(() => { |
{ | ||
"name": "caccl-authorizer", | ||
"version": "1.0.4", | ||
"version": "1.0.5", | ||
"description": "Acquires Canvas tokens through via OAuth, stores refresh tokens, and refreshes access tokens when they expire.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "node test" | ||
}, | ||
@@ -31,20 +31,28 @@ "repository": { | ||
"body-parser": "^1.18.3", | ||
"eslint": "^5.8.0", | ||
"caccl": "^1.0.41", | ||
"caccl-canvas-partial-simulator": "^1.0.9", | ||
"dce-selenium": "^1.0.12", | ||
"eslint": "^5.13.0", | ||
"eslint-config-airbnb": "^17.1.0", | ||
"eslint-plugin-import": "^2.14.0", | ||
"eslint-plugin-jsx-a11y": "^6.1.2", | ||
"eslint-plugin-react": "^7.11.1", | ||
"eslint-plugin-cypress": "^2.2.0", | ||
"eslint-plugin-import": "^2.16.0", | ||
"eslint-plugin-jsx-a11y": "^6.2.0", | ||
"eslint-plugin-react": "^7.12.4", | ||
"express": "^4.16.4", | ||
"express-session": "^1.15.6", | ||
"fs": "0.0.1-security", | ||
"https": "^1.0.0" | ||
"https": "^1.0.0", | ||
"ims-lti": "^3.0.2", | ||
"mocha": "^5.2.0" | ||
}, | ||
"dependencies": { | ||
"axios": "^0.18.0", | ||
"caccl-api": "^1.0.9", | ||
"caccl-error": "^1.0.0", | ||
"caccl-send-request": "^1.0.7", | ||
"caccl-lti": "^1.0.5", | ||
"caccl-send-request": "^1.0.9", | ||
"ejs": "^2.6.1", | ||
"qs": "^6.5.2", | ||
"qs": "^6.6.0", | ||
"randomstring": "^1.1.5" | ||
} | ||
} |
@@ -1,2 +0,85 @@ | ||
# caccl-token-manager | ||
Acquires Canvas tokens through via OAuth, stores refresh tokens, and refreshes access tokens when they expire. | ||
# caccl-authorizer | ||
Handles app authorization, redirecting users to the "Authorize this App" page, acquiring access tokens, and refreshing access tokens when they expire. | ||
Your LTI app accepts launches at your `launchPath` via POST. `caccl-authorizer` uses that same path to kick off the Canvas authorization process: direct users to your `launchPath` via GET and they'll be authorized and then redirected to the `defaultAuthorizedRedirect` path. | ||
## Part of the CACCL library | ||
**C**anvas | ||
**A**pp | ||
**C**omplete | ||
**C**onnection | ||
**L**ibrary | ||
## Quickstart | ||
After creating your express app but before adding routes, initialize `caccl-authorizer` to add routes and middleware to make authorization possible. | ||
```js | ||
const initAuthorization = require('caccl-authorizer'); | ||
// TODO: create express app | ||
initAuthorization({ | ||
app: /* express app with express-session enabled*/, | ||
developerCredentials: { | ||
client_id: /* developer client id */, | ||
client_secret: /* developer client secret */, | ||
}, | ||
canvasHost: /* your canvas host name */, | ||
}); | ||
// TODO: add routes to express app | ||
``` | ||
To authorize a user, redirect them to the `launchPath` via GET. `caccl-authorizer` will handle the entire authorization process then redirect them to the `defaultAuthorizedRedirect` path. After authorization, the user's access token will appear in their session: `req.session.accessToken`. | ||
**Important:** you must initialize `caccl-authorizer` before adding refreshed routes (see `autoRefreshRoutes` below). | ||
## Configuration Options | ||
When initializing `caccl-authorizer`, you can pass in many different configuration options to customize CACCL's behavior or turn on/off certain functionality. | ||
**Note:** configuration options are _optional_ unless otherwise stated | ||
### Main Configuration Options | ||
Important configuration options you probably should include. | ||
Config Option | Type | Description | Default/Required | ||
:--- | :--- | :--- | :--- | ||
app | express app | the server express app with express-session enabled | **required** | ||
developerCredentials | object | canvas app developer credentials in the form: `{ client_id, client_secret }` | **required** | ||
canvasHost | string | canvas host to use for oauth exchange | canvas.instructure.com | ||
allowAuthorizationWithoutLaunch | boolean | if true, allows user to be authorized even without a launch (when no LTI launch occurred and simulateLaunchOnAuthorize is false) | false | ||
### App Information | ||
Optional information about your app. | ||
Config Option | Type | Description | Default | ||
:--- | :--- | :--- | :--- | ||
appName | string | the name of the app for use in simulated launches and in errors | "this app" | ||
launchPath | string | redirect users to this path via GET to kick off the authorization process | "/launch" | ||
### Authorization Configuration | ||
Options that change how authorization functions. By default, when authorization is complete, the user will be redirected to `/`, and when the access token expires (after 1hr), it will automatically be refreshed when the user visits any route (`*`). The refresh tokens are stored in a `memory token store`. All of these features can be customized via the config options below. | ||
Config Option | Type | Description | Default | ||
:--- | :--- | :--- | :--- | ||
defaultAuthorizedRedirect | string | the default route to visit after authorization is complete (you can override this for a specific authorization call by including `query.next`. example: `/launch?next=/profile`) | "/" | ||
autoRefreshRoutes | string[] | list of routes to automatically refresh the access token for (if the access token has expired), these routes must be added _after_ `caccl-authorizer` has been initialized | `["*"]` | ||
tokenStore | [TokenStore](https://github.com/harvard-edtech/caccl-authorizer/blob/master/docs/TokenStore.md) | null to turn off storage of refresh tokens, exclude to use memory token store, or include a custom token store (see [these docs](https://github.com/harvard-edtech/caccl-authorizer/blob/master/docs/TokenStore.md)) | memory store | ||
**Tip:** we recommend setting `autoRefreshRoutes` to all the paths where you will need access to the Canvas API. Then, the accessToken will never have expired when the user visits one of those paths. | ||
### Simulated Launch Options | ||
Enabling this feature allows users to visit the `launchPath` (GET), go through the authorization process, and then `caccl-authorizer` simulates an LTI launch. This essentially makes it possible for users to launch your app without visiting Canvas, simply by visiting the `launchPath`. | ||
To enable this feature, | ||
Config Option | Type | Description | Default | ||
:--- | :--- | :--- | :--- | ||
simulateLaunchOnAuthorize | boolean | if true, simulates an LTI launch upon successful authorization (if the user hasn't already launched via LTI) | false |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
77074
39
1747
0
86
8
16
4
2
+ Addedcaccl-api@^1.0.9
+ Addedcaccl-lti@^1.0.5
+ Addedasync@2.6.4(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedbuffer-from@1.1.2(transitive)
+ Addedcaccl-api@1.0.79(transitive)
+ Addedcaccl-lti@1.1.9(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedconcat-stream@2.0.0(transitive)
+ Addedcron-parser@2.18.0(transitive)
+ Addedcrypto-js@3.1.9-1(transitive)
+ Addedcsv-parse@4.16.3(transitive)
+ Addeddefine-properties@1.2.1(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedfast-clone@1.5.13(transitive)
+ Addedform-data@4.0.0(transitive)
+ Addedinherits@2.0.32.0.4(transitive)
+ Addedis-nan@1.3.2(transitive)
+ Addedis-number@7.0.0(transitive)
+ Addedjs-tokens@4.0.0(transitive)
+ Addedlocks@0.2.2(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedlong-timeout@0.1.1(transitive)
+ Addedloose-envify@1.4.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedmoment-timezone@0.5.45(transitive)
+ Addednode-schedule@1.3.3(transitive)
+ Addedoauth-signature@1.5.0(transitive)
+ Addedobject-assign@4.1.1(transitive)
+ Addedobject-keys@1.1.1(transitive)
+ Addedparse-link-header@1.0.1(transitive)
+ Addedpath@0.12.7(transitive)
+ Addedprocess@0.11.10(transitive)
+ Addedprop-types@15.8.1(transitive)
+ Addedpunycode@1.4.12.3.1(transitive)
+ Addedreact-is@16.13.1(transitive)
+ Addedreadable-stream@3.6.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsorted-array-functions@1.3.0(transitive)
+ Addedstring_decoder@1.3.0(transitive)
+ Addedtypedarray@0.0.6(transitive)
+ Addeduri-js@3.0.2(transitive)
+ Addedurl@0.11.4(transitive)
+ Addedutil@0.10.4(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedxtend@4.0.2(transitive)
Updatedcaccl-send-request@^1.0.9
Updatedqs@^6.6.0