├── .jscsrc ├── .jshintrc ├── README.md ├── examples ├── db.js └── index.js ├── index.js ├── package-lock.json └── package.json /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "validateIndentation": 4, 3 | "validateQuoteMarks": "'", 4 | "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], 5 | "requireAlignedMultilineParams": "firstParam", 6 | "disallowNewlineBeforeBlockStatements": true, 7 | "disallowOperatorBeforeLineBreak": true 8 | } 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "strict": true, 4 | "node": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "undef": true, 8 | "unused": true, 9 | "varstmt": true 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Koa 2 OAuth Server 2 | ================== 3 | 4 | *Koa 2 wrapper for [node-oauth2-server][noa2s]* 5 | 6 | The wrapper is based on [express-oauth-server][eoas]. 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | TODO: Installation instructions, when published on npmjs. 13 | 14 | 15 | Configuration 16 | ------------- 17 | 18 | A complete reference implementation is available in the `/examples` directory. 19 | The sample implements the following grant flows: 20 | 21 | * Password (resource owner password credentials) 22 | * Authorization code 23 | * Refresh token 24 | * Client credentials 25 | 26 | 27 | Additional features 28 | ------------------- 29 | 30 | This middleware extends upon the base oauth2 library by providing the following: 31 | 32 | ### Scope verification middleware 33 | 34 | Allows for protecting individual routes or routers with scope keys. 35 | If no method is provided, a default method performing substring matching will 36 | be used. 37 | 38 | ### `model.checkScope(requiredScope, token) => Boolean|String` 39 | 40 | Takes `requiredScope` and `token` as input, should return boolean `true` to 41 | indicate that the required scope was encountered, or boolean `false` or a 42 | string to indicate that it was not. 43 | 44 | If `false` is returned, the default error message will read: 45 | > "Required scope: \`{requiredScope}\`" 46 | 47 | ##### Example 48 | 49 | Note: The below corresponds to the fallback `checkScope` implementation. 50 | 51 | ```js 52 | model.checkScope = (requiredScope, token) => { 53 | return token.scope.indexOf(requiredScope) !== -1; 54 | }; 55 | ``` 56 | 57 | ```js 58 | const protected = new Router(), 59 | account = new Router(); 60 | 61 | protected.use(oauth.authenticate()); // Requires bearer token 62 | account.use(oauth.scope('account')); // Requires `account` scope 63 | 64 | account.get('/edit', oauth.scope('edit'), (...) => { // Requires `edit` too 65 | // Update account information 66 | }); 67 | 68 | protected.use(account); 69 | ``` 70 | 71 | ### Token grant metadata access 72 | 73 | Exposes the `ctx.request` object to the model, allowing for processing and 74 | storage of metadata (IP, User Agent, etc.). 75 | 76 | #### `model.saveTokenMetadata(token, data) => Promise` 77 | 78 | Takes `token` and `data` objects as input, should return a Promise that 79 | resolves with the `token` object on completion. 80 | 81 | ##### Example 82 | 83 | Use some geolocation service to look up the user, then update the token entry. 84 | 85 | ```js 86 | model.saveTokenMetadata = (token, data) => { 87 | return geoDataLookup(data.ip).then((geoData) => { 88 | return token.update({ geoData: geoData }); 89 | }); 90 | }; 91 | ``` 92 | 93 | [noa2s]: https://github.com/thomseddon/node-oauth2-server 94 | [eoas]: https://github.com/seegno/express-oauth-server 95 | -------------------------------------------------------------------------------- /examples/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Mock data, this would normally be database bindings, ORM wrappers, etc. 4 | 5 | module.exports = { 6 | clients: [{ 7 | id: 'someClient', 8 | secret: 'superSecret', 9 | name: 'Sample client application', 10 | accessTokenLifetime: 3600, // If omitted, server default will be used 11 | refreshTokenLifetime: 604800, // ^ 12 | redirectUris: ['https://www.getpostman.com/oauth2/callback'], 13 | grants: ['client_credentials', 'refresh_token', 'authorization_code', 'password'], 14 | validScopes: ['account', 'edit'], 15 | }], 16 | users: [{ 17 | id: 1, 18 | name: 'AzureDiamond', 19 | username: 'foo@example.com', 20 | password: 'hunter2', 21 | }], 22 | tokens: [], 23 | authCodes: [] 24 | }; 25 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Exhaustive sample implementing password (ropc), authorization code, 4 | // client credentials, and refresh token grant flows. 5 | // Run with `DEBUG=koa:oauth*,example node index.js` 6 | 7 | const Koa = require('koa'), 8 | debug = require('debug')('example'), 9 | Router = require('koa-router'), 10 | session = require('koa-session'), 11 | convert = require('koa-convert'), // convert session for koa2 12 | bodyParser = require('koa-bodyparser'), 13 | OAuthServer = require('../'); 14 | 15 | const db = require('./db'), // Mock data 16 | model = {}; // OAuth2 model container 17 | 18 | // Client lookup - Note that for *authcode* grants, the secret is not provided 19 | model.getClient = (id, secret) => { 20 | debug(`Looking up client ${id}:${secret}`); 21 | 22 | const lookupMethod = typeof secret === 'undefined' 23 | ? (client) => { return client.id === id; } 24 | : (client) => { return client.id === id && client.secret === secret }; 25 | 26 | return db.clients.find(lookupMethod); 27 | }; 28 | 29 | model.getUser = (username, password) => { 30 | debug(`Looking up user ${username}:${password}`); 31 | 32 | return db.users.find((user) => { 33 | return user.username === username && user.password === password; 34 | }); 35 | }; 36 | 37 | // In the client credentials grant flow, the client itself needs to be related 38 | // with some form of user representation 39 | model.getUserFromClient = (client) => { 40 | debug(`Looking up user for client ${client.name}`); 41 | return { name: client.name, isClient: true }; 42 | }; 43 | 44 | // Performs a lookup on the provided string and returns a token object 45 | model.getAccessToken = (accessToken) => { 46 | debug(`Get access token ${accessToken}`); 47 | 48 | const token = db.tokens.find((token) => { 49 | return token.accessToken === accessToken; 50 | }); 51 | 52 | if(!token) { return false; } 53 | 54 | // Populate with user and client model instances 55 | token.user = db.users.find((user) => { 56 | return user.id === token.user.id; 57 | }); 58 | 59 | token.client = db.clients.find((client) => { 60 | return client.id === token.client.id; 61 | }); 62 | 63 | return token; 64 | }; 65 | 66 | // Performs a lookup on the provided string and returns a token object 67 | model.getRefreshToken = (refreshToken) => { 68 | debug(`Get refresh token ${refreshToken}`); 69 | const token = db.tokens.find((token) => { 70 | return token.refreshToken === refreshToken; 71 | }); 72 | 73 | if(!token) { return false; } 74 | 75 | // Populate with user and client model instances 76 | token.user = db.users.find((user) => { 77 | return user.id === token.user.id; 78 | }); 79 | 80 | token.client = db.clients.find((client) => { 81 | return client.id === token.client.id; 82 | }); 83 | 84 | return token; 85 | }; 86 | 87 | // Saves the newly generated token object 88 | model.saveToken = (token, client, user) => { 89 | debug(`Save token ${token.accessToken}`); 90 | 91 | token.user = { id: user.id }; 92 | token.client = { id: client.id }; 93 | 94 | db.tokens.push(token); 95 | return token; 96 | }; 97 | 98 | // Revoke refresh token after use - note ExpiresAt detail! 99 | model.revokeToken = (token) => { 100 | debug(`Revoke token ${token.refreshToken}`); 101 | 102 | // Note: This is normally the DB object instance from getRefreshToken, so 103 | // just token.delete() or similar rather than the below findIndex. 104 | const idx = db.tokens.findIndex((item) => { 105 | return item.refreshToken === token.refreshToken; 106 | }); 107 | 108 | db.tokens.splice(idx, 1); 109 | 110 | // Note: Presently, this method must return the revoked token object with 111 | // an expired date. This is currently being discussed in 112 | // https://github.com/thomseddon/node-oauth2-server/issues/251 113 | 114 | token.refreshTokenExpiresAt = new Date(1984); 115 | return token; 116 | }; 117 | 118 | // Retrieves an authorization code 119 | model.getAuthorizationCode = (code) => { 120 | debug(`Retrieving authorization code ${code}`); 121 | 122 | return db.authCodes.find((authCode) => { 123 | return authCode.authorizationCode === code; 124 | }); 125 | }; 126 | 127 | // Saves the newly generated authorization code object 128 | model.saveAuthorizationCode = (code, client, user) => { 129 | debug(`Saving authorization code ${code.authorizationCode}`); 130 | code.user = { id: user.id }; 131 | code.client = { id: client.id }; 132 | 133 | db.authCodes.push(code); 134 | return code; 135 | }; 136 | 137 | // Revokes the authorization code after use - note ExpiresAt detail! 138 | model.revokeAuthorizationCode = (code) => { 139 | debug(`Revoking authorization code ${code.authorizationCode}`); 140 | 141 | const idx = db.authCodes.findIndex((authCode) => { 142 | return authCode.authorizationCode === code.authorizationCode; 143 | }); 144 | 145 | if(!idx) { return false; } 146 | 147 | db.authCodes.splice(idx, 1); 148 | code.expiresAt.setYear(1984); // Same as for `revokeToken()` 149 | 150 | return code; 151 | }; 152 | 153 | // Called in `authenticate()` - basic check for scope existance 154 | // `scope` corresponds to the oauth server configuration option, which 155 | // could either be a string or boolean true. 156 | // Since we utilize router-based scope check middleware, here we simply check 157 | // for scope existance. 158 | model.verifyScope = (token, scope) => { 159 | debug(`Verify scope ${scope} in token ${token.accessToken}`); 160 | if(scope && !token.scope) { return false; } 161 | return token; 162 | }; 163 | 164 | // Can be used to sanitize or purely validate requested scope string 165 | model.validateScope = (user, client, scope) => { 166 | debug(`Validating requested scope: ${scope}`); 167 | 168 | const validScope = (scope || '').split(' ').filter((key) => { 169 | return client.validScopes.indexOf(key) !== -1; 170 | }); 171 | 172 | if(!validScope.length) { return false; } 173 | 174 | return validScope.join(' '); 175 | }; 176 | 177 | // OAuth server initialization 178 | 179 | const oauth = new OAuthServer({ 180 | scope: true, // Alternatively string with required scopes (see verifyScope) 181 | model: model, 182 | allowBearerTokensInQueryString: true, 183 | accessTokenLifetime: 3600, // 1 hour 184 | refreshTokenLifetime: 604800 // 1 week 185 | }); 186 | 187 | // Application setup 188 | 189 | const app = new Koa(), 190 | rPublic = new Router(), 191 | rPrivate = new Router(), 192 | rAccount = new Router(); 193 | 194 | rPublic.get('/', (ctx) => { 195 | ctx.response.body = { message: 'I am a public resource!' }; 196 | }); 197 | 198 | rPublic.get('/login', (ctx) => { 199 | ctx.response.body = '
' 200 | + '

Ye olde login form

' 201 | + '

Sign in as foo@example.com:hunter2

' 202 | + '' 203 | + '' 204 | + '' 205 | + '
'; 206 | }); 207 | 208 | rPublic.post('/login', (ctx) => { 209 | const creds = ctx.request.body; 210 | debug(`Authenticating ${creds.username}`); 211 | 212 | const user = db.users.find((user) => { 213 | return user.username === creds.username 214 | && user.password === creds.password; 215 | }); 216 | 217 | if(!user) { 218 | debug('Invalid credentials'); 219 | ctx.redirect('/login'); 220 | return; 221 | } 222 | 223 | debug(`Success!`); 224 | ctx.session.userId = user.id; 225 | 226 | // If we were sent here from grant page, redirect back 227 | if(ctx.session.hasOwnProperty('query')) { 228 | debug('Redirecting back to grant dialog'); 229 | ctx.redirect('/oauth/authorize'); 230 | return; 231 | } 232 | 233 | // If not do whatever you fancy 234 | ctx.redirect('/'); 235 | }); 236 | 237 | rPublic.get('/logout', (ctx) => { 238 | ctx.session.userId = null; 239 | ctx.redirect('/login'); 240 | }); 241 | 242 | // Token acquisition endpoint 243 | rPublic.all('/oauth/token', oauth.token()); 244 | 245 | rPublic.get('/oauth/authorize', (ctx) => { 246 | if(!ctx.session.userId) { 247 | debug('User not authenticated, redirecting to /login'); 248 | ctx.session.query = { 249 | state: ctx.request.query.state, 250 | scope: ctx.request.query.scope, 251 | client_id: ctx.request.query.client_id, 252 | redirect_uri: ctx.request.query.redirect_uri, 253 | response_type: ctx.request.query.response_type 254 | }; 255 | 256 | ctx.redirect('/login'); 257 | return; 258 | } 259 | 260 | const client = db.clients.find((client) => { 261 | return client.id === ctx.session.query.client_id; 262 | }); 263 | 264 | if(!client) { ctx.throw(401, 'No such client'); } 265 | 266 | ctx.response.body = `

Grant access to "${client.name}"?

` 267 | + `

The application requests access to ${ctx.session.query.scope}

` 268 | + '
' 269 | + '
'; 270 | }); 271 | 272 | // OAuth authorization endpoint (authcode grant flow) 273 | rPublic.post('/oauth/authorize', (ctx, next) => { 274 | if(!ctx.session.userId) { 275 | debug('User not authenticated, redirecting to /login'); 276 | ctx.redirect('/login'); 277 | return; 278 | } 279 | 280 | ctx.request.body = ctx.session.query; 281 | ctx.request.body.user_id = ctx.session.userId; 282 | ctx.session.query = null; 283 | 284 | return next(); 285 | }, oauth.authorize({ 286 | authenticateHandler: { 287 | handle: (req, res) => { 288 | return db.users.find((user) => { 289 | return user.id === req.body.user_id; 290 | }); 291 | } 292 | } 293 | })); 294 | 295 | rAccount.get('/', (ctx) => { 296 | ctx.response.body = { message: 'Displaying user account information.' }; 297 | }); 298 | 299 | rAccount.post('/', oauth.scope('edit'), (ctx) => { 300 | ctx.response.body = { message: 'Account information updated!' }; 301 | }); 302 | 303 | rPrivate.use(oauth.authenticate()); 304 | rPrivate.use('/account', oauth.scope('account'), rAccount.routes()); 305 | 306 | // Application initialization 307 | 308 | app.keys = ['superupersessionsecret']; // For koa-session 309 | app 310 | .use(convert(session(app))) 311 | .use(bodyParser()) 312 | .use((ctx, next) => { 313 | debug(`${ctx.method} ${ctx.url}`); 314 | 315 | return next().catch((err) => { 316 | debug('Caught error: ', err); 317 | 318 | ctx.status = err.status || 500; 319 | ctx.body = { 320 | name: err.name, 321 | message: err.message 322 | }; 323 | }); 324 | }) 325 | .use(rPublic.routes()) 326 | .use(rPrivate.routes()) 327 | .listen(8080, () => { 328 | debug('Example application listening on localhost:8080'); 329 | }); 330 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('koa:oauth2-server'), 4 | OAuthServer = require('oauth2-server'), 5 | Request = OAuthServer.Request, 6 | Response = OAuthServer.Response; 7 | 8 | const ePath = 'oauth2-server/lib/errors/', 9 | OAuthError = require(ePath + 'oauth-error'), 10 | InvalidScopeError = require(ePath + 'invalid-scope-error'), 11 | InvalidArgumentError = require(ePath + 'invalid-argument-error'), 12 | UnauthorizedRequestError = require(ePath + 'unauthorized-request-error'); 13 | 14 | class KoaOAuthServer { 15 | constructor(options) { 16 | this.options = options || {}; 17 | 18 | if(!options.model) { 19 | throw new InvalidArgumentError('Missing parameter: `model`'); 20 | } 21 | 22 | // If no `saveTokenMetadata` method is set via the model, we create 23 | // a simple passthrough mechanism instead 24 | this.saveTokenMetadata = options.model.saveTokenMetadata 25 | ? options.model.saveTokenMetadata 26 | : (token, data) => { return Promise.resolve(token); }; 27 | 28 | // If no `checkScope` method is set via the model, we provide a default 29 | this.checkScope = options.model.checkScope 30 | ? options.model.checkScope 31 | : (scope, token) => { return token.scope.indexOf(scope) !== -1; } 32 | 33 | this.server = new OAuthServer(options); 34 | } 35 | 36 | // Returns token authentication middleware 37 | authenticate() { 38 | debug('Creating authentication endpoint middleware'); 39 | return async (ctx, next) => { 40 | debug('Running authenticate endpoint middleware'); 41 | const request = new Request(ctx.request), 42 | response = new Response(ctx.response); 43 | 44 | await this.server 45 | .authenticate(request, response) 46 | .then(async (token) => { 47 | ctx.state.oauth = { token: token }; 48 | await next(); 49 | }) 50 | .catch((err) => { handleError(err, ctx); }); 51 | }; 52 | } 53 | 54 | // Returns authorization endpoint middleware 55 | // Used by the client to obtain authorization from the resource owner 56 | authorize(options) { 57 | debug('Creating authorization endpoint middleware'); 58 | return async (ctx, next) => { 59 | debug('Running authorize endpoint middleware'); 60 | const request = new Request(ctx.request), 61 | response = new Response(ctx.response); 62 | 63 | await this.server 64 | .authorize(request, response, options) 65 | .then(async (code) => { 66 | ctx.state.oauth = { code: code }; 67 | handleResponse(ctx, response); 68 | await next(); 69 | }) 70 | .catch((err) => { handleError(err, ctx); }); 71 | }; 72 | } 73 | 74 | // Returns token endpoint middleware 75 | // Used by the client to exchange authorization grant for access token 76 | token() { 77 | debug('Creating token endpoint middleware'); 78 | return async (ctx, next) => { 79 | debug('Running token endpoint middleware'); 80 | const request = new Request(ctx.request), 81 | response = new Response(ctx.response); 82 | 83 | await this.server 84 | .token(request, response) 85 | .then((token) => { 86 | return this.saveTokenMetadata(token, ctx.request); 87 | }) 88 | .then(async (token) => { 89 | ctx.state.oauth = { token: token }; 90 | handleResponse(ctx, response); 91 | await next(); 92 | }) 93 | .catch((err) => { handleError(err, ctx); }); 94 | }; 95 | } 96 | 97 | // Returns scope check middleware 98 | // Used to limit access to a route or router to carriers of a certain scope. 99 | scope(required) { 100 | debug(`Creating scope check middleware (${required})`); 101 | return (ctx, next) => { 102 | const result = this.checkScope(required, ctx.state.oauth.token); 103 | if(result !== true) { 104 | const err = result === false 105 | ? `Required scope: \`${required}\`` 106 | : result; 107 | 108 | handleError(new InvalidScopeError(err), ctx); 109 | return; 110 | } 111 | 112 | return next(); 113 | }; 114 | } 115 | } 116 | 117 | function handleResponse(ctx, response) { 118 | debug(`Preparing success response (${response.status})`); 119 | ctx.set(response.headers); 120 | ctx.status = response.status; 121 | ctx.body = response.body; 122 | } 123 | 124 | // Add custom headers to the context, then propagate error upwards 125 | function handleError(err, ctx) { 126 | debug(`Preparing error response (${err.code || 500})`); 127 | 128 | const response = new Response(ctx.response); 129 | ctx.set(response.headers); 130 | 131 | ctx.status = err.code || 500; 132 | throw err; 133 | } 134 | 135 | // Expose error classes 136 | KoaOAuthServer.OAuthError = OAuthError; 137 | KoaOAuthServer.InvalidScopeError = InvalidScopeError; 138 | KoaOAuthServer.InvalidArgumentError = InvalidArgumentError; 139 | KoaOAuthServer.UnauthorizedRequestError = UnauthorizedRequestError; 140 | 141 | module.exports = KoaOAuthServer; 142 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ubx-koa2-oauth2-server", 3 | "version": "0.0.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.4", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 10 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", 11 | "dev": true, 12 | "requires": { 13 | "mime-types": "2.1.17", 14 | "negotiator": "0.6.1" 15 | } 16 | }, 17 | "any-promise": { 18 | "version": "1.3.0", 19 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 20 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", 21 | "dev": true 22 | }, 23 | "basic-auth": { 24 | "version": "1.1.0", 25 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", 26 | "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=" 27 | }, 28 | "bluebird": { 29 | "version": "3.5.0", 30 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", 31 | "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" 32 | }, 33 | "bytes": { 34 | "version": "2.4.0", 35 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", 36 | "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=", 37 | "dev": true 38 | }, 39 | "co": { 40 | "version": "4.6.0", 41 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 42 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", 43 | "dev": true 44 | }, 45 | "co-bluebird": { 46 | "version": "1.1.0", 47 | "resolved": "https://registry.npmjs.org/co-bluebird/-/co-bluebird-1.1.0.tgz", 48 | "integrity": "sha1-yLnzqTIKftMJh9zKGlw8/1llXHw=", 49 | "requires": { 50 | "bluebird": "2.11.0", 51 | "co-use": "1.1.0" 52 | }, 53 | "dependencies": { 54 | "bluebird": { 55 | "version": "2.11.0", 56 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", 57 | "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" 58 | } 59 | } 60 | }, 61 | "co-body": { 62 | "version": "4.2.0", 63 | "resolved": "https://registry.npmjs.org/co-body/-/co-body-4.2.0.tgz", 64 | "integrity": "sha1-dN8g+nMmISXcRUgq8E40LqjbNRU=", 65 | "dev": true, 66 | "requires": { 67 | "inflation": "2.0.0", 68 | "qs": "4.0.0", 69 | "raw-body": "2.1.7", 70 | "type-is": "1.6.15" 71 | } 72 | }, 73 | "co-use": { 74 | "version": "1.1.0", 75 | "resolved": "https://registry.npmjs.org/co-use/-/co-use-1.1.0.tgz", 76 | "integrity": "sha1-xrs83xDLc17Kqdru2kbXJclKTmI=" 77 | }, 78 | "content-disposition": { 79 | "version": "0.5.2", 80 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 81 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", 82 | "dev": true 83 | }, 84 | "content-type": { 85 | "version": "1.0.4", 86 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 87 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", 88 | "dev": true 89 | }, 90 | "cookies": { 91 | "version": "0.7.1", 92 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", 93 | "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", 94 | "dev": true, 95 | "requires": { 96 | "depd": "1.1.1", 97 | "keygrip": "1.0.2" 98 | } 99 | }, 100 | "debug": { 101 | "version": "2.6.9", 102 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 103 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 104 | "requires": { 105 | "ms": "2.0.0" 106 | } 107 | }, 108 | "deep-equal": { 109 | "version": "1.0.1", 110 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 111 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", 112 | "dev": true 113 | }, 114 | "delegates": { 115 | "version": "1.0.0", 116 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 117 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", 118 | "dev": true 119 | }, 120 | "depd": { 121 | "version": "1.1.1", 122 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 123 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", 124 | "dev": true 125 | }, 126 | "destroy": { 127 | "version": "1.0.4", 128 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 129 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", 130 | "dev": true 131 | }, 132 | "ee-first": { 133 | "version": "1.1.1", 134 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 135 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", 136 | "dev": true 137 | }, 138 | "error-inject": { 139 | "version": "1.0.0", 140 | "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", 141 | "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=", 142 | "dev": true 143 | }, 144 | "escape-html": { 145 | "version": "1.0.3", 146 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 147 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", 148 | "dev": true 149 | }, 150 | "fresh": { 151 | "version": "0.5.2", 152 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 153 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", 154 | "dev": true 155 | }, 156 | "http-assert": { 157 | "version": "1.3.0", 158 | "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", 159 | "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", 160 | "dev": true, 161 | "requires": { 162 | "deep-equal": "1.0.1", 163 | "http-errors": "1.6.2" 164 | } 165 | }, 166 | "http-errors": { 167 | "version": "1.6.2", 168 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 169 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 170 | "dev": true, 171 | "requires": { 172 | "depd": "1.1.1", 173 | "inherits": "2.0.3", 174 | "setprototypeof": "1.0.3", 175 | "statuses": "1.3.1" 176 | } 177 | }, 178 | "iconv-lite": { 179 | "version": "0.4.13", 180 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", 181 | "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", 182 | "dev": true 183 | }, 184 | "inflation": { 185 | "version": "2.0.0", 186 | "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", 187 | "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=", 188 | "dev": true 189 | }, 190 | "inherits": { 191 | "version": "2.0.3", 192 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 193 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 194 | "dev": true 195 | }, 196 | "is-generator": { 197 | "version": "1.0.3", 198 | "resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz", 199 | "integrity": "sha1-wUwhBX7TbjKNuANHlmxpP4hjifM=" 200 | }, 201 | "is-generator-function": { 202 | "version": "1.0.6", 203 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.6.tgz", 204 | "integrity": "sha1-nnFlPNFf/zQcecQVFGChMdMen8Q=", 205 | "dev": true 206 | }, 207 | "isarray": { 208 | "version": "0.0.1", 209 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 210 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 211 | "dev": true 212 | }, 213 | "keygrip": { 214 | "version": "1.0.2", 215 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", 216 | "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=", 217 | "dev": true 218 | }, 219 | "koa": { 220 | "version": "2.3.0", 221 | "resolved": "https://registry.npmjs.org/koa/-/koa-2.3.0.tgz", 222 | "integrity": "sha1-nh6OTaQBg5xXuFJ+rcV/dhJ1Vac=", 223 | "dev": true, 224 | "requires": { 225 | "accepts": "1.3.4", 226 | "content-disposition": "0.5.2", 227 | "content-type": "1.0.4", 228 | "cookies": "0.7.1", 229 | "debug": "2.6.9", 230 | "delegates": "1.0.0", 231 | "depd": "1.1.1", 232 | "destroy": "1.0.4", 233 | "error-inject": "1.0.0", 234 | "escape-html": "1.0.3", 235 | "fresh": "0.5.2", 236 | "http-assert": "1.3.0", 237 | "http-errors": "1.6.2", 238 | "is-generator-function": "1.0.6", 239 | "koa-compose": "4.0.0", 240 | "koa-convert": "1.2.0", 241 | "koa-is-json": "1.0.0", 242 | "mime-types": "2.1.17", 243 | "on-finished": "2.3.0", 244 | "only": "0.0.2", 245 | "parseurl": "1.3.2", 246 | "statuses": "1.3.1", 247 | "type-is": "1.6.15", 248 | "vary": "1.1.2" 249 | } 250 | }, 251 | "koa-bodyparser": { 252 | "version": "3.2.0", 253 | "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-3.2.0.tgz", 254 | "integrity": "sha1-uRbeF+IDn+gmUEgZc9fClPELVxk=", 255 | "dev": true, 256 | "requires": { 257 | "co-body": "4.2.0" 258 | } 259 | }, 260 | "koa-compose": { 261 | "version": "4.0.0", 262 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", 263 | "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=", 264 | "dev": true 265 | }, 266 | "koa-convert": { 267 | "version": "1.2.0", 268 | "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", 269 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", 270 | "dev": true, 271 | "requires": { 272 | "co": "4.6.0", 273 | "koa-compose": "3.2.1" 274 | }, 275 | "dependencies": { 276 | "koa-compose": { 277 | "version": "3.2.1", 278 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 279 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 280 | "dev": true, 281 | "requires": { 282 | "any-promise": "1.3.0" 283 | } 284 | } 285 | } 286 | }, 287 | "koa-is-json": { 288 | "version": "1.0.0", 289 | "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", 290 | "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=", 291 | "dev": true 292 | }, 293 | "koa-router": { 294 | "version": "7.2.1", 295 | "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.2.1.tgz", 296 | "integrity": "sha1-tApKs8attLQIld69AKnGQDBOMDk=", 297 | "dev": true, 298 | "requires": { 299 | "debug": "2.6.9", 300 | "http-errors": "1.6.2", 301 | "koa-compose": "3.2.1", 302 | "methods": "1.1.2", 303 | "path-to-regexp": "1.7.0" 304 | }, 305 | "dependencies": { 306 | "koa-compose": { 307 | "version": "3.2.1", 308 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 309 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 310 | "dev": true, 311 | "requires": { 312 | "any-promise": "1.3.0" 313 | } 314 | } 315 | } 316 | }, 317 | "koa-session": { 318 | "version": "3.4.0", 319 | "resolved": "https://registry.npmjs.org/koa-session/-/koa-session-3.4.0.tgz", 320 | "integrity": "sha1-zicCkaT8+6/hmau2TM2YMOjHh78=", 321 | "dev": true, 322 | "requires": { 323 | "debug": "2.6.9", 324 | "deep-equal": "1.0.1" 325 | } 326 | }, 327 | "lodash": { 328 | "version": "4.17.4", 329 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 330 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 331 | }, 332 | "media-typer": { 333 | "version": "0.3.0", 334 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 335 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 336 | }, 337 | "methods": { 338 | "version": "1.1.2", 339 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 340 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", 341 | "dev": true 342 | }, 343 | "mime-db": { 344 | "version": "1.30.0", 345 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 346 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 347 | }, 348 | "mime-types": { 349 | "version": "2.1.17", 350 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 351 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 352 | "requires": { 353 | "mime-db": "1.30.0" 354 | } 355 | }, 356 | "ms": { 357 | "version": "2.0.0", 358 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 359 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 360 | }, 361 | "negotiator": { 362 | "version": "0.6.1", 363 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 364 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", 365 | "dev": true 366 | }, 367 | "oauth2-server": { 368 | "version": "3.0.0", 369 | "resolved": "https://registry.npmjs.org/oauth2-server/-/oauth2-server-3.0.0.tgz", 370 | "integrity": "sha1-xGJ2t0w9KGNNWe6YH3a1imRZzCg=", 371 | "requires": { 372 | "basic-auth": "1.1.0", 373 | "bluebird": "3.5.0", 374 | "lodash": "4.17.4", 375 | "promisify-any": "2.0.1", 376 | "statuses": "1.3.1", 377 | "type-is": "1.6.15" 378 | } 379 | }, 380 | "on-finished": { 381 | "version": "2.3.0", 382 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 383 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 384 | "dev": true, 385 | "requires": { 386 | "ee-first": "1.1.1" 387 | } 388 | }, 389 | "only": { 390 | "version": "0.0.2", 391 | "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", 392 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", 393 | "dev": true 394 | }, 395 | "parseurl": { 396 | "version": "1.3.2", 397 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 398 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", 399 | "dev": true 400 | }, 401 | "path-to-regexp": { 402 | "version": "1.7.0", 403 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", 404 | "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", 405 | "dev": true, 406 | "requires": { 407 | "isarray": "0.0.1" 408 | } 409 | }, 410 | "promisify-any": { 411 | "version": "2.0.1", 412 | "resolved": "https://registry.npmjs.org/promisify-any/-/promisify-any-2.0.1.tgz", 413 | "integrity": "sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU=", 414 | "requires": { 415 | "bluebird": "2.11.0", 416 | "co-bluebird": "1.1.0", 417 | "is-generator": "1.0.3" 418 | }, 419 | "dependencies": { 420 | "bluebird": { 421 | "version": "2.11.0", 422 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", 423 | "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" 424 | } 425 | } 426 | }, 427 | "qs": { 428 | "version": "4.0.0", 429 | "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", 430 | "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc=", 431 | "dev": true 432 | }, 433 | "raw-body": { 434 | "version": "2.1.7", 435 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", 436 | "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", 437 | "dev": true, 438 | "requires": { 439 | "bytes": "2.4.0", 440 | "iconv-lite": "0.4.13", 441 | "unpipe": "1.0.0" 442 | } 443 | }, 444 | "setprototypeof": { 445 | "version": "1.0.3", 446 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 447 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", 448 | "dev": true 449 | }, 450 | "statuses": { 451 | "version": "1.3.1", 452 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 453 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 454 | }, 455 | "type-is": { 456 | "version": "1.6.15", 457 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 458 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", 459 | "requires": { 460 | "media-typer": "0.3.0", 461 | "mime-types": "2.1.17" 462 | } 463 | }, 464 | "unpipe": { 465 | "version": "1.0.0", 466 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 467 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", 468 | "dev": true 469 | }, 470 | "vary": { 471 | "version": "1.1.2", 472 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 473 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", 474 | "dev": true 475 | } 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ubx-koa2-oauth2-server", 3 | "version": "0.0.4", 4 | "description": "Koa 2 wrapper for node-oauth2-server.", 5 | "main": "index.js", 6 | "publishConfig": { 7 | "registry": "https://npm.ubilogix.com" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Lars F. Karlström ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "debug": "^2.2.0", 16 | "oauth2-server": "^3.0.0" 17 | }, 18 | "devDependencies": { 19 | "koa": "^2.0.0-alpha.3", 20 | "koa-bodyparser": "^3.0.0", 21 | "koa-convert": "^1.2.0", 22 | "koa-router": "^7.0.1", 23 | "koa-session": "^3.3.1" 24 | } 25 | } 26 | --------------------------------------------------------------------------------