diff --git a/src/auth/authenticator.ts b/src/auth/authenticator.ts index e892a15..196b643 100644 --- a/src/auth/authenticator.ts +++ b/src/auth/authenticator.ts @@ -18,6 +18,12 @@ import { AuthenticationException } from './errors.js' * guards to login users and authenticate requests. */ export class Authenticator> { + /** + * 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 @@ -59,27 +65,45 @@ export class Authenticator> { /** * 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['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 }) { @@ -95,7 +119,11 @@ export class Authenticator> { getUserOrFail(): { [K in keyof KnownGuards]: ReturnType['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['getUserOrFail']> }[keyof KnownGuards] } @@ -128,6 +156,13 @@ export class Authenticator> { return guardInstance as ReturnType } + /** + * Authenticate current request using the default guard + */ + authenticate() { + return this.authenticateUsing() + } + /** * Authenticate the request using all of the mentioned * guards or the default guard. @@ -140,12 +175,13 @@ export class Authenticator> { */ 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 @@ -155,7 +191,7 @@ export class Authenticator> { throw new AuthenticationException('Unauthorized access', { code: 'E_UNAUTHORIZED_ACCESS', - guardDriverName: lastUsedGuardDriver!, + guardDriverName: lastUsedDriver!, redirectTo: options?.loginRoute, }) } diff --git a/tests/auth/auth_manager.spec.ts b/tests/auth/auth_manager.spec.ts index 2a3011f..a8ed3e4 100644 --- a/tests/auth/auth_manager.spec.ts +++ b/tests/auth/auth_manager.spec.ts @@ -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 }) => { @@ -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'?]>() + }) }) diff --git a/tests/auth/authenticator.spec.ts b/tests/auth/authenticator.spec.ts index 472678d..eafc225 100644 --- a/tests/auth/authenticator.spec.ts +++ b/tests/auth/authenticator.spec.ts @@ -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) @@ -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)