Skip to content

Commit 04bd2e4

Browse files
committed
refactor(service): users/auth: move local idp app layer operations to internal services to allow dependency injection and reuse
1 parent bf4e5c1 commit 04bd2e4

File tree

7 files changed

+99
-78
lines changed

7 files changed

+99
-78
lines changed

service/src/ingress/ingress.app.impl.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
import { entityNotFound, infrastructureError } from '../app.api/app.api.errors'
1+
import { entityNotFound, infrastructureError, invalidInput } from '../app.api/app.api.errors'
22
import { AppResponse } from '../app.api/app.api.global'
33
import { AdmitFromIdentityProviderOperation, AdmitFromIdentityProviderRequest, authenticationFailedError, EnrollMyselfOperation, EnrollMyselfRequest } from './ingress.app.api'
44
import { IdentityProviderRepository, IdentityProviderUser } from './ingress.entities'
55
import { AdmissionDeniedReason, AdmitUserFromIdentityProviderAccount, EnrollNewUser } from './ingress.services.api'
6-
import { LocalIdpCreateAccountOperation } from './local-idp.app.api'
6+
import { LocalIdpError, LocalIdpInvalidPasswordError } from './local-idp.entities'
7+
import { MageLocalIdentityProviderService } from './local-idp.services.api'
78
import { JWTService, TokenAssertion } from './verification'
89

910

10-
export function CreateEnrollMyselfOperation(createLocalIdpAccount: LocalIdpCreateAccountOperation, idpRepo: IdentityProviderRepository, enrollNewUser: EnrollNewUser): EnrollMyselfOperation {
11+
export function CreateEnrollMyselfOperation(localIdp: MageLocalIdentityProviderService, idpRepo: IdentityProviderRepository, enrollNewUser: EnrollNewUser): EnrollMyselfOperation {
1112
return async function enrollMyself(req: EnrollMyselfRequest): ReturnType<EnrollMyselfOperation> {
12-
const localAccountCreate = await createLocalIdpAccount(req)
13-
if (localAccountCreate.error) {
14-
return AppResponse.error(localAccountCreate.error)
13+
const localIdpAccount = await localIdp.createAccount(req)
14+
if (localIdpAccount instanceof LocalIdpError) {
15+
if (localIdpAccount instanceof LocalIdpInvalidPasswordError) {
16+
return AppResponse.error(invalidInput(localIdpAccount.message))
17+
}
18+
console.error('error creating local idp account for self-enrollment', localIdpAccount)
19+
return AppResponse.error(invalidInput('Error creating local Mage account'))
1520
}
16-
const localAccount = localAccountCreate.success!
1721
const candidateMageAccount: IdentityProviderUser = {
18-
username: localAccount.username,
22+
username: localIdpAccount.username,
1923
displayName: req.displayName,
2024
phones: [],
2125
}
@@ -25,12 +29,11 @@ export function CreateEnrollMyselfOperation(createLocalIdpAccount: LocalIdpCreat
2529
if (req.phone) {
2630
candidateMageAccount.phones = [ { number: req.phone, type: 'Main' } ]
2731
}
28-
const localIdp = await idpRepo.findIdpByName('local')
29-
if (!localIdp) {
32+
const idp = await idpRepo.findIdpByName('local')
33+
if (!idp) {
3034
throw new Error('local idp not found')
3135
}
32-
const enrollmentResult = await enrollNewUser(candidateMageAccount, localIdp)
33-
36+
const enrollmentResult = await enrollNewUser(candidateMageAccount, idp)
3437

3538
// TODO: auto-activate account after enrollment policy
3639
throw new Error('unimplemented')

service/src/ingress/ingress.protocol.local.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import express from 'express'
33
import { Strategy as LocalStrategy, VerifyFunction as LocalStrategyVerifyFunction } from 'passport-local'
44
import { LocalIdpAccount } from './local-idp.entities'
55
import { IdentityProviderUser } from './ingress.entities'
6-
import { LocalIdpAuthenticateOperation } from './local-idp.app.api'
76
import { IngressProtocolWebBinding, IngressResponseType } from './ingress.protocol.bindings'
7+
import { MageLocalIdentityProviderService } from './local-idp.services.api'
8+
import { permissionDenied } from '../app.api/app.api.errors'
89

910

1011
function userForLocalIdpAccount(account: LocalIdpAccount): IdentityProviderUser {
@@ -15,15 +16,15 @@ function userForLocalIdpAccount(account: LocalIdpAccount): IdentityProviderUser
1516
}
1617
}
1718

18-
function createLocalStrategy(localIdpAuthenticate: LocalIdpAuthenticateOperation, flowState: string | undefined): passport.Strategy {
19+
function createLocalStrategy(localIdp: MageLocalIdentityProviderService, flowState: string | undefined): passport.Strategy {
1920
const verify: LocalStrategyVerifyFunction = async function LocalIngressProtocolVerify(username, password, done) {
20-
const authResult = await localIdpAuthenticate({ username, password })
21-
if (authResult.success) {
22-
const localAccount = authResult.success
23-
const localIdpUser = userForLocalIdpAccount(localAccount)
24-
return done(null, { admittingFromIdentityProvider: { idpName: 'local', account: localIdpUser, flowState } })
21+
const authResult = await localIdp.authenticate({ username, password })
22+
if (!authResult || authResult.failed) {
23+
return done(permissionDenied('local authentication failed', username))
2524
}
26-
return done(authResult.error)
25+
const localAccount = authResult.authenticated
26+
const localIdpUser = userForLocalIdpAccount(localAccount)
27+
return done(null, { admittingFromIdentityProvider: { idpName: 'local', account: localIdpUser, flowState } })
2728
}
2829
return new LocalStrategy(verify)
2930
}
@@ -40,7 +41,7 @@ const validateSigninRequest: express.RequestHandler = function LocalProtocolIngr
4041
next()
4142
}
4243

43-
export function createWebBinding(passport: passport.Authenticator, localIdpAuthenticate: LocalIdpAuthenticateOperation): IngressProtocolWebBinding {
44+
export function createLocalProtocolWebBinding(passport: passport.Authenticator, localIdpAuthenticate: MageLocalIdentityProviderService): IngressProtocolWebBinding {
4445
return {
4546
ingressResponseType: IngressResponseType.Direct,
4647
beginIngressFlow: (req, res, next, flowState): any => {

service/src/ingress/local-idp.app.api.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

service/src/ingress/local-idp.app.impl.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

service/src/ingress/local-idp.entities.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ export class LocalIdpFailedAuthenticationError extends LocalIdpError {
158158
}
159159
}
160160

161+
export class LocalIdpAccountNotFoundError extends LocalIdpError {
162+
163+
}
164+
161165
function invalidPasswordError(reason: string): LocalIdpError {
162166
return new LocalIdpError(reason)
163167
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { LocalIdpAccount, LocalIdpAuthenticationResult, LocalIdpCredentials, LocalIdpError } from './local-idp.entities'
2+
3+
4+
export interface MageLocalIdentityProviderService {
5+
createAccount(credentials: LocalIdpCredentials): Promise<LocalIdpAccount | LocalIdpError>
6+
/**
7+
* Return `null` if no account for the given username exists.
8+
*/
9+
deleteAccount(username: string): Promise<LocalIdpAccount | null>
10+
/**
11+
* Return `null` if no account for the given username exists. If authentication fails, update the corresponding
12+
* account according to the service's account lock policy.
13+
*/
14+
authenticate(credentials: LocalIdpCredentials): Promise<LocalIdpAuthenticationResult | null>
15+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { attemptAuthentication, LocalIdpAccount, LocalIdpAuthenticationResult, LocalIdpCredentials, LocalIdpDuplicateUsernameError, LocalIdpError, LocalIdpInvalidPasswordError, LocalIdpRepository, prepareNewAccount } from './local-idp.entities'
2+
import { MageLocalIdentityProviderService } from './local-idp.services.api'
3+
4+
5+
export function createLocalIdentityProviderService(repo: LocalIdpRepository): MageLocalIdentityProviderService {
6+
7+
async function createAccount(credentials: LocalIdpCredentials): Promise<LocalIdpAccount | LocalIdpError> {
8+
const securityPolicy = await repo.readSecurityPolicy()
9+
const { username, password } = credentials
10+
const candidateAccount = await prepareNewAccount(username, password, securityPolicy)
11+
if (candidateAccount instanceof LocalIdpInvalidPasswordError) {
12+
return candidateAccount
13+
}
14+
const createdAccount = await repo.createLocalAccount(candidateAccount)
15+
if (createdAccount instanceof LocalIdpError) {
16+
if (createdAccount instanceof LocalIdpDuplicateUsernameError) {
17+
console.error(`attempted to create local account with duplicate username ${username}`, createdAccount)
18+
}
19+
return createdAccount
20+
}
21+
return createdAccount
22+
}
23+
24+
function deleteAccount(username: string): Promise<LocalIdpAccount | null> {
25+
return repo.deleteLocalAccount(username)
26+
}
27+
28+
async function authenticate(credentials: LocalIdpCredentials): Promise<LocalIdpAuthenticationResult | null> {
29+
const { username, password } = credentials
30+
const account = await repo.readLocalAccount(username)
31+
if (!account) {
32+
console.info('local account does not exist:', username)
33+
return null
34+
}
35+
const securityPolicy = await repo.readSecurityPolicy()
36+
const attempt = await attemptAuthentication(account, password, securityPolicy.accountLock)
37+
if (attempt.failed) {
38+
console.info('local authentication failed', attempt.failed)
39+
return attempt
40+
}
41+
const accountSaved = await repo.updateLocalAccount(attempt.authenticated)
42+
if (accountSaved) {
43+
attempt.authenticated = accountSaved
44+
return attempt
45+
}
46+
console.error(`account for username ${username} did not exist for update after authentication attempt`)
47+
return null
48+
}
49+
50+
return {
51+
createAccount,
52+
deleteAccount,
53+
authenticate,
54+
}
55+
}

0 commit comments

Comments
 (0)