-
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(csrf): setup csrf token generation. Add basic tests Install the csrf package, create csrf middleware function, handle disabled csrf cases, add tests for csrf generation on every request' fix #12 * feat(csrf): exclude csrf checks using request methods When a user defines request methods in the csrf config, we run csrf checks only on those request methods. f#12 * feat(csrf): refactor csrf implementation to class Remove generic exceptions and install @poppins/utils, add sessions package f#12 * chore(csrf): add peer dependency for @adonisjs/session * chore(csrf): add comments to classes and functions * feat(csrf): implement filterUris for csrf verification Install path-to-regexp package, create method to verify url does not match any defined in filter fix #12 * feat(csrf): share csrf field and token with view Install @adonisjs/view as a dev package, add tests to make sure view local data is correctly shared, and tests to make sure secure cookie is added to response' fix #12 * chore(csrf): remove path-to-regexp package * feat(csrf): change filterUris option in csrf config to exceptRoutes fix #12 * feat(csrf): add meta helper for meta tag with csrf token fix #12 * feat(csrf): change cookie name from x-xsrf-token to xsrf-token fix #12' * chore(csrf): remove edge.js from dev dependency list
- Loading branch information
1 parent
46222a9
commit 4b45cf7
Showing
11 changed files
with
455 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
process.env.TS_NODE_FILES = true | ||
|
||
require('ts-node/register') | ||
|
||
const { configure } = require('japa') | ||
configure({ | ||
files: ['test/**/*.spec.ts'] | ||
files: ['test/**/*.spec.ts'], | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
/* | ||
* @adonisjs/shield | ||
* | ||
* (c) ? (Please advice before merge. Thanks !) | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
/// <reference path="../adonis-typings/index.ts" /> | ||
|
||
import Tokens from 'csrf' | ||
import { unpack } from '@poppinss/cookie' | ||
import { Exception } from '@poppinss/utils' | ||
import { CsrfOptions } from '@ioc:Adonis/Addons/Shield' | ||
import { RequestContract } from '@ioc:Adonis/Core/Request' | ||
import { SessionContract } from '@ioc:Adonis/Addons/Session' | ||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' | ||
|
||
import { noop } from './noop' | ||
|
||
const Csrf = new Tokens() | ||
|
||
/** | ||
* A wrapper around all the functionality | ||
* for handling csrf verification | ||
* | ||
*/ | ||
export class CsrfMiddleware { | ||
/** | ||
* The session instance of the application. | ||
* This would be injected from the | ||
* http context. | ||
*/ | ||
public session: SessionContract | ||
|
||
/** | ||
* Csrf configurations defined by the | ||
* user in the shield.js file. | ||
*/ | ||
private options: CsrfOptions | ||
|
||
/** | ||
* The application key defined as APP_SECRET | ||
* in environment variables. This would | ||
* be injected from the config. | ||
*/ | ||
private applicationKey: string | ||
|
||
constructor (session: SessionContract, options: CsrfOptions, applicationKey: string) { | ||
this.session = session | ||
this.options = options | ||
this.applicationKey = applicationKey | ||
} | ||
|
||
/** | ||
* Get the request method, check if the user defined | ||
* methods allowed for csrf verification in the | ||
* config. If it did, check if the request | ||
* method is one of the allowed. If not, | ||
* return false. | ||
*/ | ||
private requestMethodShouldEnforceCsrf (request: RequestContract): boolean { | ||
const method = request.method().toLowerCase() | ||
|
||
if (!this.options.methods || this.options.methods.length === 0) { | ||
return true | ||
} | ||
|
||
return this.options.methods | ||
.filter(definedMethod => definedMethod.toLowerCase() === method) | ||
.length > 0 | ||
} | ||
|
||
/** | ||
* Check if the current request url has been | ||
* excluded from csrf protection. | ||
*/ | ||
private requestUrlShouldEnforceCsrf (ctx: HttpContextContract): boolean { | ||
if (!this.options.exceptRoutes || this.options.exceptRoutes.length === 0) { | ||
return true | ||
} | ||
|
||
return !this.options.exceptRoutes.includes(ctx.route!.pattern) | ||
} | ||
|
||
/** | ||
* Check if csrf secret has been saved to | ||
* session. If not, generate a new one, | ||
* save it to session, and return it. | ||
*/ | ||
public async getCsrfSecret (): Promise<string> { | ||
let csrfSecret = this.session.get('csrf-secret') | ||
|
||
if (!csrfSecret) { | ||
csrfSecret = await Csrf.secret() | ||
|
||
this.session.put('csrf-secret', csrfSecret) | ||
} | ||
|
||
return csrfSecret | ||
} | ||
|
||
/** | ||
* Extract the csrf token from the request by | ||
* checking headers and inputs. Decode the | ||
* token if it was encrypted. | ||
*/ | ||
private getCsrfTokenFromRequest (request: RequestContract): string|null { | ||
const token = request.input('_csrf') || request.header('x-csrf-token') | ||
|
||
if (token) { | ||
return token | ||
} | ||
|
||
const encryptedToken = request.header('x-xsrf-token') | ||
const unpackedToken = encryptedToken ? unpack(token, this.applicationKey) : null | ||
|
||
return unpackedToken ? unpackedToken.value : null | ||
} | ||
|
||
/** | ||
* Generate a new csrf token using | ||
* the csrf secret extracted | ||
* from session. | ||
*/ | ||
public generateCsrfToken (csrfSecret): string { | ||
return Csrf.create(csrfSecret) | ||
} | ||
|
||
/** | ||
* Set the xsrf cookie on | ||
* response | ||
*/ | ||
private setXsrfCookie (ctx: HttpContextContract): void { | ||
ctx.response.cookie('xsrf-token', ctx.request.csrfToken) | ||
} | ||
|
||
/** | ||
* Set the csrf token on | ||
* request | ||
*/ | ||
private setCsrfToken (ctx: HttpContextContract, csrfSecret: string): void { | ||
ctx.request.csrfToken = this.generateCsrfToken(csrfSecret) | ||
} | ||
|
||
/** | ||
* This would make a csrfToken variable available to | ||
* the edge view templates. This would also create | ||
* a helpful method called csrfField to be used | ||
* on the frontend to generate a hidden | ||
* field called _csrf | ||
*/ | ||
private shareCsrfViewLocals (ctx: HttpContextContract): void { | ||
if (!ctx.view) { | ||
return | ||
} | ||
|
||
ctx.view.share({ | ||
csrfToken: ctx.request.csrfToken, | ||
csrfMeta: (compilerContext) => compilerContext.safe(`<meta name='csrf-token' content='${ctx.request.csrfToken}'>`), | ||
csrfField: (compilerContext) => compilerContext.safe(`<input type='hidden' name='_csrf' value='${ctx.request.csrfToken}'>`), | ||
}) | ||
} | ||
|
||
/** | ||
* Handle csrf verification. First, get the secret, | ||
* next, check if the request method should be | ||
* verified. Next, attach the newly generated | ||
* csrf token to the request object. | ||
*/ | ||
public async handle (ctx: HttpContextContract): Promise<void> { | ||
const { request } = ctx | ||
|
||
const csrfSecret = await this.getCsrfSecret() | ||
|
||
if (this.requestMethodShouldEnforceCsrf(request) && this.requestUrlShouldEnforceCsrf(ctx)) { | ||
const csrfToken = this.getCsrfTokenFromRequest(request) | ||
|
||
if (!csrfToken || !Csrf.verify(csrfSecret, csrfToken)) { | ||
throw new Exception('Invalid CSRF Token', 403, 'E_BAD_CSRF_TOKEN') | ||
} | ||
} | ||
|
||
this.setCsrfToken(ctx, csrfSecret) | ||
|
||
this.setXsrfCookie(ctx) | ||
|
||
this.shareCsrfViewLocals(ctx) | ||
} | ||
} | ||
|
||
/** | ||
* Check if csrf is enabled. If yes, verifies the | ||
* old token and generates a new one for | ||
* the next request. | ||
*/ | ||
export function csrf (options: CsrfOptions, applicationKey: string) { | ||
if (!options.enabled) { | ||
return noop | ||
} | ||
|
||
return async function csrfMiddlewareFn (ctx: HttpContextContract) { | ||
const csrfMiddleware = new CsrfMiddleware(ctx.session, options, applicationKey) | ||
|
||
return csrfMiddleware.handle(ctx) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Csrf Field: {{ csrfField() }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Csrf Meta: {{ csrfMeta() }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Csrf Token: {{ csrfToken }} |
Oops, something went wrong.