├── .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