Example showing CSRF protection+JWT Authentication using Angular and Node
In this story,I am sharing a solution of implementing JWT authentication and authorisation with protection against CSRF attacks using double submit cookie method.
Let me describe the problem of CSRF and how the double submit cookie method helps us solve this problem.
Assume a scenario where a user has logged onto a bank website. An attacker might trick the user into navigating from the bank website to his malicious website through attractive images or anything that captures the attention of the user. Behind the attractive image might be a hidden form something like below:
<form method="POST" action="https://bank.com/transferMoney.php" >
<input type="hidden" name="fromAccount" value="00000003" />
<input type="hidden" name="toAccount" value="13371337" />
<input type="hidden" name="amount" value="$1,000,000" />
<button type="submit" >View Dog Pictures</button>
This form will be similar to the form that the bank website uses to transfer money. Clicking the image will trigger the click of the submit button on the form.
If the user accidently clicks the image, then browser will send the request to the bank website.This request will succeed because the session cookies set by the bank website on the browser will be sent by the browser along with the request and the server has no reason to complain because the presence of the cookies prove that the user is authenticated.
Using the double cookie submit method, the server generates a unique CSRF token+secret and sends it to the client in a cookie even before the user is authenticated.
Each time the client makes a state changing request to the server, 2 things are sent back to the server:
=>CSRF token and secret cookies
=>CSRF token is also sent back to the server in a custom http header,query or response body.
The server then validates if the CSRF token in the cookie matches the CSRF token sent in the header,query or body.
If the validation is successfull, the server can ensure that an attacker impersonated as the user has not sent the request.
Let me explain the example briefly. The end-user can be an admin or non-admin user. The application authenticates the end-user using JWT tokens. CSRF protection is provided on all mutating/state-changing requests.
On login,the user is navigated to a dashboard where he can create products and list out the created products.
Only an admin user can perform both functions. A non-admin user can only view products.
Lets begin with the Node server configuration.
I have produced a skeleton of the project using npm module express-generator.
- modules.js contains all the imported modules at 1 place to keep it neat.
2.app.js contains all the middleware functions.
=>cors module is required because the Angular dev server(http://localhost:4200) and the Node server(http://localhost:8080) have different origins and the Same Origin Policy restricts interaction between 2 resources of different origins.
But CORS errors are taken care of by incorporating proxy in the angular application.We are adding restrictions on the origins and methods that can access the server resources using the cors module.
An Origin is basically a combination of 3 elements: protocol,domain/subdomain and port. If either of these 3 differ in 2 web resources then they are considered to be of different origins.
In order to relax this policy, we are using CORS(Cross origin resource sharing), where use headers to specify which origins and methods are permitted to access the resources. There are many other headers you could specify as well based on the requirement.
const corsOptions = {
origin: ‘http://localhost:4200',
methods:[‘GET’,’POST’,’PUT’,’DELETE’]}
=>jsonwebtoken module is used for signing the payload,generating token and verifying the token.
We need private and public keys to sign and verify respectively. I have generated 512 bit keys from http://travistidwell.com/jsencrypt/demo/ and copied them into a public.key and private.key file.
Private key is for signing the JWT token and Public key is for verifying the JWT token.
=>csurf module is required to generate CSRF token and secret and also verifying the token with the secret for mutating requests.
3. This application has 2 types of users:an admin user and non-admin user.We have defined this in roles.js.
module.exports={
user:”User”,
admin:”Admin”
}
4. We have defined the authentication and authorisation logic inside the below Auth.js
IsAdminAuthenticated() verifies the validity of the JWT token for authentication and also checks if the user is an admin.
IsUserAuthenticated() also does the job of authentication but the user can be an admin or a non-admin user.
If authentication and authorisation steps pass in both the methods, the CSRF token is also validated against the secret for all mutating requests(PUT,POST and DELETE)
5.index.js defines all the routes. I have omitted the logic for reading and adding products since we are focussing only on authentication and authorisation.
We are using cookies(not session)to send the CSRF token and secret to the client, hence you need to set either cookie property to true or to an object containing options for the cookie.
function customValue(req){
return req.headers['x-xsrf-token'];
}
const csrfProtection = csurf({ cookie:{httpOnly:true},value:customValue});
Its good practice to define the below options in case of an HTTPS connection. The secure option makes Man In the Middle attack very difficult although not impossible.
const csrfProtection = csurf({ cookie :{httpOnly:true,secure:true,signed:true},value:customValue});
The value property provides a function that the CSURF module will call to read the CSRF token from a mutating request(other than GET,HEAD or OPTIONS)for validation. The function is called as value(req)
and is expected to return the token as a string.
The default value is a function that reads the token from the following locations, in order:
req.body._csrf
- typically generated by thebody-parser
module.req.query._csrf
- a built-in from Express.js to read from the URL query string.req.headers['csrf-token']
- theCSRF-Token
HTTP request header.req.headers['xsrf-token']
- theXSRF-Token
HTTP request header.req.headers['x-csrf-token']
- theX-CSRF-Token
HTTP request header.req.headers['x-xsrf-token']
- theX-XSRF-Token
HTTP request header.
We have specifically mentioned that that CSURF should read the CSRF token only from a request header named “X-XSRF-TOKEN” to minimise the chances of CSRF attacks.
Inserting the CSRF token in the custom HTTP request header in the Angular application is considered more secure than adding the token in the hidden field form parameter because it uses custom request headers.
The entire Node project is available at the below link:
https://github.com/ramya22111992/JWTCSRFNode.git
Now lets see how the entire thing works:
Step 1: As soon as the angular bootstrap component loads, we are making a call to server.
router.get(‘/’,csrfProtection,function(req,res,next) is executed which sends the client two cookies, with the names of _csrf and XSRF-TOKEN.
_csrf is generated automatically because we chose to set { cookie: true }. This is a secret, not the CSRF token. The server uses this secret to match the actual token against it.
CSRF tokens can be generated only if csrfProtection is passed as an argument to the route.
We use req.csrfToken() to generate the token and send it to the browser in a cookie named “XSRF-TOKEN”.
The _csrf secret is generated by the server every time the client sends a request provided the following conditions are satisfied: (1) Request doesn’t include the _csrf cookie, and (2) Request hits a route that uses the csrfProtection middleware.
Step-2: Now lets proceed to the login. The user enters his username and password in a form and submits it to the server.
router.post(‘/login’,csrfProtection,function(req,res,next) is executed.
When the angular application sends a request to the server, we know that the cookies are sent as well i.e _csrf and XSRF-TOKEN cookie. Also the angular app needs to send back the CSRF token either in the body, query string, or headers of every mutating request for validation.
In this example,the angular app is sending the CSRF token value in a header named “X-XSRF-TOKEN”.
The CSURF module will check the _csrf cookie against the value of “X-XSRF-TOKEN”. This check is done for all methods automatically except GET,HEAD and OPTIONS.
If the validation fails then the route is not executed any further and a Forbidden error response is returned with a 403 status code.
I am not using database here to validate the username and password for simplicity.Instead I have a users.json file with user details. I shall compare the username and password in the request with the entries in the users.json file for a match.
If no match is found we are sending back a response with 401 status code.
If a match is found, we proceed to generate a JWT token and new CSRF token. As you know, a JWT token has a payload,header and a signature.
We are passing the username,the role of the user and the newly generated CSRF token into the payload. The private.key file will be used to sign this payload and generate the token.
Storing the CSRF token in a JWT makes it possible for the server to verify that it produced the token itself.Combining the CSRF token with an account identifier makes it impossible for attackers to reuse a token for another user.
Once the JWT token is ready, we are sending back 4 cookies containing the JWT token,the username,the CSRF token and the expiry timestamp of the JWT token respectively to the browser.
We are using cookies over Web Storage and Headers for storing JWT tokens because HttpOnly cookies are immune to XSS attacks and CSRF attacks can be prevented using the method described above. Man-In-The-Middle attacks can be made difficult by using secure cookies.Another advantage is that cookies is sent by the browser to the server in each request.
Web Storage and Headers although immune to CSRF attacks, they are vulnerable to XSS attacks.
Setting httpOnly attribute to true makes the cookie immune to XSS attacks. We dont wish an attacker to find the JWT token using javascript.
The cookie containing the CSRF token has httpOnly attribute as false because Angular requires a JS readable cookie for a purpose which we shall see later.
I have set secure attribute to false because our example runs on a HTTP connection and not HTTPS.
Setting secure attribute to true ensures that the cookies are shared only on https connection and minimises the chances of man-in-the-middle attack.
Step-3: Once we have successfully logged in, the angular application navigates the user to a dashboard.
For every request of user in the dashboard,it is important to validate the authenticated state of the user before the server executes the request.
Consider a situation ,where the user submits a request to create a new product or retrieve a list of existing products in the dashboard or when a user tries to navigate to the dashboard by entering the URL in the browser directly,we would want the server to check 3 points:
=>If the user is still logged in by verifying the JWT token.
=>We check if the role of the user is correct for the action being performed.
=>Check if an attacker is impersonating as a user and making the request by verifying the CSRF token.
If the user submits request for creating a product, router.post('/addProduct',AdminAuthFunc,function(req,res,next) will get executed. Before it executes, the CSURF module will check the _csrf cookie against the value of “X-XSRF-TOKEN”request header.
If the validation succeeds,Next,the middleware function IsAdminAuthenticated() is called.
In this function, we validate 3 points:
=>Use the verify() of the jsonwebtoken module to check the JWT token inside the sessionId cookie provided as 1st argument. It is verified against the verification options provided as the 3rd argument in the verify().
=>If verification is successfull, we check if the CSRF token in the JWT token matches the CSRF token sent in the “X-XSRF-TOKEN” request header. If there is a match, then the route gets executed.
=>If there is an error if JWT verification or if the role of the user is not an Admin then we send back an error response.
When the user submits a request to retrieve a list of existing products,router.get(‘/getProducts’,UserAuthFunc,function(req,res,next) is executed.
We validate the same 3 points but the IsUserAuthenticated() called instead where both a non-admin and an admin user is authorised to access this route,but the user needs to be authenticated.
Similarly, if a user tries to hit the dashboard url directly in the browser, Angular makes a call to the server and router.get(‘/IsLoggedIn’,function(req,res,next) is executed.
Here we are checking if the current timestamp is before the timestamp contained in the expiry cookie. If yes, then the user has a valid JWT. If no,the user is not authenticated or the JWT has expired
Note that for GET,HEAD and OPTION methods, CSURF module will not check the _csrf cookie against the value of “X-XSRF-TOKEN”request header.
Finally lets move to the Angular project.
We have 2 main components :LoginComponent and DashboardComponent
We have 2 main services: LoginService and DashboardService
Lets first define the proxy to avoid CORS errors
//proxy.conf.json
{
“/csrf/*”:{
“target”:”http://localhost:8080",
“secure”:false,
“logLevel”:”debug”,
“changeOrigin”:true
}
}
Next the environment:
export const environment = {
production: false,
baseUrl:’/csrf’
};
Modify the package.json as below to consider the proxy:
“scripts”: {
“ng”: “ng”,
“start”: “ng serve — proxyConfig proxy.conf.json”,
“build”: “ng build”,
“test”: “ng test”,
“lint”: “ng lint”,
“e2e”: “ng e2e”
}
Routing Definition:
const routes: Routes = [
{
path:””,
pathMatch:”full”,
redirectTo:”loginPage”
},
{
path:’loginPage’,
component:LoginComponent
},
{
path:’dash’,
component:DashboardComponent,
canActivate:[RouteActivateService]
}
];
Angular support for CSRF:
HttpClient
supports a common mechanism used to prevent XSRF/CSRF attacks. When performing HTTP requests, an interceptor reads a token from a cookie, by default XSRF-TOKEN
, and sets it as an HTTP header, X-XSRF-TOKEN
. Since only code that runs on your domain could read the cookie, the backend can be certain that the HTTP request came from your client application and not an attacker.
By default, an interceptor sends this header on all mutating requests (such as POST) to relative URLs, but not on GET/HEAD requests or on requests with an absolute URL.
Thus Angular does the job of setting the X-XSRF-TOKEN request header for us for all mutating requests. We just need to add the HttpClientXsrfModule module to the imports array as below.
imports:[BrowserModule,AppRoutingModule,ReactiveFormsModule,HttpClientModule,HttpClientXsrfModule],
LoginComponent Template just has a login form for the user to enter his email id and password.
<form [formGroup]=”loginForm” class=”form-group jumbotron”>
<label for=”user”>Username</label>
<input type=”email” class=”form-control” formControlName=”username” name=”username” id=”user” placeholder=”abc@xyz.com”>
<span *ngIf=”loginForm.get(‘username’).errors?.required && loginForm.get(‘username’).touched”>Username needed</span>
<span *ngIf=”loginForm.get(‘username’).errors?.email && loginForm.get(‘username’).touched”>Username invalid</span>
<br>
<label for=”pass”>Password</label>
<input type=”password” class=”form-control” formControlName=”password” name=”password” id=”pass”>
<span *ngIf=”loginForm.get(‘password’).errors?.required && loginForm.get(‘password’).touched”>Password needed</span>
<br>
<button [disabled]=”loginForm.invalid” class=”btn btn-primary” (click)=”loginUser()”>Login</button>
</form>
<app-error></app-error>
<app-error></app-error> is the ErrorComponent where we display any kind of client or server errors.
We are displaying the LoginComponent when the application loads. When the component loads we are calling the generateCSRF() in the LoginService to send a “/” GET request to the server. The node server as discussed earlier generates a CSRF token+secret and sends it back to the browser in a cookie.
Once the user fills out the username and password and submits, the loginUser() in the component is called. The component calls loginUser() in the service, which in turn sends a POST request to /login.
export class LoginComponent implements OnInit {
loginForm:FormGroup;
constructor(private serv:LoginService,private router:Router) { }
ngOnInit() {
this.loginForm=new FormGroup({
username:new FormControl("",[Validators.required,Validators.email]),
password:new FormControl("",[Validators.required])
})
this.serv.generateCSRF().subscribe(data=>console.log(data),err=>console.log(err));
}
loginUser(){
this.serv.loginUser(this.loginForm.value).subscribe(data=>{this.router.navigate(['/dash'],{state:data});},
err=>{console.log(err)});
}}
If the login is successfull, we are navigating to the dashboard, http://localhost:4200/dash.
Note the use of AuthGaurd for the DashBoard Component in the routing definition. Before the DashBoard Component loads, the AuthGuard is executed which calls the isLoggedIn() in the LoginService. The service sends a /IsLoggedIn GET request to the server to check if the user is authenticated or not.
This kind of guard is useful if a user tries to hit the dashboard url directly in the browser.
export class RouteActivateService implements CanActivate {
constructor(private serv:LoginService) { }
canActivate():Observable<boolean>
{
return this.serv.isLoggedIn().pipe(map(x=>{
if(x)
{
return true;
}
}),catchError(()=>
{
this.router.navigate(['/loginPage']);
return of(false);
}))
}
}
LoginService:
export class LoginService {
constructor(private http:HttpClient) { }
loginUser(credentials):Observable<any>{
return this.http.post(environment.baseUrl+"/login",credentials)
}
generateCSRF():Observable<any>{
return this.http.get(environment.baseUrl+"/")
}
isLoggedIn():Observable<boolean>{
return this.http.get<boolean>(environment.baseUrl+"/IsLoggedIn")
}
}
In DashboardComponent, the authenticated user can create product and get a list of products.
export class DashboardComponent implements OnInit {
commentForm:FormGroup;
constructor(private serv:DashboardService,private router:Router) { }
user:string;
role:string;
availableRoles={
user:"User",
admin:"Admin"
}
ngOnInit() {this.ProductForm=new FormGroup({
name:new FormControl("",[Validators.required]),
price:new FormControl("",[Validators.required]),
productID:new FormControl("",[])
})
this.user=history.state.user;
this.role=history.state.role;
}
submitProduct(){
//logic for calling submitProduct() in the DashboardService
}
retrieveProduct(){
//logic for calling retrieveProduct() in the DashboardService
}
}
DashboardService:
export class DashboardService {
constructor(private http:HttpClient) { }
createProductId():Observable<string>{
return this.http.get(environment.baseUrl+"/productID",{responseType:'text'})
}
submitProduct(form){
return this.http.post(environment.baseUrl+"/addProduct",form)
}
retrieveProduct():Observable<any>{
return this.http.get(environment.baseUrl+"/getProducts")
}
}
You can find the entire Angular project at the below link
https://github.com/ramya22111992/JWT-CSRFAuthentication.git
Please let me know your comments:)