Skip to content

Commit

Permalink
refactor(service): users/auth: move local idp app layer operations to…
Browse files Browse the repository at this point in the history
… internal services to allow dependency injection and reuse
  • Loading branch information
restjohn committed Nov 25, 2024
1 parent bf4e5c1 commit 04bd2e4
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 78 deletions.
27 changes: 15 additions & 12 deletions service/src/ingress/ingress.app.impl.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { entityNotFound, infrastructureError } from '../app.api/app.api.errors'
import { entityNotFound, infrastructureError, invalidInput } from '../app.api/app.api.errors'
import { AppResponse } from '../app.api/app.api.global'
import { AdmitFromIdentityProviderOperation, AdmitFromIdentityProviderRequest, authenticationFailedError, EnrollMyselfOperation, EnrollMyselfRequest } from './ingress.app.api'
import { IdentityProviderRepository, IdentityProviderUser } from './ingress.entities'
import { AdmissionDeniedReason, AdmitUserFromIdentityProviderAccount, EnrollNewUser } from './ingress.services.api'
import { LocalIdpCreateAccountOperation } from './local-idp.app.api'
import { LocalIdpError, LocalIdpInvalidPasswordError } from './local-idp.entities'
import { MageLocalIdentityProviderService } from './local-idp.services.api'
import { JWTService, TokenAssertion } from './verification'


export function CreateEnrollMyselfOperation(createLocalIdpAccount: LocalIdpCreateAccountOperation, idpRepo: IdentityProviderRepository, enrollNewUser: EnrollNewUser): EnrollMyselfOperation {
export function CreateEnrollMyselfOperation(localIdp: MageLocalIdentityProviderService, idpRepo: IdentityProviderRepository, enrollNewUser: EnrollNewUser): EnrollMyselfOperation {
return async function enrollMyself(req: EnrollMyselfRequest): ReturnType<EnrollMyselfOperation> {
const localAccountCreate = await createLocalIdpAccount(req)
if (localAccountCreate.error) {
return AppResponse.error(localAccountCreate.error)
const localIdpAccount = await localIdp.createAccount(req)
if (localIdpAccount instanceof LocalIdpError) {
if (localIdpAccount instanceof LocalIdpInvalidPasswordError) {
return AppResponse.error(invalidInput(localIdpAccount.message))
}
console.error('error creating local idp account for self-enrollment', localIdpAccount)
return AppResponse.error(invalidInput('Error creating local Mage account'))
}
const localAccount = localAccountCreate.success!
const candidateMageAccount: IdentityProviderUser = {
username: localAccount.username,
username: localIdpAccount.username,
displayName: req.displayName,
phones: [],
}
Expand All @@ -25,12 +29,11 @@ export function CreateEnrollMyselfOperation(createLocalIdpAccount: LocalIdpCreat
if (req.phone) {
candidateMageAccount.phones = [ { number: req.phone, type: 'Main' } ]
}
const localIdp = await idpRepo.findIdpByName('local')
if (!localIdp) {
const idp = await idpRepo.findIdpByName('local')
if (!idp) {
throw new Error('local idp not found')
}
const enrollmentResult = await enrollNewUser(candidateMageAccount, localIdp)

const enrollmentResult = await enrollNewUser(candidateMageAccount, idp)

// TODO: auto-activate account after enrollment policy
throw new Error('unimplemented')
Expand Down
19 changes: 10 additions & 9 deletions service/src/ingress/ingress.protocol.local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import express from 'express'
import { Strategy as LocalStrategy, VerifyFunction as LocalStrategyVerifyFunction } from 'passport-local'
import { LocalIdpAccount } from './local-idp.entities'
import { IdentityProviderUser } from './ingress.entities'
import { LocalIdpAuthenticateOperation } from './local-idp.app.api'
import { IngressProtocolWebBinding, IngressResponseType } from './ingress.protocol.bindings'
import { MageLocalIdentityProviderService } from './local-idp.services.api'
import { permissionDenied } from '../app.api/app.api.errors'


function userForLocalIdpAccount(account: LocalIdpAccount): IdentityProviderUser {
Expand All @@ -15,15 +16,15 @@ function userForLocalIdpAccount(account: LocalIdpAccount): IdentityProviderUser
}
}

function createLocalStrategy(localIdpAuthenticate: LocalIdpAuthenticateOperation, flowState: string | undefined): passport.Strategy {
function createLocalStrategy(localIdp: MageLocalIdentityProviderService, flowState: string | undefined): passport.Strategy {
const verify: LocalStrategyVerifyFunction = async function LocalIngressProtocolVerify(username, password, done) {
const authResult = await localIdpAuthenticate({ username, password })
if (authResult.success) {
const localAccount = authResult.success
const localIdpUser = userForLocalIdpAccount(localAccount)
return done(null, { admittingFromIdentityProvider: { idpName: 'local', account: localIdpUser, flowState } })
const authResult = await localIdp.authenticate({ username, password })
if (!authResult || authResult.failed) {
return done(permissionDenied('local authentication failed', username))
}
return done(authResult.error)
const localAccount = authResult.authenticated
const localIdpUser = userForLocalIdpAccount(localAccount)
return done(null, { admittingFromIdentityProvider: { idpName: 'local', account: localIdpUser, flowState } })
}
return new LocalStrategy(verify)
}
Expand All @@ -40,7 +41,7 @@ const validateSigninRequest: express.RequestHandler = function LocalProtocolIngr
next()
}

export function createWebBinding(passport: passport.Authenticator, localIdpAuthenticate: LocalIdpAuthenticateOperation): IngressProtocolWebBinding {
export function createLocalProtocolWebBinding(passport: passport.Authenticator, localIdpAuthenticate: MageLocalIdentityProviderService): IngressProtocolWebBinding {
return {
ingressResponseType: IngressResponseType.Direct,
beginIngressFlow: (req, res, next, flowState): any => {
Expand Down
12 changes: 0 additions & 12 deletions service/src/ingress/local-idp.app.api.ts

This file was deleted.

45 changes: 0 additions & 45 deletions service/src/ingress/local-idp.app.impl.ts

This file was deleted.

4 changes: 4 additions & 0 deletions service/src/ingress/local-idp.entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export class LocalIdpFailedAuthenticationError extends LocalIdpError {
}
}

export class LocalIdpAccountNotFoundError extends LocalIdpError {

}

function invalidPasswordError(reason: string): LocalIdpError {
return new LocalIdpError(reason)
}
Expand Down
15 changes: 15 additions & 0 deletions service/src/ingress/local-idp.services.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { LocalIdpAccount, LocalIdpAuthenticationResult, LocalIdpCredentials, LocalIdpError } from './local-idp.entities'


export interface MageLocalIdentityProviderService {
createAccount(credentials: LocalIdpCredentials): Promise<LocalIdpAccount | LocalIdpError>
/**
* Return `null` if no account for the given username exists.
*/
deleteAccount(username: string): Promise<LocalIdpAccount | null>
/**
* Return `null` if no account for the given username exists. If authentication fails, update the corresponding
* account according to the service's account lock policy.
*/
authenticate(credentials: LocalIdpCredentials): Promise<LocalIdpAuthenticationResult | null>
}
55 changes: 55 additions & 0 deletions service/src/ingress/local-idp.services.impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { attemptAuthentication, LocalIdpAccount, LocalIdpAuthenticationResult, LocalIdpCredentials, LocalIdpDuplicateUsernameError, LocalIdpError, LocalIdpInvalidPasswordError, LocalIdpRepository, prepareNewAccount } from './local-idp.entities'
import { MageLocalIdentityProviderService } from './local-idp.services.api'


export function createLocalIdentityProviderService(repo: LocalIdpRepository): MageLocalIdentityProviderService {

async function createAccount(credentials: LocalIdpCredentials): Promise<LocalIdpAccount | LocalIdpError> {
const securityPolicy = await repo.readSecurityPolicy()
const { username, password } = credentials
const candidateAccount = await prepareNewAccount(username, password, securityPolicy)
if (candidateAccount instanceof LocalIdpInvalidPasswordError) {
return candidateAccount
}
const createdAccount = await repo.createLocalAccount(candidateAccount)
if (createdAccount instanceof LocalIdpError) {
if (createdAccount instanceof LocalIdpDuplicateUsernameError) {
console.error(`attempted to create local account with duplicate username ${username}`, createdAccount)
}
return createdAccount
}
return createdAccount
}

function deleteAccount(username: string): Promise<LocalIdpAccount | null> {
return repo.deleteLocalAccount(username)
}

async function authenticate(credentials: LocalIdpCredentials): Promise<LocalIdpAuthenticationResult | null> {
const { username, password } = credentials
const account = await repo.readLocalAccount(username)
if (!account) {
console.info('local account does not exist:', username)
return null
}
const securityPolicy = await repo.readSecurityPolicy()
const attempt = await attemptAuthentication(account, password, securityPolicy.accountLock)
if (attempt.failed) {
console.info('local authentication failed', attempt.failed)
return attempt
}
const accountSaved = await repo.updateLocalAccount(attempt.authenticated)
if (accountSaved) {
attempt.authenticated = accountSaved
return attempt
}
console.error(`account for username ${username} did not exist for update after authentication attempt`)
return null
}

return {
createAccount,
deleteAccount,
authenticate,
}
}

0 comments on commit 04bd2e4

Please sign in to comment.