How to secure the backend api?
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.
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
ok, great. i will work on that. thanks!
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 Quick question before I check through this, is AuthHttp able to handle https? Thanks in advance.
Yep.
that's sounds good, i'm trying to implement. just a few questions, because i'm new to this:
- where do i put, and how do i use the validation method, ensuerLoggedIn?
- what is the purpose of the shared module?
- is there a way to limit not only to logged in users, but to users who belong to a role, like admin? Thanks!
@AmirGilboa
- 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);
});
- To load any components your app has a dependency on
- 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
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 ;)
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;
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.
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
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.