├── .editorconfig ├── .env ├── .github └── workflows │ ├── checks.yml │ ├── labels.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── bin └── test.ts ├── configure.ts ├── docker-compose.yml ├── eslint.config.js ├── factories ├── access_tokens │ └── main.ts ├── auth │ └── main.ts ├── basic_auth │ └── main.ts └── session │ └── main.ts ├── index.ts ├── modules ├── access_tokens_guard │ ├── access_token.ts │ ├── crc32.ts │ ├── define_config.ts │ ├── guard.ts │ ├── main.ts │ ├── token_providers │ │ └── db.ts │ ├── types.ts │ └── user_providers │ │ └── lucid.ts ├── basic_auth_guard │ ├── define_config.ts │ ├── guard.ts │ ├── main.ts │ ├── types.ts │ └── user_providers │ │ └── lucid.ts └── session_guard │ ├── define_config.ts │ ├── guard.ts │ ├── main.ts │ ├── remember_me_token.ts │ ├── token_providers │ └── db.ts │ ├── types.ts │ └── user_providers │ └── lucid.ts ├── package.json ├── providers └── auth_provider.ts ├── services └── auth.ts ├── src ├── auth_manager.ts ├── authenticator.ts ├── authenticator_client.ts ├── debug.ts ├── define_config.ts ├── errors.ts ├── middleware │ └── initialize_auth_middleware.ts ├── mixins │ └── lucid.ts ├── plugins │ └── japa │ │ ├── api_client.ts │ │ └── browser_client.ts ├── symbols.ts └── types.ts ├── tests ├── access_tokens │ ├── access_token.spec.ts │ ├── define_config.spec.ts │ ├── guard │ │ └── authenticate.spec.ts │ ├── token_providers │ │ └── db.spec.ts │ └── user_providers │ │ └── lucid.spec.ts ├── auth │ ├── auth_manager.spec.ts │ ├── authenticator.spec.ts │ ├── authenticator_client.spec.ts │ ├── configure.ts │ ├── define_config.spec.ts │ ├── e_invalid_credentials.spec.ts │ ├── e_unauthorized_access.spec.ts │ ├── mixins │ │ └── with_auth_finder.spec.ts │ └── plugins │ │ ├── api_client.spec.ts │ │ ├── browser_client.spec.ts │ │ └── global_types.ts ├── basic_auth │ ├── define_config.spec.ts │ ├── guard │ │ └── authenticate.spec.ts │ └── user_providers │ │ └── lucid.spec.ts ├── helpers.ts └── session │ ├── define_config.spec.ts │ ├── guard │ ├── authenticate.spec.ts │ ├── login.spec.ts │ └── logout.spec.ts │ ├── remember_me_token.spec.ts │ ├── tokens_providers │ └── db.spec.ts │ └── user_providers │ └── lucid.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.txt] 25 | indent_style = space 26 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MYSQL_HOST=localhost 2 | MYSQL_PASSWORD=secret 3 | MYSQL_DATABASE=auth 4 | MYSQL_PORT=3306 5 | MYSQL_USER=virk 6 | 7 | PG_HOST=localhost 8 | PG_PASSWORD=secret 9 | PG_DATABASE=auth 10 | PG_PORT=5432 11 | PG_USER=virk 12 | 13 | MSSQL_HOST=localhost 14 | MSSQL_PASSWORD=secrearandom&233passwordt 15 | MSSQL_PORT=1433 16 | MSSQL_USER=sa 17 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - push 4 | - pull_request 5 | - workflow_call 6 | jobs: 7 | lint: 8 | uses: adonisjs/.github/.github/workflows/lint.yml@main 9 | 10 | typecheck: 11 | uses: adonisjs/.github/.github/workflows/typecheck.yml@main 12 | 13 | test-postgres: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | node-version: [21] 19 | postgres-version: [11] 20 | services: 21 | postgres: 22 | image: postgres:${{ matrix.postgres-version }} 23 | env: 24 | POSTGRES_DB: auth 25 | POSTGRES_USER: virk 26 | POSTGRES_PASSWORD: secret 27 | ports: 28 | - 5432:5432 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - name: Install Playwright Browsers 35 | run: npx playwright install --with-deps 36 | - name: Install 37 | run: npm install 38 | - name: Run Postgres Tests 39 | run: npm run test:pg 40 | 41 | test-mysql: 42 | runs-on: ubuntu-latest 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | mysql: [{ version: '8.0', command: 'mysql' }] 47 | node-version: [21] 48 | services: 49 | mysql: 50 | image: mysql:${{ matrix.mysql.version }} 51 | env: 52 | MYSQL_DATABASE: auth 53 | MYSQL_USER: virk 54 | MYSQL_PASSWORD: secret 55 | MYSQL_ROOT_PASSWORD: secret 56 | MYSQL_PORT: 3306 57 | ports: 58 | - '3306:3306' 59 | steps: 60 | - uses: actions/checkout@v3 61 | - uses: actions/setup-node@v3 62 | with: 63 | node-version: ${{ matrix.node-version }} 64 | - name: Install 65 | run: npm install 66 | - name: Install Playwright Browsers 67 | run: npx playwright install --with-deps 68 | - name: Run Mysql Tests 69 | run: npm run test:${{ matrix.mysql.command }} 70 | 71 | test-mssql: 72 | runs-on: ubuntu-latest 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | node-version: [21] 77 | services: 78 | mssql: 79 | image: mcr.microsoft.com/mssql/server:2019-latest 80 | env: 81 | SA_PASSWORD: 'secrearandom&233passwordt' 82 | ACCEPT_EULA: 'Y' 83 | ports: 84 | - '1433:1433' 85 | steps: 86 | - uses: actions/checkout@v3 87 | - uses: actions/setup-node@v3 88 | with: 89 | node-version: ${{ matrix.node-version }} 90 | - name: Install 91 | run: npm install 92 | - name: Install Playwright Browsers 93 | run: npx playwright install --with-deps 94 | - name: Run Mssql Tests 95 | run: npm run test:mssql 96 | 97 | test-sqlite: 98 | runs-on: ubuntu-latest 99 | strategy: 100 | fail-fast: false 101 | matrix: 102 | lib: ['sqlite'] 103 | node-version: [20, 21] 104 | steps: 105 | - uses: actions/checkout@v3 106 | - uses: actions/setup-node@v3 107 | with: 108 | node-version: ${{ matrix.node-version }} 109 | - name: Install 110 | run: npm install 111 | - name: Install Playwright Browsers 112 | run: npx playwright install --with-deps 113 | - name: Run Sqlite Tests 114 | run: npm run test:${{ matrix.lib }} 115 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | on: 3 | workflow_dispatch: 4 | permissions: 5 | issues: write 6 | jobs: 7 | labels: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: EndBug/label-sync@v2 12 | with: 13 | config-file: 'https://raw.githubusercontent.com/thetutlage/static/main/labels.yml' 14 | delete-other-labels: true 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: workflow_dispatch 3 | permissions: 4 | contents: write 5 | id-token: write 6 | jobs: 7 | checks: 8 | uses: ./.github/workflows/checks.yml 9 | release: 10 | needs: checks 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - name: git config 20 | run: | 21 | git config user.name "${GITHUB_ACTOR}" 22 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 23 | - name: Init npm config 24 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 25 | env: 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | - run: npm install 28 | - run: npm run release -- --ci 29 | env: 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | stale-issue-message: 'This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue' 13 | stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request' 14 | close-issue-message: 'This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue' 15 | close-pr-message: 'This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request' 16 | days-before-stale: 21 17 | days-before-close: 5 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | yarn.lock 13 | shrinkwrap.yaml 14 | package-lock.json 15 | test/__app 16 | backup 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2023 Harminder Virk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @adonisjs/auth 2 | 3 |
4 | 5 | [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] 6 | 7 | ## Introduction 8 | 9 | ## Official Documentation 10 | The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/authentication/introduction) 11 | 12 | ## Contributing 13 | One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. 14 | 15 | We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. 16 | 17 | ## Code of Conduct 18 | In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). 19 | 20 | ## License 21 | AdonisJS auth is open-sourced software licensed under the [MIT license](LICENSE.md). 22 | 23 | [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/auth/checks.yml?style=for-the-badge 24 | [gh-workflow-url]: https://github.com/adonisjs/auth/actions/workflows/checks.yml "Github action" 25 | 26 | [npm-image]: https://img.shields.io/npm/v/@adonisjs/auth/latest.svg?style=for-the-badge&logo=npm 27 | [npm-url]: https://www.npmjs.com/package/@adonisjs/auth/v/latest "npm" 28 | 29 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 30 | 31 | [license-url]: LICENSE.md 32 | [license-image]: https://img.shields.io/github/license/adonisjs/auth?style=for-the-badge 33 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from '@japa/assert' 2 | import { snapshot } from '@japa/snapshot' 3 | import { fileSystem } from '@japa/file-system' 4 | import { expectTypeOf } from '@japa/expect-type' 5 | import { configure, processCLIArgs, run } from '@japa/runner' 6 | 7 | processCLIArgs(process.argv.splice(2)) 8 | configure({ 9 | suites: [ 10 | { 11 | name: 'session', 12 | files: ['tests/session/**/*.spec.ts'], 13 | }, 14 | { 15 | name: 'access_tokens', 16 | files: ['tests/access_tokens/**/*.spec.ts'], 17 | }, 18 | { 19 | name: 'basic_auth', 20 | files: ['tests/basic_auth/**/*.spec.ts'], 21 | }, 22 | { 23 | name: 'auth', 24 | files: ['tests/auth/**/*.spec.ts'], 25 | }, 26 | ], 27 | plugins: [assert(), fileSystem(), expectTypeOf(), snapshot()], 28 | }) 29 | 30 | run() 31 | -------------------------------------------------------------------------------- /configure.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { presetAuth } from '@adonisjs/presets/auth' 11 | import type Configure from '@adonisjs/core/commands/configure' 12 | 13 | /** 14 | * Configures the auth package 15 | */ 16 | export async function configure(command: Configure) { 17 | const codemods = await command.createCodemods() 18 | let guard: string | undefined = command.parsedFlags.guard 19 | 20 | /** 21 | * Prompts user to select a guard when not mentioned via 22 | * the CLI 23 | */ 24 | if (guard === undefined) { 25 | guard = await command.prompt.choice( 26 | 'Select the auth guard you want to use', 27 | [ 28 | { 29 | name: 'session', 30 | message: 'Session', 31 | }, 32 | { 33 | name: 'access_tokens', 34 | message: 'Opaque access tokens', 35 | }, 36 | { 37 | name: 'basic_auth', 38 | message: 'Basic Auth', 39 | }, 40 | ], 41 | { 42 | validate(value) { 43 | return !!value 44 | }, 45 | } 46 | ) 47 | } 48 | 49 | /** 50 | * Ensure selected or guard defined via the CLI flag is 51 | * valid 52 | */ 53 | if (!['session', 'access_tokens', 'basic_auth'].includes(guard!)) { 54 | command.logger.error( 55 | `The selected guard "${guard}" is invalid. Select one from: session, access_tokens, basic_auth` 56 | ) 57 | command.exitCode = 1 58 | return 59 | } 60 | 61 | await presetAuth(codemods, command.app, { 62 | guard: guard as 'session' | 'access_tokens' | 'basic_auth', 63 | userProvider: 'lucid', 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | mysql: 5 | platform: linux/x86_64 6 | image: mysql:8.0 7 | command: --default-authentication-plugin=mysql_native_password --sync_binlog=0 --innodb_doublewrite=OFF --innodb-flush-log-at-trx-commit=0 --innodb-flush-method=nosync 8 | container_name: mysql 9 | env_file: ./.env 10 | environment: 11 | - MYSQL_USER=$MYSQL_USER 12 | - MYSQL_PASSWORD=$MYSQL_PASSWORD 13 | - MYSQL_DATABASE=$MYSQL_DATABASE 14 | - MYSQL_ROOT_PASSWORD=$MYSQL_PASSWORD 15 | ports: 16 | - $MYSQL_PORT:3306 17 | expose: 18 | - $MYSQL_PORT 19 | 20 | pg: 21 | image: postgres:11 22 | container_name: pg 23 | env_file: ./.env 24 | environment: 25 | - POSTGRES_DB=$PG_DATABASE 26 | - POSTGRES_USER=$PG_USER 27 | - POSTGRES_PASSWORD=$PG_PASSWORD 28 | ports: 29 | - $PG_PORT:5432 30 | expose: 31 | - $PG_PORT 32 | 33 | mssql: 34 | platform: linux/x86_64 35 | image: mcr.microsoft.com/mssql/server:2019-latest 36 | container_name: mssql 37 | env_file: ./.env 38 | environment: 39 | - SA_PASSWORD=$MSSQL_PASSWORD 40 | - ACCEPT_EULA='Y' 41 | ports: 42 | - $MSSQL_PORT:1433 43 | expose: 44 | - $MSSQL_PORT 45 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { configPkg } from '@adonisjs/eslint-config' 2 | export default configPkg() 3 | -------------------------------------------------------------------------------- /factories/access_tokens/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Secret } from '@adonisjs/core/helpers' 11 | import stringHelpers from '@adonisjs/core/helpers/string' 12 | import { PROVIDER_REAL_USER } from '../../src/symbols.js' 13 | import { AccessToken } from '../../modules/access_tokens_guard/access_token.js' 14 | import { AccessTokensUserProviderContract } from '../../modules/access_tokens_guard/types.js' 15 | 16 | /** 17 | * Representation of a fake user used to test 18 | * the access token guard. 19 | * 20 | * @note 21 | * Should not be exported to the outside world 22 | */ 23 | export type AccessTokensFakeUser = { 24 | id: number 25 | email: string 26 | password: string 27 | } 28 | 29 | /** 30 | * Collection of dummy users 31 | */ 32 | const users: AccessTokensFakeUser[] = [ 33 | { 34 | id: 1, 35 | email: 'virk@adonisjs.com', 36 | password: 'secret', 37 | }, 38 | { 39 | id: 2, 40 | email: 'romain@adonisjs.com', 41 | password: 'secret', 42 | }, 43 | ] 44 | 45 | /** 46 | * Implementation of a user provider to be used by access tokens 47 | * guard for authentication. Used for testing. 48 | * 49 | * @note 50 | * Should not be exported to the outside world 51 | */ 52 | export class AccessTokensFakeUserProvider 53 | implements AccessTokensUserProviderContract 54 | { 55 | declare [PROVIDER_REAL_USER]: AccessTokensFakeUser 56 | #tokens: { 57 | id: string 58 | tokenableId: number 59 | type: string 60 | abilities: string 61 | name: string | null 62 | hash: string 63 | createdAt: Date 64 | updatedAt: Date 65 | lastUsedAt: Date | null 66 | expiresAt: Date | null 67 | }[] = [] 68 | 69 | deleteToken(identifier: string | number | BigInt) { 70 | this.#tokens = this.#tokens.filter((token) => token.id !== identifier) 71 | } 72 | 73 | async createToken( 74 | user: AccessTokensFakeUser, 75 | abilities?: string[], 76 | options?: { 77 | name?: string 78 | expiresIn?: string | number 79 | } 80 | ): Promise { 81 | const transientToken = AccessToken.createTransientToken(user.id, 40, options?.expiresIn) 82 | const id = stringHelpers.random(15) 83 | const createdAt = new Date() 84 | const updatedAt = new Date() 85 | 86 | this.#tokens.push({ 87 | id, 88 | createdAt, 89 | updatedAt, 90 | name: options?.name || null, 91 | hash: transientToken.hash, 92 | lastUsedAt: null, 93 | tokenableId: user.id, 94 | type: 'auth_tokens', 95 | expiresAt: transientToken.expiresAt || null, 96 | abilities: JSON.stringify(abilities || ['*']), 97 | }) 98 | 99 | return new AccessToken({ 100 | identifier: id, 101 | abilities: abilities || ['*'], 102 | tokenableId: user.id, 103 | secret: transientToken.secret, 104 | prefix: 'oat_', 105 | type: 'auth_tokens', 106 | name: options?.name || null, 107 | hash: transientToken.hash, 108 | createdAt: createdAt, 109 | updatedAt: updatedAt, 110 | expiresAt: transientToken.expiresAt || null, 111 | lastUsedAt: null, 112 | }) 113 | } 114 | 115 | async createUserForGuard(user: AccessTokensFakeUser) { 116 | return { 117 | getId() { 118 | return user.id 119 | }, 120 | getOriginal() { 121 | return user 122 | }, 123 | } 124 | } 125 | 126 | async findById(id: number) { 127 | const user = users.find(({ id: userId }) => userId === id) 128 | if (!user) { 129 | return null 130 | } 131 | 132 | return this.createUserForGuard(user) 133 | } 134 | 135 | async invalidateToken(tokenValue: Secret) { 136 | const decodedToken = AccessToken.decode('oat_', tokenValue.release()) 137 | if (!decodedToken) { 138 | return false 139 | } 140 | 141 | const index = this.#tokens.findIndex(({ id }) => id === decodedToken.identifier) 142 | 143 | if (index === -1) { 144 | return false 145 | } 146 | this.#tokens.splice(index, 1) 147 | return true 148 | } 149 | 150 | async verifyToken(tokenValue: Secret): Promise { 151 | const decodedToken = AccessToken.decode('oat_', tokenValue.release()) 152 | if (!decodedToken) { 153 | return null 154 | } 155 | 156 | const token = this.#tokens.find(({ id }) => id === decodedToken.identifier) 157 | if (!token) { 158 | return null 159 | } 160 | 161 | const accessToken = new AccessToken({ 162 | identifier: token.id, 163 | abilities: JSON.parse(token.abilities), 164 | tokenableId: token.tokenableId, 165 | type: token.type, 166 | name: token.name, 167 | hash: token.hash, 168 | createdAt: token.createdAt, 169 | updatedAt: token.updatedAt, 170 | expiresAt: token.expiresAt, 171 | lastUsedAt: token.lastUsedAt, 172 | }) 173 | 174 | if (!accessToken.verify(decodedToken.secret) || accessToken.isExpired()) { 175 | return null 176 | } 177 | 178 | return accessToken 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /factories/auth/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { GUARD_KNOWN_EVENTS } from '../../src/symbols.js' 11 | import { E_UNAUTHORIZED_ACCESS } from '../../src/errors.js' 12 | import { AuthClientResponse, GuardContract } from '../../src/types.js' 13 | 14 | /** 15 | * @note 16 | * Should not be exported to the outside world 17 | */ 18 | export type FakeUser = { 19 | id: number 20 | } 21 | 22 | /** 23 | * Fake guard is an implementation of the auth guard contract 24 | * that uses in-memory values used for testing the auth 25 | * layer. 26 | * 27 | * @note 28 | * Should not be exported to the outside world 29 | */ 30 | export class FakeGuard implements GuardContract { 31 | isAuthenticated: boolean = false 32 | authenticationAttempted: boolean = false 33 | driverName: string = 'fake' 34 | user?: FakeUser; 35 | 36 | declare [GUARD_KNOWN_EVENTS]: undefined 37 | 38 | getUserOrFail(): FakeUser { 39 | if (!this.user) { 40 | throw new E_UNAUTHORIZED_ACCESS('Unauthorized access', { guardDriverName: this.driverName }) 41 | } 42 | return this.user 43 | } 44 | 45 | async authenticate(): Promise { 46 | if (this.authenticationAttempted) { 47 | return this.getUserOrFail() 48 | } 49 | 50 | this.authenticationAttempted = true 51 | this.isAuthenticated = true 52 | this.user = { 53 | id: 1, 54 | } 55 | 56 | return this.user 57 | } 58 | 59 | async check(): Promise { 60 | try { 61 | await this.authenticate() 62 | return true 63 | } catch { 64 | return false 65 | } 66 | } 67 | 68 | async authenticateAsClient( 69 | _user: FakeUser, 70 | _abilities?: string[], 71 | _expiresIn?: string | number 72 | ): Promise { 73 | return {} 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /factories/basic_auth/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { PROVIDER_REAL_USER } from '../../src/symbols.js' 11 | import { 12 | BasicAuthGuardUser, 13 | BasicAuthUserProviderContract, 14 | } from '../../modules/basic_auth_guard/types.js' 15 | 16 | /** 17 | * Representation of a fake user used to test 18 | * the basic auth guard. 19 | * 20 | * @note 21 | * Should not be exported to the outside world 22 | */ 23 | export type BasicAuthFakeUser = { 24 | id: number 25 | email: string 26 | password: string 27 | } 28 | 29 | /** 30 | * Collection of dummy users 31 | */ 32 | const users: BasicAuthFakeUser[] = [ 33 | { 34 | id: 1, 35 | email: 'virk@adonisjs.com', 36 | password: 'secret', 37 | }, 38 | { 39 | id: 2, 40 | email: 'romain@adonisjs.com', 41 | password: 'secret', 42 | }, 43 | ] 44 | 45 | /** 46 | * Implementation of a user provider to be used by basic auth guard for 47 | * authentication. Used for testing. 48 | * 49 | * @note 50 | * Should not be exported to the outside world 51 | */ 52 | export class BasicAuthFakeUserProvider implements BasicAuthUserProviderContract { 53 | declare [PROVIDER_REAL_USER]: BasicAuthFakeUser 54 | 55 | /** 56 | * Creates the adapter user for the guard 57 | */ 58 | async createUserForGuard(user: BasicAuthFakeUser) { 59 | return { 60 | getId() { 61 | return user.id 62 | }, 63 | getOriginal() { 64 | return user 65 | }, 66 | } 67 | } 68 | 69 | /** 70 | * Verifies user credentials 71 | */ 72 | async verifyCredentials( 73 | uid: string, 74 | password: string 75 | ): Promise | null> { 76 | const user = users.find(({ email }) => email === uid) 77 | if (!user) { 78 | return null 79 | } 80 | 81 | if (user.password === password) { 82 | return this.createUserForGuard(user) 83 | } 84 | 85 | return null 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /factories/session/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Secret } from '@adonisjs/core/helpers' 11 | import { setTimeout } from 'node:timers/promises' 12 | import stringHelpers from '@adonisjs/core/helpers/string' 13 | 14 | import { PROVIDER_REAL_USER } from '../../src/symbols.js' 15 | import { 16 | RememberMeTokenDbColumns, 17 | SessionUserProviderContract, 18 | SessionWithTokensUserProviderContract, 19 | } from '../../modules/session_guard/types.js' 20 | import { RememberMeToken } from '../../modules/session_guard/remember_me_token.js' 21 | 22 | /** 23 | * Representation of a fake user used to test 24 | * the session guard. 25 | * 26 | * @note 27 | * Should not be exported to the outside world 28 | */ 29 | export type SessionFakeUser = { 30 | id: number 31 | email: string 32 | password: string 33 | } 34 | 35 | /** 36 | * Collection of dummy users 37 | */ 38 | const users: SessionFakeUser[] = [ 39 | { 40 | id: 1, 41 | email: 'virk@adonisjs.com', 42 | password: 'secret', 43 | }, 44 | { 45 | id: 2, 46 | email: 'romain@adonisjs.com', 47 | password: 'secret', 48 | }, 49 | ] 50 | 51 | /** 52 | * Implementation of a user provider to be used by session guard for 53 | * authentication. Used for testing. 54 | * 55 | * @note 56 | * Should not be exported to the outside world 57 | */ 58 | export class SessionFakeUserProvider implements SessionUserProviderContract { 59 | declare [PROVIDER_REAL_USER]: SessionFakeUser 60 | 61 | /** 62 | * Creates the adapter user for the guard 63 | */ 64 | async createUserForGuard(user: SessionFakeUser) { 65 | return { 66 | getId() { 67 | return user.id 68 | }, 69 | getOriginal() { 70 | return user 71 | }, 72 | } 73 | } 74 | 75 | /** 76 | * Finds a user id 77 | */ 78 | async findById(id: number) { 79 | const user = users.find(({ id: userId }) => userId === id) 80 | if (!user) { 81 | return null 82 | } 83 | 84 | return this.createUserForGuard(user) 85 | } 86 | } 87 | 88 | /** 89 | * Implementation with tokens methods as well 90 | * 91 | * @note 92 | * Should not be exported to the outside world 93 | */ 94 | export class SessionFakeUserWithTokensProvider 95 | extends SessionFakeUserProvider 96 | implements SessionWithTokensUserProviderContract 97 | { 98 | tokens: RememberMeTokenDbColumns[] = [] 99 | 100 | /** 101 | * Creates a remember me token for a given user 102 | */ 103 | async createRememberToken( 104 | user: SessionFakeUser, 105 | expiresIn: string | number 106 | ): Promise { 107 | const transientToken = RememberMeToken.createTransientToken(user.id, 40, expiresIn) 108 | const id = stringHelpers.random(15) 109 | const createdAt = new Date() 110 | const updatedAt = new Date() 111 | 112 | this.tokens.push({ 113 | id, 114 | tokenable_id: user.id, 115 | hash: transientToken.hash, 116 | created_at: createdAt, 117 | updated_at: updatedAt, 118 | expires_at: transientToken.expiresAt, 119 | }) 120 | 121 | return new RememberMeToken({ 122 | identifier: id, 123 | tokenableId: user.id, 124 | hash: transientToken.hash, 125 | secret: transientToken.secret, 126 | createdAt, 127 | updatedAt, 128 | expiresAt: transientToken.expiresAt, 129 | }) 130 | } 131 | 132 | /** 133 | * Deletes token by the token id 134 | */ 135 | async deleteRemeberToken( 136 | _: SessionFakeUser, 137 | tokenIdentifier: string | number | BigInt 138 | ): Promise { 139 | this.tokens = this.tokens.filter((token) => token.id !== tokenIdentifier) 140 | return 1 141 | } 142 | 143 | /** 144 | * Verifies a given token 145 | */ 146 | async verifyRememberToken(tokenValue: Secret): Promise { 147 | const decodedToken = RememberMeToken.decode(tokenValue.release()) 148 | if (!decodedToken) { 149 | return null 150 | } 151 | 152 | const token = this.tokens.find(({ id }) => id === decodedToken.identifier) 153 | if (!token) { 154 | return null 155 | } 156 | 157 | const rememberMeToken = new RememberMeToken({ 158 | identifier: token.id, 159 | tokenableId: token.tokenable_id, 160 | hash: token.hash, 161 | createdAt: token.created_at, 162 | updatedAt: token.updated_at, 163 | expiresAt: token.expires_at, 164 | }) 165 | 166 | if (!rememberMeToken.verify(decodedToken.secret) || rememberMeToken.isExpired()) { 167 | return null 168 | } 169 | 170 | return rememberMeToken 171 | } 172 | 173 | /** 174 | * Recycles token by deleting the old one and creating a new one 175 | */ 176 | async recycleRememberToken( 177 | user: SessionFakeUser, 178 | tokenIdentifier: string | number | BigInt, 179 | expiresIn: string | number 180 | ): Promise { 181 | await this.deleteRemeberToken(user, tokenIdentifier) 182 | await setTimeout(100) 183 | return this.createRememberToken(user, expiresIn) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export * as errors from './src/errors.js' 11 | export { configure } from './configure.js' 12 | export * as symbols from './src/symbols.js' 13 | export { AuthManager } from './src/auth_manager.js' 14 | export { defineConfig } from './src/define_config.js' 15 | export { Authenticator } from './src/authenticator.js' 16 | export { AuthenticatorClient } from './src/authenticator_client.js' 17 | import type { withAuthFinder as withAuthFinderType } from './src/mixins/lucid.js' 18 | 19 | function isModuleInstalled(moduleName: string) { 20 | try { 21 | import.meta.resolve(moduleName) 22 | return true 23 | } catch (e) { 24 | return false 25 | } 26 | } 27 | 28 | /** 29 | * @deprecated Import `withAuthFinder` from `@adonisjs/auth/mixins/lucid` instead 30 | */ 31 | let withAuthFinder: typeof withAuthFinderType 32 | 33 | if (isModuleInstalled('@adonisjs/lucid')) { 34 | const { withAuthFinder: withAuthFinderFn } = await import('./src/mixins/lucid.js') 35 | withAuthFinder = withAuthFinderFn 36 | } 37 | 38 | export { withAuthFinder } 39 | -------------------------------------------------------------------------------- /modules/access_tokens_guard/access_token.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { createHash } from 'node:crypto' 11 | import string from '@adonisjs/core/helpers/string' 12 | import { RuntimeException } from '@adonisjs/core/exceptions' 13 | import { Secret, base64, safeEqual } from '@adonisjs/core/helpers' 14 | 15 | import { CRC32 } from './crc32.js' 16 | import { E_UNAUTHORIZED_ACCESS } from '../../src/errors.js' 17 | 18 | /** 19 | * Access token represents a token created for a user to authenticate 20 | * using the auth module. 21 | * 22 | * It encapsulates the logic of creating an opaque token, generating 23 | * its hash and verifying its hash. 24 | */ 25 | export class AccessToken { 26 | /** 27 | * Decodes a publicly shared token and return the series 28 | * and the token value from it. 29 | * 30 | * Returns null when unable to decode the token because of 31 | * invalid format or encoding. 32 | */ 33 | static decode( 34 | prefix: string, 35 | value: string 36 | ): null | { identifier: string; secret: Secret } { 37 | /** 38 | * Ensure value is a string and starts with the prefix. 39 | */ 40 | if (typeof value !== 'string' || !value.startsWith(`${prefix}`)) { 41 | return null 42 | } 43 | 44 | /** 45 | * Remove prefix from the rest of the token. 46 | */ 47 | const token = value.replace(new RegExp(`^${prefix}`), '') 48 | if (!token) { 49 | return null 50 | } 51 | 52 | const [identifier, ...tokenValue] = token.split('.') 53 | if (!identifier || tokenValue.length === 0) { 54 | return null 55 | } 56 | 57 | const decodedIdentifier = base64.urlDecode(identifier) 58 | const decodedSecret = base64.urlDecode(tokenValue.join('.')) 59 | if (!decodedIdentifier || !decodedSecret) { 60 | return null 61 | } 62 | 63 | return { 64 | identifier: decodedIdentifier, 65 | secret: new Secret(decodedSecret), 66 | } 67 | } 68 | 69 | /** 70 | * Creates a transient token that can be shared with the persistence 71 | * layer. 72 | */ 73 | static createTransientToken( 74 | userId: string | number | BigInt, 75 | size: number, 76 | expiresIn?: string | number 77 | ) { 78 | let expiresAt: Date | undefined 79 | if (expiresIn) { 80 | expiresAt = new Date() 81 | expiresAt.setSeconds(expiresAt.getSeconds() + string.seconds.parse(expiresIn)) 82 | } 83 | 84 | return { 85 | userId, 86 | expiresAt, 87 | ...this.seed(size), 88 | } 89 | } 90 | 91 | /** 92 | * Creates a secret opaque token and its hash. The secret is 93 | * suffixed with a crc32 checksum for secret scanning tools 94 | * to easily identify the token. 95 | */ 96 | static seed(size: number) { 97 | const seed = string.random(size) 98 | const secret = new Secret(`${seed}${new CRC32().calculate(seed)}`) 99 | const hash = createHash('sha256').update(secret.release()).digest('hex') 100 | return { secret, hash } 101 | } 102 | 103 | /** 104 | * Identifer is a unique sequence to identify the 105 | * token within database. It should be the 106 | * primary/unique key 107 | */ 108 | identifier: string | number | BigInt 109 | 110 | /** 111 | * Reference to the user id for whom the token 112 | * is generated. 113 | */ 114 | tokenableId: string | number | BigInt 115 | 116 | /** 117 | * The value is a public representation of a token. It is created 118 | * by combining the "identifier"."secret" 119 | */ 120 | value?: Secret 121 | 122 | /** 123 | * Recognizable name for the token 124 | */ 125 | name: string | null 126 | 127 | /** 128 | * A unique type to identify a bucket of tokens inside the 129 | * storage layer. 130 | */ 131 | type: string 132 | 133 | /** 134 | * Hash is computed from the seed to later verify the validity 135 | * of seed 136 | */ 137 | hash: string 138 | 139 | /** 140 | * Date/time when the token instance was created 141 | */ 142 | createdAt: Date 143 | 144 | /** 145 | * Date/time when the token was updated 146 | */ 147 | updatedAt: Date 148 | 149 | /** 150 | * Timestamp at which the token was used for authentication 151 | */ 152 | lastUsedAt: Date | null 153 | 154 | /** 155 | * Timestamp at which the token will expire 156 | */ 157 | expiresAt: Date | null 158 | 159 | /** 160 | * An array of abilities the token can perform. The abilities 161 | * is an array of abritary string values 162 | */ 163 | abilities: string[] 164 | 165 | constructor(attributes: { 166 | identifier: string | number | BigInt 167 | tokenableId: string | number | BigInt 168 | type: string 169 | hash: string 170 | createdAt: Date 171 | updatedAt: Date 172 | lastUsedAt: Date | null 173 | expiresAt: Date | null 174 | name: string | null 175 | prefix?: string 176 | secret?: Secret 177 | abilities?: string[] 178 | }) { 179 | this.identifier = attributes.identifier 180 | this.tokenableId = attributes.tokenableId 181 | this.name = attributes.name 182 | this.hash = attributes.hash 183 | this.type = attributes.type 184 | this.createdAt = attributes.createdAt 185 | this.updatedAt = attributes.updatedAt 186 | this.expiresAt = attributes.expiresAt 187 | this.lastUsedAt = attributes.lastUsedAt 188 | this.abilities = attributes.abilities || ['*'] 189 | 190 | /** 191 | * Compute value when secret is provided 192 | */ 193 | if (attributes.secret) { 194 | if (!attributes.prefix) { 195 | throw new RuntimeException('Cannot compute token value without the prefix') 196 | } 197 | this.value = new Secret( 198 | `${attributes.prefix}${base64.urlEncode(String(this.identifier))}.${base64.urlEncode( 199 | attributes.secret.release() 200 | )}` 201 | ) 202 | } 203 | } 204 | 205 | /** 206 | * Check if the token allows the given ability. 207 | */ 208 | allows(ability: string) { 209 | return this.abilities.includes(ability) || this.abilities.includes('*') 210 | } 211 | 212 | /** 213 | * Check if the token denies the ability. 214 | */ 215 | denies(ability: string) { 216 | return !this.abilities.includes(ability) && !this.abilities.includes('*') 217 | } 218 | 219 | /** 220 | * Authorize ability access using the current access token 221 | */ 222 | authorize(ability: string) { 223 | if (this.denies(ability)) { 224 | throw new E_UNAUTHORIZED_ACCESS('Unauthorized access', { guardDriverName: 'access_tokens' }) 225 | } 226 | } 227 | 228 | /** 229 | * Check if the token has been expired. Verifies 230 | * the "expiresAt" timestamp with the current 231 | * date. 232 | * 233 | * Tokens with no expiry never expire 234 | */ 235 | isExpired() { 236 | if (!this.expiresAt) { 237 | return false 238 | } 239 | 240 | return this.expiresAt < new Date() 241 | } 242 | 243 | /** 244 | * Verifies the value of a token against the pre-defined hash 245 | */ 246 | verify(secret: Secret): boolean { 247 | const newHash = createHash('sha256').update(secret.release()).digest('hex') 248 | return safeEqual(this.hash, newHash) 249 | } 250 | 251 | toJSON() { 252 | return { 253 | type: 'bearer', 254 | name: this.name, 255 | token: this.value ? this.value.release() : undefined, 256 | abilities: this.abilities, 257 | lastUsedAt: this.lastUsedAt, 258 | expiresAt: this.expiresAt, 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /modules/access_tokens_guard/crc32.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /** 11 | * We use CRC32 just to add a recognizable checksum to tokens. This helps 12 | * secret scanning tools like https://docs.github.com/en/github/administering-a-repository/about-secret-scanning easily detect tokens generated by a given program. 13 | * 14 | * You can learn more about appending checksum to a hash here in this Github 15 | * article. https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ 16 | * 17 | * Code taken from: 18 | * https://github.com/tsxper/crc32/blob/main/src/CRC32.ts 19 | */ 20 | 21 | export class CRC32 { 22 | /** 23 | * Lookup table calculated for 0xEDB88320 divisor 24 | */ 25 | #lookupTable = [ 26 | 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, 249268274, 27 | 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, 498536548, 28 | 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990, 29 | 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, 997073096, 30 | 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, 901097722, 31 | 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980, 32 | 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, 671266974, 33 | 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, 1994146192, 34 | 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290, 35 | 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, 1802195444, 36 | 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, 1706088902, 37 | 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960, 38 | 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, 39 | 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, 1342533948, 40 | 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054, 41 | 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, 3988292384, 42 | 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, 3814918930, 43 | 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580, 44 | 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, 4275313526, 45 | 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, 3604390888, 46 | 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850, 47 | 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, 3412177804, 48 | 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, 3317316542, 49 | 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920, 50 | 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, 2512341634, 51 | 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, 2262029012, 52 | 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934, 53 | 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, 2685067896, 54 | 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, 2932959818, 55 | 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108, 56 | 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, 3009837614, 57 | 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117, 58 | ] 59 | 60 | #initialCRC = 0xffffffff 61 | 62 | #calculateBytes(bytes: Uint8Array, accumulator?: number): number { 63 | let crc = accumulator || this.#initialCRC 64 | for (const byte of bytes) { 65 | const tableIndex = (crc ^ byte) & 0xff 66 | const tableVal = this.#lookupTable[tableIndex] as number 67 | crc = (crc >>> 8) ^ tableVal 68 | } 69 | return crc 70 | } 71 | 72 | #crcToUint(crc: number): number { 73 | return this.#toUint32(crc ^ 0xffffffff) 74 | } 75 | 76 | #strToBytes(input: string): Uint8Array { 77 | const encoder = new TextEncoder() 78 | return encoder.encode(input) 79 | } 80 | 81 | #toUint32(num: number): number { 82 | if (num >= 0) { 83 | return num 84 | } 85 | return 0xffffffff - num * -1 + 1 86 | } 87 | 88 | calculate(input: string): number { 89 | return this.forString(input) 90 | } 91 | 92 | forString(input: string): number { 93 | const bytes = this.#strToBytes(input) 94 | return this.forBytes(bytes) 95 | } 96 | 97 | forBytes(bytes: Uint8Array, accumulator?: number): number { 98 | const crc = this.#calculateBytes(bytes, accumulator) 99 | return this.#crcToUint(crc) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /modules/access_tokens_guard/define_config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { HttpContext } from '@adonisjs/core/http' 11 | import type { ConfigProvider } from '@adonisjs/core/types' 12 | 13 | import { AccessTokensGuard } from './guard.js' 14 | import type { GuardConfigProvider } from '../../src/types.js' 15 | import { AccessTokensLucidUserProvider } from './user_providers/lucid.js' 16 | import type { 17 | LucidTokenable, 18 | AccessTokensUserProviderContract, 19 | AccessTokensLucidUserProviderOptions, 20 | } from './types.js' 21 | 22 | /** 23 | * Configures access tokens guard for authentication 24 | */ 25 | export function tokensGuard< 26 | UserProvider extends AccessTokensUserProviderContract, 27 | >(config: { 28 | provider: UserProvider | ConfigProvider 29 | }): GuardConfigProvider<(ctx: HttpContext) => AccessTokensGuard> { 30 | return { 31 | async resolver(name, app) { 32 | const emitter = await app.container.make('emitter') 33 | const provider = 34 | 'resolver' in config.provider ? await config.provider.resolver(app) : config.provider 35 | return (ctx) => new AccessTokensGuard(name, ctx, emitter as any, provider) 36 | }, 37 | } 38 | } 39 | 40 | /** 41 | * Configures user provider that uses Lucid models to verify 42 | * access tokens and find users during authentication. 43 | */ 44 | export function tokensUserProvider< 45 | TokenableProperty extends string, 46 | Model extends LucidTokenable, 47 | >( 48 | config: AccessTokensLucidUserProviderOptions 49 | ): AccessTokensLucidUserProvider { 50 | return new AccessTokensLucidUserProvider(config) 51 | } 52 | -------------------------------------------------------------------------------- /modules/access_tokens_guard/guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Secret } from '@adonisjs/core/helpers' 11 | import type { HttpContext } from '@adonisjs/core/http' 12 | import type { EmitterLike } from '@adonisjs/core/types/events' 13 | 14 | import type { AccessToken } from './access_token.js' 15 | import { E_UNAUTHORIZED_ACCESS } from '../../src/errors.js' 16 | import type { AuthClientResponse, GuardContract } from '../../src/types.js' 17 | import { GUARD_KNOWN_EVENTS, PROVIDER_REAL_USER } from '../../src/symbols.js' 18 | import type { AccessTokensGuardEvents, AccessTokensUserProviderContract } from './types.js' 19 | 20 | /** 21 | * Implementation of access tokens guard for the Auth layer. The heavy lifting 22 | * of verifying tokens is done by the user provider. However, the guard is 23 | * used to seamlessly integrate with the auth layer of the package. 24 | */ 25 | export class AccessTokensGuard> 26 | implements 27 | GuardContract 28 | { 29 | /** 30 | * Events emitted by the guard 31 | */ 32 | declare [GUARD_KNOWN_EVENTS]: AccessTokensGuardEvents< 33 | UserProvider[typeof PROVIDER_REAL_USER] & { currentAccessToken: AccessToken } 34 | > 35 | 36 | /** 37 | * A unique name for the guard. 38 | */ 39 | #name: string 40 | 41 | /** 42 | * Reference to the current HTTP context 43 | */ 44 | #ctx: HttpContext 45 | 46 | /** 47 | * Provider to lookup user details 48 | */ 49 | #userProvider: UserProvider 50 | 51 | /** 52 | * Emitter to emit events 53 | */ 54 | #emitter: EmitterLike< 55 | AccessTokensGuardEvents< 56 | UserProvider[typeof PROVIDER_REAL_USER] & { currentAccessToken: AccessToken } 57 | > 58 | > 59 | 60 | /** 61 | * Driver name of the guard 62 | */ 63 | driverName: 'access_tokens' = 'access_tokens' 64 | 65 | /** 66 | * Whether or not the authentication has been attempted 67 | * during the current request. 68 | */ 69 | authenticationAttempted = false 70 | 71 | /** 72 | * A boolean to know if the current request has 73 | * been authenticated 74 | */ 75 | isAuthenticated = false 76 | 77 | /** 78 | * Reference to an instance of the authenticated user. 79 | * The value only exists after calling one of the 80 | * following methods. 81 | * 82 | * - authenticate 83 | * - check 84 | * 85 | * You can use the "getUserOrFail" method to throw an exception if 86 | * the request is not authenticated. 87 | */ 88 | user?: UserProvider[typeof PROVIDER_REAL_USER] & { currentAccessToken: AccessToken } 89 | 90 | constructor( 91 | name: string, 92 | ctx: HttpContext, 93 | emitter: EmitterLike< 94 | AccessTokensGuardEvents< 95 | UserProvider[typeof PROVIDER_REAL_USER] & { currentAccessToken: AccessToken } 96 | > 97 | >, 98 | userProvider: UserProvider 99 | ) { 100 | this.#name = name 101 | this.#ctx = ctx 102 | this.#emitter = emitter 103 | this.#userProvider = userProvider 104 | } 105 | 106 | /** 107 | * Emits authentication failure and returns an exception 108 | * to end the authentication cycle. 109 | */ 110 | #authenticationFailed() { 111 | const error = new E_UNAUTHORIZED_ACCESS('Unauthorized access', { 112 | guardDriverName: this.driverName, 113 | }) 114 | 115 | this.#emitter.emit('access_tokens_auth:authentication_failed', { 116 | ctx: this.#ctx, 117 | guardName: this.#name, 118 | error, 119 | }) 120 | 121 | return error 122 | } 123 | 124 | /** 125 | * Returns the bearer token from the request headers or fails 126 | */ 127 | #getBearerToken(): string { 128 | const bearerToken = this.#ctx.request.header('authorization', '')! 129 | const [, token] = bearerToken.split('Bearer ') 130 | if (!token) { 131 | throw this.#authenticationFailed() 132 | } 133 | 134 | return token 135 | } 136 | 137 | /** 138 | * Returns an instance of the authenticated user. Or throws 139 | * an exception if the request is not authenticated. 140 | */ 141 | getUserOrFail(): UserProvider[typeof PROVIDER_REAL_USER] & { currentAccessToken: AccessToken } { 142 | if (!this.user) { 143 | throw new E_UNAUTHORIZED_ACCESS('Unauthorized access', { 144 | guardDriverName: this.driverName, 145 | }) 146 | } 147 | 148 | return this.user 149 | } 150 | 151 | /** 152 | * Authenticate the current HTTP request by verifying the bearer 153 | * token or fails with an exception 154 | */ 155 | async authenticate(): Promise< 156 | UserProvider[typeof PROVIDER_REAL_USER] & { currentAccessToken: AccessToken } 157 | > { 158 | /** 159 | * Return early when authentication has already 160 | * been attempted 161 | */ 162 | if (this.authenticationAttempted) { 163 | return this.getUserOrFail() 164 | } 165 | 166 | /** 167 | * Notify we begin to attempt the authentication 168 | */ 169 | this.authenticationAttempted = true 170 | this.#emitter.emit('access_tokens_auth:authentication_attempted', { 171 | ctx: this.#ctx, 172 | guardName: this.#name, 173 | }) 174 | 175 | /** 176 | * Decode token or fail when unable to do so 177 | */ 178 | const bearerToken = new Secret(this.#getBearerToken()) 179 | 180 | /** 181 | * Verify for token via the user provider 182 | */ 183 | const token = await this.#userProvider.verifyToken(bearerToken) 184 | if (!token) { 185 | throw this.#authenticationFailed() 186 | } 187 | 188 | /** 189 | * Check if a user for the token exists. Otherwise abort 190 | * authentication 191 | */ 192 | const providerUser = await this.#userProvider.findById(token.tokenableId) 193 | if (!providerUser) { 194 | throw this.#authenticationFailed() 195 | } 196 | 197 | /** 198 | * Update local state 199 | */ 200 | this.isAuthenticated = true 201 | this.user = providerUser.getOriginal() as UserProvider[typeof PROVIDER_REAL_USER] & { 202 | currentAccessToken: AccessToken 203 | } 204 | this.user!.currentAccessToken = token 205 | 206 | /** 207 | * Notify 208 | */ 209 | this.#emitter.emit('access_tokens_auth:authentication_succeeded', { 210 | ctx: this.#ctx, 211 | token, 212 | guardName: this.#name, 213 | user: this.user, 214 | }) 215 | 216 | return this.user 217 | } 218 | 219 | /** 220 | * Create a token for a user (sign in) 221 | */ 222 | async createToken( 223 | user: UserProvider[typeof PROVIDER_REAL_USER], 224 | abilities?: string[], 225 | options?: { 226 | expiresIn?: string | number 227 | name?: string 228 | } 229 | ) { 230 | return await this.#userProvider.createToken(user, abilities, options) 231 | } 232 | 233 | /** 234 | * Invalidates the currently authenticated token (sign out) 235 | */ 236 | async invalidateToken() { 237 | const bearerToken = new Secret(this.#getBearerToken()) 238 | return await this.#userProvider.invalidateToken(bearerToken) 239 | } 240 | 241 | /** 242 | * Returns the Authorization header clients can use to authenticate 243 | * the request. 244 | */ 245 | async authenticateAsClient( 246 | user: UserProvider[typeof PROVIDER_REAL_USER], 247 | abilities?: string[], 248 | options?: { 249 | expiresIn?: string | number 250 | name?: string 251 | } 252 | ): Promise { 253 | const token = await this.#userProvider.createToken(user, abilities, options) 254 | return { 255 | headers: { 256 | authorization: `Bearer ${token.value!.release()}`, 257 | }, 258 | } 259 | } 260 | 261 | /** 262 | * Silently check if the user is authenticated or not. The 263 | * method is same the "authenticate" method but does not 264 | * throw any exceptions. 265 | */ 266 | async check(): Promise { 267 | try { 268 | await this.authenticate() 269 | return true 270 | } catch (error) { 271 | if (error instanceof E_UNAUTHORIZED_ACCESS) { 272 | return false 273 | } 274 | 275 | throw error 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /modules/access_tokens_guard/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export { AccessToken } from './access_token.js' 11 | export { AccessTokensGuard } from './guard.js' 12 | export { DbAccessTokensProvider } from './token_providers/db.js' 13 | export { tokensGuard, tokensUserProvider } from './define_config.js' 14 | export { AccessTokensLucidUserProvider } from './user_providers/lucid.js' 15 | -------------------------------------------------------------------------------- /modules/access_tokens_guard/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Secret } from '@adonisjs/core/helpers' 11 | import type { HttpContext } from '@adonisjs/core/http' 12 | import type { Exception } from '@adonisjs/core/exceptions' 13 | import type { LucidModel } from '@adonisjs/lucid/types/model' 14 | 15 | import type { AccessToken } from './access_token.js' 16 | import type { PROVIDER_REAL_USER } from '../../src/symbols.js' 17 | 18 | /** 19 | * Options accepted by the tokens provider that uses lucid 20 | * database service to fetch and persist tokens. 21 | */ 22 | export type DbAccessTokensProviderOptions = { 23 | /** 24 | * The user model for which to generate tokens. Note, the model 25 | * is not used for tokens, but is used to associate a user 26 | * with the token 27 | */ 28 | tokenableModel: TokenableModel 29 | 30 | /** 31 | * Database table to use for querying tokens. 32 | * 33 | * Defaults to "auth_access_tokens" 34 | */ 35 | table?: string 36 | 37 | /** 38 | * The default expiry for all the tokens. You can also customize 39 | * expiry at the time of creating a token as well. 40 | * 41 | * By default tokens do not expire 42 | */ 43 | expiresIn?: string | number 44 | 45 | /** 46 | * The length for the token secret. A secret is a cryptographically 47 | * secure random string. 48 | * 49 | * Defaults to 40 50 | */ 51 | tokenSecretLength?: number 52 | 53 | /** 54 | * A unique type for the value. The type is used to identify a 55 | * bucket of tokens within the storage layer. 56 | * 57 | * Defaults to auth_token 58 | */ 59 | type?: string 60 | 61 | /** 62 | * A unique prefix to append to the publicly shared token value. 63 | * 64 | * Defaults to oat_ 65 | */ 66 | prefix?: string 67 | } 68 | 69 | /** 70 | * The database columns expected at the database level 71 | */ 72 | export type AccessTokenDbColumns = { 73 | /** 74 | * Token primary key. It can be an integer, bigInteger or 75 | * even a UUID or any other string based value. 76 | * 77 | * The id should not have ". (dots)" inside it. 78 | */ 79 | id: number | string | BigInt 80 | 81 | /** 82 | * The user or entity for whom the token is 83 | * generated 84 | */ 85 | tokenable_id: string | number | BigInt 86 | 87 | /** 88 | * A unique type for the token. It is used to 89 | * unique identify tokens within the storage 90 | * layer. 91 | */ 92 | type: string 93 | 94 | /** 95 | * Optional name for the token 96 | */ 97 | name: string | null 98 | 99 | /** 100 | * Token hash is used to verify the token shared 101 | * with the user 102 | */ 103 | hash: string 104 | 105 | /** 106 | * Timestamps 107 | */ 108 | created_at: Date 109 | updated_at: Date 110 | 111 | /** 112 | * An array of abilities stored as JSON. 113 | */ 114 | abilities: string 115 | 116 | /** 117 | * The date after which the token will be considered 118 | * expired. 119 | * 120 | * A null value means the token is long-lived 121 | */ 122 | expires_at: null | Date 123 | 124 | /** 125 | * Last time the token was used for authentication 126 | */ 127 | last_used_at: null | Date 128 | } 129 | 130 | /** 131 | * Access token providers are used verify an access token 132 | * during authentication 133 | */ 134 | export interface AccessTokensProviderContract { 135 | /** 136 | * Create a token for a given user 137 | */ 138 | create( 139 | user: InstanceType, 140 | abilities?: string[], 141 | options?: { 142 | name?: string 143 | expiresIn?: string | number 144 | } 145 | ): Promise 146 | 147 | /** 148 | * Verifies a publicly shared access token and returns an 149 | * access token for it. 150 | */ 151 | verify(tokenValue: Secret): Promise 152 | 153 | /** 154 | * Invalidates a token identified by its publicly shared token 155 | */ 156 | invalidate(tokenValue: Secret): Promise 157 | } 158 | 159 | /** 160 | * A lucid model with a tokens provider to verify tokens during 161 | * authentication 162 | */ 163 | export type LucidTokenable = LucidModel & { 164 | [K in TokenableProperty]: AccessTokensProviderContract 165 | } 166 | 167 | /** 168 | * Options accepted by the user provider that uses a lucid 169 | * model to lookup a user during authentication and verify 170 | * tokens 171 | */ 172 | export type AccessTokensLucidUserProviderOptions< 173 | TokenableProperty extends string, 174 | Model extends LucidTokenable, 175 | > = { 176 | tokens: TokenableProperty 177 | model: () => Promise<{ default: Model }> 178 | } 179 | 180 | /** 181 | * Guard user is an adapter between the user provider 182 | * and the guard. 183 | * 184 | * The guard is user provider agnostic and therefore it 185 | * needs a adapter to known some basic info about the 186 | * user. 187 | */ 188 | export type AccessTokensGuardUser = { 189 | getId(): string | number | BigInt 190 | getOriginal(): RealUser 191 | } 192 | 193 | /** 194 | * The user provider used by access tokens guard to lookup 195 | * users and verify tokens. 196 | */ 197 | export interface AccessTokensUserProviderContract { 198 | [PROVIDER_REAL_USER]: RealUser 199 | 200 | /** 201 | * Create a user object that acts as an adapter between 202 | * the guard and real user value. 203 | */ 204 | createUserForGuard(user: RealUser): Promise> 205 | 206 | /** 207 | * Create a token for a given user 208 | */ 209 | createToken( 210 | user: RealUser, 211 | abilities?: string[], 212 | options?: { 213 | name?: string 214 | expiresIn?: string | number 215 | } 216 | ): Promise 217 | 218 | /** 219 | * Invalidates a token identified by its publicly shared token. 220 | */ 221 | invalidateToken(tokenValue: Secret): Promise 222 | 223 | /** 224 | * Find a user by the user id. 225 | */ 226 | findById(identifier: string | number | BigInt): Promise | null> 227 | 228 | /** 229 | * Verify a token by its publicly shared value. 230 | */ 231 | verifyToken(tokenValue: Secret): Promise 232 | } 233 | 234 | /** 235 | * Events emitted by the access tokens guard during 236 | * authentication 237 | */ 238 | export type AccessTokensGuardEvents = { 239 | /** 240 | * Attempting to authenticate the user 241 | */ 242 | 'access_tokens_auth:authentication_attempted': { 243 | ctx: HttpContext 244 | guardName: string 245 | } 246 | 247 | /** 248 | * Authentication was successful 249 | */ 250 | 'access_tokens_auth:authentication_succeeded': { 251 | ctx: HttpContext 252 | guardName: string 253 | user: RealUser 254 | token: AccessToken 255 | } 256 | 257 | /** 258 | * Authentication failed 259 | */ 260 | 'access_tokens_auth:authentication_failed': { 261 | ctx: HttpContext 262 | guardName: string 263 | error: Exception 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /modules/access_tokens_guard/user_providers/lucid.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Secret } from '@adonisjs/core/helpers' 11 | import type { LucidRow } from '@adonisjs/lucid/types/model' 12 | import { RuntimeException } from '@adonisjs/core/exceptions' 13 | 14 | import { AccessToken } from '../access_token.js' 15 | import { PROVIDER_REAL_USER } from '../../../src/symbols.js' 16 | import type { 17 | LucidTokenable, 18 | AccessTokensGuardUser, 19 | AccessTokensUserProviderContract, 20 | AccessTokensLucidUserProviderOptions, 21 | } from '../types.js' 22 | 23 | /** 24 | * Uses a lucid model to verify access tokens and find a user during 25 | * authentication 26 | */ 27 | export class AccessTokensLucidUserProvider< 28 | TokenableProperty extends string, 29 | UserModel extends LucidTokenable, 30 | > implements AccessTokensUserProviderContract> 31 | { 32 | declare [PROVIDER_REAL_USER]: InstanceType 33 | 34 | /** 35 | * Reference to the lazily imported model 36 | */ 37 | protected model?: UserModel 38 | 39 | constructor( 40 | /** 41 | * Lucid provider options 42 | */ 43 | protected options: AccessTokensLucidUserProviderOptions 44 | ) {} 45 | 46 | /** 47 | * Imports the model from the provider, returns and caches it 48 | * for further operations. 49 | */ 50 | protected async getModel() { 51 | if (this.model && !('hot' in import.meta)) { 52 | return this.model 53 | } 54 | 55 | const importedModel = await this.options.model() 56 | this.model = importedModel.default 57 | return this.model 58 | } 59 | 60 | /** 61 | * Returns the tokens provider associated with the user model 62 | */ 63 | protected async getTokensProvider() { 64 | const model = await this.getModel() 65 | 66 | if (!model[this.options.tokens]) { 67 | throw new RuntimeException( 68 | `Cannot use "${model.name}" model for verifying access tokens. Make sure to assign a token provider to the model.` 69 | ) 70 | } 71 | 72 | return model[this.options.tokens] 73 | } 74 | 75 | /** 76 | * Creates an adapter user for the guard 77 | */ 78 | async createUserForGuard( 79 | user: InstanceType 80 | ): Promise>> { 81 | const model = await this.getModel() 82 | if (user instanceof model === false) { 83 | throw new RuntimeException( 84 | `Invalid user object. It must be an instance of the "${model.name}" model` 85 | ) 86 | } 87 | 88 | return { 89 | getId() { 90 | /** 91 | * Ensure user has a primary key 92 | */ 93 | if (!user.$primaryKeyValue) { 94 | throw new RuntimeException( 95 | `Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null` 96 | ) 97 | } 98 | 99 | return user.$primaryKeyValue 100 | }, 101 | getOriginal() { 102 | return user 103 | }, 104 | } 105 | } 106 | 107 | /** 108 | * Create a token for a given user 109 | */ 110 | async createToken( 111 | user: InstanceType, 112 | abilities?: string[] | undefined, 113 | options?: { 114 | name?: string 115 | expiresIn?: string | number 116 | } 117 | ): Promise { 118 | const tokensProvider = await this.getTokensProvider() 119 | return tokensProvider.create(user as LucidRow, abilities, options) 120 | } 121 | 122 | /** 123 | * Invalidates a token identified by its publicly shared token 124 | */ 125 | async invalidateToken(tokenValue: Secret) { 126 | const tokensProvider = await this.getTokensProvider() 127 | return tokensProvider.invalidate(tokenValue) 128 | } 129 | 130 | /** 131 | * Finds a user by the user id 132 | */ 133 | async findById( 134 | identifier: string | number | BigInt 135 | ): Promise> | null> { 136 | const model = await this.getModel() 137 | const user = await model.find(identifier) 138 | 139 | if (!user) { 140 | return null 141 | } 142 | 143 | return this.createUserForGuard(user) 144 | } 145 | 146 | /** 147 | * Verifies a publicly shared access token and returns an 148 | * access token for it. 149 | */ 150 | async verifyToken(tokenValue: Secret): Promise { 151 | const tokensProvider = await this.getTokensProvider() 152 | return tokensProvider.verify(tokenValue) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /modules/basic_auth_guard/define_config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { HttpContext } from '@adonisjs/core/http' 11 | import type { ConfigProvider } from '@adonisjs/core/types' 12 | 13 | import { BasicAuthGuard } from './guard.js' 14 | import type { GuardConfigProvider } from '../../src/types.js' 15 | import { BasicAuthLucidUserProvider } from './user_providers/lucid.js' 16 | import type { 17 | LucidAuthenticatable, 18 | BasicAuthUserProviderContract, 19 | BasicAuthLucidUserProviderOptions, 20 | } from './types.js' 21 | 22 | /** 23 | * Configures basic auth guard for authentication 24 | */ 25 | export function basicAuthGuard< 26 | UserProvider extends BasicAuthUserProviderContract, 27 | >(config: { 28 | provider: UserProvider | ConfigProvider 29 | }): GuardConfigProvider<(ctx: HttpContext) => BasicAuthGuard> { 30 | return { 31 | async resolver(name, app) { 32 | const emitter = await app.container.make('emitter') 33 | const provider = 34 | 'resolver' in config.provider ? await config.provider.resolver(app) : config.provider 35 | return (ctx) => new BasicAuthGuard(name, ctx, emitter as any, provider) 36 | }, 37 | } 38 | } 39 | 40 | /** 41 | * Configures user provider that uses Lucid models to authenticate 42 | * users using basic auth 43 | */ 44 | export function basicAuthUserProvider( 45 | config: BasicAuthLucidUserProviderOptions 46 | ): BasicAuthLucidUserProvider { 47 | return new BasicAuthLucidUserProvider(config) 48 | } 49 | -------------------------------------------------------------------------------- /modules/basic_auth_guard/guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import auth from 'basic-auth' 11 | import { base64 } from '@adonisjs/core/helpers' 12 | import type { HttpContext } from '@adonisjs/core/http' 13 | import type { EmitterLike } from '@adonisjs/core/types/events' 14 | 15 | import { E_UNAUTHORIZED_ACCESS } from '../../src/errors.js' 16 | import type { AuthClientResponse, GuardContract } from '../../src/types.js' 17 | import { GUARD_KNOWN_EVENTS, PROVIDER_REAL_USER } from '../../src/symbols.js' 18 | import type { BasicAuthGuardEvents, BasicAuthUserProviderContract } from './types.js' 19 | 20 | /** 21 | * BasicAuth guard implements the HTTP Authentication protocol 22 | */ 23 | export class BasicAuthGuard> 24 | implements GuardContract 25 | { 26 | /** 27 | * Events emitted by the guard 28 | */ 29 | declare [GUARD_KNOWN_EVENTS]: BasicAuthGuardEvents 30 | 31 | /** 32 | * A unique name for the guard. 33 | */ 34 | #name: string 35 | 36 | /** 37 | * Reference to the current HTTP context 38 | */ 39 | #ctx: HttpContext 40 | 41 | /** 42 | * Provider to lookup user details 43 | */ 44 | #userProvider: UserProvider 45 | 46 | /** 47 | * Emitter to emit events 48 | */ 49 | #emitter: EmitterLike> 50 | 51 | /** 52 | * Driver name of the guard 53 | */ 54 | driverName: 'basic_auth' = 'basic_auth' 55 | 56 | /** 57 | * Whether or not the authentication has been attempted 58 | * during the current request. 59 | */ 60 | authenticationAttempted = false 61 | 62 | /** 63 | * A boolean to know if the current request has 64 | * been authenticated 65 | */ 66 | isAuthenticated = false 67 | 68 | /** 69 | * Reference to an instance of the authenticated user. 70 | * The value only exists after calling one of the 71 | * following methods. 72 | * 73 | * - authenticate 74 | * - check 75 | * 76 | * You can use the "getUserOrFail" method to throw an exception if 77 | * the request is not authenticated. 78 | */ 79 | user?: UserProvider[typeof PROVIDER_REAL_USER] 80 | 81 | constructor( 82 | name: string, 83 | ctx: HttpContext, 84 | emitter: EmitterLike>, 85 | userProvider: UserProvider 86 | ) { 87 | this.#name = name 88 | this.#ctx = ctx 89 | this.#emitter = emitter 90 | this.#userProvider = userProvider 91 | } 92 | 93 | /** 94 | * Emits authentication failure, updates the local state, 95 | * and returns an exception to end the authentication 96 | * cycle. 97 | */ 98 | #authenticationFailed() { 99 | this.isAuthenticated = false 100 | this.user = undefined 101 | 102 | const error = new E_UNAUTHORIZED_ACCESS('Invalid basic auth credentials', { 103 | guardDriverName: this.driverName, 104 | }) 105 | 106 | this.#emitter.emit('basic_auth:authentication_failed', { 107 | ctx: this.#ctx, 108 | guardName: this.#name, 109 | error, 110 | }) 111 | 112 | return error 113 | } 114 | 115 | /** 116 | * Emits the authentication succeeded event and updates 117 | * the local state to reflect successful authentication 118 | */ 119 | #authenticationSucceeded(user: UserProvider[typeof PROVIDER_REAL_USER]) { 120 | this.isAuthenticated = true 121 | this.user = user 122 | 123 | this.#emitter.emit('basic_auth:authentication_succeeded', { 124 | ctx: this.#ctx, 125 | guardName: this.#name, 126 | user, 127 | }) 128 | } 129 | 130 | /** 131 | * Returns an instance of the authenticated user. Or throws 132 | * an exception if the request is not authenticated. 133 | */ 134 | getUserOrFail(): UserProvider[typeof PROVIDER_REAL_USER] { 135 | if (!this.user) { 136 | throw new E_UNAUTHORIZED_ACCESS('Invalid basic auth credentials', { 137 | guardDriverName: this.driverName, 138 | }) 139 | } 140 | 141 | return this.user 142 | } 143 | 144 | /** 145 | * Authenticates the incoming HTTP request by looking for BasicAuth 146 | * credentials inside the request authorization header. 147 | * 148 | * Returns the authenticated user or throws an exception. 149 | */ 150 | async authenticate(): Promise { 151 | /** 152 | * Avoid re-authenticating when already authenticated 153 | */ 154 | if (this.authenticationAttempted) { 155 | return this.getUserOrFail() 156 | } 157 | 158 | /** 159 | * Beginning authentication attempt 160 | */ 161 | this.authenticationAttempted = true 162 | this.#emitter.emit('basic_auth:authentication_attempted', { 163 | ctx: this.#ctx, 164 | guardName: this.#name, 165 | }) 166 | 167 | /** 168 | * Fetch credentials from the header or fail 169 | */ 170 | const credentials = auth(this.#ctx.request.request) 171 | if (!credentials) { 172 | throw this.#authenticationFailed() 173 | } 174 | 175 | /** 176 | * Verify user credentials or fail 177 | */ 178 | const user = await this.#userProvider.verifyCredentials(credentials.name, credentials.pass) 179 | if (!user) { 180 | throw this.#authenticationFailed() 181 | } 182 | 183 | /** 184 | * Mark user as authenticated 185 | */ 186 | this.#authenticationSucceeded(user.getOriginal()) 187 | return this.getUserOrFail() 188 | } 189 | 190 | /** 191 | * Silently attempt to authenticate the user. 192 | * 193 | * The method returns a boolean indicating if the authentication 194 | * succeeded or failed. 195 | */ 196 | async check(): Promise { 197 | try { 198 | await this.authenticate() 199 | return true 200 | } catch (error) { 201 | if (error instanceof E_UNAUTHORIZED_ACCESS) { 202 | return false 203 | } 204 | 205 | throw error 206 | } 207 | } 208 | 209 | /** 210 | * Does not support authenticating as client. Instead use "basicAuth" 211 | * helper on Japa APIClient 212 | */ 213 | async authenticateAsClient(uid: string, password: string): Promise { 214 | return { 215 | headers: { 216 | authorization: `Basic ${base64.encode(`${uid}:${password}`)}`, 217 | }, 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /modules/basic_auth_guard/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export { BasicAuthGuard } from './guard.js' 11 | export { basicAuthGuard, basicAuthUserProvider } from './define_config.js' 12 | export { BasicAuthLucidUserProvider } from './user_providers/lucid.js' 13 | -------------------------------------------------------------------------------- /modules/basic_auth_guard/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { HttpContext } from '@adonisjs/core/http' 11 | import type { Exception } from '@adonisjs/core/exceptions' 12 | import type { LucidModel } from '@adonisjs/lucid/types/model' 13 | import type { PROVIDER_REAL_USER } from '../../src/symbols.js' 14 | 15 | /** 16 | * A lucid model with verify credentials method to verify user 17 | * credentials during authentication. 18 | */ 19 | export type LucidAuthenticatable = LucidModel & { 20 | /** 21 | * Verify credentials method should return the user instance 22 | * or throw an exception 23 | */ 24 | verifyCredentials(uid: string, password: string): Promise> 25 | } 26 | 27 | /** 28 | * Options accepted by the user provider that uses a lucid 29 | * model to lookup a user during authentication and verify 30 | * credentials 31 | */ 32 | export type BasicAuthLucidUserProviderOptions = { 33 | /** 34 | * The model to use for users lookup 35 | */ 36 | model: () => Promise<{ default: Model }> 37 | } 38 | 39 | /** 40 | * Guard user is an adapter between the user provider 41 | * and the guard. 42 | * 43 | * The guard is user provider agnostic and therefore it 44 | * needs a adapter to known some basic info about the 45 | * user. 46 | */ 47 | export type BasicAuthGuardUser = { 48 | getId(): string | number | BigInt 49 | getOriginal(): RealUser 50 | } 51 | 52 | /** 53 | * The user provider used by basic auth guard to lookup users 54 | * during authentication 55 | */ 56 | export interface BasicAuthUserProviderContract { 57 | [PROVIDER_REAL_USER]: RealUser 58 | 59 | /** 60 | * Create a user object that acts as an adapter between 61 | * the guard and real user value. 62 | */ 63 | createUserForGuard(user: RealUser): Promise> 64 | 65 | /** 66 | * Verify user credentials and must return an instance of the 67 | * user back or null when the credentials are invalid 68 | */ 69 | verifyCredentials(uid: string, password: string): Promise | null> 70 | } 71 | 72 | /** 73 | * Events emitted by the basic auth guard 74 | */ 75 | export type BasicAuthGuardEvents = { 76 | /** 77 | * Attempting to authenticate the user 78 | */ 79 | 'basic_auth:authentication_attempted': { 80 | ctx: HttpContext 81 | guardName: string 82 | } 83 | 84 | /** 85 | * Authentication was successful 86 | */ 87 | 'basic_auth:authentication_succeeded': { 88 | ctx: HttpContext 89 | guardName: string 90 | user: User 91 | } 92 | 93 | /** 94 | * Authentication failed 95 | */ 96 | 'basic_auth:authentication_failed': { 97 | ctx: HttpContext 98 | guardName: string 99 | error: Exception 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /modules/basic_auth_guard/user_providers/lucid.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { RuntimeException } from '@adonisjs/core/exceptions' 11 | import { PROVIDER_REAL_USER } from '../../../src/symbols.js' 12 | import type { 13 | BasicAuthGuardUser, 14 | LucidAuthenticatable, 15 | BasicAuthUserProviderContract, 16 | BasicAuthLucidUserProviderOptions, 17 | } from '../types.js' 18 | 19 | /** 20 | * Uses a Lucid model to verify access tokens and find a user during 21 | * authentication 22 | */ 23 | export class BasicAuthLucidUserProvider 24 | implements BasicAuthUserProviderContract> 25 | { 26 | declare [PROVIDER_REAL_USER]: InstanceType 27 | 28 | /** 29 | * Reference to the lazily imported model 30 | */ 31 | protected model?: UserModel 32 | 33 | constructor( 34 | /** 35 | * Lucid provider options 36 | */ 37 | protected options: BasicAuthLucidUserProviderOptions 38 | ) {} 39 | 40 | /** 41 | * Imports the model from the provider, returns and caches it 42 | * for further operations. 43 | */ 44 | protected async getModel() { 45 | if (this.model && !('hot' in import.meta)) { 46 | return this.model 47 | } 48 | 49 | const importedModel = await this.options.model() 50 | this.model = importedModel.default 51 | return this.model 52 | } 53 | 54 | /** 55 | * Creates an adapter user for the guard 56 | */ 57 | async createUserForGuard( 58 | user: InstanceType 59 | ): Promise>> { 60 | const model = await this.getModel() 61 | if (user instanceof model === false) { 62 | throw new RuntimeException( 63 | `Invalid user object. It must be an instance of the "${model.name}" model` 64 | ) 65 | } 66 | 67 | return { 68 | getId() { 69 | /** 70 | * Ensure user has a primary key 71 | */ 72 | if (!user.$primaryKeyValue) { 73 | throw new RuntimeException( 74 | `Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null` 75 | ) 76 | } 77 | 78 | return user.$primaryKeyValue 79 | }, 80 | getOriginal() { 81 | return user 82 | }, 83 | } 84 | } 85 | 86 | /** 87 | * Verifies credentials using the underlying model 88 | */ 89 | async verifyCredentials( 90 | uid: string, 91 | password: string 92 | ): Promise> | null> { 93 | const model = await this.getModel() 94 | try { 95 | const user = await model.verifyCredentials(uid, password) 96 | return this.createUserForGuard(user as InstanceType) 97 | } catch { 98 | return null 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /modules/session_guard/define_config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { HttpContext } from '@adonisjs/core/http' 11 | import type { ConfigProvider } from '@adonisjs/core/types' 12 | 13 | import { SessionGuard } from './guard.js' 14 | import type { GuardConfigProvider } from '../../src/types.js' 15 | import { SessionLucidUserProvider } from './user_providers/lucid.js' 16 | import type { 17 | SessionGuardOptions, 18 | LucidAuthenticatable, 19 | SessionUserProviderContract, 20 | SessionLucidUserProviderOptions, 21 | SessionWithTokensUserProviderContract, 22 | } from './types.js' 23 | 24 | /** 25 | * Configures session tokens guard for authentication 26 | */ 27 | export function sessionGuard< 28 | UseRememberTokens extends boolean, 29 | UserProvider extends UseRememberTokens extends true 30 | ? SessionWithTokensUserProviderContract 31 | : SessionUserProviderContract, 32 | >( 33 | config: { 34 | provider: UserProvider | ConfigProvider 35 | } & SessionGuardOptions 36 | ): GuardConfigProvider<(ctx: HttpContext) => SessionGuard> { 37 | return { 38 | async resolver(name, app) { 39 | const emitter = await app.container.make('emitter') 40 | const provider = 41 | 'resolver' in config.provider ? await config.provider.resolver(app) : config.provider 42 | return (ctx) => new SessionGuard(name, ctx, config, emitter as any, provider) 43 | }, 44 | } 45 | } 46 | 47 | /** 48 | * Configures user provider that uses Lucid models to authenticate 49 | * users using sessions 50 | */ 51 | export function sessionUserProvider( 52 | config: SessionLucidUserProviderOptions 53 | ): SessionLucidUserProvider { 54 | return new SessionLucidUserProvider(config) 55 | } 56 | -------------------------------------------------------------------------------- /modules/session_guard/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export { RememberMeToken } from './remember_me_token.js' 11 | export { SessionGuard } from './guard.js' 12 | export { DbRememberMeTokensProvider } from './token_providers/db.js' 13 | export { sessionGuard, sessionUserProvider } from './define_config.js' 14 | export { SessionLucidUserProvider } from './user_providers/lucid.js' 15 | -------------------------------------------------------------------------------- /modules/session_guard/remember_me_token.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { createHash } from 'node:crypto' 11 | import string from '@adonisjs/core/helpers/string' 12 | import { Secret, base64, safeEqual } from '@adonisjs/core/helpers' 13 | 14 | /** 15 | * Remember me token represents an opaque token that can be 16 | * used to automatically login a user without asking them 17 | * to re-login 18 | */ 19 | export class RememberMeToken { 20 | /** 21 | * Decodes a publicly shared token and return the series 22 | * and the token value from it. 23 | * 24 | * Returns null when unable to decode the token because of 25 | * invalid format or encoding. 26 | */ 27 | static decode(value: string): null | { identifier: string; secret: Secret } { 28 | /** 29 | * Ensure value is a string and starts with the prefix. 30 | */ 31 | if (typeof value !== 'string') { 32 | return null 33 | } 34 | 35 | /** 36 | * Remove prefix from the rest of the token. 37 | */ 38 | if (!value) { 39 | return null 40 | } 41 | 42 | const [identifier, ...tokenValue] = value.split('.') 43 | if (!identifier || tokenValue.length === 0) { 44 | return null 45 | } 46 | 47 | const decodedIdentifier = base64.urlDecode(identifier) 48 | const decodedSecret = base64.urlDecode(tokenValue.join('.')) 49 | if (!decodedIdentifier || !decodedSecret) { 50 | return null 51 | } 52 | 53 | return { 54 | identifier: decodedIdentifier, 55 | secret: new Secret(decodedSecret), 56 | } 57 | } 58 | 59 | /** 60 | * Creates a transient token that can be shared with the persistence 61 | * layer. 62 | */ 63 | static createTransientToken( 64 | userId: string | number | BigInt, 65 | size: number, 66 | expiresIn: string | number 67 | ) { 68 | const expiresAt = new Date() 69 | expiresAt.setSeconds(expiresAt.getSeconds() + string.seconds.parse(expiresIn)) 70 | 71 | return { 72 | userId, 73 | expiresAt, 74 | ...this.seed(size), 75 | } 76 | } 77 | 78 | /** 79 | * Creates a secret opaque token and its hash. 80 | */ 81 | static seed(size: number) { 82 | const seed = string.random(size) 83 | const secret = new Secret(seed) 84 | const hash = createHash('sha256').update(secret.release()).digest('hex') 85 | return { secret, hash } 86 | } 87 | 88 | /** 89 | * Identifer is a unique sequence to identify the 90 | * token within database. It should be the 91 | * primary/unique key 92 | */ 93 | identifier: string | number | BigInt 94 | 95 | /** 96 | * Reference to the user id for whom the token 97 | * is generated. 98 | */ 99 | tokenableId: string | number | BigInt 100 | 101 | /** 102 | * The value is a public representation of a token. It is created 103 | * by combining the "identifier"."secret" 104 | */ 105 | value?: Secret 106 | 107 | /** 108 | * Hash is computed from the seed to later verify the validity 109 | * of seed 110 | */ 111 | hash: string 112 | 113 | /** 114 | * Date/time when the token instance was created 115 | */ 116 | createdAt: Date 117 | 118 | /** 119 | * Date/time when the token was updated 120 | */ 121 | updatedAt: Date 122 | 123 | /** 124 | * Timestamp at which the token will expire 125 | */ 126 | expiresAt: Date 127 | 128 | constructor(attributes: { 129 | identifier: string | number | BigInt 130 | tokenableId: string | number | BigInt 131 | hash: string 132 | createdAt: Date 133 | updatedAt: Date 134 | expiresAt: Date 135 | secret?: Secret 136 | }) { 137 | this.identifier = attributes.identifier 138 | this.tokenableId = attributes.tokenableId 139 | this.hash = attributes.hash 140 | this.createdAt = attributes.createdAt 141 | this.updatedAt = attributes.updatedAt 142 | this.expiresAt = attributes.expiresAt 143 | 144 | /** 145 | * Compute value when secret is provided 146 | */ 147 | if (attributes.secret) { 148 | this.value = new Secret( 149 | `${base64.urlEncode(String(this.identifier))}.${base64.urlEncode( 150 | attributes.secret.release() 151 | )}` 152 | ) 153 | } 154 | } 155 | 156 | /** 157 | * Check if the token has been expired. Verifies 158 | * the "expiresAt" timestamp with the current 159 | * date. 160 | */ 161 | isExpired() { 162 | return this.expiresAt < new Date() 163 | } 164 | 165 | /** 166 | * Verifies the value of a token against the pre-defined hash 167 | */ 168 | verify(secret: Secret): boolean { 169 | const newHash = createHash('sha256').update(secret.release()).digest('hex') 170 | return safeEqual(this.hash, newHash) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /modules/session_guard/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Secret } from '@adonisjs/core/helpers' 11 | import type { HttpContext } from '@adonisjs/core/http' 12 | import type { Exception } from '@adonisjs/core/exceptions' 13 | import type { LucidModel } from '@adonisjs/lucid/types/model' 14 | 15 | import { PROVIDER_REAL_USER } from '../../src/symbols.js' 16 | import type { RememberMeToken } from './remember_me_token.js' 17 | 18 | /** 19 | * Options accepted by the tokens provider that uses lucid 20 | * database service to fetch and persist tokens. 21 | */ 22 | export type DbRememberMeTokensProviderOptions = { 23 | /** 24 | * The user model for which to generate tokens. Note, the model 25 | * is not used for tokens, but is used to associate a user 26 | * with the token 27 | */ 28 | tokenableModel: TokenableModel 29 | 30 | /** 31 | * Database table to use for querying tokens. 32 | * 33 | * Defaults to "remember_me_tokens" 34 | */ 35 | table?: string 36 | 37 | /** 38 | * The length for the token secret. A secret is a cryptographically 39 | * secure random string. 40 | * 41 | * Defaults to 40 42 | */ 43 | tokenSecretLength?: number 44 | } 45 | 46 | /** 47 | * Remember me token providers are used verify a remember me 48 | * token during authentication 49 | */ 50 | export interface RememberMeTokensProviderContract { 51 | /** 52 | * Create a token for a given user 53 | */ 54 | create(user: InstanceType, expiresIn: string | number): Promise 55 | 56 | /** 57 | * Verifies the remember me token shared as cookie and returns an 58 | * instance of remember me token 59 | */ 60 | verify(tokenValue: Secret): Promise 61 | 62 | /** 63 | * Delete token for a user by the token identifier. 64 | */ 65 | delete(user: InstanceType, identifier: string | number | BigInt): Promise 66 | 67 | /** 68 | * Recycle an existing token by its id. Recycling tokens helps 69 | * detect compromised tokens. 70 | * https://web.archive.org/web/20130214051957/http://jaspan.com/improved_persistent_login_cookie_best_practice 71 | */ 72 | recycle( 73 | user: InstanceType, 74 | identifier: string | number | BigInt, 75 | expiresIn: string | number 76 | ): Promise 77 | } 78 | 79 | /** 80 | * A lucid model with a tokens provider to verify remember me tokens during 81 | * authentication 82 | */ 83 | export type LucidAuthenticatable = LucidModel & { 84 | rememberMeTokens?: RememberMeTokensProviderContract 85 | } 86 | 87 | /** 88 | * Options accepted by the user provider that uses a lucid 89 | * model to lookup a user during authentication and verify 90 | * tokens 91 | */ 92 | export type SessionLucidUserProviderOptions = { 93 | /** 94 | * The model to use for users lookup 95 | */ 96 | model: () => Promise<{ default: Model }> 97 | } 98 | 99 | /** 100 | * The database columns expected at the database level 101 | */ 102 | export type RememberMeTokenDbColumns = { 103 | /** 104 | * Token primary key. It can be an integer, bigInteger or 105 | * even a UUID or any other string based value. 106 | * 107 | * The id should not have ". (dots)" inside it. 108 | */ 109 | id: number | string | BigInt 110 | 111 | /** 112 | * The user or entity for whom the token is 113 | * generated 114 | */ 115 | tokenable_id: string | number | BigInt 116 | 117 | /** 118 | * Token hash is used to verify the token shared 119 | * with the user 120 | */ 121 | hash: string 122 | 123 | /** 124 | * Timestamps 125 | */ 126 | created_at: Date 127 | updated_at: Date 128 | 129 | /** 130 | * The date after which the token will be considered 131 | * expired. 132 | */ 133 | expires_at: Date 134 | } 135 | 136 | /** 137 | * Guard user is an adapter between the user provider 138 | * and the guard. 139 | * 140 | * The guard is user provider agnostic and therefore it 141 | * needs a adapter to known some basic info about the 142 | * user. 143 | */ 144 | export type SessionGuardUser = { 145 | getId(): string | number | BigInt 146 | getOriginal(): RealUser 147 | } 148 | 149 | /** 150 | * The user provider used by session guard to lookup users 151 | * during authentication 152 | */ 153 | export interface SessionUserProviderContract { 154 | [PROVIDER_REAL_USER]: RealUser 155 | 156 | /** 157 | * Create a user object that acts as an adapter between 158 | * the guard and real user value. 159 | */ 160 | createUserForGuard(user: RealUser): Promise> 161 | 162 | /** 163 | * Find a user by their id. 164 | */ 165 | findById(identifier: string | number | BigInt): Promise | null> 166 | } 167 | 168 | /** 169 | * The user provider used by session guard with support for tokens 170 | */ 171 | export interface SessionWithTokensUserProviderContract 172 | extends SessionUserProviderContract { 173 | /** 174 | * Create a token for a given user. Must be implemented when 175 | * "supportsRememberMeTokens" flag is true 176 | */ 177 | createRememberToken(user: RealUser, expiresIn: string | number): Promise 178 | 179 | /** 180 | * Verify a token by its publicly shared value. Must be implemented when 181 | * "supportsRememberMeTokens" flag is true 182 | */ 183 | verifyRememberToken(tokenValue: Secret): Promise 184 | 185 | /** 186 | * Recycle a token for a user by the token identifier. Must be 187 | * implemented when "supportsRememberMeTokens" flag is true 188 | */ 189 | recycleRememberToken( 190 | user: RealUser, 191 | tokenIdentifier: string | number | BigInt, 192 | expiresIn: string | number 193 | ): Promise 194 | 195 | /** 196 | * Delete a token for a user by the token identifier. Must be 197 | * implemented when "supportsRememberMeTokens" flag is true 198 | */ 199 | deleteRemeberToken(user: RealUser, tokenIdentifier: string | number | BigInt): Promise 200 | } 201 | 202 | /** 203 | * Options accepted by the session guard 204 | */ 205 | export type SessionGuardOptions = { 206 | /** 207 | * Whether or not use remember me tokens during authentication 208 | * and login. 209 | * 210 | * If enabled, the provided user provider must implement the APIs 211 | * needed to manage remember me tokens 212 | */ 213 | useRememberMeTokens: UseRememberTokens 214 | 215 | /** 216 | * The age of remember me tokens after which they 217 | * should expire. 218 | * 219 | * Defaults to "2 years" 220 | */ 221 | rememberMeTokensAge?: string | number 222 | } 223 | 224 | /** 225 | * Events emitted by the session guard 226 | */ 227 | export type SessionGuardEvents = { 228 | /** 229 | * The event is emitted when login is attempted for 230 | * a given user. 231 | */ 232 | 'session_auth:login_attempted': { 233 | ctx: HttpContext 234 | guardName: string 235 | user: User 236 | } 237 | 238 | /** 239 | * The event is emitted when user has been logged in 240 | * successfully 241 | */ 242 | 'session_auth:login_succeeded': { 243 | ctx: HttpContext 244 | guardName: string 245 | user: User 246 | sessionId: string 247 | rememberMeToken?: RememberMeToken 248 | } 249 | 250 | /** 251 | * Attempting to authenticate the user 252 | */ 253 | 'session_auth:authentication_attempted': { 254 | ctx: HttpContext 255 | guardName: string 256 | sessionId: string 257 | } 258 | 259 | /** 260 | * Authentication was successful 261 | */ 262 | 'session_auth:authentication_succeeded': { 263 | ctx: HttpContext 264 | guardName: string 265 | user: User 266 | sessionId: string 267 | rememberMeToken?: RememberMeToken 268 | } 269 | 270 | /** 271 | * Authentication failed 272 | */ 273 | 'session_auth:authentication_failed': { 274 | ctx: HttpContext 275 | guardName: string 276 | error: Exception 277 | sessionId: string 278 | } 279 | 280 | /** 281 | * The event is emitted when user has been logged out 282 | * sucessfully 283 | */ 284 | 'session_auth:logged_out': { 285 | ctx: HttpContext 286 | guardName: string 287 | user: User | null 288 | sessionId: string 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /modules/session_guard/user_providers/lucid.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Secret } from '@adonisjs/core/helpers' 11 | import { RuntimeException } from '@adonisjs/core/exceptions' 12 | 13 | import { RememberMeToken } from '../remember_me_token.js' 14 | import { PROVIDER_REAL_USER } from '../../../src/symbols.js' 15 | import type { 16 | SessionGuardUser, 17 | LucidAuthenticatable, 18 | SessionLucidUserProviderOptions, 19 | SessionUserProviderContract, 20 | } from '../types.js' 21 | 22 | /** 23 | * Uses a lucid model to verify access tokens and find a user during 24 | * authentication 25 | */ 26 | export class SessionLucidUserProvider 27 | implements SessionUserProviderContract> 28 | { 29 | declare [PROVIDER_REAL_USER]: InstanceType 30 | 31 | /** 32 | * Reference to the lazily imported model 33 | */ 34 | protected model?: UserModel 35 | 36 | constructor( 37 | /** 38 | * Lucid provider options 39 | */ 40 | protected options: SessionLucidUserProviderOptions 41 | ) {} 42 | 43 | /** 44 | * Imports the model from the provider, returns and caches it 45 | * for further operations. 46 | */ 47 | protected async getModel() { 48 | if (this.model && !('hot' in import.meta)) { 49 | return this.model 50 | } 51 | 52 | const importedModel = await this.options.model() 53 | this.model = importedModel.default 54 | return this.model 55 | } 56 | 57 | /** 58 | * Returns the tokens provider associated with the user model 59 | */ 60 | protected async getTokensProvider() { 61 | const model = await this.getModel() 62 | 63 | if (!model.rememberMeTokens) { 64 | throw new RuntimeException( 65 | `Cannot use "${model.name}" model for verifying remember me tokens. Make sure to assign a token provider to the model.` 66 | ) 67 | } 68 | 69 | return model.rememberMeTokens 70 | } 71 | 72 | /** 73 | * Creates an adapter user for the guard 74 | */ 75 | async createUserForGuard( 76 | user: InstanceType 77 | ): Promise>> { 78 | const model = await this.getModel() 79 | if (user instanceof model === false) { 80 | throw new RuntimeException( 81 | `Invalid user object. It must be an instance of the "${model.name}" model` 82 | ) 83 | } 84 | 85 | return { 86 | getId() { 87 | /** 88 | * Ensure user has a primary key 89 | */ 90 | if (!user.$primaryKeyValue) { 91 | throw new RuntimeException( 92 | `Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null` 93 | ) 94 | } 95 | 96 | return user.$primaryKeyValue 97 | }, 98 | getOriginal() { 99 | return user 100 | }, 101 | } 102 | } 103 | 104 | /** 105 | * Finds a user by their primary key value 106 | */ 107 | async findById( 108 | identifier: string | number | BigInt 109 | ): Promise> | null> { 110 | const model = await this.getModel() 111 | const user = await model.find(identifier) 112 | 113 | if (!user) { 114 | return null 115 | } 116 | 117 | return this.createUserForGuard(user) 118 | } 119 | 120 | /** 121 | * Creates a remember token for a given user 122 | */ 123 | async createRememberToken( 124 | user: InstanceType, 125 | expiresIn: string | number 126 | ): Promise { 127 | const tokensProvider = await this.getTokensProvider() 128 | return tokensProvider.create(user, expiresIn) 129 | } 130 | 131 | /** 132 | * Verify a token by its publicly shared value 133 | */ 134 | async verifyRememberToken(tokenValue: Secret): Promise { 135 | const tokensProvider = await this.getTokensProvider() 136 | return tokensProvider.verify(tokenValue) 137 | } 138 | 139 | /** 140 | * Delete a token for a user by the token identifier 141 | */ 142 | async deleteRemeberToken( 143 | user: InstanceType, 144 | identifier: string | number | BigInt 145 | ): Promise { 146 | const tokensProvider = await this.getTokensProvider() 147 | return tokensProvider.delete(user, identifier) 148 | } 149 | 150 | /** 151 | * Recycle a token for a user by the token identifier 152 | */ 153 | async recycleRememberToken( 154 | user: InstanceType, 155 | identifier: string | number | BigInt, 156 | expiresIn: string | number 157 | ) { 158 | const tokensProvider = await this.getTokensProvider() 159 | return tokensProvider.recycle(user, identifier, expiresIn) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adonisjs/auth", 3 | "description": "Official authentication provider for Adonis framework", 4 | "version": "9.4.0", 5 | "engines": { 6 | "node": ">=18.16.0" 7 | }, 8 | "main": "build/index.js", 9 | "type": "module", 10 | "files": [ 11 | "build", 12 | "!build/bin", 13 | "!build/tests", 14 | "!build/factories" 15 | ], 16 | "exports": { 17 | ".": "./build/index.js", 18 | "./types": "./build/src/types.js", 19 | "./auth_provider": "./build/providers/auth_provider.js", 20 | "./mixins/lucid": "./build/src/mixins/lucid.js", 21 | "./plugins/api_client": "./build/src/plugins/japa/api_client.js", 22 | "./plugins/browser_client": "./build/src/plugins/japa/browser_client.js", 23 | "./services/main": "./build/services/auth.js", 24 | "./initialize_auth_middleware": "./build/src/middleware/initialize_auth_middleware.js", 25 | "./access_tokens": "./build/modules/access_tokens_guard/main.js", 26 | "./types/access_tokens": "./build/modules/access_tokens_guard/types.js", 27 | "./session": "./build/modules/session_guard/main.js", 28 | "./types/session": "./build/modules/session_guard/types.js", 29 | "./basic_auth": "./build/modules/basic_auth_guard/main.js", 30 | "./types/basic_auth": "./build/modules/basic_auth_guard/types.js" 31 | }, 32 | "scripts": { 33 | "pretest": "npm run lint", 34 | "test": "npm run test:mysql && npm run test:pg && npm run test:mssql && c8 npm run test:sqlite", 35 | "test:sqlite": "npm run quick:test", 36 | "test:mysql": "cross-env DB=mysql npm run quick:test", 37 | "test:mssql": "cross-env DB=mssql npm run quick:test", 38 | "test:pg": "cross-env DB=pg npm run quick:test", 39 | "lint": "eslint .", 40 | "format": "prettier --write .", 41 | "typecheck": "tsc --noEmit", 42 | "clean": "del-cli build", 43 | "copy:templates": "copyfiles \"stubs/**/*.stub\" --up=\"1\" build", 44 | "precompile": "npm run lint && npm run clean", 45 | "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", 46 | "postcompile": "npm run copy:templates", 47 | "build": "npm run compile", 48 | "prepublishOnly": "npm run build", 49 | "release": "release-it", 50 | "version": "npm run build", 51 | "quick:test": "cross-env NODE_DEBUG=\"adonisjs:auth:*\" node --enable-source-maps --import=ts-node-maintained/register/esm ./bin/test.js" 52 | }, 53 | "devDependencies": { 54 | "@adonisjs/assembler": "^7.8.2", 55 | "@adonisjs/core": "^6.17.2", 56 | "@adonisjs/eslint-config": "^2.0.0", 57 | "@adonisjs/hash": "^9.0.5", 58 | "@adonisjs/i18n": "^2.2.0", 59 | "@adonisjs/lucid": "^21.6.0", 60 | "@adonisjs/prettier-config": "^1.4.0", 61 | "@adonisjs/session": "^7.5.1", 62 | "@adonisjs/tsconfig": "^1.4.0", 63 | "@japa/api-client": "^3.0.3", 64 | "@japa/assert": "^4.0.1", 65 | "@japa/browser-client": "^2.1.1", 66 | "@japa/expect-type": "^2.0.3", 67 | "@japa/file-system": "^2.3.2", 68 | "@japa/plugin-adonisjs": "^4.0.0", 69 | "@japa/runner": "^4.2.0", 70 | "@japa/snapshot": "^2.0.8", 71 | "@release-it/conventional-changelog": "^10.0.0", 72 | "@swc/core": "1.10.7", 73 | "@types/basic-auth": "^1.1.8", 74 | "@types/luxon": "^3.4.2", 75 | "@types/node": "^22.13.5", 76 | "@types/set-cookie-parser": "^2.4.10", 77 | "@types/sinon": "^17.0.4", 78 | "c8": "^10.1.3", 79 | "convert-hrtime": "^5.0.0", 80 | "copyfiles": "^2.4.1", 81 | "cross-env": "^7.0.3", 82 | "del-cli": "^6.0.0", 83 | "dotenv": "^16.4.7", 84 | "eslint": "^9.21.0", 85 | "luxon": "^3.5.0", 86 | "mysql2": "^3.12.0", 87 | "nock": "^14.0.1", 88 | "pg": "^8.13.3", 89 | "playwright": "^1.50.1", 90 | "prettier": "^3.5.2", 91 | "release-it": "^18.1.2", 92 | "set-cookie-parser": "^2.7.1", 93 | "sinon": "^19.0.2", 94 | "sqlite3": "^5.1.7", 95 | "tedious": "^18.6.1", 96 | "timekeeper": "^2.3.1", 97 | "ts-node-maintained": "^10.9.5", 98 | "tsup": "^8.3.6", 99 | "typescript": "^5.7.3" 100 | }, 101 | "dependencies": { 102 | "@adonisjs/presets": "^2.6.4", 103 | "@poppinss/utils": "^6.9.2", 104 | "basic-auth": "^2.0.1" 105 | }, 106 | "peerDependencies": { 107 | "@adonisjs/core": "^6.11.0", 108 | "@adonisjs/lucid": "^20.0.0 || ^21.0.1", 109 | "@adonisjs/session": "^7.4.1", 110 | "@japa/api-client": "^2.0.3 || ^3.0.0", 111 | "@japa/browser-client": "^2.0.3", 112 | "@japa/plugin-adonisjs": "^3.0.1 || ^4.0.0" 113 | }, 114 | "peerDependenciesMeta": { 115 | "@adonisjs/lucid": { 116 | "optional": true 117 | }, 118 | "@adonisjs/session": { 119 | "optional": true 120 | }, 121 | "@japa/api-client": { 122 | "optional": true 123 | }, 124 | "@japa/browser-client": { 125 | "optional": true 126 | }, 127 | "@japa/plugin-adonisjs": { 128 | "optional": true 129 | } 130 | }, 131 | "homepage": "https://github.com/adonisjs/auth#readme", 132 | "repository": { 133 | "type": "git", 134 | "url": "git+https://github.com/adonisjs/auth.git" 135 | }, 136 | "bugs": { 137 | "url": "https://github.com/adonisjs/auth/issues" 138 | }, 139 | "keywords": [ 140 | "adonisjs", 141 | "authentication", 142 | "auth" 143 | ], 144 | "author": "Harminder Virk ", 145 | "contributors": [ 146 | "Romain Lanz " 147 | ], 148 | "license": "MIT", 149 | "publishConfig": { 150 | "access": "public", 151 | "provenance": true 152 | }, 153 | "release-it": { 154 | "git": { 155 | "requireCleanWorkingDir": true, 156 | "requireUpstream": true, 157 | "commitMessage": "chore(release): ${version}", 158 | "tagAnnotation": "v${version}", 159 | "push": true, 160 | "tagName": "v${version}" 161 | }, 162 | "github": { 163 | "release": true 164 | }, 165 | "npm": { 166 | "publish": true, 167 | "skipChecks": true 168 | }, 169 | "plugins": { 170 | "@release-it/conventional-changelog": { 171 | "preset": { 172 | "name": "angular" 173 | } 174 | } 175 | } 176 | }, 177 | "tsup": { 178 | "entry": [ 179 | "./index.ts", 180 | "./src/types.ts", 181 | "./providers/auth_provider.ts", 182 | "./src/plugins/japa/api_client.ts", 183 | "./src/plugins/japa/browser_client.ts", 184 | "./services/auth.ts", 185 | "./src/mixins/lucid.ts", 186 | "./src/middleware/initialize_auth_middleware.ts", 187 | "./modules/access_tokens_guard/main.ts", 188 | "./modules/access_tokens_guard/types.ts", 189 | "./modules/session_guard/main.ts", 190 | "./modules/session_guard/types.ts", 191 | "./modules/basic_auth_guard/main.ts", 192 | "./modules/basic_auth_guard/types.ts" 193 | ], 194 | "outDir": "./build", 195 | "clean": true, 196 | "format": "esm", 197 | "dts": false, 198 | "sourcemap": false, 199 | "target": "esnext" 200 | }, 201 | "c8": { 202 | "reporter": [ 203 | "text", 204 | "html" 205 | ], 206 | "exclude": [ 207 | "tests/**", 208 | "backup/**", 209 | "factories/**" 210 | ] 211 | }, 212 | "prettier": "@adonisjs/prettier-config" 213 | } 214 | -------------------------------------------------------------------------------- /providers/auth_provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { configProvider } from '@adonisjs/core' 11 | import { RuntimeException } from '@poppinss/utils' 12 | import type { ApplicationService } from '@adonisjs/core/types' 13 | 14 | import type { AuthService } from '../src/types.js' 15 | import { AuthManager } from '../src/auth_manager.js' 16 | 17 | declare module '@adonisjs/core/types' { 18 | export interface ContainerBindings { 19 | 'auth.manager': AuthService 20 | } 21 | } 22 | 23 | export default class AuthProvider { 24 | constructor(protected app: ApplicationService) {} 25 | 26 | register() { 27 | this.app.container.singleton('auth.manager', async () => { 28 | const authConfigProvider = this.app.config.get('auth') 29 | const config = await configProvider.resolve(this.app, authConfigProvider) 30 | 31 | if (!config) { 32 | throw new RuntimeException( 33 | 'Invalid config exported from "config/auth.ts" file. Make sure to use the defineConfig method' 34 | ) 35 | } 36 | 37 | return new AuthManager(config) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /services/auth.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import app from '@adonisjs/core/services/app' 11 | import { AuthService } from '../src/types.js' 12 | 13 | let auth: AuthService 14 | 15 | /** 16 | * Returns a singleton instance of the Auth manager class 17 | */ 18 | await app.booted(async () => { 19 | auth = await app.container.make('auth.manager') 20 | }) 21 | 22 | export { auth as default } 23 | -------------------------------------------------------------------------------- /src/auth_manager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { HttpContext } from '@adonisjs/core/http' 11 | 12 | import type { GuardFactory } from './types.js' 13 | import { Authenticator } from './authenticator.js' 14 | import { AuthenticatorClient } from './authenticator_client.js' 15 | 16 | /** 17 | * Auth manager exposes the API to register and manage authentication 18 | * guards from the config 19 | */ 20 | export class AuthManager> { 21 | /** 22 | * Name of the default guard 23 | */ 24 | get defaultGuard() { 25 | return this.config.default 26 | } 27 | 28 | constructor(public config: { default: keyof KnownGuards; guards: KnownGuards }) { 29 | this.config = config 30 | } 31 | 32 | /** 33 | * Create an authenticator for a given HTTP request. The authenticator 34 | * is used to authenticated in incoming HTTP request 35 | */ 36 | createAuthenticator(ctx: HttpContext) { 37 | return new Authenticator(ctx, this.config) 38 | } 39 | 40 | /** 41 | * Creates an instance of the authenticator client. The client is 42 | * used to setup authentication state during testing. 43 | */ 44 | createAuthenticatorClient() { 45 | return new AuthenticatorClient(this.config) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/authenticator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { HttpContext } from '@adonisjs/core/http' 11 | import { RuntimeException } from '@adonisjs/core/exceptions' 12 | 13 | import debug from './debug.js' 14 | import type { GuardFactory } from './types.js' 15 | import { E_UNAUTHORIZED_ACCESS } from './errors.js' 16 | 17 | /** 18 | * Authenticator is used to authenticate incoming HTTP requests 19 | * using one or more known guards. 20 | */ 21 | export class Authenticator> { 22 | /** 23 | * Registered guards 24 | */ 25 | #config: { 26 | default: keyof KnownGuards 27 | guards: KnownGuards 28 | } 29 | 30 | /** 31 | * Cache of guards created during the HTTP request 32 | */ 33 | #guardsCache: Partial> = {} 34 | 35 | /** 36 | * Last guard that was used to perform the authentication via 37 | * the "authenticateUsing" method. 38 | * 39 | * @note 40 | * Reset on every call made to "authenticate", "check" and 41 | * "authenticateUsing" method. 42 | */ 43 | #authenticationAttemptedViaGuard?: keyof KnownGuards 44 | 45 | /** 46 | * Name of the guard using which the request has 47 | * been authenticated successfully. 48 | * 49 | * @note 50 | * Reset on every call made to "authenticate", "check" and 51 | * "authenticateUsing" method. 52 | */ 53 | #authenticatedViaGuard?: keyof KnownGuards 54 | 55 | /** 56 | * Reference to HTTP context 57 | */ 58 | #ctx: HttpContext 59 | 60 | /** 61 | * Name of the default guard 62 | */ 63 | get defaultGuard(): keyof KnownGuards { 64 | return this.#config.default 65 | } 66 | 67 | /** 68 | * Reference to the guard using which the current 69 | * request has been authenticated. 70 | */ 71 | get authenticatedViaGuard(): keyof KnownGuards | undefined { 72 | return this.#authenticatedViaGuard 73 | } 74 | 75 | /** 76 | * A boolean to know if the current request has been authenticated. The 77 | * property returns false when "authenticate" or "authenticateUsing" 78 | * methods are not used. 79 | */ 80 | get isAuthenticated(): boolean { 81 | if (!this.#authenticationAttemptedViaGuard) { 82 | return false 83 | } 84 | 85 | return this.use(this.#authenticationAttemptedViaGuard).isAuthenticated 86 | } 87 | 88 | /** 89 | * Reference to the currently authenticated user. The property returns 90 | * undefined when "authenticate" or "authenticateUsing" methods are 91 | * not used. 92 | */ 93 | get user(): { 94 | [K in keyof KnownGuards]: ReturnType['user'] 95 | }[keyof KnownGuards] { 96 | if (!this.#authenticationAttemptedViaGuard) { 97 | return undefined 98 | } 99 | 100 | return this.use(this.#authenticationAttemptedViaGuard).user 101 | } 102 | 103 | /** 104 | * Whether or not the authentication has been attempted during 105 | * the current request. The property returns false when the 106 | * "authenticate" or "authenticateUsing" methods are not 107 | * used. 108 | */ 109 | get authenticationAttempted(): boolean { 110 | if (!this.#authenticationAttemptedViaGuard) { 111 | return false 112 | } 113 | 114 | return this.use(this.#authenticationAttemptedViaGuard).authenticationAttempted 115 | } 116 | 117 | constructor(ctx: HttpContext, config: { default: keyof KnownGuards; guards: KnownGuards }) { 118 | this.#ctx = ctx 119 | this.#config = config 120 | debug('creating authenticator. config %O', this.#config) 121 | } 122 | 123 | /** 124 | * Returns an instance of the logged-in user or throws an 125 | * exception 126 | */ 127 | getUserOrFail(): { 128 | [K in keyof KnownGuards]: ReturnType['getUserOrFail']> 129 | }[keyof KnownGuards] { 130 | if (!this.#authenticationAttemptedViaGuard) { 131 | throw new RuntimeException( 132 | 'Cannot access authenticated user. Please call "auth.authenticate" method first.' 133 | ) 134 | } 135 | 136 | return this.use(this.#authenticationAttemptedViaGuard).getUserOrFail() as { 137 | [K in keyof KnownGuards]: ReturnType['getUserOrFail']> 138 | }[keyof KnownGuards] 139 | } 140 | 141 | /** 142 | * Returns an instance of a known guard. Guards instances are 143 | * cached during the lifecycle of an HTTP request. 144 | */ 145 | use(guard?: Guard): ReturnType { 146 | const guardToUse = guard || this.#config.default 147 | 148 | /** 149 | * Use cached copy if exists 150 | */ 151 | const cachedGuard = this.#guardsCache[guardToUse] 152 | if (cachedGuard) { 153 | debug('authenticator: using guard from cache. name: "%s"', guardToUse) 154 | return cachedGuard as ReturnType 155 | } 156 | 157 | const guardFactory = this.#config.guards[guardToUse] 158 | 159 | /** 160 | * Construct guard and cache it 161 | */ 162 | debug('authenticator: creating guard. name: "%s"', guardToUse) 163 | const guardInstance = guardFactory(this.#ctx) 164 | this.#guardsCache[guardToUse] = guardInstance 165 | 166 | return guardInstance as ReturnType 167 | } 168 | 169 | /** 170 | * Authenticate current request using the default guard. Calling this 171 | * method multiple times triggers multiple authentication with the 172 | * guard. 173 | */ 174 | async authenticate() { 175 | await this.authenticateUsing() 176 | return this.getUserOrFail() 177 | } 178 | 179 | /** 180 | * Silently attempt to authenticate the request using the default 181 | * guard. Calling this method multiple times triggers multiple 182 | * authentication with the guard. 183 | */ 184 | async check() { 185 | this.#authenticationAttemptedViaGuard = this.defaultGuard 186 | const isAuthenticated = await this.use().check() 187 | if (isAuthenticated) { 188 | this.#authenticatedViaGuard = this.defaultGuard 189 | } 190 | 191 | return isAuthenticated 192 | } 193 | 194 | /** 195 | * Authenticate the request using all of the mentioned guards 196 | * or the default guard. 197 | * 198 | * The authentication process will stop after any of the mentioned 199 | * guards is able to authenticate the request successfully. 200 | * 201 | * Otherwise, "E_UNAUTHORIZED_ACCESS" will be raised. 202 | */ 203 | async authenticateUsing( 204 | guards?: (keyof KnownGuards)[], 205 | options?: { loginRoute?: string } 206 | ): Promise< 207 | { 208 | [K in keyof KnownGuards]: ReturnType['getUserOrFail']> 209 | }[keyof KnownGuards] 210 | > { 211 | const guardsToUse = guards || [this.defaultGuard] 212 | let lastUsedDriver: string | undefined 213 | 214 | for (let guardName of guardsToUse) { 215 | debug('attempting to authenticate using guard "%s"', guardName) 216 | 217 | this.#authenticationAttemptedViaGuard = guardName 218 | const guard = this.use(guardName) 219 | lastUsedDriver = guard.driverName 220 | 221 | if (await guard.check()) { 222 | this.#authenticatedViaGuard = guardName 223 | return this.getUserOrFail() 224 | } 225 | } 226 | 227 | throw new E_UNAUTHORIZED_ACCESS('Unauthorized access', { 228 | guardDriverName: lastUsedDriver!, 229 | redirectTo: options?.loginRoute, 230 | }) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/authenticator_client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import debug from './debug.js' 11 | import type { GuardFactory } from './types.js' 12 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 13 | 14 | /** 15 | * Authenticator client is used to create guard instances for testing. 16 | * It passes a fake HTTPContext to the guards, so make sure to not 17 | * call server side APIs that might be relying on a real 18 | * HTTPContext instance. 19 | */ 20 | export class AuthenticatorClient> { 21 | /** 22 | * Registered guards 23 | */ 24 | #config: { 25 | default: keyof KnownGuards 26 | guards: KnownGuards 27 | } 28 | 29 | /** 30 | * Cache of guards 31 | */ 32 | #guardsCache: Partial> = {} 33 | 34 | /** 35 | * Name of the default guard 36 | */ 37 | get defaultGuard(): keyof KnownGuards { 38 | return this.#config.default 39 | } 40 | 41 | constructor(config: { default: keyof KnownGuards; guards: KnownGuards }) { 42 | this.#config = config 43 | debug('creating authenticator client. config %O', this.#config) 44 | } 45 | 46 | /** 47 | * Returns an instance of a known guard. Guards instances are 48 | * cached during the lifecycle of an HTTP request. 49 | */ 50 | use(guard?: Guard): ReturnType { 51 | const guardToUse = guard || this.#config.default 52 | 53 | /** 54 | * Use cached copy if exists 55 | */ 56 | const cachedGuard = this.#guardsCache[guardToUse] 57 | if (cachedGuard) { 58 | debug('authenticator client: using guard from cache. name: "%s"', guardToUse) 59 | return cachedGuard as ReturnType 60 | } 61 | 62 | const guardFactory = this.#config.guards[guardToUse] 63 | 64 | /** 65 | * Construct guard and cache it 66 | */ 67 | debug('authenticator client: creating guard. name: "%s"', guardToUse) 68 | const guardInstance = guardFactory(new HttpContextFactory().create()) 69 | this.#guardsCache[guardToUse] = guardInstance 70 | 71 | return guardInstance as ReturnType 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { debuglog } from 'node:util' 11 | 12 | export default debuglog('adonisjs:auth') 13 | -------------------------------------------------------------------------------- /src/define_config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { configProvider } from '@adonisjs/core' 11 | import type { ConfigProvider } from '@adonisjs/core/types' 12 | import type { GuardConfigProvider, GuardFactory } from './types.js' 13 | 14 | /** 15 | * Config resolved by the "defineConfig" method 16 | */ 17 | export type ResolvedAuthConfig< 18 | KnownGuards extends Record>, 19 | > = { 20 | default: keyof KnownGuards 21 | guards: { 22 | [K in keyof KnownGuards]: KnownGuards[K] extends GuardConfigProvider 23 | ? A 24 | : KnownGuards[K] 25 | } 26 | } 27 | 28 | /** 29 | * Define configuration for the auth package. The function returns 30 | * a config provider that is invoked inside the auth service 31 | * provider 32 | */ 33 | export function defineConfig< 34 | KnownGuards extends Record>, 35 | >(config: { 36 | default: keyof KnownGuards 37 | guards: KnownGuards 38 | }): ConfigProvider> { 39 | return configProvider.create(async (app) => { 40 | const guardsList = Object.keys(config.guards) 41 | const guards = {} as Record 42 | 43 | for (let guardName of guardsList) { 44 | const guard = config.guards[guardName] 45 | if (typeof guard === 'function') { 46 | guards[guardName] = guard 47 | } else { 48 | guards[guardName] = await guard.resolver(guardName, app) 49 | } 50 | } 51 | 52 | return { 53 | default: config.default, 54 | guards: guards, 55 | } as ResolvedAuthConfig 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { I18n } from '@adonisjs/i18n' 11 | import { Exception } from '@adonisjs/core/exceptions' 12 | import type { HttpContext } from '@adonisjs/core/http' 13 | 14 | /** 15 | * The "E_UNAUTHORIZED_ACCESS" exception is raised when unable to 16 | * authenticate an incoming HTTP request. 17 | * 18 | * The "error.guardDriverName" can be used to know the driver which 19 | * raised the error. 20 | */ 21 | export const E_UNAUTHORIZED_ACCESS = class extends Exception { 22 | static status: number = 401 23 | static code: string = 'E_UNAUTHORIZED_ACCESS' 24 | 25 | /** 26 | * Endpoint to redirect to. Only used by "session" driver 27 | * renderer 28 | */ 29 | redirectTo?: string 30 | 31 | /** 32 | * Translation identifier. Can be customized 33 | */ 34 | identifier: string = 'errors.E_UNAUTHORIZED_ACCESS' 35 | 36 | /** 37 | * The guard name reference that raised the exception. It allows 38 | * us to customize the logic of handling the exception. 39 | */ 40 | guardDriverName: string 41 | 42 | /** 43 | * A collection of renderers to render the exception to a 44 | * response. 45 | * 46 | * The collection is a key-value pair, where the key is 47 | * the guard driver name and value is a factory function 48 | * to respond to the request. 49 | */ 50 | renderers: Record< 51 | string, 52 | (message: string, error: this, ctx: HttpContext) => Promise | void 53 | > = { 54 | /** 55 | * Response when session driver is used 56 | */ 57 | session: (message, error, ctx) => { 58 | switch (ctx.request.accepts(['html', 'application/vnd.api+json', 'json'])) { 59 | case 'html': 60 | case null: 61 | ctx.session.flashExcept(['_csrf']) 62 | ctx.session.flashErrors({ [error.code!]: message }) 63 | ctx.response.redirect(error.redirectTo || '/', true) 64 | break 65 | case 'json': 66 | ctx.response.status(error.status).send({ 67 | errors: [ 68 | { 69 | message, 70 | }, 71 | ], 72 | }) 73 | break 74 | case 'application/vnd.api+json': 75 | ctx.response.status(error.status).send({ 76 | errors: [ 77 | { 78 | code: error.code, 79 | title: message, 80 | }, 81 | ], 82 | }) 83 | break 84 | } 85 | }, 86 | 87 | /** 88 | * Response when basic auth driver is used 89 | */ 90 | basic_auth: (message, _, ctx) => { 91 | ctx.response 92 | .status(this.status) 93 | .header('WWW-Authenticate', `Basic realm="Authenticate", charset="UTF-8"`) 94 | .send(message) 95 | }, 96 | 97 | /** 98 | * Response when access tokens driver is used 99 | */ 100 | access_tokens: (message, error, ctx) => { 101 | switch (ctx.request.accepts(['html', 'application/vnd.api+json', 'json'])) { 102 | case 'html': 103 | case null: 104 | ctx.response.status(error.status).send(message) 105 | break 106 | case 'json': 107 | ctx.response.status(error.status).send({ 108 | errors: [ 109 | { 110 | message, 111 | }, 112 | ], 113 | }) 114 | break 115 | case 'application/vnd.api+json': 116 | ctx.response.status(error.status).send({ 117 | errors: [ 118 | { 119 | code: error.code, 120 | title: message, 121 | }, 122 | ], 123 | }) 124 | break 125 | } 126 | }, 127 | } 128 | 129 | /** 130 | * Returns the message to be sent in the HTTP response. 131 | * Feel free to override this method and return a custom 132 | * response. 133 | */ 134 | getResponseMessage(error: this, ctx: HttpContext) { 135 | if ('i18n' in ctx) { 136 | return (ctx.i18n as I18n).t(error.identifier, {}, error.message) 137 | } 138 | return error.message 139 | } 140 | 141 | constructor( 142 | message: string, 143 | options: { 144 | redirectTo?: string 145 | guardDriverName: string 146 | } 147 | ) { 148 | super(message, {}) 149 | this.guardDriverName = options.guardDriverName 150 | this.redirectTo = options.redirectTo 151 | } 152 | 153 | /** 154 | * Converts exception to an HTTP response 155 | */ 156 | async handle(error: this, ctx: HttpContext) { 157 | const renderer = this.renderers[this.guardDriverName] 158 | const message = error.getResponseMessage(error, ctx) 159 | 160 | if (!renderer) { 161 | return ctx.response.status(error.status).send(message) 162 | } 163 | 164 | return renderer(message, error, ctx) 165 | } 166 | } 167 | 168 | /** 169 | * Exception is raised when user credentials are invalid 170 | */ 171 | export const E_INVALID_CREDENTIALS = class extends Exception { 172 | static status: number = 400 173 | static code: string = 'E_INVALID_CREDENTIALS' 174 | 175 | /** 176 | * Translation identifier. Can be customized 177 | */ 178 | identifier: string = 'errors.E_INVALID_CREDENTIALS' 179 | 180 | /** 181 | * Returns the message to be sent in the HTTP response. 182 | * Feel free to override this method and return a custom 183 | * response. 184 | */ 185 | getResponseMessage(error: this, ctx: HttpContext) { 186 | if ('i18n' in ctx) { 187 | return (ctx.i18n as I18n).t(error.identifier, {}, error.message) 188 | } 189 | return error.message 190 | } 191 | 192 | /** 193 | * Converts exception to an HTTP response 194 | */ 195 | async handle(error: this, ctx: HttpContext) { 196 | const message = this.getResponseMessage(error, ctx) 197 | 198 | switch (ctx.request.accepts(['html', 'application/vnd.api+json', 'json'])) { 199 | case 'html': 200 | case null: 201 | if (ctx.session) { 202 | ctx.session.flashExcept(['_csrf', '_method', 'password', 'password_confirmation']) 203 | ctx.session.flashErrors({ [error.code!]: message }) 204 | ctx.response.redirect('back', true) 205 | } else { 206 | ctx.response.status(error.status).send(message) 207 | } 208 | break 209 | case 'json': 210 | ctx.response.status(error.status).send({ 211 | errors: [ 212 | { 213 | message, 214 | }, 215 | ], 216 | }) 217 | break 218 | case 'application/vnd.api+json': 219 | ctx.response.status(error.status).send({ 220 | errors: [ 221 | { 222 | code: error.code, 223 | title: message, 224 | }, 225 | ], 226 | }) 227 | break 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/middleware/initialize_auth_middleware.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type { HttpContext } from '@adonisjs/core/http' 4 | import type { NextFn } from '@adonisjs/core/types/http' 5 | 6 | import type { Authenticator } from '../authenticator.js' 7 | import type { Authenticators, GuardFactory } from '../types.js' 8 | 9 | /** 10 | * The "InitializeAuthMiddleware" is used to create a request 11 | * specific authenticator instance for every HTTP request. 12 | * 13 | * This middleware does not protect routes from unauthenticated 14 | * users. Please use the "auth" middleware for that. 15 | */ 16 | export default class InitializeAuthMiddleware { 17 | async handle(ctx: HttpContext, next: NextFn) { 18 | const auth = await ctx.containerResolver.make('auth.manager') 19 | 20 | /** 21 | * Initialize the authenticator for the current HTTP 22 | * request 23 | */ 24 | ctx.auth = auth.createAuthenticator(ctx) 25 | 26 | /** 27 | * Sharing authenticator with templates 28 | */ 29 | if ('view' in ctx) { 30 | ctx.view.share({ auth: ctx.auth }) 31 | } 32 | 33 | return next() 34 | } 35 | } 36 | 37 | declare module '@adonisjs/core/http' { 38 | export interface HttpContext { 39 | auth: Authenticator< 40 | Authenticators extends Record ? Authenticators : never 41 | > 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/mixins/lucid.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Hash } from '@adonisjs/core/hash' 11 | import { RuntimeException } from '@adonisjs/core/exceptions' 12 | import { beforeSave, type BaseModel } from '@adonisjs/lucid/orm' 13 | import type { NormalizeConstructor } from '@adonisjs/core/types/helpers' 14 | import { E_INVALID_CREDENTIALS } from '../errors.js' 15 | 16 | type UserWithUserFinderRow = { 17 | verifyPassword(plainPassword: string): Promise 18 | } 19 | 20 | type UserWithUserFinderClass< 21 | Model extends NormalizeConstructor = NormalizeConstructor, 22 | > = Model & { 23 | hashPassword(this: T, user: InstanceType): Promise 24 | findForAuth( 25 | this: T, 26 | uids: string[], 27 | value: string 28 | ): Promise | null> 29 | verifyCredentials( 30 | this: T, 31 | uid: string, 32 | password: string 33 | ): Promise> 34 | new (...args: any[]): UserWithUserFinderRow 35 | } 36 | 37 | /** 38 | * Mixing to add user lookup and password verification methods 39 | * on a model. 40 | * 41 | * Under the hood, this mixin defines following methods and hooks 42 | * 43 | * - beforeSave hook to hash user password 44 | * - findForAuth method to find a user during authentication 45 | * - verifyCredentials method to verify user credentials and prevent 46 | * timing attacks. 47 | */ 48 | export function withAuthFinder( 49 | hash: () => Hash, 50 | options: { 51 | uids: string[] 52 | passwordColumnName: string 53 | } 54 | ) { 55 | return function >( 56 | superclass: Model 57 | ): UserWithUserFinderClass { 58 | class UserWithUserFinder extends superclass { 59 | /** 60 | * Hook to verify user password when creating or updating 61 | * the user model. 62 | */ 63 | @beforeSave() 64 | static async hashPassword(this: T, user: InstanceType) { 65 | if (user.$dirty[options.passwordColumnName]) { 66 | ;(user as any)[options.passwordColumnName] = await hash().make( 67 | (user as any)[options.passwordColumnName] 68 | ) 69 | } 70 | } 71 | 72 | /** 73 | * Finds the user for authentication via "verifyCredentials". 74 | * Feel free to override this method customize the user 75 | * lookup behavior. 76 | */ 77 | static findForAuth( 78 | this: T, 79 | uids: string[], 80 | value: string 81 | ): Promise | null> { 82 | const query = this.query() 83 | uids.forEach((uid) => query.orWhere(uid, value)) 84 | return query.limit(1).first() 85 | } 86 | 87 | /** 88 | * Find a user by uid and verify their password. This method is 89 | * safe from timing attacks. 90 | */ 91 | static async verifyCredentials( 92 | this: T, 93 | uid: string, 94 | password: string 95 | ) { 96 | /** 97 | * Fail when uid or the password are missing 98 | */ 99 | if (!uid || !password) { 100 | throw new E_INVALID_CREDENTIALS('Invalid user credentials') 101 | } 102 | 103 | const user = await this.findForAuth(options.uids, uid) 104 | if (!user) { 105 | await hash().make(password) 106 | throw new E_INVALID_CREDENTIALS('Invalid user credentials') 107 | } 108 | 109 | if (await user.verifyPassword(password)) { 110 | return user 111 | } 112 | 113 | throw new E_INVALID_CREDENTIALS('Invalid user credentials') 114 | } 115 | 116 | /** 117 | * Verifies the plain password against the user's password 118 | * hash 119 | */ 120 | verifyPassword(plainPassword: string): Promise { 121 | const passwordHash = (this as any)[options.passwordColumnName] 122 | if (!passwordHash) { 123 | throw new RuntimeException( 124 | `Cannot verify password. The value for "${options.passwordColumnName}" column is undefined or null` 125 | ) 126 | } 127 | return hash().verify(passwordHash, plainPassword) 128 | } 129 | } 130 | 131 | return UserWithUserFinder 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/plugins/japa/api_client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /// 11 | 12 | import type { PluginFn } from '@japa/runner/types' 13 | import { ApiClient, ApiRequest } from '@japa/api-client' 14 | import type { ApplicationService } from '@adonisjs/core/types' 15 | 16 | import debug from '../../debug.js' 17 | import type { Authenticators, GuardContract, GuardFactory } from '../../types.js' 18 | 19 | declare module '@japa/api-client' { 20 | export interface ApiRequest { 21 | authData: { 22 | guard: keyof Authenticators | '__default__' 23 | args: [unknown, ...any[]] 24 | } 25 | 26 | /** 27 | * Login a user using the default authentication guard 28 | * when making an API call 29 | */ 30 | loginAs( 31 | ...args: { 32 | [K in keyof Authenticators]: Authenticators[K] extends GuardFactory 33 | ? ReturnType extends GuardContract 34 | ? Parameters['authenticateAsClient']> 35 | : never 36 | : never 37 | }[keyof Authenticators] 38 | ): this 39 | 40 | /** 41 | * Define the authentication guard for login 42 | */ 43 | withGuard( 44 | this: Self, 45 | guard: K 46 | ): { 47 | /** 48 | * Login a user using a specific auth guard 49 | */ 50 | loginAs( 51 | ...args: ReturnType extends GuardContract 52 | ? Parameters['authenticateAsClient']> 53 | : never 54 | ): Self 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Auth API client to authenticate users when making 61 | * HTTP requests using the Japa API client 62 | */ 63 | export const authApiClient = (app: ApplicationService) => { 64 | const pluginFn: PluginFn = function () { 65 | debug('installing auth api client plugin') 66 | 67 | /** 68 | * Login a user using the default authentication guard 69 | * when making an API call 70 | */ 71 | ApiRequest.macro('loginAs', function (this: ApiRequest, user, ...args: any[]) { 72 | this.authData = { 73 | guard: '__default__', 74 | args: [user, ...args], 75 | } 76 | return this 77 | }) 78 | 79 | /** 80 | * Define the authentication guard for login 81 | */ 82 | ApiRequest.macro('withGuard', function < 83 | K extends keyof Authenticators, 84 | Self extends ApiRequest, 85 | >(this: Self, guard: K) { 86 | return { 87 | loginAs: (...args) => { 88 | this.authData = { 89 | guard, 90 | args: args, 91 | } 92 | return this 93 | }, 94 | } 95 | }) 96 | 97 | /** 98 | * Hook into the request and login the user 99 | */ 100 | ApiClient.setup(async (request) => { 101 | const auth = await app.container.make('auth.manager') 102 | const authData = request['authData'] 103 | if (!authData) { 104 | return 105 | } 106 | 107 | const client = auth.createAuthenticatorClient() 108 | const guard = authData.guard === '__default__' ? client.use() : client.use(authData.guard) 109 | const requestData = await (guard as GuardContract).authenticateAsClient( 110 | ...authData.args 111 | ) 112 | 113 | /* c8 ignore next 13 */ 114 | if (requestData.headers) { 115 | debug('defining headers with api client request %O', requestData.headers) 116 | request.headers(requestData.headers) 117 | } 118 | if (requestData.session) { 119 | debug('defining session with api client request %O', requestData.session) 120 | request.withSession(requestData.session) 121 | } 122 | if (requestData.cookies) { 123 | debug('defining session with api client request %O', requestData.session) 124 | request.cookies(requestData.cookies) 125 | } 126 | }) 127 | } 128 | 129 | return pluginFn 130 | } 131 | -------------------------------------------------------------------------------- /src/plugins/japa/browser_client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adoniss/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /// 11 | /// 12 | 13 | import type { PluginFn } from '@japa/runner/types' 14 | import { decoratorsCollection } from '@japa/browser-client' 15 | import { RuntimeException } from '@adonisjs/core/exceptions' 16 | import type { ApplicationService } from '@adonisjs/core/types' 17 | 18 | import debug from '../../debug.js' 19 | import type { Authenticators, GuardContract, GuardFactory } from '../../types.js' 20 | 21 | declare module 'playwright' { 22 | export interface BrowserContext { 23 | /** 24 | * Login a user using the default authentication guard when 25 | * using the browser context to make page visits 26 | */ 27 | loginAs( 28 | ...args: { 29 | [K in keyof Authenticators]: Authenticators[K] extends GuardFactory 30 | ? ReturnType extends GuardContract 31 | ? Parameters['authenticateAsClient']> 32 | : never 33 | : never 34 | }[keyof Authenticators] 35 | ): Promise 36 | 37 | /** 38 | * Define the authentication guard for login 39 | */ 40 | withGuard( 41 | guard: K 42 | ): { 43 | /** 44 | * Login a user using a specific auth guard 45 | */ 46 | loginAs( 47 | ...args: ReturnType extends GuardContract 48 | ? Parameters['authenticateAsClient']> 49 | : never 50 | ): Promise 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Browser API client to authenticate users when making 57 | * HTTP requests using the Japa Browser client. 58 | */ 59 | export const authBrowserClient = (app: ApplicationService) => { 60 | const pluginFn: PluginFn = async function () { 61 | debug('installing auth browser client plugin') 62 | 63 | const auth = await app.container.make('auth.manager') 64 | 65 | decoratorsCollection.register({ 66 | context(context) { 67 | /** 68 | * Define the authentication guard for login and perform 69 | * login 70 | */ 71 | context.withGuard = function (guardName) { 72 | return { 73 | async loginAs(...args) { 74 | const client = auth.createAuthenticatorClient() 75 | const guard = client.use(guardName) as GuardContract 76 | const requestData = await guard.authenticateAsClient( 77 | ...(args as [user: unknown, ...any[]]) 78 | ) 79 | 80 | /* c8 ignore next 17 */ 81 | if (requestData.headers) { 82 | throw new RuntimeException( 83 | `Cannot use "${guard.driverName}" guard with browser client` 84 | ) 85 | } 86 | 87 | if (requestData.cookies) { 88 | debug('defining cookies with browser context %O', requestData.cookies) 89 | Object.keys(requestData.cookies).forEach((cookie) => { 90 | context.setCookie(cookie, requestData.cookies![cookie]) 91 | }) 92 | } 93 | 94 | if (requestData.session) { 95 | debug('defining session with browser context %O', requestData.session) 96 | context.setSession(requestData.session) 97 | } 98 | }, 99 | } 100 | } 101 | 102 | /** 103 | * Login a user using the default authentication guard when 104 | * using the browser context to make page visits 105 | */ 106 | context.loginAs = async function (user, ...args) { 107 | const client = auth.createAuthenticatorClient() 108 | const guard = client.use() as GuardContract 109 | const requestData = await guard.authenticateAsClient(user, ...args) 110 | 111 | /* c8 ignore next 15 */ 112 | if (requestData.headers) { 113 | throw new RuntimeException(`Cannot use "${guard.driverName}" guard with browser client`) 114 | } 115 | 116 | if (requestData.cookies) { 117 | debug('defining cookies with browser context %O', requestData.cookies) 118 | Object.keys(requestData.cookies).forEach((cookie) => { 119 | context.setCookie(cookie, requestData.cookies![cookie]) 120 | }) 121 | } 122 | 123 | if (requestData.session) { 124 | debug('defining session with browser context %O', requestData.session) 125 | context.setSession(requestData.session) 126 | } 127 | } 128 | }, 129 | }) 130 | } 131 | 132 | return pluginFn 133 | } 134 | -------------------------------------------------------------------------------- /src/symbols.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/lucid 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /** 11 | * A symbol to identify the type of the real user for a given 12 | * user provider 13 | */ 14 | export const PROVIDER_REAL_USER = Symbol.for('PROVIDER_REAL_USER') 15 | 16 | /** 17 | * A symbol to identify the type for the events emitted by a guard 18 | */ 19 | export const GUARD_KNOWN_EVENTS = Symbol.for('GUARD_KNOWN_EVENTS') 20 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { HttpContext } from '@adonisjs/core/http' 11 | import type { ApplicationService, ConfigProvider } from '@adonisjs/core/types' 12 | 13 | import type { AuthManager } from './auth_manager.js' 14 | import type { GUARD_KNOWN_EVENTS } from './symbols.js' 15 | 16 | /** 17 | * Authentication response to login a user as a client. 18 | * This response is used by Japa plugins 19 | */ 20 | export interface AuthClientResponse { 21 | headers?: Record 22 | cookies?: Record 23 | session?: Record 24 | } 25 | 26 | /** 27 | * A set of properties a guard must implement to authenticate 28 | * incoming HTTP requests 29 | */ 30 | export interface GuardContract { 31 | /** 32 | * A unique name for the guard driver 33 | */ 34 | readonly driverName: string 35 | 36 | /** 37 | * Reference to the currently authenticated user 38 | */ 39 | user?: User 40 | 41 | /** 42 | * Returns logged-in user or throws an exception 43 | */ 44 | getUserOrFail(): User 45 | 46 | /** 47 | * A boolean to know if the current request has 48 | * been authenticated 49 | */ 50 | isAuthenticated: boolean 51 | 52 | /** 53 | * Whether or not the authentication has been attempted 54 | * during the current request 55 | */ 56 | authenticationAttempted: boolean 57 | 58 | /** 59 | * Authenticates the current request and throws an 60 | * exception if the request is not authenticated. 61 | */ 62 | authenticate(): Promise 63 | 64 | /** 65 | * Check if the current request has been authenticated 66 | * without throwing an exception. 67 | */ 68 | check(): Promise 69 | 70 | /** 71 | * The method is used to authenticate the user as client. 72 | * This method should return cookies, headers, or 73 | * session state. 74 | * 75 | * The rest of the arguments can be anything the guard wants 76 | * to accept 77 | */ 78 | authenticateAsClient(user: User, ...args: any[]): Promise 79 | 80 | /** 81 | * Aymbol for infer the events emitted by a specific 82 | * guard 83 | */ 84 | [GUARD_KNOWN_EVENTS]: unknown 85 | } 86 | 87 | /** 88 | * The guard factory method is called by the create an instance 89 | * of a guard during an HTTP request 90 | */ 91 | export type GuardFactory = (ctx: HttpContext) => GuardContract 92 | 93 | /** 94 | * Config provider for registering guards. The "name" property 95 | * is reference to the object key to which the guard is 96 | * assigned. 97 | */ 98 | export type GuardConfigProvider = { 99 | resolver: (name: string, app: ApplicationService) => Promise 100 | } 101 | 102 | /** 103 | * Authenticators are inferred inside the user application 104 | * from the config file 105 | */ 106 | export interface Authenticators {} 107 | 108 | /** 109 | * Infer authenticators from the auth config 110 | */ 111 | export type InferAuthenticators< 112 | Config extends ConfigProvider<{ 113 | default: unknown 114 | guards: unknown 115 | }>, 116 | > = Awaited>['guards'] 117 | 118 | /** 119 | * Helper to convert union to intersection 120 | */ 121 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void 122 | ? I 123 | : never 124 | 125 | /** 126 | * Infer events based upon the configure authenticators 127 | */ 128 | export type InferAuthEvents> = 129 | UnionToIntersection< 130 | { 131 | [K in keyof KnownAuthenticators]: ReturnType< 132 | KnownAuthenticators[K] 133 | >[typeof GUARD_KNOWN_EVENTS] 134 | }[keyof KnownAuthenticators] 135 | > 136 | 137 | /** 138 | * Auth service is a singleton instance of the AuthManager 139 | * configured using the config stored within the user 140 | * app. 141 | */ 142 | export interface AuthService 143 | extends AuthManager< 144 | Authenticators extends Record ? Authenticators : never 145 | > {} 146 | -------------------------------------------------------------------------------- /tests/access_tokens/define_config.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { configProvider } from '@adonisjs/core' 12 | import { BaseModel, column } from '@adonisjs/lucid/orm' 13 | import { AppFactory } from '@adonisjs/core/factories/app' 14 | import type { ApplicationService } from '@adonisjs/core/types' 15 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 16 | 17 | import { createEmitter } from '../helpers.js' 18 | import { 19 | tokensGuard, 20 | AccessTokensGuard, 21 | tokensUserProvider, 22 | DbAccessTokensProvider, 23 | AccessTokensLucidUserProvider, 24 | } from '../../modules/access_tokens_guard/main.js' 25 | 26 | test.group('defineConfig', () => { 27 | test('configure lucid user provider', ({ assert, expectTypeOf }) => { 28 | class User extends BaseModel { 29 | @column({ isPrimary: true }) 30 | declare id: number 31 | 32 | @column() 33 | declare username: string 34 | 35 | @column() 36 | declare email: string 37 | 38 | @column() 39 | declare password: string 40 | 41 | static authTokens = DbAccessTokensProvider.forModel(User) 42 | } 43 | 44 | const userProvider = tokensUserProvider({ 45 | tokens: 'authTokens', 46 | async model() { 47 | return { 48 | default: User, 49 | } 50 | }, 51 | }) 52 | assert.instanceOf(userProvider, AccessTokensLucidUserProvider) 53 | expectTypeOf(userProvider).toEqualTypeOf< 54 | AccessTokensLucidUserProvider<'authTokens', typeof User> 55 | >() 56 | }) 57 | 58 | test('configure access tokens guard', async ({ assert, expectTypeOf }) => { 59 | class User extends BaseModel { 60 | @column({ isPrimary: true }) 61 | declare id: number 62 | 63 | @column() 64 | declare username: string 65 | 66 | @column() 67 | declare email: string 68 | 69 | @column() 70 | declare password: string 71 | 72 | static authTokens = DbAccessTokensProvider.forModel(User) 73 | } 74 | 75 | const ctx = new HttpContextFactory().create() 76 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 77 | await app.init() 78 | app.container.bind('emitter', () => createEmitter()) 79 | 80 | const guard = await tokensGuard({ 81 | provider: tokensUserProvider({ 82 | tokens: 'authTokens', 83 | async model() { 84 | return { 85 | default: User, 86 | } 87 | }, 88 | }), 89 | }).resolver('api', app) 90 | 91 | assert.instanceOf(guard(ctx), AccessTokensGuard) 92 | expectTypeOf(guard).returns.toEqualTypeOf< 93 | AccessTokensGuard> 94 | >() 95 | }) 96 | 97 | test('register user provider from a config provider', async ({ assert, expectTypeOf }) => { 98 | class User extends BaseModel { 99 | @column({ isPrimary: true }) 100 | declare id: number 101 | 102 | @column() 103 | declare username: string 104 | 105 | @column() 106 | declare email: string 107 | 108 | @column() 109 | declare password: string 110 | 111 | static authTokens = DbAccessTokensProvider.forModel(User) 112 | } 113 | 114 | const ctx = new HttpContextFactory().create() 115 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 116 | await app.init() 117 | app.container.bind('emitter', () => createEmitter()) 118 | 119 | const userProvider = configProvider.create(async () => { 120 | return tokensUserProvider({ 121 | tokens: 'authTokens', 122 | async model() { 123 | return { 124 | default: User, 125 | } 126 | }, 127 | }) 128 | }) 129 | const guard = await tokensGuard({ provider: userProvider }).resolver('api', app) 130 | 131 | assert.instanceOf(guard(ctx), AccessTokensGuard) 132 | expectTypeOf(guard).returns.toEqualTypeOf< 133 | AccessTokensGuard> 134 | >() 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /tests/auth/auth_manager.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 12 | 13 | import { AuthManager } from '../../src/auth_manager.js' 14 | import { FakeGuard } from '../../factories/auth/main.js' 15 | import { Authenticator } from '../../src/authenticator.js' 16 | import { AuthenticatorClient } from '../../src/authenticator_client.js' 17 | 18 | test.group('Auth manager', () => { 19 | test('create authenticator from auth manager', async ({ assert, expectTypeOf }) => { 20 | const ctx = new HttpContextFactory().create() 21 | 22 | const authManager = new AuthManager({ 23 | default: 'web', 24 | guards: { 25 | web: () => new FakeGuard(), 26 | }, 27 | }) 28 | 29 | assert.equal(authManager.defaultGuard, 'web') 30 | assert.instanceOf(authManager.createAuthenticator(ctx), Authenticator) 31 | expectTypeOf(authManager.createAuthenticator(ctx).use).parameters.toMatchTypeOf<['web'?]>() 32 | }) 33 | 34 | test('create authenticator client from auth manager', async ({ assert, expectTypeOf }) => { 35 | const authManager = new AuthManager({ 36 | default: 'web', 37 | guards: { 38 | web: () => new FakeGuard(), 39 | }, 40 | }) 41 | 42 | assert.equal(authManager.defaultGuard, 'web') 43 | assert.instanceOf(authManager.createAuthenticatorClient(), AuthenticatorClient) 44 | expectTypeOf(authManager.createAuthenticatorClient().use).parameters.toMatchTypeOf<['web'?]>() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/auth/authenticator.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 12 | 13 | import { Authenticator } from '../../src/authenticator.js' 14 | import { E_UNAUTHORIZED_ACCESS } from '../../src/errors.js' 15 | import { FakeGuard, FakeUser } from '../../factories/auth/main.js' 16 | 17 | test.group('Authenticator', () => { 18 | test('create authenticator with guards', async ({ assert, expectTypeOf }) => { 19 | const ctx = new HttpContextFactory().create() 20 | 21 | const authenticator = new Authenticator(ctx, { 22 | default: 'web', 23 | guards: { 24 | web: () => new FakeGuard(), 25 | }, 26 | }) 27 | 28 | assert.instanceOf(authenticator, Authenticator) 29 | expectTypeOf(authenticator.use).parameters.toMatchTypeOf<['web'?]>() 30 | }) 31 | 32 | test('access guard using its name', async ({ assert, expectTypeOf }) => { 33 | const ctx = new HttpContextFactory().create() 34 | 35 | const authenticator = new Authenticator(ctx, { 36 | default: 'web', 37 | guards: { 38 | web: () => new FakeGuard(), 39 | }, 40 | }) 41 | 42 | const webGuard = authenticator.use('web') 43 | assert.instanceOf(webGuard, FakeGuard) 44 | assert.equal(authenticator.defaultGuard, 'web') 45 | assert.equal(webGuard.driverName, 'fake') 46 | assert.strictEqual(authenticator.use('web'), authenticator.use('web')) 47 | expectTypeOf(webGuard.user).toMatchTypeOf() 48 | }) 49 | 50 | test('authenticate using the default guard', async ({ assert, expectTypeOf }) => { 51 | const ctx = new HttpContextFactory().create() 52 | 53 | const authenticator = new Authenticator(ctx, { 54 | default: 'web', 55 | guards: { 56 | web: () => new FakeGuard(), 57 | }, 58 | }) 59 | 60 | await authenticator.authenticate() 61 | 62 | assert.equal(authenticator.user!.id, 1) 63 | expectTypeOf(authenticator.user).toMatchTypeOf() 64 | expectTypeOf(authenticator.getUserOrFail()).toMatchTypeOf() 65 | assert.equal(authenticator.authenticatedViaGuard, 'web') 66 | assert.isTrue(authenticator.isAuthenticated) 67 | assert.isTrue(authenticator.authenticationAttempted) 68 | }) 69 | 70 | test('check authentication using the default guard', async ({ assert, expectTypeOf }) => { 71 | const ctx = new HttpContextFactory().create() 72 | 73 | const authenticator = new Authenticator(ctx, { 74 | default: 'web', 75 | guards: { 76 | web: () => new FakeGuard(), 77 | }, 78 | }) 79 | 80 | await authenticator.check() 81 | 82 | assert.equal(authenticator.user!.id, 1) 83 | expectTypeOf(authenticator.user).toMatchTypeOf() 84 | expectTypeOf(authenticator.getUserOrFail()).toMatchTypeOf() 85 | assert.equal(authenticator.authenticatedViaGuard, 'web') 86 | assert.isTrue(authenticator.isAuthenticated) 87 | assert.isTrue(authenticator.authenticationAttempted) 88 | }) 89 | 90 | test('authenticate using the guard instance', async ({ assert }) => { 91 | const ctx = new HttpContextFactory().create() 92 | const authenticator = new Authenticator(ctx, { 93 | default: 'web', 94 | guards: { 95 | web: () => new FakeGuard(), 96 | }, 97 | }) 98 | 99 | const user = await authenticator.use().authenticate() 100 | 101 | assert.equal(user.id, 1) 102 | assert.isUndefined(authenticator.user) 103 | assert.isUndefined(authenticator.authenticatedViaGuard) 104 | assert.isFalse(authenticator.isAuthenticated) 105 | assert.isFalse(authenticator.authenticationAttempted) 106 | }) 107 | 108 | test('access properties without authenticating user', async ({ assert }) => { 109 | const ctx = new HttpContextFactory().create() 110 | const authenticator = new Authenticator(ctx, { 111 | default: 'web', 112 | guards: { 113 | web: () => new FakeGuard(), 114 | }, 115 | }) 116 | 117 | assert.isUndefined(authenticator.user) 118 | assert.isUndefined(authenticator.authenticatedViaGuard) 119 | assert.isFalse(authenticator.isAuthenticated) 120 | assert.isFalse(authenticator.authenticationAttempted) 121 | assert.throws( 122 | () => authenticator.getUserOrFail(), 123 | 'Cannot access authenticated user. Please call "auth.authenticate" method first.' 124 | ) 125 | }) 126 | 127 | test('throw error when unable to authenticate', async ({ assert }) => { 128 | assert.plan(5) 129 | 130 | const ctx = new HttpContextFactory().create() 131 | const authenticator = new Authenticator(ctx, { 132 | default: 'web', 133 | guards: { 134 | web: () => new FakeGuard(), 135 | }, 136 | }) 137 | 138 | authenticator.use('web').authenticate = async function () { 139 | this.authenticationAttempted = true 140 | return this.getUserOrFail() 141 | } 142 | 143 | try { 144 | await authenticator.authenticateUsing() 145 | } catch (error) { 146 | assert.instanceOf(error, E_UNAUTHORIZED_ACCESS) 147 | assert.equal(error.message, 'Unauthorized access') 148 | assert.equal(error.guardDriverName, 'fake') 149 | } 150 | 151 | assert.isFalse(authenticator.isAuthenticated) 152 | assert.isTrue(authenticator.authenticationAttempted) 153 | }) 154 | 155 | test('do not throw error when unable to authenticate via check method', async ({ assert }) => { 156 | const ctx = new HttpContextFactory().create() 157 | const authenticator = new Authenticator(ctx, { 158 | default: 'web', 159 | guards: { 160 | web: () => new FakeGuard(), 161 | }, 162 | }) 163 | 164 | authenticator.use('web').authenticate = async function () { 165 | this.authenticationAttempted = true 166 | return this.getUserOrFail() 167 | } 168 | 169 | const isAuthenticated = await authenticator.check() 170 | assert.isFalse(isAuthenticated) 171 | assert.isFalse(authenticator.isAuthenticated) 172 | assert.isTrue(authenticator.authenticationAttempted) 173 | }) 174 | }) 175 | -------------------------------------------------------------------------------- /tests/auth/authenticator_client.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { FakeGuard, FakeUser } from '../../factories/auth/main.js' 12 | import { AuthenticatorClient } from '../../src/authenticator_client.js' 13 | 14 | test.group('Authenticator client', () => { 15 | test('create authenticator client with guards', async ({ assert, expectTypeOf }) => { 16 | const client = new AuthenticatorClient({ 17 | default: 'web', 18 | guards: { 19 | web: () => new FakeGuard(), 20 | }, 21 | }) 22 | 23 | assert.instanceOf(client, AuthenticatorClient) 24 | expectTypeOf(client.use).parameters.toMatchTypeOf<['web'?]>() 25 | }) 26 | 27 | test('access guard using its name', async ({ assert, expectTypeOf }) => { 28 | const client = new AuthenticatorClient({ 29 | default: 'web', 30 | guards: { 31 | web: () => new FakeGuard(), 32 | }, 33 | }) 34 | 35 | const webGuard = client.use('web') 36 | assert.instanceOf(webGuard, FakeGuard) 37 | assert.equal(client.defaultGuard, 'web') 38 | assert.equal(webGuard.driverName, 'fake') 39 | assert.strictEqual(client.use('web'), client.use('web')) 40 | assert.strictEqual(client.use(), client.use('web')) 41 | expectTypeOf(webGuard.user).toMatchTypeOf() 42 | }) 43 | 44 | test('call authenticateAsClient via client', async ({ assert }) => { 45 | const client = new AuthenticatorClient({ 46 | default: 'web', 47 | guards: { 48 | web: () => new FakeGuard(), 49 | }, 50 | }) 51 | 52 | await assert.doesNotReject(() => client.use('web').authenticateAsClient({ id: 1 })) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/auth/configure.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { fileURLToPath } from 'node:url' 12 | import { IgnitorFactory } from '@adonisjs/core/factories' 13 | import Configure from '@adonisjs/core/commands/configure' 14 | 15 | const BASE_URL = new URL('./tmp/', import.meta.url) 16 | 17 | test.group('Configure', (group) => { 18 | group.each.setup(({ context }) => { 19 | context.fs.baseUrl = BASE_URL 20 | context.fs.basePath = fileURLToPath(BASE_URL) 21 | }) 22 | 23 | group.each.disableTimeout() 24 | 25 | test('register provider and middleware', async ({ fs, assert }) => { 26 | const ignitor = new IgnitorFactory() 27 | .withCoreProviders() 28 | .withCoreConfig() 29 | .create(BASE_URL, { 30 | importer: (filePath) => { 31 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 32 | return import(new URL(filePath, BASE_URL).href) 33 | } 34 | 35 | return import(filePath) 36 | }, 37 | }) 38 | 39 | await fs.create( 40 | 'start/kernel.ts', 41 | `router.use([]) 42 | export const { middleware } = router.named({ 43 | })` 44 | ) 45 | await fs.createJson('tsconfig.json', {}) 46 | await fs.create('adonisrc.ts', `export default defineConfig({}) {}`) 47 | 48 | const app = ignitor.createApp('web') 49 | await app.init() 50 | await app.boot() 51 | 52 | const ace = await app.container.make('ace') 53 | const command = await ace.create(Configure, ['../../../index.js', '--guard=session']) 54 | await command.exec() 55 | 56 | await assert.fileContains('adonisrc.ts', '@adonisjs/auth/auth_provider') 57 | await assert.fileExists('app/middleware/auth_middleware.ts') 58 | await assert.fileExists('app/middleware/guest_middleware.ts') 59 | 60 | await assert.fileContains( 61 | 'start/kernel.ts', 62 | `export const { middleware } = router.named({ 63 | guest: () => import('#middleware/guest_middleware'), 64 | auth: () => import('#middleware/auth_middleware') 65 | })` 66 | ) 67 | await assert.fileContains( 68 | 'start/kernel.ts', 69 | `router.use([() => import('@adonisjs/auth/initialize_auth_middleware')])` 70 | ) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/auth/define_config.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { AppFactory } from '@adonisjs/core/factories/app' 12 | import type { ApplicationService } from '@adonisjs/core/types' 13 | 14 | import { AuthManager } from '../../src/auth_manager.js' 15 | import { FakeGuard } from '../../factories/auth/main.js' 16 | import { defineConfig } from '../../src/define_config.js' 17 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 18 | 19 | const BASE_URL = new URL('./', import.meta.url) 20 | const app = new AppFactory().create(BASE_URL, () => {}) as ApplicationService 21 | await app.init() 22 | 23 | test.group('Define config', () => { 24 | test('define and resolve config for the auth manager', async ({ assert }) => { 25 | const authConfigProvider = defineConfig({ 26 | default: 'web', 27 | guards: { 28 | web: () => new FakeGuard(), 29 | }, 30 | }) 31 | const ctx = new HttpContextFactory().create() 32 | 33 | const authConfig = await authConfigProvider.resolver(app) 34 | const authManager = new AuthManager(authConfig) 35 | assert.instanceOf(authManager, AuthManager) 36 | assert.instanceOf(authManager.createAuthenticator(ctx).use('web'), FakeGuard) 37 | }) 38 | 39 | test('resolve guard registered as provider', async ({ assert }) => { 40 | const authConfigProvider = defineConfig({ 41 | default: 'web', 42 | guards: { 43 | web: { 44 | async resolver(name) { 45 | assert.equal(name, 'web') 46 | return () => new FakeGuard() 47 | }, 48 | }, 49 | }, 50 | }) 51 | 52 | const ctx = new HttpContextFactory().create() 53 | const authConfig = await authConfigProvider.resolver(app) 54 | const authManager = new AuthManager(authConfig) 55 | assert.instanceOf(authManager, AuthManager) 56 | assert.instanceOf(authManager.createAuthenticator(ctx).use('web'), FakeGuard) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/auth/e_invalid_credentials.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { I18nManagerFactory } from '@adonisjs/i18n/factories' 12 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 13 | import { SessionMiddlewareFactory } from '@adonisjs/session/factories' 14 | 15 | import { E_INVALID_CREDENTIALS } from '../../src/errors.js' 16 | 17 | test.group('Errors | E_INVALID_CREDENTIALS', () => { 18 | test('report error via flash messages and redirect', async ({ assert }) => { 19 | const sessionMiddleware = await new SessionMiddlewareFactory().create() 20 | const error = new E_INVALID_CREDENTIALS('Invalid credentials') 21 | 22 | const ctx = new HttpContextFactory().create() 23 | await sessionMiddleware.handle(ctx, async () => { 24 | return error.handle(error, ctx) 25 | }) 26 | 27 | assert.deepEqual(ctx.session.responseFlashMessages.all(), { 28 | errorsBag: { E_INVALID_CREDENTIALS: 'Invalid credentials' }, 29 | input: {}, 30 | }) 31 | assert.equal(ctx.response.getHeader('location'), '/') 32 | }) 33 | 34 | test('respond with text message when session middleware is not configured', async ({ 35 | assert, 36 | }) => { 37 | const error = new E_INVALID_CREDENTIALS('Invalid credentials') 38 | 39 | const ctx = new HttpContextFactory().create() 40 | await error.handle(error, ctx) 41 | 42 | assert.isUndefined(ctx.response.getHeader('location')) 43 | assert.deepEqual(ctx.response.getBody(), 'Invalid credentials') 44 | }) 45 | 46 | test('respond with json', async ({ assert }) => { 47 | const error = new E_INVALID_CREDENTIALS('Invalid credentials') 48 | 49 | const ctx = new HttpContextFactory().create() 50 | 51 | /** 52 | * Force JSON response 53 | */ 54 | ctx.request.request.headers.accept = 'application/json' 55 | await error.handle(error, ctx) 56 | 57 | assert.isUndefined(ctx.response.getHeader('location')) 58 | assert.deepEqual(ctx.response.getBody(), { 59 | errors: [ 60 | { 61 | message: 'Invalid credentials', 62 | }, 63 | ], 64 | }) 65 | }) 66 | 67 | test('respond with JSONAPI response', async ({ assert }) => { 68 | const error = new E_INVALID_CREDENTIALS('Invalid credentials') 69 | 70 | const ctx = new HttpContextFactory().create() 71 | 72 | /** 73 | * Force JSONAPI response 74 | */ 75 | ctx.request.request.headers.accept = 'application/vnd.api+json' 76 | await error.handle(error, ctx) 77 | 78 | assert.isUndefined(ctx.response.getHeader('location')) 79 | assert.deepEqual(ctx.response.getBody(), { 80 | errors: [ 81 | { 82 | title: 'Invalid credentials', 83 | code: 'E_INVALID_CREDENTIALS', 84 | }, 85 | ], 86 | }) 87 | }) 88 | 89 | test('translate error message using i18n', async ({ assert }) => { 90 | const error = new E_INVALID_CREDENTIALS('Invalid credentials') 91 | const i18nManager = new I18nManagerFactory() 92 | .merge({ 93 | config: { 94 | loaders: [ 95 | () => { 96 | return { 97 | async load() { 98 | return { 99 | en: { 100 | 'errors.E_INVALID_CREDENTIALS': 'Incorrect email or password', 101 | }, 102 | } 103 | }, 104 | } 105 | }, 106 | ], 107 | }, 108 | }) 109 | .create() 110 | 111 | const ctx = new HttpContextFactory().create() 112 | await i18nManager.loadTranslations() 113 | ctx.i18n = i18nManager.locale('en') 114 | 115 | /** 116 | * Force JSON response 117 | */ 118 | ctx.request.request.headers.accept = 'application/json' 119 | await error.handle(error, ctx) 120 | 121 | assert.isUndefined(ctx.response.getHeader('location')) 122 | assert.deepEqual(ctx.response.getBody(), { 123 | errors: [ 124 | { 125 | message: 'Incorrect email or password', 126 | }, 127 | ], 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /tests/auth/plugins/api_client.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import nock from 'nock' 11 | import sinon from 'sinon' 12 | import { test } from '@japa/runner' 13 | import { apiClient } from '@japa/api-client' 14 | import { runner } from '@japa/runner/factories' 15 | import { AppFactory } from '@adonisjs/core/factories/app' 16 | import type { ApplicationService } from '@adonisjs/core/types' 17 | 18 | import { Guards } from './global_types.js' 19 | import { AuthManager } from '../../../src/auth_manager.js' 20 | import { FakeGuard, FakeUser } from '../../../factories/auth/main.js' 21 | import { authApiClient } from '../../../src/plugins/japa/api_client.js' 22 | 23 | test.group('Api client | loginAs', () => { 24 | test('login user using the guard authenticate as client method', async ({ 25 | assert, 26 | expectTypeOf, 27 | }) => { 28 | const fakeGuard = new FakeGuard() 29 | const guards: Guards = { 30 | web: () => fakeGuard, 31 | } 32 | 33 | const authManager = new AuthManager({ 34 | default: 'web', 35 | guards: guards, 36 | }) 37 | 38 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 39 | await app.init() 40 | 41 | app.container.singleton('auth.manager', () => authManager) 42 | const spy = sinon.spy(fakeGuard, 'authenticateAsClient') 43 | 44 | nock('http://localhost:3333').get('/').reply(200) 45 | 46 | await runner() 47 | .configure({ 48 | plugins: [apiClient({ baseURL: 'http://localhost:3333' }), authApiClient(app)], 49 | files: ['*'], 50 | }) 51 | .runTest('sample test', async ({ client }) => { 52 | const request = client.get('/') 53 | await request.loginAs({ id: 1 }) 54 | expectTypeOf(request.loginAs).parameters.toEqualTypeOf< 55 | [ 56 | user: FakeUser, 57 | abilities?: string[] | undefined, 58 | expiresIn?: string | number | undefined, 59 | ] 60 | >() 61 | }) 62 | 63 | assert.isTrue(spy.calledOnceWithExactly({ id: 1 })) 64 | }) 65 | 66 | test('pass additional params to loginAs method', async ({ assert, expectTypeOf }) => { 67 | const fakeGuard = new FakeGuard() 68 | const guards: Guards = { 69 | web: () => fakeGuard, 70 | } 71 | 72 | const authManager = new AuthManager({ 73 | default: 'web', 74 | guards: guards, 75 | }) 76 | 77 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 78 | await app.init() 79 | 80 | app.container.singleton('auth.manager', () => authManager) 81 | const spy = sinon.spy(fakeGuard, 'authenticateAsClient') 82 | 83 | nock('http://localhost:3333').get('/').reply(200) 84 | 85 | await runner() 86 | .configure({ 87 | plugins: [apiClient({ baseURL: 'http://localhost:3333' }), authApiClient(app)], 88 | files: ['*'], 89 | }) 90 | .runTest('sample test', async ({ client }) => { 91 | const request = client.get('/') 92 | await request.withGuard('web').loginAs({ id: 1 }, ['*'], '20 mins') 93 | expectTypeOf(request.withGuard('web').loginAs).parameters.toEqualTypeOf< 94 | [ 95 | user: FakeUser, 96 | abilities?: string[] | undefined, 97 | expiresIn?: string | number | undefined, 98 | ] 99 | >() 100 | }) 101 | 102 | assert.isTrue(spy.calledOnceWithExactly({ id: 1 }, ['*'], '20 mins')) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /tests/auth/plugins/browser_client.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import nock from 'nock' 11 | import sinon from 'sinon' 12 | import { test } from '@japa/runner' 13 | import { runner } from '@japa/runner/factories' 14 | import { browserClient } from '@japa/browser-client' 15 | import { AppFactory } from '@adonisjs/core/factories/app' 16 | import type { ApplicationService } from '@adonisjs/core/types' 17 | 18 | import { Guards } from './global_types.js' 19 | import { AuthManager } from '../../../src/auth_manager.js' 20 | import { FakeGuard, FakeUser } from '../../../factories/auth/main.js' 21 | import { authBrowserClient } from '../../../src/plugins/japa/browser_client.js' 22 | 23 | test.group('Browser client | loginAs', (group) => { 24 | group.each.timeout(0) 25 | 26 | test('login user using the guard authenticateAsClient method', async ({ 27 | assert, 28 | expectTypeOf, 29 | }) => { 30 | const fakeGuard = new FakeGuard() 31 | const guards: Guards = { 32 | web: () => fakeGuard, 33 | } 34 | 35 | const authManager = new AuthManager({ 36 | default: 'web', 37 | guards: guards, 38 | }) 39 | 40 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 41 | await app.init() 42 | 43 | app.container.singleton('auth.manager', () => authManager) 44 | const spy = sinon.spy(fakeGuard, 'authenticateAsClient') 45 | 46 | nock('http://localhost:3333').get('/').reply(200) 47 | 48 | await runner() 49 | .configure({ 50 | plugins: [browserClient({}), authBrowserClient(app)], 51 | files: ['*'], 52 | }) 53 | .runTest('sample test', async ({ browserContext }) => { 54 | await browserContext.loginAs({ id: 1 }) 55 | expectTypeOf(browserContext.loginAs).parameters.toEqualTypeOf< 56 | [ 57 | user: FakeUser, 58 | abilities?: string[] | undefined, 59 | expiresIn?: string | number | undefined, 60 | ] 61 | >() 62 | }) 63 | 64 | assert.isTrue(spy.calledOnceWithExactly({ id: 1 })) 65 | }) 66 | 67 | test('pass additional params to loginAs method', async ({ assert, expectTypeOf }) => { 68 | const fakeGuard = new FakeGuard() 69 | const guards = { 70 | web: () => fakeGuard, 71 | } 72 | 73 | const authManager = new AuthManager({ 74 | default: 'web', 75 | guards: guards, 76 | }) 77 | 78 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 79 | await app.init() 80 | 81 | app.container.singleton('auth.manager', () => authManager) 82 | const spy = sinon.spy(fakeGuard, 'authenticateAsClient') 83 | 84 | nock('http://localhost:3333').get('/').reply(200) 85 | 86 | await runner() 87 | .configure({ 88 | plugins: [browserClient({}), authBrowserClient(app)], 89 | files: ['*'], 90 | }) 91 | .runTest('sample test', async ({ browserContext }) => { 92 | await browserContext.withGuard('web').loginAs({ id: 1 }, ['*'], '20 mins') 93 | expectTypeOf(browserContext.withGuard('web').loginAs).parameters.toEqualTypeOf< 94 | [ 95 | user: FakeUser, 96 | abilities?: string[] | undefined, 97 | expiresIn?: string | number | undefined, 98 | ] 99 | >() 100 | }) 101 | 102 | assert.isTrue(spy.calledOnceWithExactly({ id: 1 }, ['*'], '20 mins')) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /tests/auth/plugins/global_types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { FakeGuard } from '../../../factories/auth/main.js' 11 | 12 | /** 13 | * Guard to use for testing 14 | */ 15 | export type Guards = { 16 | web: () => FakeGuard 17 | } 18 | 19 | /** 20 | * Inferrring types for the authenticators, since 21 | * the japa plugins relies on the singleton 22 | * service 23 | */ 24 | declare module '@adonisjs/auth/types' { 25 | interface Authenticators extends Guards {} 26 | } 27 | -------------------------------------------------------------------------------- /tests/basic_auth/define_config.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { configProvider } from '@adonisjs/core' 12 | import { compose } from '@adonisjs/core/helpers' 13 | import { BaseModel, column } from '@adonisjs/lucid/orm' 14 | import { AppFactory } from '@adonisjs/core/factories/app' 15 | import type { ApplicationService } from '@adonisjs/core/types' 16 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 17 | 18 | import { withAuthFinder } from '../../src/mixins/lucid.js' 19 | import { createEmitter, getHasher } from '../helpers.js' 20 | import { 21 | basicAuthGuard, 22 | basicAuthUserProvider, 23 | } from '../../modules/basic_auth_guard/define_config.js' 24 | import { BasicAuthGuard, BasicAuthLucidUserProvider } from '../../modules/basic_auth_guard/main.js' 25 | 26 | test.group('defineConfig', () => { 27 | test('configure lucid user provider', ({ assert, expectTypeOf }) => { 28 | const hash = getHasher() 29 | 30 | class User extends compose( 31 | BaseModel, 32 | withAuthFinder(() => hash, { 33 | uids: ['email'], 34 | passwordColumnName: 'password', 35 | }) 36 | ) { 37 | @column({ isPrimary: true }) 38 | declare id: number 39 | 40 | @column() 41 | declare username: string 42 | 43 | @column() 44 | declare email: string 45 | 46 | @column() 47 | declare password: string 48 | } 49 | 50 | const userProvider = basicAuthUserProvider({ 51 | async model() { 52 | return { 53 | default: User, 54 | } 55 | }, 56 | }) 57 | assert.instanceOf(userProvider, BasicAuthLucidUserProvider) 58 | expectTypeOf(userProvider).toEqualTypeOf>() 59 | }) 60 | 61 | test('configure basic auth guard', async ({ assert, expectTypeOf }) => { 62 | const hash = getHasher() 63 | 64 | class User extends compose( 65 | BaseModel, 66 | withAuthFinder(() => hash, { 67 | uids: ['email'], 68 | passwordColumnName: 'password', 69 | }) 70 | ) { 71 | @column({ isPrimary: true }) 72 | declare id: number 73 | 74 | @column() 75 | declare username: string 76 | 77 | @column() 78 | declare email: string 79 | 80 | @column() 81 | declare password: string 82 | } 83 | 84 | const userProvider = basicAuthUserProvider({ 85 | async model() { 86 | return { 87 | default: User, 88 | } 89 | }, 90 | }) 91 | 92 | const ctx = new HttpContextFactory().create() 93 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 94 | await app.init() 95 | app.container.bind('emitter', () => createEmitter()) 96 | 97 | const guard = await basicAuthGuard({ 98 | provider: userProvider, 99 | }).resolver('api', app) 100 | 101 | assert.instanceOf(guard(ctx), BasicAuthGuard) 102 | expectTypeOf(guard).returns.toEqualTypeOf< 103 | BasicAuthGuard> 104 | >() 105 | }) 106 | 107 | test('register user provider from a config provider', async ({ assert, expectTypeOf }) => { 108 | const hash = getHasher() 109 | 110 | class User extends compose( 111 | BaseModel, 112 | withAuthFinder(() => hash, { 113 | uids: ['email'], 114 | passwordColumnName: 'password', 115 | }) 116 | ) { 117 | @column({ isPrimary: true }) 118 | declare id: number 119 | 120 | @column() 121 | declare username: string 122 | 123 | @column() 124 | declare email: string 125 | 126 | @column() 127 | declare password: string 128 | } 129 | 130 | const userProvider = configProvider.create(async () => { 131 | return basicAuthUserProvider({ 132 | async model() { 133 | return { 134 | default: User, 135 | } 136 | }, 137 | }) 138 | }) 139 | 140 | const ctx = new HttpContextFactory().create() 141 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 142 | await app.init() 143 | app.container.bind('emitter', () => createEmitter()) 144 | 145 | const guard = await basicAuthGuard({ 146 | provider: userProvider, 147 | }).resolver('api', app) 148 | 149 | assert.instanceOf(guard(ctx), BasicAuthGuard) 150 | expectTypeOf(guard).returns.toEqualTypeOf< 151 | BasicAuthGuard> 152 | >() 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /tests/basic_auth/user_providers/lucid.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { compose } from '@adonisjs/core/helpers' 12 | import { BaseModel, column } from '@adonisjs/lucid/orm' 13 | 14 | import { withAuthFinder } from '../../../src/mixins/lucid.js' 15 | import { createDatabase, createTables, getHasher } from '../../helpers.js' 16 | import { BasicAuthGuardUser } from '../../../modules/basic_auth_guard/types.js' 17 | import { BasicAuthLucidUserProvider } from '../../../modules/basic_auth_guard/user_providers/lucid.js' 18 | 19 | test.group('Basic auth user provider | Lucid | verifyCredentials', () => { 20 | test('return user when credentials are valid', async ({ assert, expectTypeOf }) => { 21 | const db = await createDatabase() 22 | await createTables(db) 23 | 24 | const hash = getHasher() 25 | class User extends compose( 26 | BaseModel, 27 | withAuthFinder(() => hash, { 28 | uids: ['email', 'username'], 29 | passwordColumnName: 'password', 30 | }) 31 | ) { 32 | @column({ isPrimary: true }) 33 | declare id: number 34 | 35 | @column() 36 | declare username: string 37 | 38 | @column() 39 | declare email: string 40 | 41 | @column() 42 | declare password: string 43 | } 44 | 45 | const userProvider = new BasicAuthLucidUserProvider({ 46 | async model() { 47 | return { 48 | default: User, 49 | } 50 | }, 51 | }) 52 | 53 | const user = await User.create({ 54 | email: 'virk@adonisjs.com', 55 | username: 'virk', 56 | password: 'secret', 57 | }) 58 | 59 | const freshUser = await userProvider.verifyCredentials('virk@adonisjs.com', 'secret') 60 | assert.instanceOf(freshUser!.getOriginal(), User) 61 | assert.equal(freshUser!.getId(), user.id) 62 | expectTypeOf(freshUser).toEqualTypeOf | null>() 63 | }) 64 | 65 | test('return null when user does not exists', async ({ assert }) => { 66 | const db = await createDatabase() 67 | await createTables(db) 68 | 69 | const hash = getHasher() 70 | class User extends compose( 71 | BaseModel, 72 | withAuthFinder(() => hash, { 73 | uids: ['email', 'username'], 74 | passwordColumnName: 'password', 75 | }) 76 | ) { 77 | @column({ isPrimary: true }) 78 | declare id: number 79 | 80 | @column() 81 | declare username: string 82 | 83 | @column() 84 | declare email: string 85 | 86 | @column() 87 | declare password: string 88 | } 89 | 90 | const userProvider = new BasicAuthLucidUserProvider({ 91 | async model() { 92 | return { 93 | default: User, 94 | } 95 | }, 96 | }) 97 | 98 | const freshUser = await userProvider.verifyCredentials('virk@adonisjs.com', 'secret') 99 | assert.isNull(freshUser) 100 | }) 101 | 102 | test('return null when password is invalid', async ({ assert }) => { 103 | const db = await createDatabase() 104 | await createTables(db) 105 | 106 | const hash = getHasher() 107 | class User extends compose( 108 | BaseModel, 109 | withAuthFinder(() => hash, { 110 | uids: ['email', 'username'], 111 | passwordColumnName: 'password', 112 | }) 113 | ) { 114 | @column({ isPrimary: true }) 115 | declare id: number 116 | 117 | @column() 118 | declare username: string 119 | 120 | @column() 121 | declare email: string 122 | 123 | @column() 124 | declare password: string 125 | } 126 | 127 | const userProvider = new BasicAuthLucidUserProvider({ 128 | async model() { 129 | return { 130 | default: User, 131 | } 132 | }, 133 | }) 134 | 135 | await User.create({ 136 | email: 'virk@adonisjs.com', 137 | username: 'virk', 138 | password: 'secret', 139 | }) 140 | 141 | const freshUser = await userProvider.verifyCredentials('virk@adonisjs.com', 'supersecret') 142 | assert.isNull(freshUser) 143 | }) 144 | }) 145 | 146 | test.group('Basic auth user provider | Lucid | createUserForGuard', () => { 147 | test('throw error via getId when user does not have an id', async ({ assert }) => { 148 | const db = await createDatabase() 149 | await createTables(db) 150 | 151 | const hash = getHasher() 152 | class User extends compose( 153 | BaseModel, 154 | withAuthFinder(() => hash, { 155 | uids: ['email', 'username'], 156 | passwordColumnName: 'password', 157 | }) 158 | ) { 159 | @column({ isPrimary: true }) 160 | declare id: number 161 | 162 | @column() 163 | declare username: string 164 | 165 | @column() 166 | declare email: string 167 | 168 | @column() 169 | declare password: string 170 | } 171 | 172 | const userProvider = new BasicAuthLucidUserProvider({ 173 | async model() { 174 | return { 175 | default: User, 176 | } 177 | }, 178 | }) 179 | 180 | const user = await userProvider.createUserForGuard(new User()) 181 | assert.throws( 182 | () => user.getId(), 183 | 'Cannot use "User" model for authentication. The value of column "id" is undefined or null' 184 | ) 185 | }) 186 | 187 | test('throw error via getId when user is not an instance of the associated model', async ({ 188 | assert, 189 | }) => { 190 | const db = await createDatabase() 191 | await createTables(db) 192 | 193 | const hash = getHasher() 194 | class User extends compose( 195 | BaseModel, 196 | withAuthFinder(() => hash, { 197 | uids: ['email', 'username'], 198 | passwordColumnName: 'password', 199 | }) 200 | ) { 201 | @column({ isPrimary: true }) 202 | declare id: number 203 | 204 | @column() 205 | declare username: string 206 | 207 | @column() 208 | declare email: string 209 | 210 | @column() 211 | declare password: string 212 | } 213 | 214 | const userProvider = new BasicAuthLucidUserProvider({ 215 | async model() { 216 | return { 217 | default: User, 218 | } 219 | }, 220 | }) 221 | 222 | await assert.rejects( 223 | // @ts-expect-error 224 | () => userProvider.createUserForGuard({}), 225 | 'Invalid user object. It must be an instance of the "User" model' 226 | ) 227 | }) 228 | }) 229 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'node:path' 11 | import timekeeper from 'timekeeper' 12 | import { Hash } from '@adonisjs/hash' 13 | import { configDotenv } from 'dotenv' 14 | import { mkdir, rm } from 'node:fs/promises' 15 | import { getActiveTest } from '@japa/runner' 16 | import { Emitter } from '@adonisjs/core/events' 17 | import { BaseModel } from '@adonisjs/lucid/orm' 18 | import { CookieClient } from '@adonisjs/core/http' 19 | import { Database } from '@adonisjs/lucid/database' 20 | import { Encryption } from '@adonisjs/core/encryption' 21 | import { Scrypt } from '@adonisjs/hash/drivers/scrypt' 22 | import { AppFactory } from '@adonisjs/core/factories/app' 23 | import { LoggerFactory } from '@adonisjs/core/factories/logger' 24 | import setCookieParser, { type CookieMap } from 'set-cookie-parser' 25 | import { EncryptionFactory } from '@adonisjs/core/factories/encryption' 26 | 27 | export const encryption: Encryption = new EncryptionFactory().create() 28 | configDotenv() 29 | 30 | /** 31 | * Creates a fresh instance of AdonisJS hash module 32 | * with scrypt driver 33 | */ 34 | export function getHasher() { 35 | return new Hash(new Scrypt({})) 36 | } 37 | 38 | /** 39 | * Creates an instance of the database class for making queries 40 | */ 41 | export async function createDatabase() { 42 | const test = getActiveTest() 43 | if (!test) { 44 | throw new Error('Cannot use "createDatabase" outside of a Japa test') 45 | } 46 | 47 | const basePath = test.context.fs.basePath 48 | await mkdir(basePath) 49 | 50 | const app = new AppFactory().create(test.context.fs.baseUrl, () => {}) 51 | const logger = new LoggerFactory().create() 52 | const emitter = new Emitter(app) 53 | const db = new Database( 54 | { 55 | connection: process.env.DB || 'sqlite', 56 | connections: { 57 | sqlite: { 58 | client: 'sqlite3', 59 | connection: { 60 | filename: join(test.context.fs.basePath, 'db.sqlite3'), 61 | }, 62 | }, 63 | pg: { 64 | client: 'pg', 65 | connection: { 66 | host: process.env.PG_HOST as string, 67 | port: Number(process.env.PG_PORT), 68 | database: process.env.PG_DATABASE as string, 69 | user: process.env.PG_USER as string, 70 | password: process.env.PG_PASSWORD as string, 71 | }, 72 | }, 73 | mssql: { 74 | client: 'mssql', 75 | connection: { 76 | server: process.env.MSSQL_HOST as string, 77 | port: Number(process.env.MSSQL_PORT! as string), 78 | user: process.env.MSSQL_USER as string, 79 | password: process.env.MSSQL_PASSWORD as string, 80 | database: 'master', 81 | options: { 82 | enableArithAbort: true, 83 | }, 84 | }, 85 | }, 86 | mysql: { 87 | client: 'mysql2', 88 | connection: { 89 | host: process.env.MYSQL_HOST as string, 90 | port: Number(process.env.MYSQL_PORT), 91 | database: process.env.MYSQL_DATABASE as string, 92 | user: process.env.MYSQL_USER as string, 93 | password: process.env.MYSQL_PASSWORD as string, 94 | }, 95 | }, 96 | }, 97 | }, 98 | logger, 99 | emitter 100 | ) 101 | 102 | test.cleanup(async () => { 103 | db.manager.closeAll() 104 | await rm(basePath, { force: true, recursive: true, maxRetries: 3 }) 105 | }) 106 | BaseModel.useAdapter(db.modelAdapter()) 107 | return db 108 | } 109 | 110 | /** 111 | * Creates needed database tables 112 | */ 113 | export async function createTables(db: Database) { 114 | const test = getActiveTest() 115 | if (!test) { 116 | throw new Error('Cannot use "createTables" outside of a Japa test') 117 | } 118 | 119 | test.cleanup(async () => { 120 | await db.connection().schema.dropTable('users') 121 | await db.connection().schema.dropTable('auth_access_tokens') 122 | await db.connection().schema.dropTable('remember_me_tokens') 123 | }) 124 | 125 | await db.connection().schema.createTable('auth_access_tokens', (table) => { 126 | table.increments() 127 | table.integer('tokenable_id').notNullable().unsigned() 128 | table.string('type').notNullable() 129 | table.string('name').nullable() 130 | table.string('hash', 80).notNullable() 131 | table.text('abilities').notNullable() 132 | table.timestamp('created_at', { precision: 6, useTz: true }).notNullable() 133 | table.timestamp('updated_at', { precision: 6, useTz: true }).notNullable() 134 | table.timestamp('expires_at', { precision: 6, useTz: true }).nullable() 135 | table.timestamp('last_used_at', { precision: 6, useTz: true }).nullable() 136 | }) 137 | 138 | await db.connection().schema.createTable('users', (table) => { 139 | table.increments() 140 | table.string('username').unique().notNullable() 141 | table.string('email').unique().notNullable() 142 | table.string('password').nullable() 143 | }) 144 | 145 | await db.connection().schema.createTable('remember_me_tokens', (table) => { 146 | table.increments() 147 | table.integer('tokenable_id').notNullable().unsigned() 148 | table.string('hash', 80).notNullable() 149 | table.timestamp('created_at', { precision: 6, useTz: true }).notNullable() 150 | table.timestamp('updated_at', { precision: 6, useTz: true }).notNullable() 151 | table.timestamp('expires_at', { precision: 6, useTz: true }).notNullable() 152 | }) 153 | } 154 | 155 | /** 156 | * Creates an emitter instance for testing with typed 157 | * events 158 | */ 159 | export function createEmitter>() { 160 | const test = getActiveTest() 161 | if (!test) { 162 | throw new Error('Cannot use "createEmitter" outside of a Japa test') 163 | } 164 | 165 | const app = new AppFactory().create(test.context.fs.baseUrl, () => {}) 166 | return new Emitter(app) 167 | } 168 | 169 | /** 170 | * Promisify an event 171 | */ 172 | export function pEvent, K extends keyof T>( 173 | emitter: Emitter, 174 | event: K, 175 | timeout: number = 500 176 | ) { 177 | return new Promise((resolve) => { 178 | function handler(data: T[K]) { 179 | emitter.off(event, handler) 180 | resolve(data) 181 | } 182 | 183 | setTimeout(() => { 184 | emitter.off(event, handler) 185 | resolve(null) 186 | }, timeout) 187 | emitter.on(event, handler) 188 | }) 189 | } 190 | 191 | /** 192 | * Parses set-cookie header 193 | */ 194 | export function parseCookies(setCookiesHeader: string | string[]) { 195 | const cookies = setCookieParser(setCookiesHeader, { map: true }) 196 | const client = new CookieClient(encryption) 197 | 198 | return Object.keys(cookies).reduce((result, key) => { 199 | const cookie = cookies[key] 200 | 201 | result[key] = { 202 | ...cookie, 203 | value: cookie.value ? client.parse(cookie.name, cookie.value) : cookie.value, 204 | } 205 | return result 206 | }, {} as CookieMap) 207 | } 208 | 209 | /** 210 | * Define cookies for the request cookie header 211 | */ 212 | export function defineCookies( 213 | cookies: { 214 | key: string 215 | value: string 216 | type: 'plain' | 'encrypted' | 'signed' 217 | }[] 218 | ) { 219 | const client = new CookieClient(encryption) 220 | 221 | return cookies 222 | .reduce((result, cookie) => { 223 | const value = 224 | cookie.type === 'plain' 225 | ? client.encode(cookie.key, cookie.value) 226 | : cookie.type === 'encrypted' 227 | ? client.encrypt(cookie.key, cookie.value) 228 | : client.sign(cookie.key, cookie.value) 229 | 230 | result.push(`${cookie.key}=${value}`) 231 | return result 232 | }, [] as string[]) 233 | .join(';') 234 | } 235 | 236 | /** 237 | * Travels time by seconds 238 | */ 239 | export function timeTravel(secondsToTravel: number) { 240 | const test = getActiveTest() 241 | if (!test) { 242 | throw new Error('Cannot use "timeTravel" outside of a Japa test') 243 | } 244 | 245 | timekeeper.reset() 246 | 247 | const date = new Date() 248 | date.setSeconds(date.getSeconds() + secondsToTravel) 249 | timekeeper.travel(date) 250 | 251 | test.cleanup(() => { 252 | timekeeper.reset() 253 | }) 254 | } 255 | 256 | /** 257 | * Freezes time in the moment 258 | */ 259 | export function freezeTime() { 260 | const test = getActiveTest() 261 | if (!test) { 262 | throw new Error('Cannot use "freezeTime" outside of a Japa test') 263 | } 264 | 265 | timekeeper.reset() 266 | 267 | const date = new Date() 268 | timekeeper.freeze(date) 269 | 270 | test.cleanup(() => { 271 | timekeeper.reset() 272 | }) 273 | } 274 | -------------------------------------------------------------------------------- /tests/session/define_config.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { configProvider } from '@adonisjs/core' 12 | import { BaseModel, column } from '@adonisjs/lucid/orm' 13 | import { AppFactory } from '@adonisjs/core/factories/app' 14 | import type { ApplicationService } from '@adonisjs/core/types' 15 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 16 | 17 | import { createEmitter } from '../helpers.js' 18 | import { SessionGuard, SessionLucidUserProvider } from '../../modules/session_guard/main.js' 19 | import { sessionGuard, sessionUserProvider } from '../../modules/session_guard/define_config.js' 20 | 21 | test.group('defineConfig', () => { 22 | test('configure lucid user provider', ({ assert, expectTypeOf }) => { 23 | class User extends BaseModel { 24 | @column({ isPrimary: true }) 25 | declare id: number 26 | 27 | @column() 28 | declare username: string 29 | 30 | @column() 31 | declare email: string 32 | 33 | @column() 34 | declare password: string 35 | } 36 | 37 | const userProvider = sessionUserProvider({ 38 | async model() { 39 | return { 40 | default: User, 41 | } 42 | }, 43 | }) 44 | assert.instanceOf(userProvider, SessionLucidUserProvider) 45 | expectTypeOf(userProvider).toEqualTypeOf>() 46 | }) 47 | 48 | test('configure session guard', async ({ assert, expectTypeOf }) => { 49 | class User extends BaseModel { 50 | @column({ isPrimary: true }) 51 | declare id: number 52 | 53 | @column() 54 | declare username: string 55 | 56 | @column() 57 | declare email: string 58 | 59 | @column() 60 | declare password: string 61 | } 62 | 63 | const ctx = new HttpContextFactory().create() 64 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 65 | await app.init() 66 | app.container.bind('emitter', () => createEmitter()) 67 | 68 | const guard = await sessionGuard({ 69 | useRememberMeTokens: false, 70 | provider: sessionUserProvider({ 71 | async model() { 72 | return { 73 | default: User, 74 | } 75 | }, 76 | }), 77 | }).resolver('api', app) 78 | 79 | assert.instanceOf(guard(ctx), SessionGuard) 80 | expectTypeOf(guard).returns.toEqualTypeOf< 81 | SessionGuard> 82 | >() 83 | }) 84 | 85 | test('configure session guard and enable remember me tokens', async ({ 86 | assert, 87 | expectTypeOf, 88 | }) => { 89 | class User extends BaseModel { 90 | @column({ isPrimary: true }) 91 | declare id: number 92 | 93 | @column() 94 | declare username: string 95 | 96 | @column() 97 | declare email: string 98 | 99 | @column() 100 | declare password: string 101 | } 102 | 103 | const ctx = new HttpContextFactory().create() 104 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 105 | await app.init() 106 | app.container.bind('emitter', () => createEmitter()) 107 | 108 | const guard = await sessionGuard({ 109 | useRememberMeTokens: true, 110 | provider: sessionUserProvider({ 111 | async model() { 112 | return { 113 | default: User, 114 | } 115 | }, 116 | }), 117 | }).resolver('api', app) 118 | 119 | assert.instanceOf(guard(ctx), SessionGuard) 120 | expectTypeOf(guard).returns.toEqualTypeOf< 121 | SessionGuard> 122 | >() 123 | }) 124 | 125 | test('register user provider from a config provider', async ({ assert, expectTypeOf }) => { 126 | class User extends BaseModel { 127 | @column({ isPrimary: true }) 128 | declare id: number 129 | 130 | @column() 131 | declare username: string 132 | 133 | @column() 134 | declare email: string 135 | 136 | @column() 137 | declare password: string 138 | } 139 | 140 | const ctx = new HttpContextFactory().create() 141 | const app = new AppFactory().create(new URL('./', import.meta.url)) as ApplicationService 142 | await app.init() 143 | app.container.bind('emitter', () => createEmitter()) 144 | 145 | const userProvider = configProvider.create(async () => { 146 | return sessionUserProvider({ 147 | async model() { 148 | return { 149 | default: User, 150 | } 151 | }, 152 | }) 153 | }) 154 | 155 | const guard = await sessionGuard({ 156 | useRememberMeTokens: false, 157 | provider: userProvider, 158 | }).resolver('api', app) 159 | 160 | assert.instanceOf(guard(ctx), SessionGuard) 161 | expectTypeOf(guard).returns.toEqualTypeOf< 162 | SessionGuard> 163 | >() 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /tests/session/guard/login.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 12 | import { SessionMiddlewareFactory } from '@adonisjs/session/factories' 13 | 14 | import { createEmitter, pEvent, parseCookies } from '../../helpers.js' 15 | import { SessionGuard } from '../../../modules/session_guard/guard.js' 16 | import type { SessionGuardEvents } from '../../../modules/session_guard/types.js' 17 | import { RememberMeToken } from '../../../modules/session_guard/remember_me_token.js' 18 | import { 19 | SessionFakeUser, 20 | SessionFakeUserProvider, 21 | SessionFakeUserWithTokensProvider, 22 | } from '../../../factories/session/main.js' 23 | 24 | test.group('Session guard | login', () => { 25 | test('create session for the user', async ({ assert, expectTypeOf }) => { 26 | const ctx = new HttpContextFactory().create() 27 | const emitter = createEmitter>() 28 | const userProvider = new SessionFakeUserProvider() 29 | 30 | const guard = new SessionGuard( 31 | 'web', 32 | ctx, 33 | { 34 | useRememberMeTokens: false, 35 | }, 36 | emitter, 37 | userProvider 38 | ) 39 | 40 | /** 41 | * Setup ctx with session 42 | */ 43 | const sessionMiddleware = await new SessionMiddlewareFactory().create() 44 | await sessionMiddleware.handle(ctx, async () => {}) 45 | 46 | const user = await userProvider.findById(1) 47 | const [attempted, succeeded] = await Promise.all([ 48 | pEvent(emitter, 'session_auth:login_attempted'), 49 | pEvent(emitter, 'session_auth:login_succeeded'), 50 | guard.login(user!.getOriginal()), 51 | ]) 52 | 53 | expectTypeOf(guard.user).toEqualTypeOf() 54 | expectTypeOf(succeeded!.user).toEqualTypeOf() 55 | 56 | assert.equal(attempted!.guardName, 'web') 57 | assert.equal(succeeded!.guardName, 'web') 58 | assert.equal(succeeded!.sessionId, ctx.session.sessionId) 59 | 60 | assert.deepEqual(guard.user, user!.getOriginal()) 61 | assert.deepEqual(guard.getUserOrFail(), user!.getOriginal()) 62 | assert.isFalse(guard.isLoggedOut) 63 | assert.isFalse(guard.isAuthenticated) 64 | assert.isFalse(guard.authenticationAttempted) 65 | assert.isFalse(guard.viaRemember) 66 | assert.isFalse(guard.attemptedViaRemember) 67 | 68 | assert.deepEqual(ctx.session.all(), { 69 | auth_web: user!.getId(), 70 | }) 71 | }) 72 | 73 | test('throw error when trying to create remember me token without enabling it', async ({ 74 | assert, 75 | }) => { 76 | const ctx = new HttpContextFactory().create() 77 | const emitter = createEmitter>() 78 | const userProvider = new SessionFakeUserProvider() 79 | 80 | const guard = new SessionGuard( 81 | 'web', 82 | ctx, 83 | { 84 | useRememberMeTokens: false, 85 | }, 86 | emitter, 87 | userProvider 88 | ) 89 | 90 | /** 91 | * Setup ctx with session 92 | */ 93 | const sessionMiddleware = await new SessionMiddlewareFactory().create() 94 | await sessionMiddleware.handle(ctx, async () => {}) 95 | 96 | const user = await userProvider.findById(1) 97 | const [attempted] = await Promise.all([ 98 | pEvent(emitter, 'session_auth:login_attempted'), 99 | (async () => { 100 | await assert.rejects( 101 | () => guard.login(user!.getOriginal(), true), 102 | 'Cannot use "rememberMe" feature. It has been disabled' 103 | ) 104 | })(), 105 | ]) 106 | 107 | assert.exists(attempted) 108 | assert.isUndefined(guard.user) 109 | assert.isFalse(guard.isLoggedOut) 110 | assert.isFalse(guard.isAuthenticated) 111 | assert.isFalse(guard.authenticationAttempted) 112 | assert.isFalse(guard.viaRemember) 113 | assert.isFalse(guard.attemptedViaRemember) 114 | 115 | assert.deepEqual(ctx.session.all(), {}) 116 | }) 117 | 118 | test('create remember me cookie and token', async ({ assert, expectTypeOf }) => { 119 | const ctx = new HttpContextFactory().create() 120 | const emitter = createEmitter>() 121 | const userProvider = new SessionFakeUserWithTokensProvider() 122 | 123 | const guard = new SessionGuard( 124 | 'web', 125 | ctx, 126 | { 127 | useRememberMeTokens: true, 128 | }, 129 | emitter, 130 | userProvider 131 | ) 132 | 133 | /** 134 | * Setup ctx with session 135 | */ 136 | const sessionMiddleware = await new SessionMiddlewareFactory().create() 137 | await sessionMiddleware.handle(ctx, async () => {}) 138 | 139 | const user = await userProvider.findById(1) 140 | const [attempted, succeeded] = await Promise.all([ 141 | pEvent(emitter, 'session_auth:login_attempted'), 142 | pEvent(emitter, 'session_auth:login_succeeded'), 143 | guard.login(user!.getOriginal(), true), 144 | ]) 145 | 146 | expectTypeOf(guard.user).toEqualTypeOf() 147 | expectTypeOf(succeeded!.user).toEqualTypeOf() 148 | 149 | assert.equal(attempted!.guardName, 'web') 150 | assert.equal(succeeded!.guardName, 'web') 151 | assert.equal(succeeded!.sessionId, ctx.session.sessionId) 152 | 153 | assert.deepEqual(guard.user, user!.getOriginal()) 154 | assert.deepEqual(guard.getUserOrFail(), user!.getOriginal()) 155 | assert.isFalse(guard.isLoggedOut) 156 | assert.isFalse(guard.isAuthenticated) 157 | assert.isFalse(guard.authenticationAttempted) 158 | assert.isFalse(guard.viaRemember) 159 | assert.isFalse(guard.attemptedViaRemember) 160 | 161 | assert.deepEqual(ctx.session.all(), { 162 | auth_web: user!.getId(), 163 | }) 164 | 165 | const responseCookies = parseCookies(ctx.response.getHeader('set-cookie') as string) 166 | assert.lengthOf(userProvider.tokens, 1) 167 | assert.equal( 168 | RememberMeToken.decode(responseCookies.remember_web.value)?.identifier, 169 | userProvider.tokens[0].id 170 | ) 171 | }) 172 | }) 173 | -------------------------------------------------------------------------------- /tests/session/guard/logout.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { HttpContextFactory } from '@adonisjs/core/factories/http' 12 | import { SessionMiddlewareFactory } from '@adonisjs/session/factories' 13 | 14 | import { SessionGuard } from '../../../modules/session_guard/guard.js' 15 | import { SessionGuardEvents } from '../../../modules/session_guard/types.js' 16 | import { createEmitter, defineCookies, parseCookies } from '../../helpers.js' 17 | import { 18 | SessionFakeUser, 19 | SessionFakeUserProvider, 20 | SessionFakeUserWithTokensProvider, 21 | } from '../../../factories/session/main.js' 22 | 23 | test.group('Session guard | logout', () => { 24 | test('delete user session and remember me cookie', async ({ assert }) => { 25 | const ctx = new HttpContextFactory().create() 26 | const emitter = createEmitter>() 27 | const userProvider = new SessionFakeUserProvider() 28 | 29 | const guard = new SessionGuard( 30 | 'web', 31 | ctx, 32 | { 33 | useRememberMeTokens: false, 34 | }, 35 | emitter, 36 | userProvider 37 | ) 38 | 39 | /** 40 | * Setup ctx with session 41 | */ 42 | const sessionMiddleware = await new SessionMiddlewareFactory().create() 43 | await sessionMiddleware.handle(ctx, async () => {}) 44 | 45 | const user = await userProvider.findById(1) 46 | ctx.session.put('auth_web', user!.getId()) 47 | 48 | await guard.authenticate() 49 | await guard.logout() 50 | 51 | assert.isUndefined(guard.user) 52 | assert.deepEqual(ctx.session.all(), {}) 53 | 54 | const responseCookies = parseCookies(ctx.response.getHeader('set-cookie') as string) 55 | assert.deepEqual(responseCookies.remember_web.expires, new Date(0)) 56 | assert.deepEqual(responseCookies.remember_web.maxAge, -1) 57 | }) 58 | 59 | test('delete remember me token using user provider', async ({ assert }) => { 60 | const ctx = new HttpContextFactory().create() 61 | const emitter = createEmitter>() 62 | const userProvider = new SessionFakeUserWithTokensProvider() 63 | 64 | const guard = new SessionGuard( 65 | 'web', 66 | ctx, 67 | { 68 | useRememberMeTokens: true, 69 | }, 70 | emitter, 71 | userProvider 72 | ) 73 | 74 | const user = await userProvider.findById(1) 75 | const token = await userProvider.createRememberToken(user!.getOriginal(), '20 mins') 76 | ctx.request.request.headers.cookie = defineCookies([ 77 | { 78 | key: 'remember_web', 79 | value: token.value!.release(), 80 | type: 'encrypted', 81 | }, 82 | ]) 83 | 84 | /** 85 | * Setup ctx with session 86 | */ 87 | const sessionMiddleware = await new SessionMiddlewareFactory().create() 88 | await sessionMiddleware.handle(ctx, async () => {}) 89 | ctx.session.put('auth_web', user!.getId()) 90 | 91 | await guard.authenticate() 92 | await guard.logout() 93 | 94 | assert.isUndefined(guard.user) 95 | assert.deepEqual(ctx.session.all(), {}) 96 | 97 | const responseCookies = parseCookies(ctx.response.getHeader('set-cookie') as string) 98 | assert.deepEqual(responseCookies.remember_web.expires, new Date(0)) 99 | assert.deepEqual(responseCookies.remember_web.maxAge, -1) 100 | 101 | assert.lengthOf(userProvider.tokens, 0) 102 | }) 103 | 104 | test('do not delete token with storage when no user was authenticated in first place', async ({ 105 | assert, 106 | }) => { 107 | const ctx = new HttpContextFactory().create() 108 | const emitter = createEmitter>() 109 | const userProvider = new SessionFakeUserWithTokensProvider() 110 | 111 | const guard = new SessionGuard( 112 | 'web', 113 | ctx, 114 | { 115 | useRememberMeTokens: true, 116 | }, 117 | emitter, 118 | userProvider 119 | ) 120 | 121 | const user = await userProvider.findById(1) 122 | const token = await userProvider.createRememberToken(user!.getOriginal(), '20 mins') 123 | ctx.request.request.headers.cookie = defineCookies([ 124 | { 125 | key: 'remember_web', 126 | value: token.value!.release(), 127 | type: 'encrypted', 128 | }, 129 | ]) 130 | 131 | /** 132 | * Setup ctx with session 133 | */ 134 | const sessionMiddleware = await new SessionMiddlewareFactory().create() 135 | await sessionMiddleware.handle(ctx, async () => {}) 136 | ctx.session.put('auth_web', user!.getId()) 137 | 138 | await guard.logout() 139 | 140 | assert.isUndefined(guard.user) 141 | assert.deepEqual(ctx.session.all(), {}) 142 | 143 | const responseCookies = parseCookies(ctx.response.getHeader('set-cookie') as string) 144 | assert.deepEqual(responseCookies.remember_web.expires, new Date(0)) 145 | assert.deepEqual(responseCookies.remember_web.maxAge, -1) 146 | 147 | assert.lengthOf(userProvider.tokens, 1) 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /tests/session/remember_me_token.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/auth 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { Secret, base64 } from '@poppinss/utils' 12 | 13 | import { freezeTime } from '../helpers.js' 14 | import { RememberMeToken } from '../../modules/session_guard/remember_me_token.js' 15 | 16 | test.group('RememberMeToken token | decode', () => { 17 | test('decode "{input}" as token') 18 | .with([ 19 | { 20 | input: null, 21 | output: null, 22 | }, 23 | { 24 | input: '', 25 | output: null, 26 | }, 27 | { 28 | input: '..', 29 | output: null, 30 | }, 31 | { 32 | input: 'foobar', 33 | output: null, 34 | }, 35 | { 36 | input: 'foo.baz', 37 | output: null, 38 | }, 39 | { 40 | input: `bar.${base64.urlEncode('baz')}`, 41 | output: null, 42 | }, 43 | { 44 | input: `${base64.urlEncode('baz')}.bar`, 45 | output: null, 46 | }, 47 | { 48 | input: `${base64.urlEncode('bar')}.${base64.urlEncode('baz')}`, 49 | output: { 50 | identifier: 'bar', 51 | secret: 'baz', 52 | }, 53 | }, 54 | ]) 55 | .run(({ assert }, { input, output }) => { 56 | const decoded = RememberMeToken.decode(input as string) 57 | if (!decoded) { 58 | assert.deepEqual(decoded, output) 59 | } else { 60 | assert.deepEqual( 61 | { identifier: decoded.identifier, secret: decoded.secret.release() }, 62 | output 63 | ) 64 | } 65 | }) 66 | }) 67 | 68 | test.group('RememberMeToken token | create', () => { 69 | test('create a transient token', ({ assert }) => { 70 | freezeTime() 71 | const date = new Date() 72 | const expiresAt = new Date() 73 | expiresAt.setSeconds(date.getSeconds() + 60 * 20) 74 | 75 | const token = RememberMeToken.createTransientToken(1, 40, '20 mins') 76 | assert.equal(token.userId, 1) 77 | assert.exists(token.hash) 78 | assert.equal(token.expiresAt!.getTime(), expiresAt.getTime()) 79 | assert.instanceOf(token.secret, Secret) 80 | }) 81 | 82 | test('create token from persisted information', ({ assert }) => { 83 | const createdAt = new Date() 84 | const updatedAt = new Date() 85 | const expiresAt = new Date() 86 | expiresAt.setSeconds(createdAt.getSeconds() + 60 * 20) 87 | 88 | const token = new RememberMeToken({ 89 | identifier: '12', 90 | tokenableId: 1, 91 | hash: '1234', 92 | createdAt, 93 | updatedAt, 94 | expiresAt, 95 | }) 96 | 97 | assert.equal(token.identifier, '12') 98 | assert.equal(token.hash, '1234') 99 | assert.equal(token.tokenableId, 1) 100 | assert.equal(token.createdAt.getTime(), createdAt.getTime()) 101 | assert.equal(token.updatedAt.getTime(), updatedAt.getTime()) 102 | assert.equal(token.expiresAt!.getTime(), expiresAt.getTime()) 103 | 104 | assert.isUndefined(token.value) 105 | assert.isFalse(token.isExpired()) 106 | }) 107 | 108 | test('create token with a secret', ({ assert }) => { 109 | const createdAt = new Date() 110 | const updatedAt = new Date() 111 | const expiresAt = new Date() 112 | expiresAt.setSeconds(createdAt.getSeconds() + 60 * 20) 113 | 114 | const transientToken = RememberMeToken.createTransientToken(1, 40, '20 mins') 115 | 116 | const token = new RememberMeToken({ 117 | identifier: '12', 118 | tokenableId: 1, 119 | hash: transientToken.hash, 120 | createdAt, 121 | updatedAt, 122 | expiresAt, 123 | secret: transientToken.secret, 124 | }) 125 | 126 | const decoded = RememberMeToken.decode(token.value!.release()) 127 | 128 | assert.equal(token.identifier, '12') 129 | assert.equal(token.tokenableId, 1) 130 | assert.equal(token.hash, transientToken.hash) 131 | assert.instanceOf(token.value, Secret) 132 | assert.isTrue(token.verify(transientToken.secret)) 133 | assert.isTrue(token.verify(decoded!.secret)) 134 | assert.equal(token.createdAt.getTime(), createdAt.getTime()) 135 | assert.equal(token.updatedAt.getTime(), updatedAt.getTime()) 136 | assert.equal(token.expiresAt!.getTime(), expiresAt.getTime()) 137 | assert.isFalse(token.isExpired()) 138 | }) 139 | 140 | test('verify token hash', ({ assert }) => { 141 | const transientToken = RememberMeToken.createTransientToken(1, 40, '20 mins') 142 | 143 | const token = new RememberMeToken({ 144 | identifier: '12', 145 | tokenableId: 1, 146 | hash: transientToken.hash, 147 | createdAt: new Date(), 148 | updatedAt: new Date(), 149 | expiresAt: new Date(), 150 | secret: transientToken.secret, 151 | }) 152 | 153 | assert.isTrue(token.verify(transientToken.secret)) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adonisjs/tsconfig/tsconfig.package.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build" 6 | } 7 | } 8 | --------------------------------------------------------------------------------