Skip to content

Commit

Permalink
random pincodes for passwordless
Browse files Browse the repository at this point in the history
  • Loading branch information
jlarsson committed Sep 20, 2023
1 parent 965d293 commit 65d6f8e
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 13 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ FS_DATA_PATH=.local/data
# Passwordless configuration
#
#PASSWORDLESS_TTL=10m
PASSWORDLESS_FIXED_PINCODE=123456

#------------------------------------------------------------
#
Expand Down
3 changes: 2 additions & 1 deletion src/login/in-memory-login-service/in-memory-login-service.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,6 +15,7 @@ export interface LoginRequestEntry {
}
export const createInMemoryLoginService = (
userMapper: UserMapper,
issuePincode: IssuePincode,
options?: Partial<Options>
): LoginService => {
const { maxAge, db }: Options = {
Expand Down
20 changes: 14 additions & 6 deletions src/login/in-memory-login-service/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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',
},
}
)
3 changes: 3 additions & 0 deletions src/login/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 0 additions & 2 deletions src/login/issue-pincode.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/login/issue-pincode/index.ts
Original file line number Diff line number Diff line change
@@ -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)
51 changes: 51 additions & 0 deletions src/login/issue-pincode/issue-incode.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>
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)
})
})
})
3 changes: 3 additions & 0 deletions src/login/issue-pincode/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IssuePincode {
(): string
}
10 changes: 9 additions & 1 deletion src/login/mongo-login-service/mongo-login-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
),
Expand Down Expand Up @@ -67,6 +74,7 @@ export const createMongoLoginConnection = ({
export const createMongoLoginService = (
userMapper: UserMapper,
connection: MongoConnection<MongoLogin>,
issuePincode: IssuePincode,
ttl: number = ms('10m'),
maxAttempts: number = 16
): LoginService => ({
Expand Down
7 changes: 6 additions & 1 deletion src/test-utils/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
(
Expand Down Expand Up @@ -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,
})

Expand Down
3 changes: 2 additions & 1 deletion src/test-utils/test-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -51,7 +52,7 @@ export const createTestServices = (services: Partial<Services>): Services => {
return {
userMapper,
settings,
login: createInMemoryLoginService(userMapper),
login: createInMemoryLoginService(userMapper, createIssuePincode('123456')),
tokens: createTokenService(userMapper, { secret: TEST_SHARED_SECRET }),
adverts: createInMemoryAdvertsRepository(),
profiles: createInMemoryProfileRepository(),
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <TService>(
Expand Down

0 comments on commit 65d6f8e

Please sign in to comment.