From 50e73469fb1916d7656925d3a999dd0bae6b3fcf Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 15 Aug 2017 14:02:22 +0530 Subject: [PATCH 1/5] feat(middleware): add auth middleware --- package.json | 2 +- providers/AuthProvider.js | 17 +++ src/Middleware/Auth.js | 83 +++++++++++ test/functional/auth-middleware.spec.js | 187 ++++++++++++++++++++++++ test/functional/setup/index.js | 10 ++ test/unit/helpers/index.js | 7 + 6 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/Auth.js create mode 100644 test/functional/auth-middleware.spec.js diff --git a/package.json b/package.json index 5f55c77..0ad2ae3 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "test/**/*.spec.js" ], "exclude": [ - "src/ExceptionHandler/index.js" + "src/ExceptionHandler/*" ] }, "standard": { diff --git a/providers/AuthProvider.js b/providers/AuthProvider.js index 950a4b3..f59abd6 100644 --- a/providers/AuthProvider.js +++ b/providers/AuthProvider.js @@ -53,6 +53,22 @@ class AuthProvider extends ServiceProvider { }) } + /** + * Register auth middleware under `Adonis/Middleware/Auth` namespace. + * + * @method _registerAuthMiddleware + * + * @return {void} + * + * @private + */ + _registerAuthMiddleware () { + this.app.bind('Adonis/Middleware/Auth', (app) => { + const Auth = require('../src/Middleware/Auth') + return new Auth(app.use('Adonis/Src/Config')) + }) + } + /** * Register namespaces to the IoC container * @@ -64,6 +80,7 @@ class AuthProvider extends ServiceProvider { this._registerAuth() this._registerAuthManager() this._registerAuthInitMiddleware() + this._registerAuthMiddleware() } /** diff --git a/src/Middleware/Auth.js b/src/Middleware/Auth.js new file mode 100644 index 0000000..f4d511e --- /dev/null +++ b/src/Middleware/Auth.js @@ -0,0 +1,83 @@ +'use strict' + +/* + * adonis-auth + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +const debug = require('debug')('adonis:auth') + +class Auth { + constructor (Config) { + const authenticator = Config.get('auth.authenticator') + this.scheme = Config.get(`auth.${authenticator}.scheme`, null) + } + + /** + * Authenticate the user using one of the defined + * schemes or the default scheme + * + * @method handle + * + * @param {Object} options.auth + * @param {Function} next + * + * @return {void} + */ + async handle ({ auth }, next, schemes) { + let lastError = null + let authenticatedScheme = null + + schemes = schemes instanceof Array === true ? schemes : [this.scheme] + debug('attempting to authenticate via %j scheme(s)', schemes) + + /** + * Loop over all the defined schemes and wait until use is logged + * via anyone + */ + for (const scheme of schemes) { + try { + await auth.authenticator(scheme).check() + debug('authenticated using %s scheme', scheme) + authenticatedScheme = scheme + lastError = null + break + } catch (error) { + debug('authentication failed using %s scheme', scheme) + lastError = error + } + } + + /** + * If there is an error from all the schemes + * then throw it back + */ + if (lastError) { + throw lastError + } + + /** + * If user got logged then set the `current` property + * on auth, which is reference to the scheme via + * which user got authenticated. + */ + if (authenticatedScheme) { + /** + * If logged in scheme is same as the default scheme, the reference + * the actual authenticator instance, otherwise create a new + * one for the scheme via which user got authenticated + */ + auth.current = authenticatedScheme === this.scheme + ? auth.authenticatorInstance + : auth.authenticator(authenticatedScheme) + } + + await next() + } +} + +module.exports = Auth diff --git a/test/functional/auth-middleware.spec.js b/test/functional/auth-middleware.spec.js new file mode 100644 index 0000000..680f382 --- /dev/null +++ b/test/functional/auth-middleware.spec.js @@ -0,0 +1,187 @@ +'use strict' + +/* + * adonis-auth + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +const { ioc } = require('@adonisjs/fold') +const test = require('japa') +const supertest = require('supertest') +const http = require('http') +const setup = require('./setup') +const groupSetup = require('../unit/setup') +const helpers = require('../unit/helpers') + +test.group('Middleware | Auth', (group) => { + groupSetup.databaseHook(group) + groupSetup.hashHook(group) + + group.before(async () => { + await setup() + }) + + group.beforeEach(() => { + this.server = http.createServer() + }) + + test('attempt to login when scheme is session but ignore silently', async (assert) => { + let fnCalled = false + + this.server.on('request', (req, res) => { + const Context = ioc.use('Adonis/Src/HttpContext') + + const ctx = new Context() + ctx.request = helpers.getRequest(req) + ctx.response = helpers.getResponse(req, res) + ctx.session = helpers.getSession(req, res) + + ctx.auth.authenticator('session') + ctx.auth._authenticatorsPool['session'].loginIfCan = function () { + fnCalled = true + } + + const authInit = ioc.use('Adonis/Middleware/AuthInit') + + authInit + .handle(ctx, function () {}) + .then((status) => { + res.writeHead(200) + res.write('skipped') + res.end() + }) + .catch(({ status, message }) => { + res.writeHead(status || 500) + res.write(message) + res.end() + }) + }) + + await supertest(this.server).get('/').expect(200) + assert.isTrue(fnCalled) + }) + + test('throw exception when unable to login via default scheme', async (assert) => { + this.server.on('request', (req, res) => { + const Context = ioc.use('Adonis/Src/HttpContext') + + const ctx = new Context() + ctx.request = helpers.getRequest(req) + ctx.response = helpers.getResponse(req, res) + ctx.session = helpers.getSession(req, res) + + const authMiddleware = ioc.use('Adonis/Middleware/Auth') + + authMiddleware + .handle(ctx, function () {}) + .then((status) => { + res.writeHead(200) + res.write('skipped') + res.end() + }) + .catch(({ status, message }) => { + res.writeHead(status || 500) + res.write(message) + res.end() + }) + }) + + const { text } = await supertest(this.server).get('/').expect(401) + assert.equal(text, 'E_INVALID_SESSION: Invalid session') + }) + + test('set current property on auth when user is logged in', async (assert) => { + await ioc.use('App/Models/User').create({ email: 'foo@bar.com', password: 'secret' }) + + this.server.on('request', (req, res) => { + const Context = ioc.use('Adonis/Src/HttpContext') + + const ctx = new Context() + ctx.request = helpers.getRequest(req) + ctx.response = helpers.getResponse(req, res) + ctx.session = helpers.getSession(req, res) + + const authMiddleware = ioc.use('Adonis/Middleware/Auth') + + authMiddleware + .handle(ctx, function () {}) + .then((status) => { + res.writeHead(200) + assert.deepEqual(ctx.auth.current, ctx.auth.authenticatorInstance) + res.end() + }) + .catch(({ status, message }) => { + res.writeHead(status || 500) + res.write(message) + res.end() + }) + }) + + await supertest(this.server).get('/').set('Cookie', 'adonis-auth=1').expect(200) + }) + + test('throw exception when all of the schemes fails to login the user', async (assert) => { + this.server.on('request', (req, res) => { + const Context = ioc.use('Adonis/Src/HttpContext') + + const ctx = new Context() + ctx.request = helpers.getRequest(req) + ctx.response = helpers.getResponse(req, res) + ctx.session = helpers.getSession(req, res) + + const authMiddleware = ioc.use('Adonis/Middleware/Auth') + + authMiddleware + .handle(ctx, function () {}, ['basic', 'jwt']) + .then((status) => { + res.writeHead(200) + res.end() + }) + .catch(({ status, message }) => { + res.writeHead(status || 500) + res.write(message) + res.end() + }) + }) + + const { text } = await supertest(this.server).get('/').expect(401) + assert.equal(text, 'E_INVALID_JWT_TOKEN: jwt must be provided') + }) + + test('skip upcoming schemes when one authenticates a user', async (assert) => { + await ioc.use('App/Models/User').create({ email: 'foo@bar.com', password: 'secret' }) + + this.server.on('request', (req, res) => { + const Context = ioc.use('Adonis/Src/HttpContext') + + const ctx = new Context() + ctx.request = helpers.getRequest(req) + ctx.response = helpers.getResponse(req, res) + ctx.session = helpers.getSession(req, res) + + const authMiddleware = ioc.use('Adonis/Middleware/Auth') + + authMiddleware + .handle(ctx, function () {}, ['basic', 'jwt']) + .then((status) => { + res.writeHead(200) + assert.isDefined(ctx.auth.current) + assert.equal(ctx.auth.current.scheme, 'basic') + assert.notDeepEqual(ctx.auth.current.scheme, ctx.auth.authenticatorInstance) + res.end() + }) + .catch(({ status, message }) => { + res.writeHead(status || 500) + res.write(message) + res.end() + }) + }) + + const userCredentials = Buffer.from('foo@bar.com:secret').toString('base64') + await supertest(this.server).get('/').set('Authorization', `Basic ${userCredentials}`).expect(200) + }) +}) diff --git a/test/functional/setup/index.js b/test/functional/setup/index.js index b40414e..db5d44f 100644 --- a/test/functional/setup/index.js +++ b/test/functional/setup/index.js @@ -51,6 +51,16 @@ module.exports = async () => { model: 'App/Models/User', serializer: 'lucid', scheme: 'basic' + }, + jwt: { + model: 'App/Models/User', + scheme: 'jwt', + serializer: 'lucid', + uid: 'email', + password: 'password', + options: { + secret: 'SECRET' + } } }) diff --git a/test/unit/helpers/index.js b/test/unit/helpers/index.js index 2316de2..cc7fb83 100644 --- a/test/unit/helpers/index.js +++ b/test/unit/helpers/index.js @@ -53,6 +53,13 @@ module.exports = { } const parsedCookies = cookie.parse(this.request.headers.cookie) return parsedCookies[key] + }, + header (key) { + key = key.toLowerCase() + return this.request.headers[key] + }, + input (key) { + return '' } } }, From 57845f9b587d8369ad289d03b2dabf29e22f90ce Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 16 Aug 2017 15:23:10 +0530 Subject: [PATCH 2/5] WIP --- src/Schemes/Api.js | 315 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 src/Schemes/Api.js diff --git a/src/Schemes/Api.js b/src/Schemes/Api.js new file mode 100644 index 0000000..7b459e1 --- /dev/null +++ b/src/Schemes/Api.js @@ -0,0 +1,315 @@ +'use strict' + +/* + * adonis-auth + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +const Resetable = require('resetable') +const jwt = require('jsonwebtoken') +const uuid = require('uuid') +const _ = require('lodash') +const BaseScheme = require('./Base') +const GE = require('@adonisjs/generic-exceptions') +const CE = require('../Exceptions') + +class ApiScheme extends BaseScheme { + /** + * Signs payload with jwtSecret and options + * + * @method _signToken + * @async + * + * @param {Object} payload + * + * @return {String} + * + * @private + * + * @throws {Error} If unable to sign payload and generate token + */ + _signToken (payload, options) { + return new Promise((resolve, reject) => { + options = _.size(options) && _.isPlainObject(options) ? options : _.omit(this.jwtOptions, 'secret') + jwt.sign(payload, this.jwtSecret, options, (error, token) => { + if (error) { + reject(error) + } else { + resolve(token) + } + }) + }) + } + + /** + * Verifies the jwt token by decoding it + * + * @method _verifyToken + * @async + * + * @param {String} token + * + * @return {Object} + * + * @private + */ + _verifyToken (token) { + return new Promise((resolve, reject) => { + const options = _.omit(this.jwtOptions, 'secret') + jwt.verify(token, this.jwtSecret, options, (error, payload) => { + if (error) { + reject(error) + } else { + resolve(payload) + } + }) + }) + } + + /** + * Saves jwt refresh token for a given user + * + * @method _saveRefreshToken + * + * @param {Object} user + * + * @return {String} + * + * @private + */ + async _saveRefreshToken (user) { + const refreshToken = uuid.v4() + await this._serializerInstance.saveToken(user, refreshToken, 'jwt_refresh_token') + return refreshToken + } + + /** + * Instruct class to generate a refresh token + * when generate jwt token + * + * @method withRefreshToken + * + * @chainable + */ + withRefreshToken () { + this._generateRefreshToken.set(true) + return this + } + + /** + * Same as withRefreshToken but a better alias + * to map with `generateForRefreshToken` + * + * @method newRefreshToken + * + * @chainable + */ + newRefreshToken () { + this._generateRefreshToken.set(true) + return this + } + + /** + * Validate user credentials + * + * @method validate + * + * @param {String} uid + * @param {String} password + * @param {Boolean} returnUser + * + * @return {Object} + * + * @throws {UserNotFoundException} If unable to find user with uid + * @throws {PasswordMisMatchException} If password mismatches + */ + async validate (uid, password, returnUser) { + const user = await this._serializerInstance.findByUid(uid) + if (!user) { + throw CE.UserNotFoundException.invoke(`Cannot find user with ${this._config.uid} as ${uid}`) + } + + const validated = await this._serializerInstance.validateCredentails(user, password) + if (!validated) { + throw CE.PasswordMisMatchException.invoke('Cannot verify user password') + } + + return returnUser ? user : !!user + } + + /** + * Attempt to valid the user credentials and then + * generates a new token for it. + * + * @method attempt + * + * @param {String} uid + * @param {String} password + * @param {Object|Boolean} [jwtPayload] Pass true when want to attach user object in the payload + * or set a custom object + * @param {Object} [jwtOptions = null] + * + * @return {String} + */ + async attempt (uid, password, jwtPayload, jwtOptions) { + const user = await this.validate(uid, password, true) + return this.generate(user, jwtPayload, jwtOptions) + } + + /** + * Generates a jwt token for a user + * + * @method generate + * @async + * + * @param {Object} user + * @param {Object|Boolean} [jwtPayload] Pass true when want to attach user object in the payload + * or set a custom object + * @param {Object} [jwtOptions = null] + * + * @return {Object} + * + * @throws {RuntimeException} If jwt secret is not defined or user doesn't have a primary key value + */ + async generate (user, jwtPayload, jwtOptions) { + /** + * Throw exception when trying to generate token without + * jwt secret + */ + if (!this.jwtSecret) { + throw GE.RuntimeException.incompleteConfig('jwt', ['secret'], 'config/auth.js') + } + + /** + * Throw exception when user is not persisted to + * database + */ + const userId = user[this.primaryKey] + if (!userId) { + throw GE.RuntimeException.invoke('Primary key value is missing for user') + } + + /** + * The jwt payload + * + * @type {Object} + */ + const payload = { uid: userId } + + if (jwtPayload === true) { + /** + * Attach user as data object only when + * jwtPayload is true + */ + payload.data = typeof (user.toJSON) === 'function' ? user.toJSON() : user + } else if (_.isPlainObject(jwtPayload)) { + /** + * Attach payload as it is when it's an object + */ + payload.data = jwtPayload + } + + /** + * Return the generate token + */ + const token = await this._signToken(payload, jwtOptions) + const withRefresh = this._generateRefreshToken.pull() + const refreshToken = withRefresh ? await this._saveRefreshToken(user) : null + return { type: 'bearer', token, refreshToken } + } + + /** + * Generate a new token using the refresh token. + * This method will revoke the existing token + * and issues a new refresh token + * + * @param {String} refreshToken + * @param {Object|Boolean} [jwtPayload] Pass true when want to attach user object in the payload + * or set a custom object + * @param {Object} [jwtOptions = null] + * + * @method generateForRefreshToken + * + * @return {Object} + */ + async generateForRefreshToken (refreshToken, jwtPayload, jwtOptions) { + const user = await this._serializerInstance.findByToken(refreshToken, 'jwt_refresh_token') + if (!user) { + throw CE.InvalidRefreshToken.invoke(refreshToken) + } + + const token = await this.generate(user, jwtPayload) + + /** + * If user generated a new refresh token, in that we + * should revoke the old one, otherwise we should + * set the refreshToken as the existing refresh + * token in the return payload + */ + if (!token.refreshToken) { + token.refreshToken = refreshToken + } else { + await this._serializerInstance.revokeTokens(user, [refreshToken]) + } + + return token + } + + /** + * Check whether a user is logged in or + * not. Also this method will re-login + * the user when remember me token + * is defined + * + * @method check + * + * @return {Boolean} + */ + async check () { + if (this.user) { + return true + } + + /** + * Verify jwt token and wrap exception inside custom + * exception classes + */ + try { + this.jwtPayload = await this._verifyToken(this.getAuthHeader()) + } catch ({ name, message }) { + if (name === 'TokenExpiredError') { + throw CE.ExpiredJwtToken.invoke() + } + throw CE.InvalidJwtToken.invoke(message) + } + + this.user = await this._serializerInstance.findById(this.jwtPayload.uid) + + /** + * Throw exception when user is not found + */ + if (!this.user) { + throw CE.InvalidJwtToken.invoke() + } + return true + } + + /** + * Makes sure user is loggedin and then + * returns the user back + * + * @method getUser + * + * @return {Object} + */ + async getUser () { + await this.check() + return this.user + } +} + +module.exports = ApiScheme From b4c98d765a0eb61f5e0d99fea08be12beb26ff2d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 16 Aug 2017 16:20:24 +0530 Subject: [PATCH 3/5] feat(schemes): add api tokens scheme --- src/Exceptions/index.js | 9 +- src/Schemes/Api.js | 214 +++---------------------------- src/Schemes/index.js | 3 +- test/unit/api-scheme.spec.js | 241 +++++++++++++++++++++++++++++++++++ 4 files changed, 266 insertions(+), 201 deletions(-) create mode 100644 test/unit/api-scheme.spec.js diff --git a/src/Exceptions/index.js b/src/Exceptions/index.js index 8832ec1..7420540 100644 --- a/src/Exceptions/index.js +++ b/src/Exceptions/index.js @@ -102,11 +102,18 @@ class ExpiredJwtToken extends InvalidLoginException { } } +class InvalidApiToken extends InvalidLoginException { + static invoke () { + return new this('The api is invalid or missing', 401, 'E_INVALID_API_TOKEN') + } +} + module.exports = { UserNotFoundException, PasswordMisMatchException, InvalidJwtToken, InvalidRefreshToken, ExpiredJwtToken, - InvalidLoginException + InvalidLoginException, + InvalidApiToken } diff --git a/src/Schemes/Api.js b/src/Schemes/Api.js index 7b459e1..6a68cbf 100644 --- a/src/Schemes/Api.js +++ b/src/Schemes/Api.js @@ -9,110 +9,12 @@ * file that was distributed with this source code. */ -const Resetable = require('resetable') -const jwt = require('jsonwebtoken') const uuid = require('uuid') -const _ = require('lodash') const BaseScheme = require('./Base') const GE = require('@adonisjs/generic-exceptions') const CE = require('../Exceptions') class ApiScheme extends BaseScheme { - /** - * Signs payload with jwtSecret and options - * - * @method _signToken - * @async - * - * @param {Object} payload - * - * @return {String} - * - * @private - * - * @throws {Error} If unable to sign payload and generate token - */ - _signToken (payload, options) { - return new Promise((resolve, reject) => { - options = _.size(options) && _.isPlainObject(options) ? options : _.omit(this.jwtOptions, 'secret') - jwt.sign(payload, this.jwtSecret, options, (error, token) => { - if (error) { - reject(error) - } else { - resolve(token) - } - }) - }) - } - - /** - * Verifies the jwt token by decoding it - * - * @method _verifyToken - * @async - * - * @param {String} token - * - * @return {Object} - * - * @private - */ - _verifyToken (token) { - return new Promise((resolve, reject) => { - const options = _.omit(this.jwtOptions, 'secret') - jwt.verify(token, this.jwtSecret, options, (error, payload) => { - if (error) { - reject(error) - } else { - resolve(payload) - } - }) - }) - } - - /** - * Saves jwt refresh token for a given user - * - * @method _saveRefreshToken - * - * @param {Object} user - * - * @return {String} - * - * @private - */ - async _saveRefreshToken (user) { - const refreshToken = uuid.v4() - await this._serializerInstance.saveToken(user, refreshToken, 'jwt_refresh_token') - return refreshToken - } - - /** - * Instruct class to generate a refresh token - * when generate jwt token - * - * @method withRefreshToken - * - * @chainable - */ - withRefreshToken () { - this._generateRefreshToken.set(true) - return this - } - - /** - * Same as withRefreshToken but a better alias - * to map with `generateForRefreshToken` - * - * @method newRefreshToken - * - * @chainable - */ - newRefreshToken () { - this._generateRefreshToken.set(true) - return this - } - /** * Validate user credentials * @@ -149,41 +51,25 @@ class ApiScheme extends BaseScheme { * * @param {String} uid * @param {String} password - * @param {Object|Boolean} [jwtPayload] Pass true when want to attach user object in the payload - * or set a custom object - * @param {Object} [jwtOptions = null] * * @return {String} */ - async attempt (uid, password, jwtPayload, jwtOptions) { + async attempt (uid, password) { const user = await this.validate(uid, password, true) - return this.generate(user, jwtPayload, jwtOptions) + return this.generate(user) } /** - * Generates a jwt token for a user + * Generates a personal API token for a user * * @method generate * @async * * @param {Object} user - * @param {Object|Boolean} [jwtPayload] Pass true when want to attach user object in the payload - * or set a custom object - * @param {Object} [jwtOptions = null] * * @return {Object} - * - * @throws {RuntimeException} If jwt secret is not defined or user doesn't have a primary key value */ - async generate (user, jwtPayload, jwtOptions) { - /** - * Throw exception when trying to generate token without - * jwt secret - */ - if (!this.jwtSecret) { - throw GE.RuntimeException.incompleteConfig('jwt', ['secret'], 'config/auth.js') - } - + async generate (user) { /** * Throw exception when user is not persisted to * database @@ -193,77 +79,15 @@ class ApiScheme extends BaseScheme { throw GE.RuntimeException.invoke('Primary key value is missing for user') } - /** - * The jwt payload - * - * @type {Object} - */ - const payload = { uid: userId } - - if (jwtPayload === true) { - /** - * Attach user as data object only when - * jwtPayload is true - */ - payload.data = typeof (user.toJSON) === 'function' ? user.toJSON() : user - } else if (_.isPlainObject(jwtPayload)) { - /** - * Attach payload as it is when it's an object - */ - payload.data = jwtPayload - } - - /** - * Return the generate token - */ - const token = await this._signToken(payload, jwtOptions) - const withRefresh = this._generateRefreshToken.pull() - const refreshToken = withRefresh ? await this._saveRefreshToken(user) : null - return { type: 'bearer', token, refreshToken } + const token = uuid.v4().replace(/-/g, '') + await this._serializerInstance.saveToken(user, token, 'api_token') + return { type: 'bearer', token } } /** - * Generate a new token using the refresh token. - * This method will revoke the existing token - * and issues a new refresh token - * - * @param {String} refreshToken - * @param {Object|Boolean} [jwtPayload] Pass true when want to attach user object in the payload - * or set a custom object - * @param {Object} [jwtOptions = null] - * - * @method generateForRefreshToken - * - * @return {Object} - */ - async generateForRefreshToken (refreshToken, jwtPayload, jwtOptions) { - const user = await this._serializerInstance.findByToken(refreshToken, 'jwt_refresh_token') - if (!user) { - throw CE.InvalidRefreshToken.invoke(refreshToken) - } - - const token = await this.generate(user, jwtPayload) - - /** - * If user generated a new refresh token, in that we - * should revoke the old one, otherwise we should - * set the refreshToken as the existing refresh - * token in the return payload - */ - if (!token.refreshToken) { - token.refreshToken = refreshToken - } else { - await this._serializerInstance.revokeTokens(user, [refreshToken]) - } - - return token - } - - /** - * Check whether a user is logged in or - * not. Also this method will re-login - * the user when remember me token - * is defined + * Check whether the api token has been passed + * in the request header and is it valid or + * not. * * @method check * @@ -274,26 +98,18 @@ class ApiScheme extends BaseScheme { return true } - /** - * Verify jwt token and wrap exception inside custom - * exception classes - */ - try { - this.jwtPayload = await this._verifyToken(this.getAuthHeader()) - } catch ({ name, message }) { - if (name === 'TokenExpiredError') { - throw CE.ExpiredJwtToken.invoke() - } - throw CE.InvalidJwtToken.invoke(message) + const token = this.getAuthHeader() + if (!token) { + throw CE.InvalidApiToken.invoke() } - this.user = await this._serializerInstance.findById(this.jwtPayload.uid) + this.user = await this._serializerInstance.findByToken(token, 'api_token') /** * Throw exception when user is not found */ if (!this.user) { - throw CE.InvalidJwtToken.invoke() + throw CE.InvalidApiToken.invoke() } return true } diff --git a/src/Schemes/index.js b/src/Schemes/index.js index c6ba21e..f4e17ac 100644 --- a/src/Schemes/index.js +++ b/src/Schemes/index.js @@ -12,5 +12,6 @@ module.exports = { session: require('./Session'), basic: require('./BasicAuth'), - jwt: require('./Jwt') + jwt: require('./Jwt'), + api: require('./Api') } diff --git a/test/unit/api-scheme.spec.js b/test/unit/api-scheme.spec.js new file mode 100644 index 0000000..cccddf1 --- /dev/null +++ b/test/unit/api-scheme.spec.js @@ -0,0 +1,241 @@ +'use strict' + +/* + * adonis-auth + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +require('@adonisjs/lucid/lib/iocResolver').setFold(require('@adonisjs/fold')) + +const test = require('japa') +const { ioc } = require('@adonisjs/fold') + +const { api: Api } = require('../../src/Schemes') +const { lucid: LucidSerializer } = require('../../src/Serializers') +const helpers = require('./helpers') +const setup = require('./setup') + +test.group('Schemes - Api', (group) => { + setup.databaseHook(group) + setup.hashHook(group) + + test('throw exception when unable to validate credentials', async (assert) => { + assert.plan(1) + + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer() + lucid.setConfig(config) + + const api = new Api() + api.setOptions(config, lucid) + + try { + await api.validate('foo@bar.com', 'secret') + } catch ({ message }) { + assert.equal(message, 'E_USER_NOT_FOUND: Cannot find user with email as foo@bar.com') + } + }) + + test('throw exception when password mismatches', async (assert) => { + assert.plan(1) + + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer(ioc.use('Hash')) + lucid.setConfig(config) + + await User.create({ email: 'foo@bar.com', password: 'secret' }) + + const api = new Api() + api.setOptions(config, lucid) + + try { + await api.validate('foo@bar.com', 'supersecret') + } catch ({ message }) { + assert.equal(message, 'E_PASSWORD_MISMATCH: Cannot verify user password') + } + }) + + test('return true when able to validate credentials', async (assert) => { + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer(ioc.use('Hash')) + lucid.setConfig(config) + + await User.create({ email: 'foo@bar.com', password: 'secret' }) + + const api = new Api() + api.setOptions(config, lucid) + const validated = await api.validate('foo@bar.com', 'secret') + assert.isTrue(validated) + }) + + test('generate token for user', async (assert) => { + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer(ioc.use('Hash')) + lucid.setConfig(config) + + const user = await User.create({ email: 'foo@bar.com', password: 'secret' }) + + const api = new Api() + api.setOptions(config, lucid) + const tokenPayload = await api.generate(user) + + assert.isDefined(tokenPayload.token) + assert.equal(tokenPayload.type, 'bearer') + }) + + test('verify user token from header', async (assert) => { + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer(ioc.use('Hash')) + lucid.setConfig(config) + + const user = await User.create({ email: 'foo@bar.com', password: 'secret' }) + await user.tokens().create({ type: 'api_token', token: 22, is_revoked: false }) + + const api = new Api() + api.setOptions(config, lucid) + api.setCtx({ + request: { + header (key) { + return `Bearer 22` + } + } + }) + + const isLoggedIn = await api.check() + assert.isTrue(isLoggedIn) + assert.instanceOf(api.user, User) + }) + + test('throw exception when api token is invalid', async (assert) => { + assert.plan(2) + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer(ioc.use('Hash')) + lucid.setConfig(config) + + await User.create({ email: 'foo@bar.com', password: 'secret' }) + + const api = new Api() + api.setOptions(config, lucid) + api.setCtx({ + request: { + header (key) { + return `Bearer 22` + } + } + }) + + try { + await api.check() + } catch ({ name, message }) { + assert.equal(message, 'E_INVALID_API_TOKEN: The api is invalid or missing') + assert.equal(name, 'InvalidApiToken') + } + }) + + test('return user when token is correct', async (assert) => { + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer(ioc.use('Hash')) + lucid.setConfig(config) + + const user = await User.create({ email: 'foo@bar.com', password: 'secret' }) + await user.tokens().create({ type: 'api_token', token: 22, is_revoked: false }) + + const api = new Api() + api.setOptions(config, lucid) + api.setCtx({ + request: { + header (key) { + return `Bearer 22` + } + } + }) + + const fetchedUser = await api.getUser() + assert.instanceOf(fetchedUser, User) + }) + + test('read token from request input', async (assert) => { + const User = helpers.getUserModel() + + const config = { + model: User, + uid: 'email', + password: 'password' + } + + const lucid = new LucidSerializer(ioc.use('Hash')) + lucid.setConfig(config) + + const user = await User.create({ email: 'foo@bar.com', password: 'secret' }) + await user.tokens().create({ type: 'api_token', token: 22, is_revoked: false }) + + const api = new Api() + api.setOptions(config, lucid) + api.setCtx({ + request: { + header () { + return null + }, + input () { + return '22' + } + } + }) + + const isLogged = await api.check() + assert.isTrue(isLogged) + }) +}) From 8deee57bb69e08bf97eceb6d64c3ed03f13835c8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 16 Aug 2017 16:22:14 +0530 Subject: [PATCH 4/5] docs(instructions): update instructions file --- instructions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instructions.md b/instructions.md index 11d2ce9..ef2431d 100644 --- a/instructions.md +++ b/instructions.md @@ -14,8 +14,11 @@ Next you need to do is register couple of middleware to ensure everything works Middleware are defined inside `start/kernel.js` file. Make sure to define the middleware after `Adonis/Middleware/Session`, since authentication relies on sessions unless you are using JWT etc. +Note: Make sure you have setup sessions middleware ( if using session scheme ) + ```js const globalMiddleware = [ + 'Adonis/Middleware/Session', // after this 'Adonis/Middleware/AuthInit' ] ``` From 47fcfe55de01f6d36d189ef7efe59c83d63fb2b1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 16 Aug 2017 16:24:54 +0530 Subject: [PATCH 5/5] chore(release): 2.0.3 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd225a0..6cb29b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ + +## [2.0.3](https://github.com/adonisjs/adonis-auth/compare/v2.0.2...v2.0.3) (2017-08-16) + + +### Features + +* **middleware:** add auth middleware ([50e7346](https://github.com/adonisjs/adonis-auth/commit/50e7346)) +* **schemes:** add api tokens scheme ([b4c98d7](https://github.com/adonisjs/adonis-auth/commit/b4c98d7)) + + + ## [2.0.2](https://github.com/adonisjs/adonis-auth/compare/v2.0.1...v2.0.2) (2017-08-08) diff --git a/package.json b/package.json index 0ad2ae3..1b323b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/auth", - "version": "2.0.2", + "version": "2.0.3", "description": "Offical authentication provider for Adonis framework", "main": "index.js", "directories": {