From 65d6f8e244c7d5d603efa85487771d7c72e3b4a5 Mon Sep 17 00:00:00 2001 From: Joakim Larsson Date: Wed, 20 Sep 2023 10:02:12 +0200 Subject: [PATCH] random pincodes for passwordless --- .env.example | 1 + .../in-memory-login-service.ts | 3 +- src/login/in-memory-login-service/index.ts | 20 +++++--- src/login/index.ts | 3 ++ src/login/issue-pincode.ts | 2 - src/login/issue-pincode/index.ts | 12 +++++ src/login/issue-pincode/issue-incode.spec.ts | 51 +++++++++++++++++++ src/login/issue-pincode/types.ts | 3 ++ .../mongo-login-service.ts | 10 +++- src/test-utils/e2e.ts | 7 ++- src/test-utils/test-app.ts | 3 +- src/types.ts | 2 +- 12 files changed, 104 insertions(+), 13 deletions(-) delete mode 100644 src/login/issue-pincode.ts create mode 100644 src/login/issue-pincode/index.ts create mode 100644 src/login/issue-pincode/issue-incode.spec.ts create mode 100644 src/login/issue-pincode/types.ts diff --git a/.env.example b/.env.example index 76fb3b1..90b0a8a 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,7 @@ FS_DATA_PATH=.local/data # Passwordless configuration # #PASSWORDLESS_TTL=10m +PASSWORDLESS_FIXED_PINCODE=123456 #------------------------------------------------------------ # diff --git a/src/login/in-memory-login-service/in-memory-login-service.ts b/src/login/in-memory-login-service/in-memory-login-service.ts index 3358bb7..78d7a23 100644 --- a/src/login/in-memory-login-service/in-memory-login-service.ts +++ b/src/login/in-memory-login-service/in-memory-login-service.ts @@ -1,8 +1,8 @@ import ms from 'ms' import type { LoginService } from '../types' import { RequestPincodeStatus } from '../types' -import { issuePincode } from '../issue-pincode' import type { UserMapper } from '../../users/types' +import type { IssuePincode } from '../issue-pincode/types' interface Options { maxAge: number @@ -15,6 +15,7 @@ export interface LoginRequestEntry { } export const createInMemoryLoginService = ( userMapper: UserMapper, + issuePincode: IssuePincode, options?: Partial ): LoginService => { const { maxAge, db }: Options = { diff --git a/src/login/in-memory-login-service/index.ts b/src/login/in-memory-login-service/index.ts index ae778a2..3d3a10f 100644 --- a/src/login/in-memory-login-service/index.ts +++ b/src/login/in-memory-login-service/index.ts @@ -1,3 +1,5 @@ +import { getEnv } from '@helsingborg-stad/gdi-api-node' +import { createIssuePincode } from '..' import type { StartupLog } from '../../types' import type { UserMapper } from '../../users/types' import type { LoginService } from '../types' @@ -9,9 +11,15 @@ export const createInMemoryLoginServiceFromEnv = ( startupLog: StartupLog, userMapper: UserMapper ): LoginService => - startupLog.echo(createInMemoryLoginService(userMapper), { - name: 'login', - config: { - on: 'memory', - }, - }) + startupLog.echo( + createInMemoryLoginService( + userMapper, + createIssuePincode(getEnv('PASSWORDLESS_FIXED_PINCODE', { fallback: '' })) + ), + { + name: 'login', + config: { + on: 'memory', + }, + } + ) diff --git a/src/login/index.ts b/src/login/index.ts index 9eb8384..6f0cb06 100644 --- a/src/login/index.ts +++ b/src/login/index.ts @@ -4,6 +4,9 @@ import type { UserMapper } from '../users/types' import { createInMemoryLoginServiceFromEnv } from './in-memory-login-service' import { tryCreateMongoLoginServiceFromEnv } from './mongo-login-service' import type { HaffaUser, HaffaUserRoles, LoginService } from './types' +import { createIssuePincode } from './issue-pincode' + +export { createIssuePincode } export const rolesToRolesArray = (roles?: HaffaUserRoles) => Object.entries(normalizeRoles(roles)) diff --git a/src/login/issue-pincode.ts b/src/login/issue-pincode.ts deleted file mode 100644 index 690c9db..0000000 --- a/src/login/issue-pincode.ts +++ /dev/null @@ -1,2 +0,0 @@ -// factory for new pincodes -export const issuePincode = () => '123456' diff --git a/src/login/issue-pincode/index.ts b/src/login/issue-pincode/index.ts new file mode 100644 index 0000000..cef2275 --- /dev/null +++ b/src/login/issue-pincode/index.ts @@ -0,0 +1,12 @@ +import { randomInt } from 'crypto' +import type { IssuePincode } from './types' + +const randomDigits = (n: number) => + [...Array(n)] + .map((_, index) => (index === 0 ? randomInt(1, 10) : randomInt(0, 10))) + .join('') + +export const createIssuePincode = + (fixedCode?: string): IssuePincode => + () => + fixedCode || randomDigits(6) diff --git a/src/login/issue-pincode/issue-incode.spec.ts b/src/login/issue-pincode/issue-incode.spec.ts new file mode 100644 index 0000000..53be50c --- /dev/null +++ b/src/login/issue-pincode/issue-incode.spec.ts @@ -0,0 +1,51 @@ +import { createIssuePincode } from '.' + +describe('createIssuePincode', () => { + it('allows for hardcoded pincode', () => { + const n = 100 + const issuer = createIssuePincode('123457') + for (let i = 0; i < n; i += 1) { + expect(issuer()).toBe('123457') + } + }) + + it('always creates pincodes starting with 1-9 followed by 5 digits 0-9', () => { + const n = 10000 + const issuer = createIssuePincode() + for (let i = 0; i < n; i += 1) { + expect(issuer()).toMatch(/^[1-9][0-9]{5}$/) + } + }) + + it('has a nice distribution of digits 0-9', () => { + const n = 10000 + const issuer = createIssuePincode() + + const freq = { + '0': 0, + '1': 0, + '2': 0, + '3': 0, + '4': 0, + '5': 0, + '6': 0, + '7': 0, + '8': 0, + '9': 0, + } as Record + for (let i = 0; i < n; i += 1) { + issuer() + .split('') + // eslint-disable-next-line no-return-assign + .forEach(char => { + freq[char] += 1 + }) + } + + expect(Object.keys(freq).sort()).toMatchObject('0123456789'.split('')) + + '0123456789'.split('').forEach(char => { + expect(freq[char]).toBeGreaterThan(1) + }) + }) +}) diff --git a/src/login/issue-pincode/types.ts b/src/login/issue-pincode/types.ts new file mode 100644 index 0000000..2d2da22 --- /dev/null +++ b/src/login/issue-pincode/types.ts @@ -0,0 +1,3 @@ +export interface IssuePincode { + (): string +} diff --git a/src/login/mongo-login-service/mongo-login-service.ts b/src/login/mongo-login-service/mongo-login-service.ts index 0f0d9cc..0f961f6 100644 --- a/src/login/mongo-login-service/mongo-login-service.ts +++ b/src/login/mongo-login-service/mongo-login-service.ts @@ -8,9 +8,10 @@ import type { } from '../../mongodb-utils/types' import type { MongoLogin } from './types' import { createMongoConnection } from '../../mongodb-utils' -import { issuePincode } from '../issue-pincode' import type { UserMapper } from '../../users/types' import type { StartupLog } from '../../types' +import type { IssuePincode } from '../issue-pincode/types' +import { createIssuePincode } from '../issue-pincode' export const tryCreateMongoLoginServiceFromEnv = ( startupLog: StartupLog, @@ -25,11 +26,17 @@ export const tryCreateMongoLoginServiceFromEnv = ( 1, parseInt(getEnv('LOGIN_ATTEMPT_MAX_COUNT', { fallback: '16' }), 10) ) + + const issuePincode = createIssuePincode( + getEnv('PASSWORDLESS_FIXED_PINCODE', { fallback: '' }) + ) + return uri ? startupLog.echo( createMongoLoginService( userMapper, createMongoLoginConnection({ uri, collectionName }), + issuePincode, ttl, maxAttempts ), @@ -67,6 +74,7 @@ export const createMongoLoginConnection = ({ export const createMongoLoginService = ( userMapper: UserMapper, connection: MongoConnection, + issuePincode: IssuePincode, ttl: number = ms('10m'), maxAttempts: number = 16 ): LoginService => ({ diff --git a/src/test-utils/e2e.ts b/src/test-utils/e2e.ts index 1b68d86..a23ca19 100644 --- a/src/test-utils/e2e.ts +++ b/src/test-utils/e2e.ts @@ -20,6 +20,7 @@ import type { SettingsService } from '../settings/types' import { createUserMapper } from '../users' import { createInMemorySettingsService } from '../settings' import { loginPolicyAdapter } from '../login-policies/login-policy-adapter' +import { createIssuePincode } from '../login' const createGqlRequest = ( @@ -89,7 +90,11 @@ export const end2endTest = ( settings, adverts: createInMemoryAdvertsRepository(adverts), profiles: createInMemoryProfileRepository(profiles), - login: createInMemoryLoginService(userMapper, { db: logins }), + login: createInMemoryLoginService( + userMapper, + createIssuePincode('123456'), + { db: logins } + ), ...config?.services, }) diff --git a/src/test-utils/test-app.ts b/src/test-utils/test-app.ts index 179463e..300be87 100644 --- a/src/test-utils/test-app.ts +++ b/src/test-utils/test-app.ts @@ -13,6 +13,7 @@ import type { NotificationService } from '../notifications/types' import { createUserMapper } from '../users' import { createInMemorySettingsService } from '../settings' import { createJobExecutorServiceFromEnv } from '../jobs' +import { createIssuePincode } from '../login' export const TEST_SHARED_SECRET = 'shared scret used in tests' @@ -51,7 +52,7 @@ export const createTestServices = (services: Partial): Services => { return { userMapper, settings, - login: createInMemoryLoginService(userMapper), + login: createInMemoryLoginService(userMapper, createIssuePincode('123456')), tokens: createTokenService(userMapper, { secret: TEST_SHARED_SECRET }), adverts: createInMemoryAdvertsRepository(), profiles: createInMemoryProfileRepository(), diff --git a/src/types.ts b/src/types.ts index acbf902..1135377 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ import type { ProfileRepository } from './profile/types' import type { NotificationService } from './notifications/types' import type { UserMapper } from './users/types' import type { SettingsService } from './settings/types' -import { JobExcecutorService } from './jobs/types' +import type { JobExcecutorService } from './jobs/types' export interface StartupLog { echo: (