Skip to content

Commit

Permalink
feat: create signature (#11)
Browse files Browse the repository at this point in the history
* chore: create basic test scaffolding

* chore: add runtypes

* chore: update eslint rules for spread

* feat: add create-signature

* docs: insert script type information

* refactor: use nicer hex digest encoding for keys

* feat: introduce signed headers

* docs: update create-signature docs

* feat: only allow 64 chars long secret

* feat: add request verification

* docs: document is-verified-request

* chore: expose is-verified-request

* test: check that verification fails with different secrets

* refactor: get rid of signed headers and use alphabetical order

* feat: do not verify old requests

* fix: include timestamp in signed headers

* docs: improve documentation

* docs: improve documentation with categories

* test: add ts-ignore for js code testing

* refactor: contentful signing header -> contentful header

* fix: replace contentful headers with new ones and let validator do the rest

This includes:
* fix typings to always expect signed headers
* streamline test mocks

* feat: export also signed headers from sign method

* docs: add explanatory comment

* feat: handle headers sorted lower than x-contentful

* refactor: is verified -> verify
  • Loading branch information
shikaan authored Nov 2, 2020
1 parent 45b749b commit 970df31
Show file tree
Hide file tree
Showing 17 changed files with 996 additions and 39 deletions.
19 changes: 8 additions & 11 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
module.exports = {
parser: "@typescript-eslint/parser",
parser: '@typescript-eslint/parser',
env: {
node: true,
mocha: true
mocha: true,
},
plugins: [
"@typescript-eslint",
"prettier"
],
extends: [
"eslint:recommended",
"plugin:prettier/recommended",
]
}
plugins: ['@typescript-eslint', 'prettier'],
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
rules: {
'no-unused-vars': ['error', { ignoreRestSiblings: true }],
},
}
2 changes: 1 addition & 1 deletion docs/assets/js/search.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kinds":{"64":"Function"},"rows":[{"id":0,"kind":64,"name":"getManagementToken","url":"globals.html#getmanagementtoken","classes":"tsd-kind-function"}],"index":{"version":"2.3.9","fields":["name","parent"],"fieldVectors":[["name/0",[0,2.877]],["parent/0",[]]],"invertedIndex":[["getmanagementtoken",{"_index":0,"name":{"0":{}},"parent":{}}]],"pipeline":[]}}
{"kinds":{"64":"Function"},"rows":[{"id":0,"kind":64,"name":"getManagementToken","url":"globals.html#getmanagementtoken","classes":"tsd-kind-function"},{"id":1,"kind":64,"name":"signRequest","url":"globals.html#signrequest","classes":"tsd-kind-function"},{"id":2,"kind":64,"name":"verifyRequest","url":"globals.html#verifyrequest","classes":"tsd-kind-function"}],"index":{"version":"2.3.9","fields":["name","parent"],"fieldVectors":[["name/0",[0,9.808]],["parent/0",[]],["name/1",[1,9.808]],["parent/1",[]],["name/2",[2,9.808]],["parent/2",[]]],"invertedIndex":[["getmanagementtoken",{"_index":0,"name":{"0":{}},"parent":{}}],["signrequest",{"_index":1,"name":{"1":{}},"parent":{}}],["verifyrequest",{"_index":2,"name":{"2":{}},"parent":{}}]],"pipeline":[]}}
158 changes: 151 additions & 7 deletions docs/globals.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,32 @@ <h2>Index</h2>
<section class="tsd-panel tsd-index-panel">
<div class="tsd-index-content">
<section class="tsd-index-section ">
<h3>Functions</h3>
<h3>Keys Functions</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-function"><a href="globals.html#getmanagementtoken" class="tsd-kind-icon">get<wbr>Management<wbr>Token</a></li>
</ul>
<h3>Requests Functions</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-function"><a href="globals.html#signrequest" class="tsd-kind-icon">sign<wbr>Request</a></li>
<li class="tsd-kind-function"><a href="globals.html#verifyrequest" class="tsd-kind-icon">verify<wbr>Request</a></li>
</ul>
</section>
</div>
</section>
</section>
<section class="tsd-panel-group tsd-member-group ">
<h2>Functions</h2>
<h2>Keys Functions</h2>
<section class="tsd-panel tsd-member tsd-kind-function">
<a name="getmanagementtoken" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagConst">Const</span> get<wbr>Management<wbr>Token</h3>
<ul class="tsd-signatures tsd-kind-function">
<li class="tsd-signature tsd-kind-icon">get<wbr>Management<wbr>Token<span class="tsd-signature-symbol">(</span>privateKey<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">unknown</span>, opts<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">GetManagementTokenOptions</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">&gt;</span></li>
<li class="tsd-signature tsd-kind-icon">get<wbr>Management<wbr>Token<span class="tsd-signature-symbol">(</span>privateKey<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">string</span>, opts<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">GetManagementTokenOptions</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">&gt;</span></li>
</ul>
<ul class="tsd-descriptions">
<li class="tsd-description">
<aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/contentful/node-apps-toolkit/blob/master/src/keys/get-management-token.ts#L140">get-management-token.ts:140</a></li>
<li>Defined in <a href="https://github.com/contentful/node-apps-toolkit/blob/master/src/keys/get-management-token.ts#L144">keys/get-management-token.ts:144</a></li>
</ul>
</aside>
<div class="tsd-comment tsd-typography">
Expand All @@ -94,8 +99,10 @@ <h3><span class="tsd-flag ts-flagConst">Const</span> get<wbr>Management<wbr>Toke
Pass <code>reuseToken: false</code> in the options for <code>getManagementToken</code> to disable this feature.</p>
</div>
<p>NodeJS Contentful Apps need a management token to interact with Contentful&#39;s APIs.
Creating a management token requires a key pair to be regsitered for the app, follow <a href="http://contentful./developers/docs/references/content-management-api/#/reference/app-keys/app-keys">this link</a> for more information on key pairs.
Once a key pair is registered the getManagementToken function can be used to generate a valid token.</p>
Creating a management token requires a key pair to be registered for the app, follow
<a href="http://contentful./developers/docs/references/content-management-api/#/reference/app-keys/app-keys">this link</a>
for more information on key pairs.</p>
<p>Once a key pair is registered the getManagementToken function can be used to generate a valid token.</p>
<pre><code>const {getManagementToken} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;contentful-node-apps-toolkit&#x27;</span>)

getManagementToken(PRIVATE_KEY, {appId, spaceId, environmentId})
Expand All @@ -107,7 +114,7 @@ <h3><span class="tsd-flag ts-flagConst">Const</span> get<wbr>Management<wbr>Toke
<h4 class="tsd-parameters-title">Parameters</h4>
<ul class="tsd-parameters">
<li>
<h5>privateKey: <span class="tsd-signature-type">unknown</span></h5>
<h5>privateKey: <span class="tsd-signature-type">string</span></h5>
</li>
<li>
<h5>opts: <span class="tsd-signature-type">GetManagementTokenOptions</span></h5>
Expand All @@ -118,6 +125,137 @@ <h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</
</ul>
</section>
</section>
<section class="tsd-panel-group tsd-member-group ">
<h2>Requests Functions</h2>
<section class="tsd-panel tsd-member tsd-kind-function">
<a name="signrequest" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagConst">Const</span> sign<wbr>Request</h3>
<ul class="tsd-signatures tsd-kind-function">
<li class="tsd-signature tsd-kind-icon">sign<wbr>Request<span class="tsd-signature-symbol">(</span>rawSecret<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">Secret</span>, rawCanonicalRequest<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">CanonicalRequest</span>, rawTimestamp<span class="tsd-signature-symbol">?: </span><span class="tsd-signature-type">Timestamp</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">SignedRequestHeaders</span></li>
</ul>
<ul class="tsd-descriptions">
<li class="tsd-description">
<aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/contentful/node-apps-toolkit/blob/master/src/requests/sign-request.ts#L104">requests/sign-request.ts:104</a></li>
</ul>
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Given a secret, a canonical request and a timestamp, generates a signature.
It can be used to verify canonical requests to assess authenticity of the
sender and integrity of the payload.</p>
</div>
<pre><code><span class="hljs-keyword">const</span> {signRequest, ContentfulHeader} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;contentful-node-apps-toolkit&#x27;</span>)
<span class="hljs-keyword">const</span> {pick} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;lodash&#x27;</span>)
<span class="hljs-keyword">const</span> {server} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./imaginary-server&#x27;</span>)

<span class="hljs-keyword">const</span> SECRET = process.env.SECRET

server.post(<span class="hljs-string">&#x27;/api/my-resources&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
<span class="hljs-keyword">const</span> incomingSignature = req.headers[<span class="hljs-string">&#x27;x-contentful-signature&#x27;</span>]
<span class="hljs-keyword">const</span> incomingTimestamp = req.headers[<span class="hljs-string">&#x27;x-contentful-timestamp&#x27;</span>]
<span class="hljs-keyword">const</span> incomingSignedHeaders = req.headers[<span class="hljs-string">&#x27;x-contentful-signed-headers&#x27;</span>]
<span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now()

<span class="hljs-keyword">if</span> (!incomingSignature) {
res.send(<span class="hljs-number">400</span>, <span class="hljs-string">&#x27;Missing signature&#x27;</span>)
}

<span class="hljs-keyword">if</span> (now - incomingTimestamp &gt; <span class="hljs-number">1000</span>) {
res.send(<span class="hljs-number">408</span>, <span class="hljs-string">&#x27;Request too old&#x27;</span>)
}

<span class="hljs-keyword">const</span> signedHeaders = incomingSignedHeaders.split(<span class="hljs-string">&#x27;,&#x27;</span>)

<span class="hljs-keyword">const</span> {[ContentfulHeader.Signature]: computedSignature} = signRequest(
SECRET,
{
<span class="hljs-attr">method</span>: req.method,
<span class="hljs-attr">path</span>: req.url,
<span class="hljs-attr">headers</span>: pick(req.headers, signedHeaders),
<span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(req.body)
},
incomingTimestamp
)

<span class="hljs-keyword">if</span> (computedSignature !== incomingSignature) {
res.send(<span class="hljs-number">403</span>, <span class="hljs-string">&#x27;Invalid signature&#x27;</span>)
}

<span class="hljs-comment">// rest of the code</span>
})
</code></pre>
</div>
<h4 class="tsd-parameters-title">Parameters</h4>
<ul class="tsd-parameters">
<li>
<h5>rawSecret: <span class="tsd-signature-type">Secret</span></h5>
</li>
<li>
<h5>rawCanonicalRequest: <span class="tsd-signature-type">CanonicalRequest</span></h5>
</li>
<li>
<h5><span class="tsd-flag ts-flagDefault value">Default value</span> rawTimestamp: <span class="tsd-signature-type">Timestamp</span><span class="tsd-signature-symbol"> = Date.now()</span></h5>
</li>
</ul>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">SignedRequestHeaders</span></h4>
</li>
</ul>
</section>
<section class="tsd-panel tsd-member tsd-kind-function">
<a name="verifyrequest" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagConst">Const</span> verify<wbr>Request</h3>
<ul class="tsd-signatures tsd-kind-function">
<li class="tsd-signature tsd-kind-icon">verify<wbr>Request<span class="tsd-signature-symbol">(</span>rawSecret<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">Secret</span>, rawCanonicalRequest<span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">CanonicalRequest</span>, rawTimeToLive<span class="tsd-signature-symbol">?: </span><span class="tsd-signature-type">TimeToLive</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">: </span><span class="tsd-signature-type">boolean</span></li>
</ul>
<ul class="tsd-descriptions">
<li class="tsd-description">
<aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/contentful/node-apps-toolkit/blob/master/src/requests/verify-request.ts#L53">requests/verify-request.ts:53</a></li>
</ul>
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Given a secret verifies a CanonicalRequest. Throws when signature is older than <code>rawTimeToLive</code> seconds.
Pass <code>rawTimeToLive = 0</code> to disable TTL checks.</p>
</div>
<pre><code><span class="hljs-keyword">const</span> {isVerifiedRequest} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;contentful-node-apps-toolkit&#x27;</span>)
<span class="hljs-keyword">const</span> {server} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./imaginary-server&#x27;</span>)
<span class="hljs-keyword">const</span> {makeCanonicalRequestFromImaginaryServerRequest} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./imaginary-utils&#x27;</span>)

<span class="hljs-keyword">const</span> SECRET = process.env.SECRET
<span class="hljs-keyword">const</span> REQUEST_TTL = <span class="hljs-built_in">Number</span>.parseInt(process.env.REQUEST_TTL, <span class="hljs-number">10</span>)

server.post(<span class="hljs-string">&#x27;/api/my-resources&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
<span class="hljs-keyword">const</span> canonicalRequest = makeCanonicalRequestFromImaginaryServerRequest(req)

<span class="hljs-keyword">if</span> (!isVerifiedRequest(SECRET, canonicalRequest, REQUEST_TTL)) {
res.send(<span class="hljs-number">403</span>, <span class="hljs-string">&#x27;Invalid signature&#x27;</span>)
}

<span class="hljs-comment">// Rest of the code</span>
})
</code></pre>
</div>
<h4 class="tsd-parameters-title">Parameters</h4>
<ul class="tsd-parameters">
<li>
<h5>rawSecret: <span class="tsd-signature-type">Secret</span></h5>
</li>
<li>
<h5>rawCanonicalRequest: <span class="tsd-signature-type">CanonicalRequest</span></h5>
</li>
<li>
<h5><span class="tsd-flag ts-flagDefault value">Default value</span> rawTimeToLive: <span class="tsd-signature-type">TimeToLive</span><span class="tsd-signature-symbol"> = 30</span></h5>
</li>
</ul>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">boolean</span></h4>
</li>
</ul>
</section>
</section>
</div>
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
<nav class="tsd-navigation primary">
Expand All @@ -132,6 +270,12 @@ <h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</
<li class=" tsd-kind-function">
<a href="globals.html#getmanagementtoken" class="tsd-kind-icon">get<wbr>Management<wbr>Token</a>
</li>
<li class=" tsd-kind-function">
<a href="globals.html#signrequest" class="tsd-kind-icon">sign<wbr>Request</a>
</li>
<li class=" tsd-kind-function">
<a href="globals.html#verifyrequest" class="tsd-kind-icon">verify<wbr>Request</a>
</li>
</ul>
</nav>
</div>
Expand Down
37 changes: 20 additions & 17 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,34 +60,31 @@ <h1>contentful-node-apps-toolkit</h1>
<div class="row">
<div class="col-8 col-content">
<div class="tsd-panel tsd-typography">
<a href="#node-apps-toolkit" id="node-apps-toolkit" style="color: inherit; text-decoration: none;">
<h1>Node Apps Toolkit</h1>
<a href="#node-toolkit-for-contentful-apps" id="node-toolkit-for-contentful-apps" style="color: inherit; text-decoration: none;">
<h1>Node Toolkit for Contentful Apps</h1>
</a>
<p>The Node Apps Toolkit is a growing collection of helpers, and utilities, for creating NodeJS Contentful Apps.</p>
<a href="#getting-started" id="getting-started" style="color: inherit; text-decoration: none;">
<h2>Getting started</h2>
<p>The <code>node-apps-toolkit</code> is a growing collection of helpers and utilities for building <a href="https://www.contentful.com/developers/docs/extensibility/app-framework/">Contentful Apps</a> with Node.js.</p>
<a href="#installation" id="installation" style="color: inherit; text-decoration: none;">
<h2>Installation</h2>
</a>
<pre><code class="language-shell">npm install --save @contentful/node-apps-toolkit
<span class="hljs-meta">#</span><span class="bash"> or</span>
yarn add @contentful/node-apps-toolkit</code></pre>
<a href="#usage" id="usage" style="color: inherit; text-decoration: none;">
<h2>Usage</h2>
</a>
<p>You can install this library with npm or yarn</p>
<pre><code>npm install --save @contentful/<span class="hljs-keyword">node</span><span class="hljs-title">-apps-toolkit</span>
<span class="hljs-keyword">or</span>
yarn add @contentful/<span class="hljs-keyword">node</span><span class="hljs-title">-apps-toolkit</span></code></pre>
<p>You can include the library in your project like this</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> { getManagementToken } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;@contentful/node-apps-toolkit&#x27;</span>);
<span class="hljs-keyword">const</span> { appInstallationId, spaceId, privateKey } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./some-constants&#x27;</span>);

getManagementToken(privateKey, {appInstallationId, spaceId})
.then(<span class="hljs-function">(<span class="hljs-params">token</span>) =&gt;</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;Here is your app token&#x27;</span>)
<span class="hljs-built_in">console</span>.log(token)
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;Here is your app token:&#x27;</span>, token)
})</code></pre>
<a href="#api-documentation" id="api-documentation" style="color: inherit; text-decoration: none;">
<h2>API Documentation</h2>
</a>
<p>In depth API documentation is available <a href="https://contentful.github.io/node-apps-toolkit/">here</a></p>
<p>For more information, check out the full <a href="https://contentful.github.io/node-apps-toolkit/">API documentation</a>.</p>
<a href="#more-coming-soon" id="more-coming-soon" style="color: inherit; text-decoration: none;">
<h2>More coming soon</h2>
</a>
<p>We&#39;re excited to expand this toolkit with new features. If you have any suggestions or requests for features you&#39;d like to see please create an issue in this repo!</p>
<p>We&#39;re excited to expand this toolkit with new features. If you have any suggestions or requests for features you&#39;d like to see, please <a href="https://github.com/contentful/node-apps-toolkit/issues/new">create an issue</a> in this repo.</p>
<a href="#contributing-and-local-development" id="contributing-and-local-development" style="color: inherit; text-decoration: none;">
<h2>Contributing and local development</h2>
</a>
Expand All @@ -107,6 +104,12 @@ <h2>Contributing and local development</h2>
<li class=" tsd-kind-function">
<a href="globals.html#getmanagementtoken" class="tsd-kind-icon">get<wbr>Management<wbr>Token</a>
</li>
<li class=" tsd-kind-function">
<a href="globals.html#signrequest" class="tsd-kind-icon">sign<wbr>Request</a>
</li>
<li class=" tsd-kind-function">
<a href="globals.html#verifyrequest" class="tsd-kind-icon">verify<wbr>Request</a>
</li>
</ul>
</nav>
</div>
Expand Down
Loading

0 comments on commit 970df31

Please sign in to comment.