Skip to content

Commit 970df31

Browse files
authored
feat: create signature (#11)
* 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
1 parent 45b749b commit 970df31

17 files changed

+996
-39
lines changed

.eslintrc.js

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
module.exports = {
2-
parser: "@typescript-eslint/parser",
2+
parser: '@typescript-eslint/parser',
33
env: {
44
node: true,
5-
mocha: true
5+
mocha: true,
66
},
7-
plugins: [
8-
"@typescript-eslint",
9-
"prettier"
10-
],
11-
extends: [
12-
"eslint:recommended",
13-
"plugin:prettier/recommended",
14-
]
15-
}
7+
plugins: ['@typescript-eslint', 'prettier'],
8+
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
9+
rules: {
10+
'no-unused-vars': ['error', { ignoreRestSiblings: true }],
11+
},
12+
}

docs/assets/js/search.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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":[]}}
1+
{"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":[]}}

docs/globals.html

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,27 +64,32 @@ <h2>Index</h2>
6464
<section class="tsd-panel tsd-index-panel">
6565
<div class="tsd-index-content">
6666
<section class="tsd-index-section ">
67-
<h3>Functions</h3>
67+
<h3>Keys Functions</h3>
6868
<ul class="tsd-index-list">
6969
<li class="tsd-kind-function"><a href="globals.html#getmanagementtoken" class="tsd-kind-icon">get<wbr>Management<wbr>Token</a></li>
7070
</ul>
71+
<h3>Requests Functions</h3>
72+
<ul class="tsd-index-list">
73+
<li class="tsd-kind-function"><a href="globals.html#signrequest" class="tsd-kind-icon">sign<wbr>Request</a></li>
74+
<li class="tsd-kind-function"><a href="globals.html#verifyrequest" class="tsd-kind-icon">verify<wbr>Request</a></li>
75+
</ul>
7176
</section>
7277
</div>
7378
</section>
7479
</section>
7580
<section class="tsd-panel-group tsd-member-group ">
76-
<h2>Functions</h2>
81+
<h2>Keys Functions</h2>
7782
<section class="tsd-panel tsd-member tsd-kind-function">
7883
<a name="getmanagementtoken" class="tsd-anchor"></a>
7984
<h3><span class="tsd-flag ts-flagConst">Const</span> get<wbr>Management<wbr>Token</h3>
8085
<ul class="tsd-signatures tsd-kind-function">
81-
<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>
86+
<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>
8287
</ul>
8388
<ul class="tsd-descriptions">
8489
<li class="tsd-description">
8590
<aside class="tsd-sources">
8691
<ul>
87-
<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>
92+
<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>
8893
</ul>
8994
</aside>
9095
<div class="tsd-comment tsd-typography">
@@ -94,8 +99,10 @@ <h3><span class="tsd-flag ts-flagConst">Const</span> get<wbr>Management<wbr>Toke
9499
Pass <code>reuseToken: false</code> in the options for <code>getManagementToken</code> to disable this feature.</p>
95100
</div>
96101
<p>NodeJS Contentful Apps need a management token to interact with Contentful&#39;s APIs.
97-
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.
98-
Once a key pair is registered the getManagementToken function can be used to generate a valid token.</p>
102+
Creating a management token requires a key pair to be registered for the app, follow
103+
<a href="http://contentful./developers/docs/references/content-management-api/#/reference/app-keys/app-keys">this link</a>
104+
for more information on key pairs.</p>
105+
<p>Once a key pair is registered the getManagementToken function can be used to generate a valid token.</p>
99106
<pre><code>const {getManagementToken} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;contentful-node-apps-toolkit&#x27;</span>)
100107

101108
getManagementToken(PRIVATE_KEY, {appId, spaceId, environmentId})
@@ -107,7 +114,7 @@ <h3><span class="tsd-flag ts-flagConst">Const</span> get<wbr>Management<wbr>Toke
107114
<h4 class="tsd-parameters-title">Parameters</h4>
108115
<ul class="tsd-parameters">
109116
<li>
110-
<h5>privateKey: <span class="tsd-signature-type">unknown</span></h5>
117+
<h5>privateKey: <span class="tsd-signature-type">string</span></h5>
111118
</li>
112119
<li>
113120
<h5>opts: <span class="tsd-signature-type">GetManagementTokenOptions</span></h5>
@@ -118,6 +125,137 @@ <h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</
118125
</ul>
119126
</section>
120127
</section>
128+
<section class="tsd-panel-group tsd-member-group ">
129+
<h2>Requests Functions</h2>
130+
<section class="tsd-panel tsd-member tsd-kind-function">
131+
<a name="signrequest" class="tsd-anchor"></a>
132+
<h3><span class="tsd-flag ts-flagConst">Const</span> sign<wbr>Request</h3>
133+
<ul class="tsd-signatures tsd-kind-function">
134+
<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>
135+
</ul>
136+
<ul class="tsd-descriptions">
137+
<li class="tsd-description">
138+
<aside class="tsd-sources">
139+
<ul>
140+
<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>
141+
</ul>
142+
</aside>
143+
<div class="tsd-comment tsd-typography">
144+
<div class="lead">
145+
<p>Given a secret, a canonical request and a timestamp, generates a signature.
146+
It can be used to verify canonical requests to assess authenticity of the
147+
sender and integrity of the payload.</p>
148+
</div>
149+
<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>)
150+
<span class="hljs-keyword">const</span> {pick} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;lodash&#x27;</span>)
151+
<span class="hljs-keyword">const</span> {server} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./imaginary-server&#x27;</span>)
152+
153+
<span class="hljs-keyword">const</span> SECRET = process.env.SECRET
154+
155+
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> {
156+
<span class="hljs-keyword">const</span> incomingSignature = req.headers[<span class="hljs-string">&#x27;x-contentful-signature&#x27;</span>]
157+
<span class="hljs-keyword">const</span> incomingTimestamp = req.headers[<span class="hljs-string">&#x27;x-contentful-timestamp&#x27;</span>]
158+
<span class="hljs-keyword">const</span> incomingSignedHeaders = req.headers[<span class="hljs-string">&#x27;x-contentful-signed-headers&#x27;</span>]
159+
<span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now()
160+
161+
<span class="hljs-keyword">if</span> (!incomingSignature) {
162+
res.send(<span class="hljs-number">400</span>, <span class="hljs-string">&#x27;Missing signature&#x27;</span>)
163+
}
164+
165+
<span class="hljs-keyword">if</span> (now - incomingTimestamp &gt; <span class="hljs-number">1000</span>) {
166+
res.send(<span class="hljs-number">408</span>, <span class="hljs-string">&#x27;Request too old&#x27;</span>)
167+
}
168+
169+
<span class="hljs-keyword">const</span> signedHeaders = incomingSignedHeaders.split(<span class="hljs-string">&#x27;,&#x27;</span>)
170+
171+
<span class="hljs-keyword">const</span> {[ContentfulHeader.Signature]: computedSignature} = signRequest(
172+
SECRET,
173+
{
174+
<span class="hljs-attr">method</span>: req.method,
175+
<span class="hljs-attr">path</span>: req.url,
176+
<span class="hljs-attr">headers</span>: pick(req.headers, signedHeaders),
177+
<span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(req.body)
178+
},
179+
incomingTimestamp
180+
)
181+
182+
<span class="hljs-keyword">if</span> (computedSignature !== incomingSignature) {
183+
res.send(<span class="hljs-number">403</span>, <span class="hljs-string">&#x27;Invalid signature&#x27;</span>)
184+
}
185+
186+
<span class="hljs-comment">// rest of the code</span>
187+
})
188+
</code></pre>
189+
</div>
190+
<h4 class="tsd-parameters-title">Parameters</h4>
191+
<ul class="tsd-parameters">
192+
<li>
193+
<h5>rawSecret: <span class="tsd-signature-type">Secret</span></h5>
194+
</li>
195+
<li>
196+
<h5>rawCanonicalRequest: <span class="tsd-signature-type">CanonicalRequest</span></h5>
197+
</li>
198+
<li>
199+
<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>
200+
</li>
201+
</ul>
202+
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">SignedRequestHeaders</span></h4>
203+
</li>
204+
</ul>
205+
</section>
206+
<section class="tsd-panel tsd-member tsd-kind-function">
207+
<a name="verifyrequest" class="tsd-anchor"></a>
208+
<h3><span class="tsd-flag ts-flagConst">Const</span> verify<wbr>Request</h3>
209+
<ul class="tsd-signatures tsd-kind-function">
210+
<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>
211+
</ul>
212+
<ul class="tsd-descriptions">
213+
<li class="tsd-description">
214+
<aside class="tsd-sources">
215+
<ul>
216+
<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>
217+
</ul>
218+
</aside>
219+
<div class="tsd-comment tsd-typography">
220+
<div class="lead">
221+
<p>Given a secret verifies a CanonicalRequest. Throws when signature is older than <code>rawTimeToLive</code> seconds.
222+
Pass <code>rawTimeToLive = 0</code> to disable TTL checks.</p>
223+
</div>
224+
<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>)
225+
<span class="hljs-keyword">const</span> {server} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./imaginary-server&#x27;</span>)
226+
<span class="hljs-keyword">const</span> {makeCanonicalRequestFromImaginaryServerRequest} = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./imaginary-utils&#x27;</span>)
227+
228+
<span class="hljs-keyword">const</span> SECRET = process.env.SECRET
229+
<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>)
230+
231+
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> {
232+
<span class="hljs-keyword">const</span> canonicalRequest = makeCanonicalRequestFromImaginaryServerRequest(req)
233+
234+
<span class="hljs-keyword">if</span> (!isVerifiedRequest(SECRET, canonicalRequest, REQUEST_TTL)) {
235+
res.send(<span class="hljs-number">403</span>, <span class="hljs-string">&#x27;Invalid signature&#x27;</span>)
236+
}
237+
238+
<span class="hljs-comment">// Rest of the code</span>
239+
})
240+
</code></pre>
241+
</div>
242+
<h4 class="tsd-parameters-title">Parameters</h4>
243+
<ul class="tsd-parameters">
244+
<li>
245+
<h5>rawSecret: <span class="tsd-signature-type">Secret</span></h5>
246+
</li>
247+
<li>
248+
<h5>rawCanonicalRequest: <span class="tsd-signature-type">CanonicalRequest</span></h5>
249+
</li>
250+
<li>
251+
<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>
252+
</li>
253+
</ul>
254+
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">boolean</span></h4>
255+
</li>
256+
</ul>
257+
</section>
258+
</section>
121259
</div>
122260
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
123261
<nav class="tsd-navigation primary">
@@ -132,6 +270,12 @@ <h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</
132270
<li class=" tsd-kind-function">
133271
<a href="globals.html#getmanagementtoken" class="tsd-kind-icon">get<wbr>Management<wbr>Token</a>
134272
</li>
273+
<li class=" tsd-kind-function">
274+
<a href="globals.html#signrequest" class="tsd-kind-icon">sign<wbr>Request</a>
275+
</li>
276+
<li class=" tsd-kind-function">
277+
<a href="globals.html#verifyrequest" class="tsd-kind-icon">verify<wbr>Request</a>
278+
</li>
135279
</ul>
136280
</nav>
137281
</div>

docs/index.html

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,34 +60,31 @@ <h1>contentful-node-apps-toolkit</h1>
6060
<div class="row">
6161
<div class="col-8 col-content">
6262
<div class="tsd-panel tsd-typography">
63-
<a href="#node-apps-toolkit" id="node-apps-toolkit" style="color: inherit; text-decoration: none;">
64-
<h1>Node Apps Toolkit</h1>
63+
<a href="#node-toolkit-for-contentful-apps" id="node-toolkit-for-contentful-apps" style="color: inherit; text-decoration: none;">
64+
<h1>Node Toolkit for Contentful Apps</h1>
6565
</a>
66-
<p>The Node Apps Toolkit is a growing collection of helpers, and utilities, for creating NodeJS Contentful Apps.</p>
67-
<a href="#getting-started" id="getting-started" style="color: inherit; text-decoration: none;">
68-
<h2>Getting started</h2>
66+
<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>
67+
<a href="#installation" id="installation" style="color: inherit; text-decoration: none;">
68+
<h2>Installation</h2>
69+
</a>
70+
<pre><code class="language-shell">npm install --save @contentful/node-apps-toolkit
71+
<span class="hljs-meta">#</span><span class="bash"> or</span>
72+
yarn add @contentful/node-apps-toolkit</code></pre>
73+
<a href="#usage" id="usage" style="color: inherit; text-decoration: none;">
74+
<h2>Usage</h2>
6975
</a>
70-
<p>You can install this library with npm or yarn</p>
71-
<pre><code>npm install --save @contentful/<span class="hljs-keyword">node</span><span class="hljs-title">-apps-toolkit</span>
72-
<span class="hljs-keyword">or</span>
73-
yarn add @contentful/<span class="hljs-keyword">node</span><span class="hljs-title">-apps-toolkit</span></code></pre>
74-
<p>You can include the library in your project like this</p>
7576
<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>);
7677
<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>);
7778

7879
getManagementToken(privateKey, {appInstallationId, spaceId})
7980
.then(<span class="hljs-function">(<span class="hljs-params">token</span>) =&gt;</span> {
80-
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;Here is your app token&#x27;</span>)
81-
<span class="hljs-built_in">console</span>.log(token)
81+
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;Here is your app token:&#x27;</span>, token)
8282
})</code></pre>
83-
<a href="#api-documentation" id="api-documentation" style="color: inherit; text-decoration: none;">
84-
<h2>API Documentation</h2>
85-
</a>
86-
<p>In depth API documentation is available <a href="https://contentful.github.io/node-apps-toolkit/">here</a></p>
83+
<p>For more information, check out the full <a href="https://contentful.github.io/node-apps-toolkit/">API documentation</a>.</p>
8784
<a href="#more-coming-soon" id="more-coming-soon" style="color: inherit; text-decoration: none;">
8885
<h2>More coming soon</h2>
8986
</a>
90-
<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>
87+
<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>
9188
<a href="#contributing-and-local-development" id="contributing-and-local-development" style="color: inherit; text-decoration: none;">
9289
<h2>Contributing and local development</h2>
9390
</a>
@@ -107,6 +104,12 @@ <h2>Contributing and local development</h2>
107104
<li class=" tsd-kind-function">
108105
<a href="globals.html#getmanagementtoken" class="tsd-kind-icon">get<wbr>Management<wbr>Token</a>
109106
</li>
107+
<li class=" tsd-kind-function">
108+
<a href="globals.html#signrequest" class="tsd-kind-icon">sign<wbr>Request</a>
109+
</li>
110+
<li class=" tsd-kind-function">
111+
<a href="globals.html#verifyrequest" class="tsd-kind-icon">verify<wbr>Request</a>
112+
</li>
110113
</ul>
111114
</nav>
112115
</div>

0 commit comments

Comments
 (0)