Skip to content

Commit

Permalink
refactor: improve authenticator api to be more reliable
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 11, 2024
1 parent 8cfd777 commit 64d26dd
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 12 deletions.
58 changes: 47 additions & 11 deletions src/auth/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import { AuthenticationException } from './errors.js'
* guards to login users and authenticate requests.
*/
export class Authenticator<KnownGuards extends Record<string, GuardFactory>> {
/**
* Name of the guard using which the authentication was last
* attempted.
*/
#authenticationAttemptedViaGuard?: keyof KnownGuards

/**
* Name of the guard using which the request has
* been authenticated
Expand Down Expand Up @@ -59,27 +65,45 @@ export class Authenticator<KnownGuards extends Record<string, GuardFactory>> {

/**
* A boolean to know if the current request has
* been authenticated
* been authenticated. The property returns false
* when "authenticate" or "authenticateUsing" methods
* are not used
*/
get isAuthenticated(): boolean {
return this.use(this.#authenticatedViaGuard || this.defaultGuard).isAuthenticated
if (!this.#authenticationAttemptedViaGuard) {
return false
}

return this.use(this.#authenticationAttemptedViaGuard).isAuthenticated
}

/**
* Reference to the currently authenticated user
* Reference to the currently authenticated user. The property
* returns undefined when "authenticate" or "authenticateUsing"
* methods are not used.
*/
get user(): {
[K in keyof KnownGuards]: ReturnType<KnownGuards[K]>['user']
}[keyof KnownGuards] {
return this.use(this.#authenticatedViaGuard || this.defaultGuard).user
if (!this.#authenticationAttemptedViaGuard) {
return undefined
}

return this.use(this.#authenticationAttemptedViaGuard).user
}

/**
* Whether or not the authentication has been attempted
* during the current request
* Whether or not the authentication has been attempted during
* the current request. The property returns false
* when "authenticate" or "authenticateUsing" methods
* are not used
*/
get authenticationAttempted(): boolean {
return this.use(this.#authenticatedViaGuard || this.defaultGuard).authenticationAttempted
if (!this.#authenticationAttemptedViaGuard) {
return false
}

return this.use(this.#authenticationAttemptedViaGuard).authenticationAttempted
}

constructor(ctx: HttpContext, config: { default: keyof KnownGuards; guards: KnownGuards }) {
Expand All @@ -95,7 +119,11 @@ export class Authenticator<KnownGuards extends Record<string, GuardFactory>> {
getUserOrFail(): {
[K in keyof KnownGuards]: ReturnType<ReturnType<KnownGuards[K]>['getUserOrFail']>
}[keyof KnownGuards] {
return this.use(this.#authenticatedViaGuard || this.defaultGuard).getUserOrFail() as {
if (!this.#authenticatedViaGuard) {
throw AuthenticationException.E_INVALID_AUTH_SESSION()
}

return this.use(this.#authenticatedViaGuard).getUserOrFail() as {
[K in keyof KnownGuards]: ReturnType<ReturnType<KnownGuards[K]>['getUserOrFail']>
}[keyof KnownGuards]
}
Expand Down Expand Up @@ -128,6 +156,13 @@ export class Authenticator<KnownGuards extends Record<string, GuardFactory>> {
return guardInstance as ReturnType<KnownGuards[Guard]>
}

/**
* Authenticate current request using the default guard
*/
authenticate() {
return this.authenticateUsing()
}

/**
* Authenticate the request using all of the mentioned
* guards or the default guard.
Expand All @@ -140,12 +175,13 @@ export class Authenticator<KnownGuards extends Record<string, GuardFactory>> {
*/
async authenticateUsing(guards?: (keyof KnownGuards)[], options?: { loginRoute?: string }) {
const guardsToUse = guards || [this.defaultGuard]
let lastUsedGuardDriver: string | undefined
let lastUsedDriver: string | undefined

for (let guardName of guardsToUse) {
debug('attempting to authenticate using guard "%s"', guardName)
const guard = this.use(guardName)
lastUsedGuardDriver = guard.driverName
this.#authenticationAttemptedViaGuard = guardName
lastUsedDriver = guard.driverName

if (await guard.check()) {
this.#authenticatedViaGuard = guardName
Expand All @@ -155,7 +191,7 @@ export class Authenticator<KnownGuards extends Record<string, GuardFactory>> {

throw new AuthenticationException('Unauthorized access', {
code: 'E_UNAUTHORIZED_ACCESS',
guardDriverName: lastUsedGuardDriver!,
guardDriverName: lastUsedDriver!,
redirectTo: options?.loginRoute,
})
}
Expand Down
18 changes: 18 additions & 0 deletions tests/auth/auth_manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { createEmitter } from '../helpers.js'
import { AuthManager } from '../../src/auth/auth_manager.js'
import { Authenticator } from '../../src/auth/authenticator.js'
import { SessionGuardFactory } from '../../factories/guards/session/guard_factory.js'
import { AuthenticatorClient } from '../../src/auth/authenticator_client.js'

test.group('Auth manager', () => {
test('create authenticator from auth manager', async ({ assert, expectTypeOf }) => {
Expand All @@ -32,4 +33,21 @@ test.group('Auth manager', () => {
assert.instanceOf(authManager.createAuthenticator(ctx), Authenticator)
expectTypeOf(authManager.createAuthenticator(ctx).use).parameters.toMatchTypeOf<['web'?]>()
})

test('create authenticator client from auth manager', async ({ assert, expectTypeOf }) => {
const emitter = createEmitter()
const ctx = new HttpContextFactory().create()
const sessionGuard = new SessionGuardFactory().create(ctx, emitter)

const authManager = new AuthManager({
default: 'web',
guards: {
web: () => sessionGuard,
},
})

assert.equal(authManager.defaultGuard, 'web')
assert.instanceOf(authManager.createAuthenticatorClient(), AuthenticatorClient)
expectTypeOf(authManager.createAuthenticatorClient().use).parameters.toMatchTypeOf<['web'?]>()
})
})
58 changes: 57 additions & 1 deletion tests/auth/authenticator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ test.group('Authenticator', () => {

await sessionMiddleware.handle(ctx, async () => {
ctx.session.put('auth_web', user.id)
await authenticator.authenticateUsing()
await authenticator.authenticate()
})

assert.instanceOf(authenticator.user, FactoryUser)
Expand All @@ -84,6 +84,62 @@ test.group('Authenticator', () => {
assert.isTrue(authenticator.authenticationAttempted)
})

test('authenticate using the guard instance', async ({ assert }) => {
const db = await createDatabase()
await createTables(db)

const emitter = createEmitter()
const ctx = new HttpContextFactory().create()
const user = await FactoryUser.createWithDefaults()
const sessionGuard = new SessionGuardFactory().create(ctx, emitter)
const sessionMiddleware = await new SessionMiddlewareFactory().create()

const authenticator = new Authenticator(ctx, {
default: 'web',
guards: {
web: () => sessionGuard,
},
})

await sessionMiddleware.handle(ctx, async () => {
ctx.session.put('auth_web', user.id)
await authenticator.use().authenticate()
})

assert.isUndefined(authenticator.user)
assert.isUndefined(authenticator.authenticatedViaGuard)
assert.isFalse(authenticator.isAuthenticated)
assert.isFalse(authenticator.authenticationAttempted)
})

test('access properties without authenticating user', async ({ assert }) => {
const db = await createDatabase()
await createTables(db)

const emitter = createEmitter()
const ctx = new HttpContextFactory().create()
const user = await FactoryUser.createWithDefaults()
const sessionGuard = new SessionGuardFactory().create(ctx, emitter)
const sessionMiddleware = await new SessionMiddlewareFactory().create()

const authenticator = new Authenticator(ctx, {
default: 'web',
guards: {
web: () => sessionGuard,
},
})

await sessionMiddleware.handle(ctx, async () => {
ctx.session.put('auth_web', user.id)
})

assert.isUndefined(authenticator.user)
assert.isUndefined(authenticator.authenticatedViaGuard)
assert.isFalse(authenticator.isAuthenticated)
assert.isFalse(authenticator.authenticationAttempted)
assert.throws(() => authenticator.getUserOrFail(), 'Invalid or expired authentication session')
})

test('throw error when unable to authenticate', async ({ assert }) => {
assert.plan(4)

Expand Down

0 comments on commit 64d26dd

Please sign in to comment.