Angular-Full-Stack icon indicating copy to clipboard operation
Angular-Full-Stack copied to clipboard

How to secure the backend api?

Open RonAlmog opened this issue 8 years ago • 12 comments

What's the best way to secure the api? I mean to give separate permissions to each endpoint. for example: api/cats - open to anybody. api/orders - available to admins only.

thanks.

RonAlmog avatar May 19 '17 03:05 RonAlmog

On the backend you need to add a new middleware to the endpoints you want to secure. app.get('/api/orders', authMiddleware, function(req, res) {...} ) That middleware should verify that there is a header like this with a valid token: Authorization Bearer: jwt_token_here

if(there is the Authorization Header)
  if(jwt.verify(token) is ok)
    next()

Of course you need to set that header somewhere once (I think at login). That header should be carried at each request. It's not that easy to do, but probably I will implement it into this project when I know how to do it correctly.

For further info search on google:

  • jwt authentication
  • authorization bearer
  • express protected/secured routes

DavideViolante avatar May 19 '17 08:05 DavideViolante

ok, great. i will work on that. thanks!

RonAlmog avatar May 19 '17 14:05 RonAlmog

On the client side:

You need to use the AuthHttp with class from angular2-jwt instead of the basic Http class. The easiest way is in the shared.module.ts file:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule, Http, RequestOptions } from '@angular/http';
import { AuthHttp, AuthConfig } from 'angular2-jwt';

import { ToastComponent } from './toast/toast.component';

import {
            InputTextModule,
            ButtonModule,
            CalendarModule,
            DropdownModule,
            PasswordModule,
            ToggleButtonModule,
            CaptchaModule 
        } from 'primeng/primeng';

import { MustBeTrueValidator } from '../../validators/mustBeTrueValidator.directive';

export function authHttpServiceFactory(http: Http, options: RequestOptions) {
  return new AuthHttp(new AuthConfig({noJwtError: true}), http, options);
}

@NgModule({
   imports: [
       BrowserModule,
       BrowserAnimationsModule,
       FormsModule,
       ReactiveFormsModule,
       HttpModule
   ],
   exports: [
       // Shared Modules
       BrowserModule,
       FormsModule,
       ReactiveFormsModule,
       HttpModule,
       CalendarModule,
       DropdownModule,
       InputTextModule,
       ButtonModule,
       PasswordModule,
       ToggleButtonModule,
       CaptchaModule,

       // Shared Components
       ToastComponent

       // Shared Validators
       
   ],
   declarations: [ToastComponent, MustBeTrueValidator],
   providers: [ToastComponent, 
        {
            provide: AuthHttp,
            useFactory: authHttpServiceFactory,
            deps: [Http, RequestOptions]
        }],
})
export class SharedModule { }

Then pass the AuthHttp dependency to the services:

export class UserService {

  private headers = new Headers({ 'Content-Type': 'application/json', 'charset': 'UTF-8' });
  private options = new RequestOptions({ headers: this.headers });

  constructor(private http: AuthHttp) { }

On the server side:

Add the express-jwt package from npm:

npm install express-jwt --save

Add a catch all route to set the req.user property in app.ts:

  app.use(jwt({secret: config.get('sessionSecret'), credentialsRequired: false}));

  setRoutes(app);

Create a function to validate that req.user is set that you can call as middleware on your protected routes:

ensureLoggedIn(req, res, next) {
        if (req.user) {
            console.info(JSON.stringify(req.user));
            next();
        } else {
            console.info('here');
            res.status(401).json(JSON.stringify({'Message': 'Unauthorized.'}));
        }
    }

Hope that helps.

simon-hardy avatar May 23 '17 23:05 simon-hardy

@simon-hardy Quick question before I check through this, is AuthHttp able to handle https? Thanks in advance.

zbagley avatar May 23 '17 23:05 zbagley

Yep.

simon-hardy avatar May 24 '17 07:05 simon-hardy

that's sounds good, i'm trying to implement. just a few questions, because i'm new to this:

  1. where do i put, and how do i use the validation method, ensuerLoggedIn?
  2. what is the purpose of the shared module?
  3. is there a way to limit not only to logged in users, but to users who belong to a role, like admin? Thanks!

AmirGilboa avatar Jun 09 '17 03:06 AmirGilboa

@AmirGilboa

  1. I had to modify the routes to use a different syntax along the lines of:

app.get('/api/cart/:cartId', ensureLoggedIn, (req, res, next) => {
      // Logic...
      return res.status(200).json(cart);
});
  1. To load any components your app has a dependency on
  2. Yes, for instance:
ensureAdminLoggedIn(req, res, next) {
  if (req.user && req.user.role === 'admin') {
    next();
  } else {
    res.status(401).json(JSON.stringify({ 'Message': 'Unauthorized' }));
  }
}

simon-hardy avatar Jun 09 '17 09:06 simon-hardy

@simon-hardy I did as you said, and it worked. thank you! One side note: when i implemented the 'ensureAdmin' validation, it did not work. i digged a bit and found that inside req.user there is a user object with role in it. means the path to test is
if( (req.user && req.user.user.role==='admin) ...

probably some error that i did somewhere, but i can't figure out. it works anyways ;)

RonAlmog avatar Jun 11 '17 03:06 RonAlmog

for the current version i have solved it like this

import * as express from 'express';
import * as jwt from 'jsonwebtoken';
import UserCtrl from './controllers/user';

function ensureAdminLoggedIn(req, res, next) {
  if (!req.headers.authorization.startsWith('Bearer ')) {
    return res.status(403).json({ error: { Message: 'Unauthorized' } });
  }
  const token = req.headers.authorization.substring(
    7,
    req.headers.authorization.length
  );
  const user = jwt.verify(token, process.env.SECRET_TOKEN);
  if (user.user.role != 'admin') {
    return res.status(403).json({ error: { Message: 'Unauthorized' } });
  }
  next();
}

function setRoutes(app): void {
  const router = express.Router();
  const userCtrl = new UserCtrl();

  // Users
  router.route('/login').post(userCtrl.login);
  router.route('/users').get(userCtrl.getAll);
  router.route('/users/count').get(userCtrl.count);
  router.route('/user').post(userCtrl.insert);
  router.route('/user/:id').get(userCtrl.get);
  router.route('/user/:id').put(userCtrl.update);
  router.route('/user/:id').delete(ensureAdminLoggedIn, userCtrl.delete);

  // Apply the routes to our application with the prefix /api
  app.use('/api', router);
}

export default setRoutes;

jjoshm avatar Jan 23 '21 14:01 jjoshm

You can also use express-jwt

isLogged = expressJwt({ secret: process.env.SECRET_TOKEN, algorithms: ['HS256']});
router.get('/something', isLogged, userCtrl.something);

I keep the issue open to let others see the solutions. In future I think I will add it to the project.

DavideViolante avatar Jan 24 '21 10:01 DavideViolante

I am thinking about how to test this. Do you have an idea? Maybe the test framework can intercept the "ensureAdminLoggedIn" function for this. I don't know about express-jwt

jjoshm avatar Jan 27 '21 15:01 jjoshm

I think express-jwt is tested itself so you don't need to but if you want more freedom I support the solution of @simon-hardy above and test just that function alone.

DavideViolante avatar Jan 27 '21 17:01 DavideViolante