From 5af757c0be06a9f35bd536a50133ca29c210717d Mon Sep 17 00:00:00 2001 From: Petr Ondrusek <34578008+PetrOndrusek@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:53:34 +0100 Subject: [PATCH] Api3 remove Date header (#6855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * APIv3: isolating documents from tests (not allowing clashes of calculated identifiers) * removing unused async keyword * fixing api v3 swagger and moving it to /api3-docs * APIv3: finishing cache invalidation tests Co-authored-by: Petr Ondrusek Co-authored-by: Petr Ondrůšek Co-authored-by: Sulka Haro --- lib/api3/const.json | 6 +- lib/api3/doc/security.md | 16 -- lib/api3/doc/tutorial.md | 18 +-- lib/api3/index.js | 1 - lib/api3/security.js | 45 +----- lib/api3/swagger.json | 256 ++------------------------------ lib/api3/swagger.yaml | 54 +------ tests/api3.security.test.js | 90 +---------- tests/fixtures/api3/instance.js | 10 +- 9 files changed, 33 insertions(+), 463 deletions(-) diff --git a/lib/api3/const.json b/lib/api3/const.json index e82104ddb53..5f63c63679a 100644 --- a/lib/api3/const.json +++ b/lib/api3/const.json @@ -1,7 +1,6 @@ { - "API3_VERSION": "3.0.2-alpha", + "API3_VERSION": "3.0.3-alpha", "API3_SECURITY_ENABLE": true, - "API3_TIME_SKEW_TOLERANCE": 5, "API3_DEDUP_FALLBACK_ENABLED": true, "API3_CREATED_AT_FALLBACK_ENABLED": true, "API3_MAX_LIMIT": 1000, @@ -34,10 +33,7 @@ "HTTP_400_SORT_SORT_DESC": "Parameters sort and sort_desc cannot be combined", "HTTP_400_UNSUPPORTED_FILTER_OPERATOR": "Unsupported filter operator {0}", "HTTP_400_IMMUTABLE_FIELD": "Field {0} cannot be modified by the client", - "HTTP_401_BAD_DATE": "Bad Date header", "HTTP_401_BAD_TOKEN": "Bad access token or JWT", - "HTTP_401_DATE_OUT_OF_TOLERANCE": "Date header out of tolerance", - "HTTP_401_MISSING_DATE": "Missing Date header", "HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT", "HTTP_403_MISSING_PERMISSION": "Missing permission {0}", "HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS", diff --git a/lib/api3/doc/security.md b/lib/api3/doc/security.md index 0fdf4c7d2aa..49a2505ca0e 100644 --- a/lib/api3/doc/security.md +++ b/lib/api3/doc/security.md @@ -27,22 +27,6 @@ There are two ways to authorize API calls: - then, to each secure API operation attach a JWT token in the HTTP header, eg. `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NUb2tlbiI6InRlc3RyZWFkYWItNzZlYWZmMjQxOGJmYjdlMCIsImlhdCI6MTU2NTAzOTczMSwiZXhwIjoxNTY1MDQzMzMxfQ.Y-OFtFJ-gZNJcnZfm9r4S7085Z7YKVPiaQxuMMnraVk` (until the JWT expires) - ---- -### Client timestamps -As previously mentioned, a potential attacker cannot decrypt the captured messages, but he can send them back to the client/server at any later time. APIv3 is partially preventing this by the temporal validity of each secured API call. - - -The client must include his current timestamp to each call so that the server can compare it against its clock. If the timestamp difference is not within the limit, the request is considered invalid. The tolerance limit is set in minutes in the `API3_TIME_SKEW_TOLERANCE` environment variable. - -There are two ways to include the client timestamp to the call: -- use `now` query parameter with UNIX epoch millisecond timestamp, eg. `now=1565041446908` -- add HTTP `Date` header to the request, eg. `Date: Sun, 12 May 2019 07:49:58 GMT` - - -The client can check each server response in the same way, because each response contains a server timestamp in the HTTP *Date* header as well. - - --- APIv3 security is enabled by default, but it can be completely disabled for development and debugging purposes by setting the web environment variable `API3_SECURITY_ENABLE=false`. This setting is hazardous and it is strongly discouraged to be used for production purposes! diff --git a/lib/api3/doc/tutorial.md b/lib/api3/doc/tutorial.md index 73bc6c99f8d..50ab57c5b93 100644 --- a/lib/api3/doc/tutorial.md +++ b/lib/api3/doc/tutorial.md @@ -47,7 +47,7 @@ It is public (there is no need to add authorization parameters/headers). Sample GET `/status` client code (to get my actual permissions): ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`, (error, response, body) => console.log(body)); @@ -86,7 +86,7 @@ Sample result: Sample GET `/entries` client code (to retrieve last 3 BG values): ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&limit=3&fields=dateString,sgv,direction`, (error, response, body) => console.log(body)); @@ -124,7 +124,7 @@ Sample result: Sample POST `/treatments` client code: ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const doc = { date: 1564591511232, // (new Date()).getTime(), app: 'AndroidAPS', @@ -158,7 +158,7 @@ Sample result: Sample GET `/treatments/{identifier}` client code: ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`, @@ -193,7 +193,7 @@ Sample result: Sample GET `/lastModified` client code (to get latest modification dates): ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`, (error, response, body) => console.log(body)); @@ -223,7 +223,7 @@ Sample result: Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4): ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; const doc = { date: 1564591511232, @@ -257,7 +257,7 @@ Sample result: Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5): ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; const doc = { insulin: 0.5 @@ -287,7 +287,7 @@ Sample result: Sample DELETE `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5): ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; request({ @@ -312,7 +312,7 @@ Sample result: Sample HISTORY `/treatments/history/{lastModified}` client code: ```javascript const request = require('request'); -const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const auth = `token=testadmin-ad3b1f9d7b3f59d5`; const lastModified = 1564521267421; request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}?${auth}`, diff --git a/lib/api3/index.js b/lib/api3/index.js index e1cde9f2ae8..83db322a452 100644 --- a/lib/api3/index.js +++ b/lib/api3/index.js @@ -66,7 +66,6 @@ function configure (env, ctx) { app.set('enabledCollections', ['devicestatus', 'entries', 'food', 'profile', 'settings', 'treatments']); self.setENVTruthy('API3_SECURITY_ENABLE', apiConst.API3_SECURITY_ENABLE); - self.setENVTruthy('API3_TIME_SKEW_TOLERANCE', apiConst.API3_TIME_SKEW_TOLERANCE); self.setENVTruthy('API3_DEDUP_FALLBACK_ENABLED', apiConst.API3_DEDUP_FALLBACK_ENABLED); self.setENVTruthy('API3_CREATED_AT_FALLBACK_ENABLED', apiConst.API3_CREATED_AT_FALLBACK_ENABLED); self.setENVTruthy('API3_MAX_LIMIT', apiConst.API3_MAX_LIMIT); diff --git a/lib/api3/security.js b/lib/api3/security.js index 6d6afe21055..7adeefbc76b 100644 --- a/lib/api3/security.js +++ b/lib/api3/security.js @@ -1,10 +1,8 @@ 'use strict'; -const moment = require('moment') - , apiConst = require('./const.json') +const apiConst = require('./const.json') , _ = require('lodash') , shiroTrie = require('shiro-trie') - , dateTools = require('./shared/dateTools') , opTools = require('./shared/operationTools') ; @@ -13,37 +11,6 @@ function getRemoteIP (req) { return req.headers['x-forwarded-for'] || req.connection.remoteAddress; } -/** - * Check if Date header in HTTP request (or 'now' query parameter) is present and valid (with error response sending) - */ -function checkDateHeader (opCtx) { - - const { app, req, res } = opCtx; - - let dateString = req.header('Date'); - if (!dateString) { - dateString = req.query.now; - } - - if (!dateString) { - return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_DATE); - } - - let dateMoment = dateTools.parseToMoment(dateString); - if (!dateMoment) { - return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_DATE); - } - - let nowMoment = moment(new Date()); - let diffMinutes = moment.duration(nowMoment.diff(dateMoment)).asMinutes(); - - if (Math.abs(diffMinutes) > app.get('API3_TIME_SKEW_TOLERANCE')) { - return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); - } - - return true; -} - function authenticate (opCtx) { return new Promise(function promise (resolve, reject) { @@ -56,16 +23,6 @@ function authenticate (opCtx) { return resolve({ shiros: [ adminShiro ] }); } -// if (req.protocol !== 'https') { -// return reject( -// opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_NOT_USING_HTTPS)); -// } - - const checkDateResult = checkDateHeader(opCtx); - if (checkDateResult !== true) { - return checkDateResult; - } - let token = ctx.authorization.extractToken(req); if (!token) { return reject( diff --git a/lib/api3/swagger.json b/lib/api3/swagger.json index a380be3e0f5..67a54d97d95 100644 --- a/lib/api3/swagger.json +++ b/lib/api3/swagger.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Nightscout API", - "description": "Nightscout API v3 is a component of cgm-remote-monitor project. It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange.\n\nAPI v3 uses these environment variables, among other things:\n- Security switch (optional, default = `true`)
API3_SECURITY_ENABLE=true
You can turn the whole security mechanism off, e.g. for debugging or development purposes, but this should never be set to false in production.\n\n- Number of minutes of acceptable time skew between client's and server's clock (optional, default = 5)
API3_TIME_SKEW_TOLERANCE=5
This security parameter is used for preventing anti-replay attacks, specifically when checking the time from `Date` header.\n\n- Maximum limit count of documents retrieved from single query
API3_MAX_LIMIT=1000
\n\n- Autopruning of obsolete documents (optional, default is only `DEVICESTATUS`=60)
API3_AUTOPRUNE_DEVICESTATUS=60\nAPI3_AUTOPRUNE_ENTRIES=365\nAPI3_AUTOPRUNE_TREATMENTS=120 
You can specify for which collections autopruning will be activated and length of retention period in days, e.g. \"Hold 60 days of devicestatus, automatically delete older documents, hold 365 days of treatments and entries, automatically delete older documents.\"\n\n- Fallback deduplication switch (optional, default = true)
API3_DEDUP_FALLBACK_ENABLED=true
API3 uses the `identifier` field for document identification and mutual distinction within a single collection. There is automatic deduplication implemented matching the equal `identifier` field. E.g. `CREATE` operation for document having the same `identifier` as another one existing in the database is automatically transformed into `UPDATE` operation of the document found in the database.\nDocuments not created via API v3 usually does not have any `identifier` field, but we would like to have some form of deduplication for them, too. This fallback deduplication is turned on by having set `API3_DEDUP_FALLBACK_ENABLED` to `true`. When searching the collection in database, the document is found to be a duplicate only when either he has equal `identifier` or he has no `identifier` and meets:
`devicestatus` collection: equal combination of `created_at` and `device`\n`entries` collection:      equal combination of `date` and `type`\n`food` collection:         equal `created_at`\n`profile` collection:      equal `created_at`\n`treatments` collection:   equal combination of `created_at` and `eventType` 
\n\n- Fallback switch for adding `created_at` field along the `date` field (optional, default = true)
API3_CREATED_AT_FALLBACK_ENABLED=true
Standard APIv3 document model uses only `date` field for storing a timestamp of the event recorded by the document. But there is a fallback option to fill `created_at` field as well automatically on each insert/update, just to keep all older components working.", + "description": "Nightscout API v3 is a component of cgm-remote-monitor project. It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange.\n\nAPI v3 uses these environment variables, among other things:\n- Security switch (optional, default = `true`)
API3_SECURITY_ENABLE=true
You can turn the whole security mechanism off, e.g. for debugging or development purposes, but this should never be set to false in production.\n\n- Maximum limit count of documents retrieved from single query
API3_MAX_LIMIT=1000
\n\n- Autopruning of obsolete documents (optional, default is only `DEVICESTATUS`=60)
API3_AUTOPRUNE_DEVICESTATUS=60\nAPI3_AUTOPRUNE_ENTRIES=365\nAPI3_AUTOPRUNE_TREATMENTS=120 
You can specify for which collections autopruning will be activated and length of retention period in days, e.g. \"Hold 60 days of devicestatus, automatically delete older documents, hold 365 days of treatments and entries, automatically delete older documents.\"\n\n- Fallback deduplication switch (optional, default = true)
API3_DEDUP_FALLBACK_ENABLED=true
API3 uses the `identifier` field for document identification and mutual distinction within a single collection. There is automatic deduplication implemented matching the equal `identifier` field. E.g. `CREATE` operation for document having the same `identifier` as another one existing in the database is automatically transformed into `UPDATE` operation of the document found in the database.\nDocuments not created via API v3 usually does not have any `identifier` field, but we would like to have some form of deduplication for them, too. This fallback deduplication is turned on by having set `API3_DEDUP_FALLBACK_ENABLED` to `true`. When searching the collection in database, the document is found to be a duplicate only when either he has equal `identifier` or he has no `identifier` and meets:
`devicestatus` collection: equal combination of `created_at` and `device`\n`entries` collection:      equal combination of `date` and `type`\n`food` collection:         equal `created_at`\n`profile` collection:      equal `created_at`\n`treatments` collection:   equal combination of `created_at` and `eventType` 
\n\n- Fallback switch for adding `created_at` field along the `date` field (optional, default = true)
API3_CREATED_AT_FALLBACK_ENABLED=true
Standard APIv3 document model uses only `date` field for storing a timestamp of the event recorded by the document. But there is a fallback option to fill `created_at` field as well automatically on each insert/update, just to keep all older components working.", "contact": { "name": "NS development discussion channel", "url": "https://gitter.im/nightscout/public" @@ -11,7 +11,7 @@ "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" }, - "version": "3.0.2" + "version": "3.0.3" }, "servers": [ { @@ -49,29 +49,6 @@ "$ref": "#/components/schemas/paramCollection" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -198,7 +175,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -265,29 +242,6 @@ "$ref": "#/components/schemas/paramCollection" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -359,7 +313,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -439,29 +393,6 @@ "$ref": "#/components/schemas/paramIdentifier" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -542,7 +473,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -630,29 +561,6 @@ "$ref": "#/components/schemas/paramIdentifier" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -724,7 +632,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -822,29 +730,6 @@ "$ref": "#/components/schemas/paramIdentifier" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -880,7 +765,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -958,29 +843,6 @@ "$ref": "#/components/schemas/paramIdentifier" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -1037,7 +899,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -1127,29 +989,6 @@ "$ref": "#/components/schemas/paramCollection" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -1248,7 +1087,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -1330,29 +1169,6 @@ "format": "int64" } }, - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -1440,7 +1256,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -1530,7 +1346,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -1569,29 +1385,6 @@ "description": "LAST MODIFIED operation inspects collections separately (in parallel) and for each of them it finds the date of any last modification (insertion, update, deletion).\nNot only `srvModified`, but also `date` and `created_at` fields are inspected (as a fallback to previous API).\n\nThis operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to.", "operationId": "LAST-MODIFIED", "parameters": [ - { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, { "name": "token", "in": "query", @@ -1616,7 +1409,7 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -2559,7 +2352,7 @@ } }, "401Unauthorized": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "description": "The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.", "content": { "application/json": { "schema": { @@ -2713,29 +2506,6 @@ } }, "parameters": { - "dateHeader": { - "name": "Date", - "in": "header", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `now` query parameter.\nExample:\n\n
Date: Wed, 17 Oct 2018 05:13:00 GMT
", - "required": false, - "style": "simple", - "explode": false, - "schema": { - "type": "string" - } - }, - "nowParam": { - "name": "now", - "in": "query", - "description": "Timestamp (defined by client's clock) when the HTTP request was constructed on client. This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. This can be set alternatively in `Date` header.\n\nExample:\n\n
now=1525383610088
", - "required": false, - "style": "form", - "explode": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, "tokenParam": { "name": "token", "in": "query", diff --git a/lib/api3/swagger.yaml b/lib/api3/swagger.yaml index 56175263924..c9b764a7409 100644 --- a/lib/api3/swagger.yaml +++ b/lib/api3/swagger.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 servers: - url: '/api/v3' info: - version: "3.0.2" + version: 3.0.3 title: Nightscout API contact: name: NS development discussion channel @@ -22,11 +22,6 @@ info: but this should never be set to false in production. - - Number of minutes of acceptable time skew between client's and server's clock (optional, default = 5) -
API3_TIME_SKEW_TOLERANCE=5
- This security parameter is used for preventing anti-replay attacks, specifically when checking the time from `Date` header. - - - Maximum limit count of documents retrieved from single query
API3_MAX_LIMIT=1000
@@ -80,8 +75,6 @@ paths: schema: $ref: '#/components/schemas/paramCollection' - - $ref: '#/components/parameters/dateHeader' - - $ref: '#/components/parameters/nowParam' - $ref: '#/components/parameters/tokenParam' ###################################################################################### @@ -203,8 +196,6 @@ paths: schema: $ref: '#/components/schemas/paramIdentifier' - - $ref: '#/components/parameters/dateHeader' - - $ref: '#/components/parameters/nowParam' - $ref: '#/components/parameters/tokenParam' ###################################################################################### @@ -417,8 +408,6 @@ paths: schema: $ref: '#/components/schemas/paramCollection' - - $ref: '#/components/parameters/dateHeader' - - $ref: '#/components/parameters/nowParam' - $ref: '#/components/parameters/tokenParam' get: @@ -486,8 +475,6 @@ paths: type: integer format: int64 - - $ref: '#/components/parameters/dateHeader' - - $ref: '#/components/parameters/nowParam' - $ref: '#/components/parameters/tokenParam' get: @@ -574,8 +561,6 @@ paths: ###################################################################################### /lastModified: parameters: - - $ref: '#/components/parameters/dateHeader' - - $ref: '#/components/parameters/nowParam' - $ref: '#/components/parameters/tokenParam' get: @@ -608,41 +593,6 @@ components: parameters: - dateHeader: - in: header - name: Date - schema: - type: string - required: false - description: - Timestamp (defined by client's clock) when the HTTP request was constructed on client. - This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. - This can be set alternatively in `now` query parameter. - - Example: - - -
Date: Wed, 17 Oct 2018 05:13:00 GMT
- - - nowParam: - in: query - name: now - schema: - type: integer - format: int64 - required: false - description: - Timestamp (defined by client's clock) when the HTTP request was constructed on client. - This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. - This can be set alternatively in `Date` header. - - - Example: - - -
now=1525383610088
- tokenParam: in: query @@ -937,7 +887,7 @@ components: example: 400 401Unauthorized: - description: The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy. + description: The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy. content: application/json: schema: diff --git a/tests/api3.security.test.js b/tests/api3.security.test.js index 7cd811acfe6..df0928ffe9c 100644 --- a/tests/api3.security.test.js +++ b/tests/api3.security.test.js @@ -3,7 +3,6 @@ const request = require('supertest') , apiConst = require('../lib/api3/const.json') - , semver = require('semver') , moment = require('moment') ; require('should'); @@ -33,91 +32,6 @@ describe('Security of REST API3', function() { }); -// it('should require HTTPS', async () => { -// if (semver.gte(process.version, '10.0.0')) { -// let res = await request(self.http.baseUrl) // hangs on 8.x.x (no reason why) -// .get('/api/v3/test') -// .expect(403); -// -// res.body.status.should.equal(403); -// res.body.message.should.equal(apiConst.MSG.HTTP_403_NOT_USING_HTTPS); -// } -// }); - - - it('should require Date header', async () => { - let res = await request(self.https.baseUrl) - .get('/api/v3/test') - .expect(401); - - res.body.status.should.equal(401); - res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_DATE); - }); - - - it('should validate Date header syntax', async () => { - let res = await request(self.https.baseUrl) - .get('/api/v3/test') - .set('Date', 'invalid date header') - .expect(401); - - res.body.status.should.equal(401); - res.body.message.should.equal(apiConst.MSG.HTTP_401_BAD_DATE); - }); - - - it('should reject Date header out of tolerance', async () => { - const oldDate = new Date((new Date() * 1) - 2 * 3600 * 1000) - , futureDate = new Date((new Date() * 1) + 2 * 3600 * 1000); - - let res = await request(self.https.baseUrl) - .get('/api/v3/test') - .set('Date', oldDate.toUTCString()) - .expect(401); - - res.body.status.should.equal(401); - res.body.message.should.equal(apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); - - res = await request(self.https.baseUrl) - .get('/api/v3/test') - .set('Date',futureDate.toUTCString()) - .expect(401); - - res.body.status.should.equal(401); - res.body.message.should.equal(apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); - }); - - - it('should reject invalid now ABC', async () => { - let res = await request(self.https.baseUrl) - .get(`/api/v3/test?now=ABC`) - .expect(401); - - res.body.status.should.equal(401); - res.body.message.should.equal('Bad Date header'); - }); - - - it('should reject invalid now -1', async () => { - let res = await request(self.https.baseUrl) - .get(`/api/v3/test?now=-1`) - .expect(401); - - res.body.status.should.equal(401); - res.body.message.should.equal('Bad Date header'); - }); - - - it('should reject invalid now - illegal format', async () => { - let res = await request(self.https.baseUrl) - .get(`/api/v3/test?now=2019-20-60T50:90:90`) - .expect(401); - - res.body.status.should.equal(401); - res.body.message.should.equal('Bad Date header'); - }); - - it('should require token', async () => { let res = await request(self.https.baseUrl) .get('/api/v3/test') @@ -164,8 +78,8 @@ describe('Security of REST API3', function() { .get(`/api/v3/test?token=${self.token.read}&now=${moment().valueOf()}`) .expect(200); }); - - + + it('should accept valid now - epoch in seconds', async () => { await request(self.https.baseUrl) .get(`/api/v3/test?token=${self.token.read}&now=${moment().unix()}`) diff --git a/tests/fixtures/api3/instance.js b/tests/fixtures/api3/instance.js index 2b19aad6145..358b42bb2c2 100644 --- a/tests/fixtures/api3/instance.js +++ b/tests/fixtures/api3/instance.js @@ -43,15 +43,15 @@ function configure () { self.addSecuredOperations = function addSecuredOperations (instance) { - instance.get = (url) => request(instance.baseUrl).get(url).set('Date', new Date().toUTCString()); + instance.get = (url) => request(instance.baseUrl).get(url); - instance.post = (url) => request(instance.baseUrl).post(url).set('Date', new Date().toUTCString()); + instance.post = (url) => request(instance.baseUrl).post(url); - instance.put = (url) => request(instance.baseUrl).put(url).set('Date', new Date().toUTCString()); + instance.put = (url) => request(instance.baseUrl).put(url); - instance.patch = (url) => request(instance.baseUrl).patch(url).set('Date', new Date().toUTCString()); + instance.patch = (url) => request(instance.baseUrl).patch(url); - instance.delete = (url) => request(instance.baseUrl).delete(url).set('Date', new Date().toUTCString()); + instance.delete = (url) => request(instance.baseUrl).delete(url); };