From 251e0469293f22c3730118b8fa86c5627bbb2fd5 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Thu, 4 Sep 2014 21:27:09 +0200 Subject: [PATCH] Added req.cookie and res.cookie helper methods to Foxx. --- .../Books/Users/Foxx/FoxxController.mdpp | 90 +++++++++------- .../org/arangodb/foxx/base_middleware.js | 100 ++++++++++++++++++ 2 files changed, 149 insertions(+), 41 deletions(-) diff --git a/Documentation/Books/Users/Foxx/FoxxController.mdpp b/Documentation/Books/Users/Foxx/FoxxController.mdpp index c93361c052..5ea81dfd28 100644 --- a/Documentation/Books/Users/Foxx/FoxxController.mdpp +++ b/Documentation/Books/Users/Foxx/FoxxController.mdpp @@ -32,13 +32,13 @@ !SECTION Documenting and constraining a specific route -If you now want to document your route, you can use JSDoc style comments (a -multiline comment block with the first line starting with */*** instead +If you now want to document your route, you can use JSDoc style comments (a +multiline comment block with the first line starting with */*** instead of */**) above your routes to do that: ```js /** Get all foxxes - * + * * If you want to get all foxxes, please use this * method to do that. */ @@ -47,11 +47,11 @@ app.get("/foxxes", function () { }); ``` -The first line will be treated as a summary (For optical reasons in the -produced documentation, the summary is restricted to 60 characters). All -following lines will be treated as additional notes shown in the detailed -view of the route documentation. With the provided information, Foxx will -generate a nice documentation for you. Furthermore you can describe your +The first line will be treated as a summary (For optical reasons in the +produced documentation, the summary is restricted to 60 characters). All +following lines will be treated as additional notes shown in the detailed +view of the route documentation. With the provided information, Foxx will +generate a nice documentation for you. Furthermore you can describe your API by chaining the following methods onto your path definition: !SUBSECTION Path Param @@ -77,7 +77,7 @@ API by chaining the following methods onto your path definition: !SUBSECTION onlyIfAuthenticated @startDocuBlock JSF_foxx_RequestContext_onlyIfAuthenticated - + !SECTION Documenting and constraining all routes In addition to documenting a specific route, you can also @@ -97,12 +97,12 @@ The following methods are available. @startDocuBlock JSF_foxx_RequestContextBuffer_onlyIfAuthenticated - + !SECTION Before and After Hooks -You can use the following two functions to do something before or respectively -after the normal routing process is happening. You could use that for logging -or to manipulate the request or response (translate it to a certain format for +You can use the following two functions to do something before or respectively +after the normal routing process is happening. You could use that for logging +or to manipulate the request or response (translate it to a certain format for example). !SUBSECTION Before @@ -120,14 +120,14 @@ example). !SECTION The Request and Response Objects -When you have created your FoxxController you can now define routes on it. -You provide each with a function that will handle the request. It gets two +When you have created your FoxxController you can now define routes on it. +You provide each with a function that will handle the request. It gets two arguments (four, to be honest. But the other two are not relevant for now): * The *request* object * The *response* object -These objects are provided by the underlying ArangoDB actions and enhanced +These objects are provided by the underlying ArangoDB actions and enhanced by the *BaseMiddleware* provided by Foxx. !SUBSECTION The Request Object @@ -136,7 +136,7 @@ The *request* object inherits several attributes from the underlying Actions: * *compatibility*: an integer specifying the compatibility version sent by the client (in request header *x-arango-version*). If the client does not send this - header, ArangoDB will set this to the minimum compatible version number. The + header, ArangoDB will set this to the minimum compatible version number. The value is 10000 * major + 100 * minor (e.g. *10400* for ArangoDB version 1.4). * *user*: the name of the current ArangoDB user. This will be populated only @@ -145,17 +145,17 @@ The *request* object inherits several attributes from the underlying Actions: * *database*: the name of the current database (e.g. *_system*) * *protocol*: *http* or *https* - -* *server*: a JSON object with sub-attributes *address* (containing server + +* *server*: a JSON object with sub-attributes *address* (containing server host name or IP address) and *port* (server port). * *path*: request URI path, with potential [database name](../Glossary/README.html#database_name) stripped off. -* *url*: request URI path + query string, with potential database name +* *url*: request URI path + query string, with potential database name stripped off * *headers*: a JSON object with the request headers as key/value pairs - + * *cookies*: a JSON object with the request cookies as key/value pairs * *requestType*: the request method (e.g. "GET", "POST", "PUT", ...) @@ -183,6 +183,10 @@ convenience methods: @startDocuBlock JSF_foxx_BaseMiddleware_request_params +!SUBSECTION Cookie + +@startDocuBlock JSF_foxx_BaseMiddleware_request_cookie + !SECTION The Response Object @@ -202,41 +206,45 @@ You provide your response body as a string here. !SUBSECTION Response Json @startDocuBlock JSF_foxx_BaseMiddleware_response_json - + +!SUBSECTION Response Cookie + +@startDocuBlock JSF_foxx_BaseMiddleware_response_cookie + !SECTION Controlling Access to Foxx Applications -Access to Foxx applications is controlled by the regular authentication mechanisms +Access to Foxx applications is controlled by the regular authentication mechanisms present in ArangoDB. The server can be run with or without HTTP authentication. -If authentication is turned on, -then every access to the server is authenticated via HTTP authentication. This -includes Foxx applications. The global authentication can be toggled +If authentication is turned on, +then every access to the server is authenticated via HTTP authentication. This +includes Foxx applications. The global authentication can be toggled via the configuration option. -If global HTTP authentication is turned on, requests to Foxx applications will +If global HTTP authentication is turned on, requests to Foxx applications will require HTTP authentication too, and only valid users present in the *_users* system collection are allowed to use the applications. -Since ArangoDB 1.4, there is an extra option to restrict the authentication to -just system API calls, such as */_api/...* and */_admin/...*. This option can be -turned on using the -"server.authenticate-system-only" configuration option. If it is turned on, -then only system API requests need authentication whereas all requests to Foxx -applications and routes will not require authentication. +Since ArangoDB 1.4, there is an extra option to restrict the authentication to +just system API calls, such as */_api/...* and */_admin/...*. This option can be +turned on using the +"server.authenticate-system-only" configuration option. If it is turned on, +then only system API requests need authentication whereas all requests to Foxx +applications and routes will not require authentication. This is recommended if you want to disable HTTP authentication for Foxx applications but still want the general database APIs to be protected with HTTP authentication. If you need more fine grained control over the access to your Foxx application, -we built an authentication system you can use. Currently we only support cookie-based -authentication, but we will add the possibility to use Auth Tokens and external OAuth +we built an authentication system you can use. Currently we only support cookie-based +authentication, but we will add the possibility to use Auth Tokens and external OAuth providers in the near future. Of course you can roll your own authentication mechanism if you want to, and you can do it in an application-specific way if required. To use the per-application authentication, you should first turn off the global -HTTP authentication (or at least restrict it to system API calls as mentioned above). +HTTP authentication (or at least restrict it to system API calls as mentioned above). Otherwise clients will need HTTP authentication and need additional authentication by -your Foxx application. +your Foxx application. To have global HTTP authentication turned on for system APIs but turned off for Foxx, your server startup parameters should look like this: @@ -246,13 +254,13 @@ your server startup parameters should look like this: **Note**: During development, you may even turn off HTTP authentication completely: --server.disable-authentication true --server.authenticate-system-only true - -Please keep in mind that turning HTTP authentication off completely will allow + +Please keep in mind that turning HTTP authentication off completely will allow unauthenticated access by anyone to all API functions, so do not use this is production. -Now it's time to configure the application-specific authentication. We built a small +Now it's time to configure the application-specific authentication. We built a small [demo application](https://github.com/arangodb/foxx-authentication) to demonstrate how -this works. +this works. To use the application-specific authentication in your own app, first activate it in your controller. diff --git a/js/server/modules/org/arangodb/foxx/base_middleware.js b/js/server/modules/org/arangodb/foxx/base_middleware.js index f95218876e..b72949570a 100644 --- a/js/server/modules/org/arangodb/foxx/base_middleware.js +++ b/js/server/modules/org/arangodb/foxx/base_middleware.js @@ -45,10 +45,61 @@ BaseMiddleware = function () { trace, _ = require("underscore"), console = require("console"), + crypto = require("org/arangodb/crypto"), actions = require("org/arangodb/actions"); requestFunctions = { +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_BaseMiddleware_request_cookie +/// +/// `request.cookie(name, cfg)` +/// +/// Read a cookie from the request. Optionally the cookie's signature can be verified. +/// +/// *Parameter* +/// +/// * *name*: the name of the cookie to read from the request. +/// * *cfg* (optional): an object with any of the following properties: +/// * *signed* (optional): an object with any of the following properties: +/// * *secret*: a secret string that was used to sign the cookie. +/// * *algorithm*: hashing algorithm that was used to sign the cookie. Default: *"sha256"*. +/// +/// If *signed* is a string, it will be used as the *secret* instead. +/// +/// If a *secret* is provided, a second cookie with the name *name + ".sig"* will +/// be read and its value will be verified as the cookie value's signature. +/// +/// If the cookie is not set or its signature is invalid, "undefined" will be returned instead. +/// +/// @EXAMPLES +/// +/// ``` +/// var sid = request.cookie("sid", {signed: "keyboardcat"}); +/// ``` +/// @endDocuBlock +//////////////////////////////////////////////////////////////////////////////// + + cookie: function (name, cfg) { + if (!cfg || typeof cfg !== 'object') { + cfg = {}; + } + var value = this.cookies[name] || undefined; + if (value && cfg.signed) { + if (typeof cfg.signed === 'string') { + cfg.signed = {secret: cfg.signed}; + } + var valid = crypto.constantEquals( + this.cookies[name + '.sig'] || '', + crypto.hmac(cfg.signed.secret, value, cfg.signed.algorithm) + ); + if (!valid) { + value = undefined; + } + } + return value; + }, + //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_BaseMiddleware_request_body /// @@ -102,6 +153,55 @@ BaseMiddleware = function () { responseFunctions = { +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_BaseMiddleware_response_cookie +/// +/// `response.cookie(name, value, cfg)` +/// +/// Add a cookie to the response. Optionally the cookie can be signed. +/// +/// *Parameter* +/// +/// * *name*: the name of the cookie to add to the response. +/// * *value*: the value of the cookie to add to the response. +/// * *cfg* (optional): an object with any of the following properties: +/// * *ttl* (optional): the number of seconds until this cookie expires. +/// * *path* (optional): the cookie path. +/// * *domain* (optional): the cookie domain. +/// * *secure* (optional): mark the cookie as safe transport (HTTPS) only. +/// * *httpOnly* (optional): mark the cookie as HTTP(S) only. +/// * *signed* (optional): an object with any of the following properties: +/// * *secret*: a secret string to sign the cookie with. +/// * *algorithm*: hashing algorithm to sign the cookie with. Default: *"sha256"*. +/// +/// If *signed* is a string, it will be used as the *secret* instead. +/// +/// If a *secret* is provided, a second cookie with the name *name + ".sig"* will +/// be added to the response, containing the cookie's HMAC signature. +/// +/// @EXAMPLES +/// +/// ``` +/// response.cookie("sid", "abcdef", {signed: "keyboardcat"}); +/// ``` +/// @endDocuBlock +//////////////////////////////////////////////////////////////////////////////// + + cookie: function (name, value, cfg) { + if (!cfg || typeof cfg !== 'object') { + cfg = {ttl: cfg}; + } + var ttl = (typeof cfg.ttl === 'number' && cfg.ttl !== Infinity) ? cfg.ttl : undefined; + actions.addCookie(this, name, value, ttl, cfg.path, cfg.domain, cfg.secure, cfg.httpOnly); + if (cfg.signed) { + if (typeof cfg.signed === 'string') { + cfg.signed = {secret: cfg.signed}; + } + var sig = crypto.hmac(cfg.signed.secret, value, cfg.signed.algorithm); + actions.addCookie(this, name + '.sig', sig, ttl, cfg.path, cfg.domain, cfg.secure, cfg.httpOnly); + } + }, + //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_BaseMiddleware_response_status ///