diff --git a/docs/configuration.md b/docs/configuration.md index f36bd7de0..fa59d70d7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -648,7 +648,6 @@ expressApp.use(prefix, oidc.callback); ### to a koa application ```js // assumes koa ^2.0.0 -// assumes koa-router ^7.0.0 const mount = require('koa-mount'); const prefix = '/oidc'; koaApp.use(mount(prefix, oidc.app)); @@ -1209,7 +1208,9 @@ This example will if (!allowed) { throw new InvalidResource('unauthorized "resource" requested'); } - return transform(resourceParam, grantedResource); // => array of validated and transformed string audiences + // => array of validated and transformed string audiences or undefined if no audiences + // are to be listed + return transform(resourceParam, grantedResource); } }, formats: { diff --git a/example/routes/koa.js b/example/routes/koa.js index e20af63b9..f1da3dc8a 100644 --- a/example/routes/koa.js +++ b/example/routes/koa.js @@ -1,7 +1,7 @@ const querystring = require('querystring'); const bodyParser = require('koa-body'); -const Router = require('koa-router'); +const Router = require('koa-trie-router'); const { renderError } = require('../../lib/helpers/defaults'); // make your own, you'll need it anyway const Account = require('../support/account'); diff --git a/example/standalone.js b/example/standalone.js index 7a697ba9b..47729acc5 100644 --- a/example/standalone.js +++ b/example/standalone.js @@ -56,7 +56,7 @@ let server; layout: '_layout', root: path.join(__dirname, 'views'), }); - provider.use(routes(provider).routes()); + provider.use(routes(provider).middleware()); server = provider.listen(PORT, () => { console.log(`application is listening on port ${PORT}, check it's /.well-known/openid-configuration`); }); diff --git a/lib/helpers/initialize_app.js b/lib/helpers/initialize_app.js index 4f63846b7..5ad42f503 100644 --- a/lib/helpers/initialize_app.js +++ b/lib/helpers/initialize_app.js @@ -1,6 +1,7 @@ const assert = require('assert'); +const querystring = require('querystring'); -const Router = require('koa-router'); +const Router = require('koa-trie-router'); const getCors = require('@koa/cors'); const { homepage, version } = require('../../package.json'); @@ -13,12 +14,12 @@ const grants = require('../actions/grants'); const responseModes = require('../response_modes'); const error = require('../shared/error_handler'); const getAuthError = require('../shared/authorization_error_handler'); -const invalidRoute = require('../shared/invalid_route'); const contextEnsureOidc = require('../shared/context_ensure_oidc'); const { InvalidRequest } = require('./errors'); const instance = require('./weak_cache'); const attention = require('./attention'); +const { ROUTER_URL_METHOD } = require('./symbols'); const webfingerRoute = '/.well-known/webfinger'; const discoveryRoute = '/.well-known/openid-configuration'; @@ -33,10 +34,12 @@ module.exports = function initializeApp() { const ensureOIDC = contextEnsureOidc(this); + const routeMap = new Map(); function routerAssert(name, route, ...stack) { assert(typeof name === 'string' && name.charAt(0) !== '/', `invalid route name ${name}`); assert(typeof route === 'string' && route.charAt(0) === '/', `invalid route ${route}`); stack.forEach(middleware => assert.deepEqual(typeof middleware, 'function'), 'invalid middleware'); + routeMap.set(name, route); } async function ensureSessionSave(ctx, next) { try { @@ -47,25 +50,48 @@ module.exports = function initializeApp() { } } } + function namedRoute(name, ctx, next) { + ctx._matchedRouteName = name; + return next(); + } + + router[ROUTER_URL_METHOD] = function routeUrl(name, opts = {}) { + let path = routeMap.get(name); + + if (!path) { + throw new Error(`No route found for name: ${name}`); + } + + Object.entries(opts).forEach(([key, value]) => { + path = path.replace(`:${key}`, value); + }); + + if ('query' in opts) { + path = `${path}?${querystring.stringify(opts.query)}`; + } + + return path; + }; + const get = (name, route, ...stack) => { routerAssert(name, route, ...stack); - router.get(name, route, ensureOIDC, ensureSessionSave, ...stack); + router.get(route, namedRoute.bind(undefined, name), ensureOIDC, ensureSessionSave, ...stack); }; const post = (name, route, ...stack) => { routerAssert(name, route, ...stack); - router.post(name, route, ensureOIDC, ensureSessionSave, ...stack); + router.post(route, namedRoute.bind(undefined, name), ensureOIDC, ensureSessionSave, ...stack); }; const del = (name, route, ...stack) => { routerAssert(name, route, ...stack); - router.del(name, route, ensureOIDC, ...stack); + router.del(route, namedRoute.bind(undefined, name), ensureOIDC, ...stack); }; const put = (name, route, ...stack) => { routerAssert(name, route, ...stack); - router.put(name, route, ensureOIDC, ...stack); + router.put(route, namedRoute.bind(undefined, name), ensureOIDC, ...stack); }; const options = (name, route, ...stack) => { routerAssert(name, route, ...stack); - router.options(name, route, ensureOIDC, ...stack); + router.options(route, namedRoute.bind(undefined, name), ensureOIDC, ...stack); }; const { routes } = configuration; @@ -188,7 +214,7 @@ module.exports = function initializeApp() { post('code_verification', routes.code_verification, error(this, 'code_verification.error'), ...codeVerification.post, ...postCodeVerification); const deviceResume = getAuthorization(this, 'device_resume'); - get('device_resume', `${routes.code_verification}/:user_code/:grant/`, error(this, 'device_resume.error'), ...deviceResume); + get('device_resume', `${routes.code_verification}/:user_code/:grant`, error(this, 'device_resume.error'), ...deviceResume); } if (configuration.features.devInteractions) { @@ -215,23 +241,20 @@ module.exports = function initializeApp() { proxyWarning.firstInternal = true; app.use(proxyWarning); - app.use(router.routes()); app.use(error(this)); - app.use(invalidRoute); - - const allowedMethodsMiddleware = router.allowedMethods({ - throw: true, - methodNotAllowed: () => new InvalidRequest('method not allowed', 405), - notImplemented: () => new InvalidRequest('not implemented', 501), - }); app.use(async (ctx, next) => { - try { - await allowedMethodsMiddleware(ctx, next); - } catch (err) { - if (err.statusCode === 405) { - err.error_description = `method ${ctx.method} not allowed on ${ctx.path}`; - } - throw err; + await next(); + if (!router.isImplementedMethod(ctx.method)) { + throw new InvalidRequest('not implemented', 501); + } + + if (ctx.status === 404 && ctx.message === 'Not Found') { + throw new InvalidRequest('unrecognized route', 404); + } + + if (ctx.status === 405) { + throw new InvalidRequest(`method ${ctx.method} not allowed on ${ctx.path}`, 405); } }); + app.use(router.middleware()); }; diff --git a/lib/helpers/symbols.js b/lib/helpers/symbols.js new file mode 100644 index 000000000..29d198dc7 --- /dev/null +++ b/lib/helpers/symbols.js @@ -0,0 +1,3 @@ +module.exports = { + ROUTER_URL_METHOD: Symbol('ROUTER_URL_METHOD'), +}; diff --git a/lib/provider.js b/lib/provider.js index 4a744dbb3..e36e9ed3d 100644 --- a/lib/provider.js +++ b/lib/provider.js @@ -22,6 +22,7 @@ if (HTTP2_STABLE) { const { DEFAULT_HTTP_OPTIONS } = require('./consts'); const attention = require('./helpers/attention'); const getConfiguration = require('./helpers/configuration'); +const { ROUTER_URL_METHOD } = require('./helpers/symbols'); const instance = require('./helpers/weak_cache'); const initializeKeystore = require('./helpers/initialize_keystore'); const initializeAdapter = require('./helpers/initialize_adapter'); @@ -201,8 +202,7 @@ class Provider extends events.EventEmitter { checkInit(this); const { router } = instance(this); - const routerUrl = router.url(name, opts); - if (routerUrl instanceof Error) throw routerUrl; // specific to koa-router + const routerUrl = router[ROUTER_URL_METHOD](name, opts); return [mountPath, routerUrl].join(''); } diff --git a/lib/shared/invalid_route.js b/lib/shared/invalid_route.js deleted file mode 100644 index b433cf207..000000000 --- a/lib/shared/invalid_route.js +++ /dev/null @@ -1,8 +0,0 @@ -const { InvalidRequest } = require('../helpers/errors'); - -module.exports = async function invalidRoute(ctx, next) { - await next(); - if (ctx.status === 404 && ctx.message === 'Not Found') { - throw new InvalidRequest('unrecognized route', 404); - } -}; diff --git a/package.json b/package.json index 0ba18ad9c..43e0eeeb0 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "got": "^9.3.2", "jsesc": "^2.5.2", "koa": "^2.6.2", - "koa-router": "^7.4.0", + "koa-trie-router": "^2.1.6", "lodash": "^4.17.11", "lru-cache": "^5.1.1", "nanoid": "^2.0.0", @@ -58,7 +58,7 @@ "uuid": "^3.3.2" }, "devDependencies": { - "@commitlint/cli": "^7.2.1", + "@commitlint/cli": "^7.5.2", "@commitlint/config-conventional": "^7.1.2", "chai": "^4.2.0", "clear-module": "^3.0.0", @@ -66,14 +66,14 @@ "eslint-config-airbnb-base": "^13.1.0", "eslint-plugin-import": "^2.14.0", "husky": "^1.2.0", - "koa-body": "^4.0.4", + "koa-body": "^4.0.8", "koa-ejs": "^4.1.2", "koa-helmet": "^4.0.0", "koa-mount": "^4.0.0", "mocha": "^5.2.0", "moment": "^2.22.2", "nock": "^10.0.2", - "nyc": "^13.1.0", + "nyc": "^13.3.0", "sinon": "^7.1.1", "supertest": "^3.3.0", "timekeeper": "^2.1.2"