Skip to content

Commit 16595da

Browse files
author
Peter Wielander
committed
feat: cache access token on repeat requests
1 parent 19ab7ac commit 16595da

File tree

7 files changed

+78
-10
lines changed

7 files changed

+78
-10
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ getManagementToken(privateKey, {appInstallationId, spaceId})
2424
})
2525
```
2626

27+
Management tokens are cached internally until until they expire.
28+
Pass `reuseToken: false` in the options for `getManagementToken` to disable this feature.
29+
2730
## API Docs
2831

2932
API documentation is available [here](https://contentful.github.io/node-apps-toolkit/)

docs/globals.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ <h3><span class="tsd-flag ts-flagConst">Const</span> get<wbr>Management<wbr>Toke
8484
<li class="tsd-description">
8585
<aside class="tsd-sources">
8686
<ul>
87-
<li>Defined in <a href="https://github.com/contentful/node-apps-toolkit/blob/master/src/keys/get-management-token.ts#L105">get-management-token.ts:105</a></li>
87+
<li>Defined in <a href="https://github.com/contentful/node-apps-toolkit/blob/master/src/keys/get-management-token.ts#L128">get-management-token.ts:128</a></li>
8888
</ul>
8989
</aside>
9090
<div class="tsd-comment tsd-typography">

docs/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ <h2>Getting started</h2>
7878
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;Here is your app token&#x27;</span>)
7979
<span class="hljs-built_in">console</span>.log(token)
8080
})</code></pre>
81+
<p>Management tokens are cached internally until until they expire.
82+
Pass <code>reuseToken: false</code> in the options for <code>getManagementToken</code> to disable this feature.</p>
8183
<a href="#api-docs" id="api-docs" style="color: inherit; text-decoration: none;">
8284
<h2>API Docs</h2>
8385
</a>

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"dependencies": {
2323
"debug": "^4.2.0",
2424
"got": "^11.7.0",
25-
"jsonwebtoken": "^8.5.1"
25+
"jsonwebtoken": "^8.5.1",
26+
"node-cache": "^5.1.2"
2627
},
2728
"devDependencies": {
2829
"@commitlint/cli": "^11.0.0",

src/keys/get-management-token.spec.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@ import * as path from 'path'
44

55
import * as sinon from 'sinon'
66

7-
import { createGetManagementToken, getManagementToken } from './get-management-token'
7+
import {
8+
createGetManagementToken,
9+
getManagementToken,
10+
GetManagementTokenOptions,
11+
} from './get-management-token'
812
import { HttpClient, HttpError, Response } from '../utils'
913
import { Logger } from '../utils'
14+
import { sign } from 'jsonwebtoken'
1015

1116
const PRIVATE_KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'keys', 'key.pem'), 'utf-8')
1217
const APP_ID = 'app_id'
1318
const SPACE_ID = 'space_id'
1419
const ENVIRONMENT_ID = 'env_id'
15-
const DEFAULT_OPTIONS = {
20+
const DEFAULT_OPTIONS: GetManagementTokenOptions = {
1621
appInstallationId: APP_ID,
1722
spaceId: SPACE_ID,
1823
environmentId: ENVIRONMENT_ID,
24+
reuseToken: false,
1925
}
2026
const noop = () => {}
2127

@@ -39,6 +45,26 @@ describe('getManagementToken', () => {
3945
)
4046
})
4147

48+
it('caches token while valid', async () => {
49+
const logger = (noop as unknown) as Logger
50+
const post = sinon.stub()
51+
const mockToken = sign({ a: 'b' }, 'a-secret-key', {
52+
expiresIn: '10 minutes',
53+
})
54+
55+
post.resolves({ statusCode: 201, body: JSON.stringify({ token: mockToken }) })
56+
const httpClient = ({ post } as unknown) as HttpClient
57+
const getManagementToken = createGetManagementToken(logger, httpClient)
58+
59+
const optionsWithCaching = {...DEFAULT_OPTIONS, reuseToken: true}
60+
const result = await getManagementToken(PRIVATE_KEY, optionsWithCaching)
61+
assert.strictEqual(result, mockToken)
62+
const secondResult = await getManagementToken(PRIVATE_KEY, optionsWithCaching)
63+
assert.strictEqual(secondResult, mockToken)
64+
65+
assert(post.calledOnce)
66+
})
67+
4268
describe('when using a keyId', () => {
4369
it('fetches a token', async () => {
4470
const mockToken = 'token'

src/keys/get-management-token.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { sign, SignOptions } from 'jsonwebtoken'
2-
1+
import { decode, sign, SignOptions } from 'jsonwebtoken'
2+
import * as NodeCache from 'node-cache'
33
import {
44
createLogger,
55
Logger,
@@ -8,13 +8,16 @@ import {
88
HttpClient,
99
} from '../utils'
1010

11-
interface GetManagementTokenOptions {
11+
export interface GetManagementTokenOptions {
1212
appInstallationId: string
1313
spaceId: string
1414
environmentId: string
1515
keyId?: string
16+
reuseToken?: boolean
1617
}
1718

19+
const cache = new NodeCache()
20+
1821
/**
1922
* Synchronously sign the given privateKey into a JSON Web Token string
2023
* @internal
@@ -67,25 +70,45 @@ const getTokenFromOneTimeToken = async (
6770
`Successfully retrieved CMA Token for app ${appInstallationId} in space ${spaceId} and environment ${environmentId}`
6871
)
6972

70-
return JSON.parse(response.body).token
73+
return JSON.parse(response.body).token as Promise<string>
7174
}
7275

7376
/**
7477
* Factory method for GetManagementToken
7578
* @internal
7679
*/
7780
export const createGetManagementToken = (log: Logger, http: HttpClient) => {
78-
return (privateKey: unknown, opts: GetManagementTokenOptions): Promise<string> => {
81+
return async (privateKey: unknown, opts: GetManagementTokenOptions): Promise<string> => {
7982
if (!(typeof privateKey === 'string')) {
8083
throw new ReferenceError('Invalid privateKey: expected a string representing a private key')
8184
}
8285

86+
const cacheKey = opts.appInstallationId + opts.environmentId + privateKey
87+
if (opts.reuseToken !== false) {
88+
const existing = cache.get(cacheKey) as string
89+
if (existing !== undefined) {
90+
return existing
91+
}
92+
}
93+
8394
const appToken = generateOneTimeToken(
8495
privateKey,
8596
{ appId: opts.appInstallationId, keyId: opts.keyId },
8697
{ log }
8798
)
88-
return getTokenFromOneTimeToken(appToken, opts, { log, http })
99+
const ott = await getTokenFromOneTimeToken(appToken, opts, { log, http })
100+
101+
if (opts.reuseToken !== false) {
102+
const decoded = decode(ott)
103+
if (decoded && typeof decoded === 'object') {
104+
// Internally expire cached tokens a bit earlier to make sure token isn't expired on arrival
105+
const safetyMargin = 10
106+
const ttlSeconds = decoded.exp - Math.floor(Date.now() / 1000) - safetyMargin
107+
cache.set(cacheKey, ott, ttlSeconds)
108+
}
109+
}
110+
111+
return ott
89112
}
90113
}
91114

0 commit comments

Comments
 (0)