diff --git a/bin/test.ts b/bin/test.ts index e6a3787..45bbeb5 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -11,6 +11,10 @@ configure({ name: 'session', files: ['tests/guards/session/**/*.spec.ts'], }, + { + name: 'auth', + files: ['tests/auth/**/*.spec.ts'], + }, { name: 'core', files: ['tests/core/**/*.spec.ts'], diff --git a/factories/database_user_provider.ts b/factories/database_user_provider.ts index e18f582..3b8c96d 100644 --- a/factories/database_user_provider.ts +++ b/factories/database_user_provider.ts @@ -10,11 +10,11 @@ import { Hash } from '@adonisjs/core/hash' import type { Database } from '@adonisjs/lucid/database' import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt' -import { DatabaseUserProvider } from '../src/core/user_providers/database.js' +import { BaseDatabaseUserProvider } from '../src/core/user_providers/database.js' export class TestDatabaseUserProvider< RealUser extends Record, -> extends DatabaseUserProvider {} +> extends BaseDatabaseUserProvider {} /** * Creates an instance of the DatabaseUserProvider with sane diff --git a/factories/lucid_user_provider.ts b/factories/lucid_user_provider.ts index d6e3d20..3055473 100644 --- a/factories/lucid_user_provider.ts +++ b/factories/lucid_user_provider.ts @@ -10,9 +10,10 @@ import { Hash } from '@adonisjs/core/hash' import { BaseModel, column } from '@adonisjs/lucid/orm' import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt' -import { LucidUserProvider } from '../src/core/user_providers/lucid.js' -import { LucidAuthenticatable, LucidUserProviderOptions } from '../src/core/types.js' -import { PROVIDER_REAL_USER } from '../src/symbols.js' + +import { PROVIDER_REAL_USER } from '../src/auth/symbols.js' +import { BaseLucidUserProvider } from '../src/core/user_providers/lucid.js' +import type { LucidAuthenticatable, LucidUserProviderOptions } from '../src/core/types.js' export class FactoryUser extends BaseModel { static table = 'users' @@ -49,7 +50,7 @@ export class FactoryUser extends BaseModel { export class TestLucidUserProvider< UserModel extends LucidAuthenticatable, -> extends LucidUserProvider { +> extends BaseLucidUserProvider { declare [PROVIDER_REAL_USER]: InstanceType } @@ -58,23 +59,20 @@ export class TestLucidUserProvider< * defaults for testing */ export class LucidUserProviderFactory { - createForModel( - model: Model, - options: LucidUserProviderOptions - ) { - return new TestLucidUserProvider( - async () => { - return { - default: model, - } - }, - { - ...options, - } - ) + createForModel(options: LucidUserProviderOptions) { + return new TestLucidUserProvider({ + ...options, + }) } create() { - return this.createForModel(FactoryUser, { uids: ['email', 'username'] }) + return this.createForModel({ + model: async () => { + return { + default: FactoryUser, + } + }, + uids: ['email', 'username'], + }) } } diff --git a/factories/session_guard_factory.ts b/factories/session_guard_factory.ts index de4d632..c445017 100644 --- a/factories/session_guard_factory.ts +++ b/factories/session_guard_factory.ts @@ -9,8 +9,11 @@ import type { HttpContext } from '@adonisjs/core/http' -import { SessionGuard } from '../src/session/guard.js' -import type { SessionGuardConfig, SessionUserProviderContract } from '../src/session/types.js' +import { SessionGuard } from '../src/guards/session/guard.js' +import type { + SessionGuardConfig, + SessionUserProviderContract, +} from '../src/guards/session/types.js' import { FactoryUser, TestLucidUserProvider, diff --git a/index.ts b/index.ts index cda0e5c..fd0f64e 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,8 @@ * file that was distributed with this source code. */ -export * as errors from './src/errors.js' -export * as symbols from './src/symbols.js' -export { Authenticator } from './src/authenticator.js' +export * as errors from './src/auth/errors.js' +export * as symbols from './src/auth/symbols.js' +export { Authenticator } from './src/auth/authenticator.js' +export { AuthManager } from './src/auth/auth_manager.js' +export { defineConfig, providers } from './src/auth/define_config.js' diff --git a/package.json b/package.json index 6f3925e..efa73eb 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,15 @@ "exports": { ".": "./build/index.js", "./types": "./build/src/types/main.js", + "./core/token": "./build/src/core/token.js", "./core/guard_user": "./build/src/core/guard_user.js", "./core/user_providers/*": "./build/src/core/user_providers/*.js", "./core/token_providers/*": "./build/src/core/token_providers/*.js", - "./session": "./build/src/session/main.js", - "./session/user_providers": "./build/src/session/user_providers/main.js", - "./session/token_providers": "./build/src/session/token_providers/main.js", - "./types/session": "./build/src/session/types.js", - "./types/core": "./build/src/core/types.js" + "./types/core": "./build/src/core/types.js", + + "./session": "./build/src/guards/session/main.js", + "./types/session": "./build/src/guards/session/types.js" }, "scripts": { "pretest": "npm run lint", @@ -65,28 +65,28 @@ "url": "https://github.com/adonisjs/auth/issues" }, "devDependencies": { - "@adonisjs/core": "^6.1.5-26", + "@adonisjs/core": "^6.1.5-30", "@adonisjs/eslint-config": "^1.1.8", - "@adonisjs/lucid": "^19.0.0-2", + "@adonisjs/lucid": "^19.0.0-3", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/session": "^7.0.0-11", + "@adonisjs/session": "^7.0.0-13", "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.7.2", - "@commitlint/config-conventional": "^17.7.0", - "@japa/assert": "^2.0.0-2", - "@japa/expect-type": "^2.0.0-1", - "@japa/file-system": "^2.0.0-2", - "@japa/runner": "^3.0.1", + "@commitlint/cli": "^18.0.0", + "@commitlint/config-conventional": "^18.0.0", + "@japa/assert": "^2.0.0", + "@japa/expect-type": "^2.0.0", + "@japa/file-system": "^2.0.0", + "@japa/runner": "^3.0.4", "@japa/snapshot": "^2.0.0", "@swc/core": "1.3.82", - "@types/luxon": "^3.3.2", - "@types/node": "^20.8.3", - "@types/set-cookie-parser": "^2.4.4", + "@types/luxon": "^3.3.3", + "@types/node": "^20.8.7", + "@types/set-cookie-parser": "^2.4.5", "c8": "^8.0.1", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0", - "eslint": "^8.25.0", + "eslint": "^8.52.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "luxon": "^3.4.3", @@ -128,7 +128,7 @@ ] }, "dependencies": { - "@poppinss/utils": "^6.5.0-7" + "@poppinss/utils": "^6.5.0" }, "peerDependencies": { "@adonisjs/core": "^6.1.5-26", diff --git a/providers/auth_provider.ts b/providers/auth_provider.ts new file mode 100644 index 0000000..875416d --- /dev/null +++ b/providers/auth_provider.ts @@ -0,0 +1,40 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { configProvider } from '@adonisjs/core' +import { RuntimeException } from '@poppinss/utils' +import type { ApplicationService } from '@adonisjs/core/types' + +import { AuthManager } from '../src/auth/auth_manager.js' +import type { AuthService } from '../src/auth/types.js' + +declare module '@adonisjs/core/types' { + export interface ContainerBindings { + 'auth.manager': AuthService + } +} + +export default class AuthProvider { + constructor(protected app: ApplicationService) {} + + register() { + this.app.container.singleton('auth.manager', async () => { + const authConfigProvider = this.app.config.get('auth') + const config = await configProvider.resolve(this.app, authConfigProvider) + + if (!config) { + throw new RuntimeException( + 'Invalid config exported from "config/auth.ts" file. Make sure to use the defineConfig method' + ) + } + + return new AuthManager(config) + }) + } +} diff --git a/services/auth.ts b/services/auth.ts new file mode 100644 index 0000000..0bb8e11 --- /dev/null +++ b/services/auth.ts @@ -0,0 +1,22 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from '@adonisjs/core/services/app' +import { AuthService } from '../src/auth/types.js' + +let auth: AuthService + +/** + * Returns a singleton instance of the Auth manager class + */ +await app.booted(async () => { + auth = await app.container.make('auth.manager') +}) + +export { auth as default } diff --git a/src/auth_manager.ts b/src/auth/auth_manager.ts similarity index 71% rename from src/auth_manager.ts rename to src/auth/auth_manager.ts index ee6bbff..7ad9dfa 100644 --- a/src/auth_manager.ts +++ b/src/auth/auth_manager.ts @@ -9,23 +9,23 @@ import type { HttpContext } from '@adonisjs/core/http' +import type { GuardFactory } from './types.js' import { Authenticator } from './authenticator.js' -import type { AuthenticatorGuardFactory } from './types/main.js' /** * Auth manager exposes the API to register and manage authentication * guards from the config */ -export class AuthManager> { +export class AuthManager> { /** * Registered guards */ #config: { - default?: keyof KnownGuards + default: keyof KnownGuards guards: KnownGuards } - constructor(config: { default?: keyof KnownGuards; guards: KnownGuards }) { + constructor(config: { default: keyof KnownGuards; guards: KnownGuards }) { this.#config = config } diff --git a/src/authenticator.ts b/src/auth/authenticator.ts similarity index 64% rename from src/authenticator.ts rename to src/auth/authenticator.ts index 1a920af..1886abf 100644 --- a/src/authenticator.ts +++ b/src/auth/authenticator.ts @@ -10,13 +10,13 @@ import type { HttpContext } from '@adonisjs/core/http' import debug from './debug.js' -import type { AuthenticatorGuardFactory } from './types/main.js' +import type { GuardFactory } from './types.js' /** * Authenticator is an HTTP request specific implementation for using * guards to login users and authenticate requests. */ -export class Authenticator> { +export class Authenticator> { /** * Reference to HTTP context */ @@ -26,7 +26,7 @@ export class Authenticator> = {} - constructor(ctx: HttpContext, config: { default?: keyof KnownGuards; guards: KnownGuards }) { + constructor(ctx: HttpContext, config: { default: keyof KnownGuards; guards: KnownGuards }) { this.#ctx = ctx this.#config = config debug('creating authenticator. config %O', this.#config) @@ -45,24 +45,26 @@ export class Authenticator(guard: Guard): ReturnType { + use(guard?: Guard): ReturnType { + const guardToUse = guard || this.#config.default + /** * Use cached copy if exists */ - const cachedGuard = this.#guardsCache[guard] + const cachedGuard = this.#guardsCache[guardToUse] if (cachedGuard) { - debug('using guard from cache. name: "%s"', guard) + debug('using guard from cache. name: "%s"', guardToUse) return cachedGuard as ReturnType } - const guardFactory = this.#config.guards[guard] + const guardFactory = this.#config.guards[guardToUse] /** * Construct guard and cache it */ - debug('creating guard. name: "%s"', guard) + debug('creating guard. name: "%s"', guardToUse) const guardInstance = guardFactory(this.#ctx) - this.#guardsCache[guard] = guardInstance + this.#guardsCache[guardToUse] = guardInstance return guardInstance as ReturnType } diff --git a/src/debug.ts b/src/auth/debug.ts similarity index 100% rename from src/debug.ts rename to src/auth/debug.ts diff --git a/src/auth/define_config.ts b/src/auth/define_config.ts new file mode 100644 index 0000000..62623a7 --- /dev/null +++ b/src/auth/define_config.ts @@ -0,0 +1,94 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// + +import { configProvider } from '@adonisjs/core' +import type { ConfigProvider } from '@adonisjs/core/types' + +import type { GuardConfigProvider, GuardFactory } from './types.js' +import type { LucidUserProvider, DatabaseUserProvider } from './user_providers/main.js' +import type { + LucidAuthenticatable, + LucidUserProviderOptions, + DatabaseUserProviderOptions, +} from '../core/types.js' + +/** + * Config resolved by the "defineConfig" method + */ +export type ResolvedAuthConfig< + KnownGuards extends Record>, +> = { + default: keyof KnownGuards + guards: { + [K in keyof KnownGuards]: KnownGuards[K] extends GuardConfigProvider + ? A + : KnownGuards[K] + } +} + +/** + * Define configuration for the auth package. The function returns + * a config provider that is invoked inside the auth service + * provider + */ +export function defineConfig< + KnownGuards extends Record>, +>(config: { + default: keyof KnownGuards + guards: KnownGuards +}): ConfigProvider> { + return configProvider.create(async (app) => { + const guardsList = Object.keys(config.guards) + const guards = {} as Record + + for (let guardName of guardsList) { + const guard = config.guards[guardName] + if (typeof guard === 'function') { + guards[guardName] = guard + } else { + guards[guardName] = await guard.resolver(guardName, app) + } + } + + return { + default: config.default, + guards: guards, + } as ResolvedAuthConfig + }) +} + +/** + * Providers helper to configure user providers for + * finding users for authentication + */ +export const providers: { + db: >( + config: DatabaseUserProviderOptions + ) => ConfigProvider> + lucid: ( + config: LucidUserProviderOptions + ) => ConfigProvider> +} = { + db(config) { + return configProvider.create(async (app) => { + const db = await app.container.make('lucid.db') + const hasher = await app.container.make('hash') + const { DatabaseUserProvider } = await import('./user_providers/main.js') + return new DatabaseUserProvider(db, hasher.use(), config) + }) + }, + lucid(config) { + return configProvider.create(async () => { + const { LucidUserProvider } = await import('./user_providers/main.js') + return new LucidUserProvider(config) + }) + }, +} diff --git a/src/errors.ts b/src/auth/errors.ts similarity index 100% rename from src/errors.ts rename to src/auth/errors.ts diff --git a/src/symbols.ts b/src/auth/symbols.ts similarity index 100% rename from src/symbols.ts rename to src/auth/symbols.ts diff --git a/src/types/main.ts b/src/auth/types.ts similarity index 52% rename from src/types/main.ts rename to src/auth/types.ts index 0bc3f98..23aa4f2 100644 --- a/src/types/main.ts +++ b/src/auth/types.ts @@ -9,9 +9,10 @@ import type { Emitter } from '@adonisjs/core/events' import type { HttpContext } from '@adonisjs/core/http' -import { ApplicationService } from '@adonisjs/core/types' +import type { ApplicationService, ConfigProvider } from '@adonisjs/core/types' -import type { GUARD_KNOWN_EVENTS } from '../symbols.js' +import type { AuthManager } from './auth_manager.js' +import type { GUARD_KNOWN_EVENTS } from './symbols.js' /** * A set of properties a guard must implement. @@ -34,21 +35,37 @@ export interface GuardContract { withEmitter(emitter: Emitter): this } -/** - * Config providers are async function that needs app instance - * and returns configuration - */ -export type ConfigProvider = (key: string, app: ApplicationService) => Promise - /** * The authenticator guard factory method is called by the * Authenticator class to create an instance of a specific * guard during an HTTP request */ -export type AuthenticatorGuardFactory = (ctx: HttpContext) => GuardContract +export type GuardFactory = (ctx: HttpContext) => GuardContract /** * Authenticators are inferred inside the user application * from the config file */ export interface Authenticators {} + +/** + * Infer authenticators from the auth config + */ +export type InferAuthenticators> = Awaited< + ReturnType +> + +/** + * Auth service is a singleton instance of the AuthManager + * configured using the config stored within the user + * app. + */ +export interface AuthService + extends AuthManager {} + +/** + * Config provider for exporting guard + */ +export type GuardConfigProvider = { + resolver: (name: string, app: ApplicationService) => Promise +} diff --git a/src/auth/user_providers/main.ts b/src/auth/user_providers/main.ts new file mode 100644 index 0000000..0653ff3 --- /dev/null +++ b/src/auth/user_providers/main.ts @@ -0,0 +1,28 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseLucidUserProvider } from '../../core/user_providers/lucid.js' +import { BaseDatabaseUserProvider } from '../../core/user_providers/database.js' +import type { LucidAuthenticatable, UserProviderContract } from '../../core/types.js' + +/** + * Using lucid models to find users for session + * auth + */ +export class LucidUserProvider + extends BaseLucidUserProvider + implements UserProviderContract> {} + +/** + * Using database query builder to find users for + * session auth + */ +export class DatabaseUserProvider> + extends BaseDatabaseUserProvider + implements UserProviderContract {} diff --git a/src/core/README.md b/src/core/README.md index caebede..e830ad4 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -3,4 +3,4 @@ The core part of the codebase provides base implementations that can be used by These base implementations must not be used inside the user-land code and the main purpose is to provide ready to use abstractions for guards and providers. -If you decide to contribut additional implementations, make sure to mark them as `abstract` to avoid direct usage. +If you decide to contribute additional implementations, make sure to mark them as `abstract` to avoid direct usage. diff --git a/src/core/token.ts b/src/core/token.ts index 69acae7..4b03f02 100644 --- a/src/core/token.ts +++ b/src/core/token.ts @@ -11,7 +11,7 @@ import { createHash } from 'node:crypto' import string from '@adonisjs/core/helpers/string' import { base64, safeEqual } from '@adonisjs/core/helpers' -import * as errors from '../errors.js' +import * as errors from '../auth/errors.js' import type { TokenContract } from './types.js' /** diff --git a/src/core/token_providers/database.ts b/src/core/token_providers/database.ts index 1311457..51fbbaa 100644 --- a/src/core/token_providers/database.ts +++ b/src/core/token_providers/database.ts @@ -9,7 +9,7 @@ import type { Database } from '@adonisjs/lucid/database' -import debug from '../../debug.js' +import debug from '../../auth/debug.js' import type { DatabaseTokenProviderOptions, TokenProviderContract } from '../types.js' /** diff --git a/src/core/types.ts b/src/core/types.ts index 8bce70d..8982bf2 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -10,7 +10,7 @@ import type { QueryClientContract } from '@adonisjs/lucid/types/database' import type { GuardUser } from './guard_user.js' -import type { PROVIDER_REAL_USER } from '../symbols.js' +import type { PROVIDER_REAL_USER } from '../auth/symbols.js' import type { LucidModel, LucidRow } from '@adonisjs/lucid/types/model' /** @@ -154,6 +154,11 @@ export type LucidUserProviderOptions = { */ client?: QueryClientContract + /** + * Model to use for authentication + */ + model: () => Promise<{ default: Model }> + /** * An array of uids to use when finding a user for login. Make * sure all fields can be used to uniquely lookup a user. diff --git a/src/core/user_providers/database.ts b/src/core/user_providers/database.ts index 02013ff..ea99031 100644 --- a/src/core/user_providers/database.ts +++ b/src/core/user_providers/database.ts @@ -11,9 +11,9 @@ import type { Hash } from '@adonisjs/core/hash' import { RuntimeException } from '@poppinss/utils' import type { Database } from '@adonisjs/lucid/database' -import debug from '../../debug.js' +import debug from '../../auth/debug.js' import { GuardUser } from '../guard_user.js' -import { PROVIDER_REAL_USER } from '../../symbols.js' +import { PROVIDER_REAL_USER } from '../../auth/symbols.js' import type { DatabaseUserProviderOptions, UserProviderContract } from '../types.js' /** @@ -71,7 +71,7 @@ class DatabaseUser> extends GuardUser> +export abstract class BaseDatabaseUserProvider> implements UserProviderContract { declare [PROVIDER_REAL_USER]: RealUser diff --git a/src/core/user_providers/lucid.ts b/src/core/user_providers/lucid.ts index 0c1a0dc..6afc40f 100644 --- a/src/core/user_providers/lucid.ts +++ b/src/core/user_providers/lucid.ts @@ -9,9 +9,9 @@ import { RuntimeException } from '@poppinss/utils' -import debug from '../../debug.js' +import debug from '../../auth/debug.js' import { GuardUser } from '../guard_user.js' -import { PROVIDER_REAL_USER } from '../../symbols.js' +import { PROVIDER_REAL_USER } from '../../auth/symbols.js' import type { UserProviderContract, LucidAuthenticatable, @@ -56,7 +56,7 @@ class LucidUser> extends Gua * Lucid user provider is used to lookup user for authentication * using a Lucid model. */ -export abstract class LucidUserProvider +export abstract class BaseLucidUserProvider implements UserProviderContract> { declare [PROVIDER_REAL_USER]: InstanceType @@ -67,11 +67,6 @@ export abstract class LucidUserProvider protected model?: UserModel constructor( - /** - * Model provider is used to lazily import the model - */ - protected modelProvider: () => Promise<{ default: UserModel }>, - /** * Lucid provider options */ @@ -89,7 +84,7 @@ export abstract class LucidUserProvider return this.model } - const importedModel = await this.modelProvider() + const importedModel = await this.options.model() this.model = importedModel.default debug('lucid_user_provider: using model %O', this.model) return this.model diff --git a/src/define_config.ts b/src/define_config.ts deleted file mode 100644 index ff94705..0000000 --- a/src/define_config.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * @adonisjs/auth - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { AuthenticatorGuardFactory, ConfigProvider } from './types/main.js' - -/** - * Define configuration for the auth package. The function returns - * a config provider that is invoked inside the auth service - * provider - */ -export function defineConfig< - KnownGuards extends Record, ->(config: { - default: keyof KnownGuards - guards: { [K in keyof KnownGuards]: ConfigProvider } -}): ConfigProvider<{ - default: keyof KnownGuards - guards: { [K in keyof KnownGuards]: KnownGuards[K] } -}> { - return async function (_, app) { - const guardsList = Object.keys(config.guards) as (keyof KnownGuards)[] - const guards = {} as { [K in keyof KnownGuards]: KnownGuards[K] } - - for (let guard of guardsList) { - guards[guard] = await config.guards[guard](guard as string, app) - } - - return { - default: config.default, - guards, - } - } -} diff --git a/src/guards/session/define_config.ts b/src/guards/session/define_config.ts new file mode 100644 index 0000000..9de5f32 --- /dev/null +++ b/src/guards/session/define_config.ts @@ -0,0 +1,79 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { configProvider } from '@adonisjs/core' +import { RuntimeException } from '@poppinss/utils' +import type { HttpContext } from '@adonisjs/core/http' +import type { ConfigProvider } from '@adonisjs/core/types' + +import { SessionGuard } from './guard.js' +import type { GuardConfigProvider } from '../../auth/types.js' +import type { + SessionGuardConfig, + RememberMeProviderContract, + SessionUserProviderContract, + DatabaseRememberMeProviderOptions, +} from './types.js' + +/** + * Helper function to configure the session guard for + * authentication. + * + * This method returns a config builder, which internally + * returns a factory function to construct a guard + * during HTTP requests. + */ +export function sessionGuard>( + config: SessionGuardConfig & { + provider: ConfigProvider + tokens?: ConfigProvider + } +): GuardConfigProvider<(ctx: HttpContext) => SessionGuard> { + return { + async resolver(guardName, app) { + const provider = await configProvider.resolve(app, config.provider) + if (!provider) { + throw new RuntimeException(`Invalid user provider defined on "${guardName}" guard`) + } + + const emitter = await app.container.make('emitter') + const tokensProvider = config.tokens + ? await configProvider.resolve(app, config.tokens) + : undefined + + /** + * Factory function needed by Authenticator to switch + * between guards and perform authentication + */ + return (ctx) => { + const guard = new SessionGuard(guardName, config, ctx, provider) + if (tokensProvider) { + guard.withRememberMeTokens(tokensProvider) + } + + return guard.withEmitter(emitter) + } + }, + } +} + +/** + * Tokens provider helper to store remember me tokens + */ +export const tokensProvider: { + db: (config: DatabaseRememberMeProviderOptions) => ConfigProvider +} = { + db(config) { + return configProvider.create(async (app) => { + const db = await app.container.make('lucid.db') + const { DatabaseRememberTokenProvider } = await import('./token_providers/main.js') + return new DatabaseRememberTokenProvider(db, config) + }) + }, +} diff --git a/src/session/guard.ts b/src/guards/session/guard.ts similarity index 95% rename from src/session/guard.ts rename to src/guards/session/guard.ts index 0706585..c8f0488 100644 --- a/src/session/guard.ts +++ b/src/guards/session/guard.ts @@ -13,15 +13,14 @@ import { Emitter } from '@adonisjs/core/events' import type { HttpContext } from '@adonisjs/core/http' import { Exception, RuntimeException } from '@poppinss/utils' -import debug from '../debug.js' -import * as errors from '../errors.js' +import debug from '../../auth/debug.js' import { RememberMeToken } from './token.js' -import type { GuardContract } from '../types/main.js' -import { GUARD_KNOWN_EVENTS, PROVIDER_REAL_USER } from '../symbols.js' +import * as errors from '../../auth/errors.js' +import type { GuardContract } from '../../auth/types.js' +import { GUARD_KNOWN_EVENTS, PROVIDER_REAL_USER } from '../../auth/symbols.js' import type { SessionGuardEvents, SessionGuardConfig, - RememberMeTokenContract, RememberMeProviderContract, SessionUserProviderContract, } from './types.js' @@ -235,14 +234,17 @@ export class SessionGuard extends UserProviderContr * The RememberMeProviderContract is used to persist and lookup tokens for * session based authentication with remember me option. */ -export interface RememberMeProviderContract - extends TokenProviderContract {} +export interface RememberMeProviderContract extends TokenProviderContract {} /** * Config accepted by the session guard @@ -56,7 +36,7 @@ export type SessionGuardConfig = { * * Defaults to "5 years" */ - rememberMeTokenAge: string | number + rememberMeTokenAge?: string | number } /** @@ -97,7 +77,7 @@ export type SessionGuardEvents = { 'session_auth:login_succeeded': { user: User sessionId: string - rememberMeToken?: RememberMeTokenContract + rememberMeToken?: RememberMeToken } /** @@ -113,7 +93,7 @@ export type SessionGuardEvents = { 'session_auth:authentication_succeeded': { user: User sessionId: string - rememberMeToken?: RememberMeTokenContract + rememberMeToken?: RememberMeToken } /** diff --git a/src/session/main.ts b/src/session/main.ts deleted file mode 100644 index cdaed59..0000000 --- a/src/session/main.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * @adonisjs/auth - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import type { HttpContext } from '@adonisjs/core/http' - -import type { - LucidAuthenticatable, - LucidUserProviderOptions, - DatabaseUserProviderOptions, -} from '../core/types.js' - -import type { - SessionGuardConfig, - RememberMeProviderContract, - SessionUserProviderContract, - DatabaseRememberMeProviderOptions, -} from './types.js' - -import { SessionGuard } from './guard.js' -import { ConfigProvider } from '../types/main.js' -import type { DatabaseRememberTokenProvider } from './token_providers/main.js' - -export { RememberMeToken } from './token.js' -export { SessionGuard } - -/** - * Helper function to configure the session guard for - * authentication. - * - * This method returns a config builder, which internally - * returns a factory function to construct a guard - * during HTTP requests. - */ -export function sessionGuard>( - config: SessionGuardConfig & { - provider: ConfigProvider - tokens?: ConfigProvider - } -): ConfigProvider<(ctx: HttpContext) => SessionGuard> { - return async (key, app) => { - const emitter = await app.container.make('emitter') - const provider = await config.provider('provider', app) - const tokensProvider = config.tokens ? await config.tokens('tokens', app) : undefined - - /** - * Factory function needed by Authenticator to switch - * between guards and perform authentication - */ - return (ctx) => { - const guard = new SessionGuard(key, config, ctx, provider) - if (tokensProvider) { - guard.withRememberMeTokens(tokensProvider) - } - - return guard.withEmitter(emitter) - } - } -} - -/** - * Helpers to configure user and tokens provider - * for the session guard - */ -export const sessionProviders: { - users: { - lucid: ( - config: LucidUserProviderOptions & { - model: () => Promise<{ default: UserModel }> - } - ) => ConfigProvider>> - db: >( - config: DatabaseUserProviderOptions - ) => ConfigProvider> - } - tokens: { - db: (config: DatabaseRememberMeProviderOptions) => ConfigProvider - } -} = { - users: { - lucid: (config) => { - return async () => { - const { LucidSessionUserProvider } = await import('./user_providers/main.js') - return new LucidSessionUserProvider(config.model, config) - } - }, - db: (config) => { - return async (_, app) => { - const db = await app.container.make('lucid.db') - const hash = await app.container.make('hash') - const { DatabaseSessionUserProvider } = await import('./user_providers/main.js') - return new DatabaseSessionUserProvider(db, hash.use(), config) - } - }, - }, - tokens: { - db: (config) => { - return async (_, app) => { - const db = await app.container.make('lucid.db') - const { DatabaseRememberTokenProvider } = await import('./token_providers/main.js') - return new DatabaseRememberTokenProvider(db, config) - } - }, - }, -} diff --git a/src/session/user_providers/main.ts b/src/session/user_providers/main.ts deleted file mode 100644 index be10720..0000000 --- a/src/session/user_providers/main.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * @adonisjs/auth - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import type { SessionUserProviderContract } from '../types.js' -import type { LucidAuthenticatable } from '../../core/types.js' -import { LucidUserProvider } from '../../core/user_providers/lucid.js' -import { DatabaseUserProvider } from '../../core/user_providers/database.js' - -/** - * Using lucid models to find users for session - * auth - */ -export class LucidSessionUserProvider - extends LucidUserProvider - implements SessionUserProviderContract> {} - -/** - * Using database query builder to find users for - * session auth - */ -export class DatabaseSessionUserProvider> - extends DatabaseUserProvider - implements SessionUserProviderContract {} diff --git a/tests/auth/authenticator.spec.ts b/tests/auth/authenticator.spec.ts new file mode 100644 index 0000000..eb8e080 --- /dev/null +++ b/tests/auth/authenticator.spec.ts @@ -0,0 +1,52 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { HttpContextFactory } from '@adonisjs/core/factories/http' + +import { createEmitter } from '../helpers.js' +import { Authenticator } from '../../src/auth/authenticator.js' +import { FactoryUser } from '../../factories/lucid_user_provider.js' +import { SessionGuardFactory } from '../../factories/session_guard_factory.js' + +test.group('Authenticator', () => { + test('create authenticator with guards', async ({ assert, expectTypeOf }) => { + const emitter = createEmitter() + const ctx = new HttpContextFactory().create() + const sessionGuard = new SessionGuardFactory().create(ctx).withEmitter(emitter) + + const authenticator = new Authenticator(ctx, { + default: 'web', + guards: { + web: () => sessionGuard, + }, + }) + + assert.instanceOf(authenticator, Authenticator) + expectTypeOf(authenticator.use).parameters.toMatchTypeOf<['web'?]>() + }) + + test('access guard using its name', async ({ assert, expectTypeOf }) => { + const emitter = createEmitter() + const ctx = new HttpContextFactory().create() + const sessionGuard = new SessionGuardFactory().create(ctx).withEmitter(emitter) + + const authenticator = new Authenticator(ctx, { + default: 'web', + guards: { + web: () => sessionGuard, + }, + }) + + const webGuard = authenticator.use('web') + assert.strictEqual(webGuard, sessionGuard) + assert.strictEqual(authenticator.use('web'), authenticator.use('web')) + expectTypeOf(webGuard.user).toMatchTypeOf() + }) +}) diff --git a/tests/auth/define_config.spec.ts b/tests/auth/define_config.spec.ts new file mode 100644 index 0000000..b94402f --- /dev/null +++ b/tests/auth/define_config.spec.ts @@ -0,0 +1,115 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { ApplicationService } from '@adonisjs/core/types' +import { AppFactory } from '@adonisjs/core/factories/app' +import { HttpContextFactory } from '@adonisjs/core/factories/http' +import { HashManagerFactory } from '@adonisjs/core/factories/hash' + +import { createDatabase, createEmitter } from '../helpers.js' +import { AuthManager } from '../../src/auth/auth_manager.js' +import { Authenticator } from '../../src/auth/authenticator.js' +import { FactoryUser } from '../../factories/lucid_user_provider.js' +import { sessionGuard } from '../../src/guards/session/define_config.js' +import { defineConfig, providers } from '../../src/auth/define_config.js' +import { DatabaseUserProvider, LucidUserProvider } from '../../src/auth/user_providers/main.js' + +const BASE_URL = new URL('./', import.meta.url) +const app = new AppFactory().create(BASE_URL, () => {}) as ApplicationService +await app.init() + +test.group('Define config | providers', () => { + test('configure lucid provider', async ({ assert }) => { + const lucidConfigProvider = providers.lucid({ + model: async () => { + return { + default: FactoryUser, + } + }, + uids: ['email'], + }) + + const lucidProvider = await lucidConfigProvider.resolver(app) + assert.instanceOf(lucidProvider, LucidUserProvider) + }) + + test('configure db provider', async ({ assert }) => { + const dbConfigProvider = providers.db({ + table: 'users', + id: 'id', + passwordColumnName: 'password', + uids: ['email'], + }) + + app.container.bind('lucid.db', () => createDatabase()) + app.container.bind('hash', () => new HashManagerFactory().create()) + + const dbProvider = await dbConfigProvider.resolver(app) + assert.instanceOf(dbProvider, DatabaseUserProvider) + }) +}) + +test.group('Define config', () => { + test('define config for auth manager', async ({ assert }) => { + const lucidConfigProvider = providers.lucid({ + model: async () => { + return { + default: FactoryUser, + } + }, + uids: ['email'], + }) + + const authConfigProvider = defineConfig({ + default: 'web', + guards: { + web: sessionGuard({ + provider: lucidConfigProvider, + }), + }, + }) + + app.container.bind('emitter', () => createEmitter() as any) + + const authConfig = await authConfigProvider.resolver(app) + const authManager = new AuthManager(authConfig) + assert.instanceOf(authManager, AuthManager) + }) + + test('create auth object from auth manager', async ({ assert, expectTypeOf }) => { + const lucidConfigProvider = providers.lucid({ + model: async () => { + return { + default: FactoryUser, + } + }, + uids: ['email'], + }) + + const authConfigProvider = defineConfig({ + default: 'web', + guards: { + web: sessionGuard({ + provider: lucidConfigProvider, + }), + }, + }) + + app.container.bind('emitter', () => createEmitter() as any) + + const ctx = new HttpContextFactory().create() + const authConfig = await authConfigProvider.resolver(app) + const authManager = new AuthManager(authConfig) + const auth = authManager.createAuthenticator(ctx) + + assert.instanceOf(auth, Authenticator) + expectTypeOf(auth.use).parameters.toMatchTypeOf<['web'?]>() + }) +}) diff --git a/tests/guards/session/authenticate.spec.ts b/tests/guards/session/authenticate.spec.ts index 94b1f82..c561c41 100644 --- a/tests/guards/session/authenticate.spec.ts +++ b/tests/guards/session/authenticate.spec.ts @@ -11,17 +11,17 @@ import { test } from '@japa/runner' import { HttpContextFactory } from '@adonisjs/core/factories/http' import { SessionMiddlewareFactory } from '@adonisjs/session/factories' -import { RememberMeToken } from '../../../src/session/token.js' +import { RememberMeToken } from '../../../src/guards/session/token.js' import { FactoryUser } from '../../../factories/lucid_user_provider.js' import { SessionGuardFactory } from '../../../factories/session_guard_factory.js' -import { DatabaseRememberTokenProvider } from '../../../src/session/token_providers/main.js' +import { DatabaseRememberTokenProvider } from '../../../src/guards/session/token_providers/main.js' import { + pEvent, timeTravel, parseCookies, createTables, defineCookies, createDatabase, - pEvent, createEmitter, } from '../../helpers.js' @@ -301,4 +301,25 @@ test.group('Session guard | authenticate', () => { assert.equal(authUser.id, user.id) }) }) + + test('silently authenticate using the check method', async ({ assert }) => { + const db = await createDatabase() + await createTables(db) + + const emitter = createEmitter() + const ctx = new HttpContextFactory().create() + await FactoryUser.createWithDefaults() + const sessionGuard = new SessionGuardFactory().create(ctx).withEmitter(emitter) + const sessionMiddleware = await new SessionMiddlewareFactory().create() + + const [authFailed, authenticateCall] = await Promise.allSettled([ + pEvent(emitter, 'session_auth:authentication_failed'), + sessionMiddleware.handle(ctx, async () => { + await sessionGuard.check() + }), + ]) + + assert.equal(authFailed.status, 'fulfilled') + assert.equal(authenticateCall.status, 'fulfilled') + }) }) diff --git a/tests/guards/session/define_config.spec.ts b/tests/guards/session/define_config.spec.ts new file mode 100644 index 0000000..7aa04ec --- /dev/null +++ b/tests/guards/session/define_config.spec.ts @@ -0,0 +1,84 @@ +/* + * @adonisjs/auth + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AppFactory } from '@adonisjs/core/factories/app' +import { ApplicationService } from '@adonisjs/core/types' +import { HttpContextFactory } from '@adonisjs/core/factories/http' +import { HashManagerFactory } from '@adonisjs/core/factories/hash' + +import { providers } from '../../../index.js' +import { FactoryUser } from '../../../factories/main.js' +import { createDatabase, createEmitter } from '../../helpers.js' +import { SessionGuard } from '../../../src/guards/session/guard.js' +import { LucidUserProvider } from '../../../src/auth/user_providers/main.js' +import { sessionGuard, tokensProvider } from '../../../src/guards/session/define_config.js' + +const BASE_URL = new URL('./', import.meta.url) +const app = new AppFactory().create(BASE_URL, () => {}) as ApplicationService +await app.init() + +test.group('sessionGuard', () => { + test('configure session guard', async ({ assert, expectTypeOf }) => { + const sessionGuardProvider = sessionGuard({ + provider: providers.lucid({ + model: async () => { + return { + default: FactoryUser, + } + }, + uids: ['email'], + }), + }) + + app.container.bind('emitter', () => createEmitter() as any) + + const sessionFactory = await sessionGuardProvider.resolver('web', app) + assert.isFunction(sessionFactory) + expectTypeOf(sessionFactory).returns.toMatchTypeOf< + SessionGuard> + >() + + const ctx = new HttpContextFactory().create() + assert.instanceOf(sessionFactory(ctx), SessionGuard) + }) + + test('throw error when no provider is provided', async () => { + await sessionGuard({} as any).resolver('web', app) + }).throws('Invalid user provider defined on "web" guard') + + test('configure session guard with tokens provider', async ({ assert, expectTypeOf }) => { + const sessionGuardProvider = sessionGuard({ + provider: providers.lucid({ + model: async () => { + return { + default: FactoryUser, + } + }, + uids: ['email'], + }), + tokens: tokensProvider.db({ + table: 'remember_me_tokens', + }), + }) + + app.container.bind('emitter', () => createEmitter() as any) + app.container.bind('lucid.db', () => createDatabase()) + app.container.bind('hash', () => new HashManagerFactory().create()) + + const sessionFactory = await sessionGuardProvider.resolver('web', app) + assert.isFunction(sessionFactory) + expectTypeOf(sessionFactory).returns.toMatchTypeOf< + SessionGuard> + >() + + const ctx = new HttpContextFactory().create() + assert.instanceOf(sessionFactory(ctx), SessionGuard) + }) +}) diff --git a/tests/guards/session/login.spec.ts b/tests/guards/session/login.spec.ts index 3b40d8d..95f1b0a 100644 --- a/tests/guards/session/login.spec.ts +++ b/tests/guards/session/login.spec.ts @@ -11,10 +11,10 @@ import { test } from '@japa/runner' import { HttpContextFactory } from '@adonisjs/core/factories/http' import { SessionMiddlewareFactory } from '@adonisjs/session/factories' -import { RememberMeToken } from '../../../src/session/token.js' +import { RememberMeToken } from '../../../src/guards/session/token.js' import { FactoryUser } from '../../../factories/lucid_user_provider.js' import { SessionGuardFactory } from '../../../factories/session_guard_factory.js' -import { DatabaseRememberTokenProvider } from '../../../src/session/token_providers/main.js' +import { DatabaseRememberTokenProvider } from '../../../src/guards/session/token_providers/main.js' import { createDatabase, createEmitter, createTables, pEvent, parseCookies } from '../../helpers.js' test.group('Session guard | login', () => { diff --git a/tests/helpers.ts b/tests/helpers.ts index 9cf36be..98a42ea 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -22,7 +22,7 @@ import setCookieParser, { CookieMap } from 'set-cookie-parser' import { LoggerFactory } from '@adonisjs/core/factories/logger' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' -import { SessionGuardEvents } from '../src/session/types.js' +import { SessionGuardEvents } from '../src/guards/session/types.js' import { FactoryUser } from '../factories/lucid_user_provider.js' /**