Skip to content

Commit

Permalink
Use in-memory token store when developing locally (#273)
Browse files Browse the repository at this point in the history
* Use in-memory token store when developing locally

Removes the need for a local Redis container

* Remove docker-compose dependency

* Default to disabled when running locally

* Rename to InMemoryTokenStore + explicitly create session MemoryStore
  • Loading branch information
marcus-bcl authored Nov 29, 2023
1 parent ca2cfee commit 3949b48
Show file tree
Hide file tree
Showing 17 changed files with 73 additions and 38 deletions.
5 changes: 2 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,8 @@ jobs:

integration_test:
executor:
name: hmpps/node_redis
node_tag: << pipeline.parameters.node-version >>
redis_tag: "7.0"
name: hmpps/node
tag: << pipeline.parameters.node-version >>
steps:
- checkout
- attach_workspace:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ And then, to build the assets and start the app with nodemon:

### Running integration tests

For local running, start a test db, redis, and wiremock instance by:
For local running, start a test db and wiremock instance by:

`docker compose -f docker-compose-test.yml up`

Expand Down
7 changes: 0 additions & 7 deletions docker-compose-test.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
version: '3.1'
services:

redis:
image: 'redis:7.2'
networks:
- hmpps_int
ports:
- '6379:6379'

wiremock:
image: wiremock/wiremock
networks:
Expand Down
13 changes: 1 addition & 12 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
version: '3.1'
services:

redis:
image: 'redis:7.2'
networks:
- hmpps
container_name: redis
environment:
- ALLOW_EMPTY_PASSWORD=yes
ports:
- '6379:6379'

hmpps-auth:
image: quay.io/hmpps/hmpps-auth:latest
networks:
Expand All @@ -33,12 +23,11 @@ services:
GIT_BRANCH: main
networks:
- hmpps
depends_on: [redis]
ports:
- "3000:3000"
environment:
- PRODUCT_ID=UNASSIGNED
- REDIS_HOST=redis
- REDIS_ENABLED=false
- HMPPS_AUTH_EXTERNAL_URL=http://localhost:9090/auth
- HMPPS_AUTH_URL=http://hmpps-auth:8080/auth
# These will need to match new creds in the seed auth service auth
Expand Down
1 change: 1 addition & 0 deletions feature.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ HMPPS_AUTH_URL=http://localhost:9091/auth
MANAGE_USERS_API_URL=http://localhost:9091/manage-users-api
TOKEN_VERIFICATION_API_URL=http://localhost:9091/verification
TOKEN_VERIFICATION_ENABLED=true
REDIS_ENABLED=false
NODE_ENV=development
API_CLIENT_ID=clientid
API_CLIENT_SECRET=clientsecret
Expand Down
1 change: 1 addition & 0 deletions helm_deploy/hmpps-template-typescript/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ generic-service:
# Environment variables to load into the deployment
env:
NODE_ENV: "production"
REDIS_ENABLED: "true"
REDIS_TLS_ENABLED: "true"
TOKEN_VERIFICATION_ENABLED: "true"
APPLICATIONINSIGHTS_CONNECTION_STRING: "InstrumentationKey=$(APPINSIGHTS_INSTRUMENTATIONKEY);IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/"
Expand Down
1 change: 1 addition & 0 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default {
https: production,
staticResourceCacheDuration: '1h',
redis: {
enabled: get('REDIS_ENABLED', 'false', requiredInProduction) === 'true',
host: get('REDIS_HOST', 'localhost', requiredInProduction),
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_AUTH_TOKEN,
Expand Down
4 changes: 2 additions & 2 deletions server/data/hmppsAuthClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import nock from 'nock'

import config from '../config'
import HmppsAuthClient from './hmppsAuthClient'
import TokenStore from './tokenStore'
import TokenStore from './tokenStore/redisTokenStore'

jest.mock('./tokenStore')
jest.mock('./tokenStore/redisTokenStore')

const tokenStore = new TokenStore(null) as jest.Mocked<TokenStore>

Expand Down
2 changes: 1 addition & 1 deletion server/data/hmppsAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { URLSearchParams } from 'url'

import superagent from 'superagent'

import type TokenStore from './tokenStore'
import type TokenStore from './tokenStore/tokenStore'
import logger from '../../logger'
import config from '../config'
import generateOauthClientToken from '../authentication/clientCredentials'
Expand Down
8 changes: 6 additions & 2 deletions server/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ buildAppInsightsClient(applicationInfo)
import HmppsAuthClient from './hmppsAuthClient'
import ManageUsersApiClient from './manageUsersApiClient'
import { createRedisClient } from './redisClient'
import TokenStore from './tokenStore'
import RedisTokenStore from './tokenStore/redisTokenStore'
import InMemoryTokenStore from './tokenStore/inMemoryTokenStore'
import config from '../config'

type RestClientBuilder<T> = (token: string) => T

export const dataAccess = () => ({
applicationInfo,
hmppsAuthClient: new HmppsAuthClient(new TokenStore(createRedisClient())),
hmppsAuthClient: new HmppsAuthClient(
config.redis.enabled ? new RedisTokenStore(createRedisClient()) : new InMemoryTokenStore(),
),
manageUsersApiClient: new ManageUsersApiClient(),
})

Expand Down
2 changes: 1 addition & 1 deletion server/data/manageUsersApiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import nock from 'nock'
import config from '../config'
import ManageUsersApiClient from './manageUsersApiClient'

jest.mock('./tokenStore')
jest.mock('./tokenStore/redisTokenStore')

const token = { access_token: 'token-1', expires_in: 300 }

Expand Down
19 changes: 19 additions & 0 deletions server/data/tokenStore/inMemoryTokenStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import TokenStore from './inMemoryTokenStore'

describe('inMemoryTokenStore', () => {
let tokenStore: TokenStore

beforeEach(() => {
tokenStore = new TokenStore()
})

it('Can store and retrieve token', async () => {
await tokenStore.setToken('user-1', 'token-1', 10)
expect(await tokenStore.getToken('user-1')).toBe('token-1')
})

it('Expires token', async () => {
await tokenStore.setToken('user-2', 'token-2', -1)
expect(await tokenStore.getToken('user-2')).toBe(null)
})
})
17 changes: 17 additions & 0 deletions server/data/tokenStore/inMemoryTokenStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import TokenStore from './tokenStore'

export default class InMemoryTokenStore implements TokenStore {
map = new Map<string, { token: string; expiry: Date }>()

public async setToken(key: string, token: string, durationSeconds: number): Promise<void> {
this.map.set(key, { token, expiry: new Date(Date.now() + durationSeconds * 1000) })
return Promise.resolve()
}

public async getToken(key: string): Promise<string> {
if (!this.map.has(key) || this.map.get(key).expiry.getTime() < Date.now()) {
return Promise.resolve(null)
}
return Promise.resolve(this.map.get(key).token)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RedisClient } from './redisClient'
import TokenStore from './tokenStore'
import { RedisClient } from '../redisClient'
import TokenStore from './redisTokenStore'

const redisClient = {
get: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { RedisClient } from './redisClient'
import type { RedisClient } from '../redisClient'

import logger from '../../logger'
import logger from '../../../logger'
import TokenStore from './tokenStore'

export default class TokenStore {
export default class RedisTokenStore implements TokenStore {
private readonly prefix = 'systemToken:'

constructor(private readonly client: RedisClient) {
Expand Down
4 changes: 4 additions & 0 deletions server/data/tokenStore/tokenStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface TokenStore {
setToken(key: string, token: string, durationSeconds: number): Promise<void>
getToken(key: string): Promise<string>
}
14 changes: 10 additions & 4 deletions server/middleware/setUpWebSession.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { v4 as uuidv4 } from 'uuid'
import session from 'express-session'
import session, { MemoryStore, Store } from 'express-session'
import RedisStore from 'connect-redis'
import express, { Router } from 'express'
import { createRedisClient } from '../data/redisClient'
import config from '../config'
import logger from '../../logger'

export default function setUpWebSession(): Router {
const client = createRedisClient()
client.connect().catch((err: Error) => logger.error(`Error connecting to Redis`, err))
let store: Store
if (config.redis.enabled) {
const client = createRedisClient()
client.connect().catch((err: Error) => logger.error(`Error connecting to Redis`, err))
store = new RedisStore({ client })
} else {
store = new MemoryStore()
}

const router = express.Router()
router.use(
session({
store: new RedisStore({ client }),
store,
cookie: { secure: config.https, sameSite: 'lax', maxAge: config.session.expiryMinutes * 60 * 1000 },
secret: config.session.secret,
resave: false, // redis implements touch so shouldn't need this
Expand Down

0 comments on commit 3949b48

Please sign in to comment.