Search code examples
javascriptrestful-authenticationadonis.js

Adonis.js RESTFUL API Workaround


I recently started developing an application on adonisjs framework. I had an option to use expressjs but I preferred adonisjs because I love the way it is structured (mostly the laravel style).

I am currently trying to build a RESTFUL API but unable to figure out the basic routing / middleware / apiController (my custom controller to handle all api requests) scenario.

Here's what I've done so far:

routes.js

Route.post('api/v1/login', 'ApiController.login')
Route.post('api/v1/register', 'ApiController.register')

// API Routes
Route.group('api', function() {

  Route.get('users', 'ApiController.getUsers')

}).prefix('/api/v1').middlewares(['auth:api'])

ApiController.js

'use strict'

const User = use('App/Model/User')
const Validator = use('Validator')

const FAIL = 0
const SUCCESS = 1

class ApiController {

  * login (request, response) {

    let jsonResponse = {}

    const email = request.input('email')
    const password = request.input('password')

    // validate form input
    const rules = {
      email: 'required|email',
      password: 'required'
    }

    const messages = {
      'email.required': 'Email field is required.',
      'password.required': 'Password field is required.'
    }

    const validation = yield Validator.validateAll(request.all(), rules, messages)

    if (validation.fails()) {

      jsonResponse.status = FAIL
      jsonResponse.response = {}
      jsonResponse.response.message = validation.messages()[0].message

    } else {

      try {

        yield request.auth.attempt(email, password)

        const user = yield User.findBy('email', email)

        const token = yield request.auth.generate(user)

        jsonResponse.status = SUCCESS
        jsonResponse.response = {}
        jsonResponse.response.message = "Logged In Successfully"
        jsonResponse.response.user = user
        jsonResponse.response.token = token

      } catch (e) {

        jsonResponse.status = FAIL
        jsonResponse.response = {}
        jsonResponse.response.message = e.message

      }

    }

    return response.json(jsonResponse)

  }

}

module.exports = ApiController

config/auth.js

'use strict'

const Config = use('Config')

module.exports = {

  /*
  |--------------------------------------------------------------------------
  | Authenticator
  |--------------------------------------------------------------------------
  |
  | Authenticator is a combination of HTTP Authentication scheme and the
  | serializer to be used for retrieving users. Below is the default
  | authenticator to be used for every request.
  |
  | Available Schemes - basic, session, jwt, api
  | Available Serializers - Lucid, Database
  |
  */
  authenticator: 'session',

  /*
  |--------------------------------------------------------------------------
  | Session Authenticator
  |--------------------------------------------------------------------------
  |
  | Session authenticator will make use of sessions to maintain the login
  | state for a given user.
  |
  */
  session: {
    serializer: 'Lucid',
    model: 'App/Model/User',
    scheme: 'session',
    uid: 'email',
    password: 'password'
  },

  /*
  |--------------------------------------------------------------------------
  | Basic Auth Authenticator
  |--------------------------------------------------------------------------
  |
  | Basic Authentication works on Http Basic auth header.
  |
  */
  basic: {
    serializer: 'Lucid',
    model: 'App/Model/User',
    scheme: 'basic',
    uid: 'email',
    password: 'password'
  },

  /*
  |--------------------------------------------------------------------------
  | JWT Authenticator
  |--------------------------------------------------------------------------
  |
  | Jwt authentication works with a payload sent with every request under
  | Http Authorization header.
  |
  */
  jwt: {
    serializer: 'Lucid',
    model: 'App/Model/User',
    scheme: 'jwt',
    uid: 'email',
    password: 'password',
    secret: Config.get('app.appKey')
  },

  /*
  |--------------------------------------------------------------------------
  | API Authenticator
  |--------------------------------------------------------------------------
  |
  | Api authenticator authenticates are requests based on Authorization
  | header.
  |
  | Make sure to define relationships on User and Token model as defined
  | in documentation
  |
  */
  api: {
    serializer: 'Lucid',
    model: 'App/Model/Token',
    scheme: 'api'
  }

}

config/shield.js

'use strict'

module.exports = {
  /*
  |--------------------------------------------------------------------------
  | Content Security Policy
  |--------------------------------------------------------------------------
  |
  | Content security policy filters out the origins not allowed to execute
  | and load resources like scripts, styles and fonts. There are wide
  | variety of options to choose from.
  | @examples
  | directives: {
  |   defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com']
  | }
  */
  csp: {
    directives: {
    },
    reportOnly: false,
    setAllHeaders: false,
    disableAndroid: true
  },

  /*
  |--------------------------------------------------------------------------
  | X-XSS-Protection
  |--------------------------------------------------------------------------
  |
  | X-XSS Protection saves from applications from XSS attacks. It is adopted
  | by IE and later followed by some other browsers.
  |
  */
  xss: {
    enabled: true,
    enableOnOldIE: false
  },

  /*
  |--------------------------------------------------------------------------
  | Iframe Options
  |--------------------------------------------------------------------------
  |
  | xframe defines whether or not your website can be embedded inside an
  | iframe. Choose from one of the following options.
  | @available options
  | DENY, SAMEORIGIN, ALLOW-FROM http://example.com
  */
  xframe: 'DENY',

  /*
  |--------------------------------------------------------------------------
  | No Sniff
  |--------------------------------------------------------------------------
  |
  | Browsers have a habit of sniffing content-type of a response. Which means
  | files with .txt extension containing Javascript code will be executed as
  | Javascript. You can disable this behavior by setting nosniff to false.
  |
  */
  nosniff: true,

  /*
  |--------------------------------------------------------------------------
  | No Open
  |--------------------------------------------------------------------------
  |
  | IE users can execute webpages in the context of your website, which is
  | a serious security risk. Below options will manage this for you.
  |
  */
  noopen: true,

  /*
  |--------------------------------------------------------------------------
  | CSRF Protection
  |--------------------------------------------------------------------------
  |
  | CSRF Protection adds another layer of security by making sure, actionable
  | routes does have a valid token to execute an action.
  |
  */
  csrf: {
    enable: true,
    methods: ['POST', 'PUT', 'DELETE'],
    filterUris: ['/api/v1/login', '/api/v1/register'],
    compareHostAndOrigin: true
  }

}

Now when i hit login web service (using postman). It validates the user but throws an exception at const token = request.auth.generate(user) and says request.auth.generate is not a function.

I don't know what is going on. Please help.

Thanks


Solution

  • You need to generate a JWT token (when user calls a login api call) and send it back so that the app which requested the login service can store it and use it to make future requests (Using "Authorization" header with value "Bearer [JWT Token String]"). When app sends another request i.e. get list of business categories (with JWT token in its header), we will validate that request in api middleware. Once the request is validated, we'll serve the request and send back the data in json format.

    Here's what your header looks like:

    image

    And this is what you actually need to do in your code:

    // ROUTES.JS

    // API Routes
    Route.post('/api/v1/register', 'ApiController.register')
    Route.post('/api/v1/login', 'ApiController.login')
    
    Route.group('api', function() {
    
      Route.get('/business_categories', 'ApiController.business_categories')
    
    }).prefix('/api/v1').middlewares(['api'])
    

    // API.JS (Middleware)

    'use strict'
    
    class Api {
    
      * handle (request, response, next) {
    
        // here goes your middleware logic
        const authenticator = request.auth.authenticator('jwt')
        const isLoggedIn = yield authenticator.check()
    
        if (!isLoggedIn) {
          return response.json({
            status: 0,
            response: {
              message: 'API Authentication Failed.'
            }
          })
        }
    
        // yield next to pass the request to next middleware or controller
        yield next
    
      }
    
    }
    
    module.exports = Api
    

    // APICONTROLLER.JS

    'use strict'
    
    // Dependencies
    const Env = use('Env')
    const Validator = use('Validator')
    const Config = use('Config')
    const Database = use('Database')
    const Helpers = use('Helpers')
    const RandomString = use('randomstring')
    const Email = use('emailjs')
    const View = use('View')
    
    // Models
    const User = use('App/Model/User')
    const UserProfile = use('App/Model/UserProfile')
    const DesignCenter = use('App/Model/DesignCenter')
    const Settings = use('App/Model/Setting')
    
    // Properties
    const FAIL      = 0
    const SUCCESS   = 1
    const SITE_URL  = "http://"+Env.get('HOST')+":"+Env.get('PORT')
    
    // Messages
    const MSG_API_AUTH_FAILED             = 'Api Authentication Failed.'
    const MSG_REGISTERED_SUCCESS          = 'Registered Successfully.'
    const MSG_LOGGED_IN_SUCCESS           = 'Logged In Successfully.'
    const MSG_LOGGED_IN_CHECK             = 'You Are Logged In.'
    const MSG_LOGGED_IN_FAIL              = 'Invalid Credentials.'
    const MSG_FORGOT_PASS_EMAIL_SUCCESS   = 'Your password reset email has been sent. Please check your inbox to continue.'
    
    class ApiController {
    
      * register (request, response) {
    
        let jsonResponse = {}
    
        // validate form input
        const validation = yield Validator.validateAll(request.all(), Config.get('validation.api.register.rules'), Config.get('validation.api.register.messages'))
    
        // show error messages upon validation fail
        if (validation.fails()) {
    
          jsonResponse.status = FAIL
          jsonResponse.response = {}
          jsonResponse.response.message = validation.messages()[0].message
    
        } else {
    
          // handle card image
          let card_image = null
    
          if ( request.file('card_image') ) {
    
            const image = request.file('card_image', {
              allowedExtensions: ['jpg', 'png', 'jpeg']
            })
    
            if (image.clientSize() > 0) {
              const filename = RandomString.generate({length: 30, capitalization: 'lowercase'}) + '.' + image.extension()
              yield image.move(Helpers.publicPath(Config.get('constants.user_card_img_upload_path')), filename)
    
              if (!image.moved()) {
                jsonResponse.status = FAIL
                jsonResponse.response = {}
                jsonResponse.response.message = image.errors()
                return response.json(jsonResponse)
              }
    
              // set value for DB
              card_image = filename
            }
    
          }
    
          // create user
          const user = yield User.create({
            username: new Date().getTime(),
            email: request.input('email'),
            password: request.input('password')
          })
    
          // create user profile
          const user_profile = yield UserProfile.create({
            user_id: user.id,
            user_type_id: 3, // designer
            first_name: request.input('first_name'),
            last_name: request.input('last_name'),
            business_name: request.input('business_name'),
            business_category_id: request.input('business_category'),
            card_image: card_image,
            phone: request.input('mobile_no'),
            is_active: 1
          })
    
          jsonResponse.status = SUCCESS
          jsonResponse.response = {}
          jsonResponse.response.message = MSG_REGISTERED_SUCCESS
          jsonResponse.response.user = {
            'id': user.id,
            'first_name': user_profile.first_name,
            'last_name': user_profile.last_name,
            'business_name': user_profile.business_name,
            'business_category_id': user_profile.business_category_id,
            'card_image': user_profile.card_image == null ? "" : SITE_URL + "/" + Config.get('constants.user_card_img_upload_path') + "/" + user_profile.card_image,
            'mobile_no': user_profile.phone
          }
    
        }
    
        return response.json(jsonResponse)
    
      }
    
      * login (request, response) {
    
        let jsonResponse = {}
    
        const email = request.input('email')
        const password = request.input('password')
    
        // validate form input
        const validation = yield Validator.validateAll(request.all(), Config.get('validation.api.login.rules'), Config.get('validation.api.login.messages'))
    
        if (validation.fails()) {
    
          jsonResponse.status = FAIL
          jsonResponse.response = {}
          jsonResponse.response.message = validation.messages()[0].message
    
        } else {
    
          try {
            const jwt = request.auth.authenticator('jwt')
            const token = yield jwt.attempt(email, password)
            const user = yield User.findBy('email', email)
            const user_profile = yield UserProfile.findBy('user_id', user.id)
    
            // check if user type is designer
            if ( user_profile.user_type_id == 3 ) {
    
              jsonResponse.status = SUCCESS
              jsonResponse.response = {}
              jsonResponse.response.message = MSG_LOGGED_IN_SUCCESS
    
              let card_image = null
              if (user_profile.card_image) {
                card_image = SITE_URL + "/" + Config.get('constants.user_card_img_upload_path') + "/" + user_profile.card_image
              }
    
              jsonResponse.response.user = {
                'id': user.id,
                'first_name': user_profile.first_name,
                'last_name': user_profile.last_name,
                'business_name': user_profile.business_name,
                'business_category_id': user_profile.business_category_id,
                'card_image': card_image,
                'mobile_no': user_profile.phone
              }
              jsonResponse.response.token = token
    
            } else {
    
              jsonResponse.status = FAIL
              jsonResponse.response = {}
              jsonResponse.response.message = MSG_LOGGED_IN_FAIL
    
            }
          } catch (e) {
            jsonResponse.status = FAIL
            jsonResponse.response = {}
            jsonResponse.response.message = e.message
          }
    
        }
    
        return response.json(jsonResponse)
    
      }
    
      * business_categories (request, response) {
    
        let jsonResponse = {}
    
        try {
    
          jsonResponse.status = SUCCESS
          const dbRecords = yield Database.select('id', 'name').from('business_categories')
          let records = []
    
          dbRecords.forEach(function(row) {
            records.push({
              id: row.id,
              name: row.name
            })
          })
    
          jsonResponse.response = records
    
        } catch (e) {
    
          jsonResponse.status = FAIL
          jsonResponse.response = {}
          jsonResponse.response.message = e.message
    
        }
    
        response.json(jsonResponse)
    
    }
    
    module.exports = ApiController
    

    // CONFIG / AUTH.JS

    Since JWT tokens remain valid unless they are expired or deleted (from app, when forcefully logging a user out). We can also set an expiry period as follows:

    jwt: {
        serializer: 'Lucid',
        model: 'App/Model/User',
        scheme: 'jwt',
        uid: 'email',
        password: 'password',
        secret: Config.get('app.appKey'),
        options: {
            // Options to be used while generating token
            expiresIn: Ms('3m') // 3 months
        }
    }
    

    // CONFIG / SHIELD.JS

    Since most of the api services are not able to send CSRF tokens while sending a POST request, you can exclude those api paths not to be checked for CSRF tokens here in this file as follow:

    csrf: {
        enable: true,
        methods: ['POST', 'PUT', 'DELETE'],
        filterUris: [
            '/api/v1/login',
            '/api/v1/register'
        ],
        compareHostAndOrigin: true
    }
    

    Hope this helps :)