@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
- package/dist/chunk-SPQUX3C7.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
Sign your users in with a session in the browser, or with API tokens, and keep guests out of the routes that need a
|
|
4
|
+
user.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
Oriel's authentication is built from **guards** and **providers**. A guard decides how a request's user is found: the
|
|
9
|
+
`session` guard reads it from the session, and the `token` guard from an API token in the `Authorization` header. A
|
|
10
|
+
provider decides how users are read from storage: the `eloquent` provider reads them with your `User` model.
|
|
11
|
+
|
|
12
|
+
Both are set in `config/auth.ts`:
|
|
13
|
+
|
|
14
|
+
```ts title="config/auth.ts"
|
|
15
|
+
import { env } from '@orieljs/framework'
|
|
16
|
+
import { User } from '../app/Models/User'
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
defaults: { guard: env('AUTH_GUARD', 'web'), passwords: env('AUTH_PASSWORD_BROKER', 'users') },
|
|
20
|
+
guards: {
|
|
21
|
+
web: { driver: 'session', provider: 'users' },
|
|
22
|
+
token: { driver: 'token', provider: 'users', expiration: null },
|
|
23
|
+
},
|
|
24
|
+
providers: { users: { driver: 'eloquent', model: User } },
|
|
25
|
+
passwords: { users: { provider: 'users', table: 'password_reset_tokens', expire: 60, throttle: 60 } },
|
|
26
|
+
password_timeout: 10800,
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The `web` guard is the default. The provider names the model class itself, which the config file imports.
|
|
31
|
+
|
|
32
|
+
### Starter kits
|
|
33
|
+
|
|
34
|
+
Every [starter kit](starter-kits.md) comes with authentication: the kits with pages have registration, login, email
|
|
35
|
+
verification, password reset and the settings pages, and the API kit has its token endpoints. Their code is in your
|
|
36
|
+
app, so this page explains what it calls.
|
|
37
|
+
|
|
38
|
+
### Database considerations
|
|
39
|
+
|
|
40
|
+
A new app's `User` model is built on `AuthenticatableModel`, which signs users in by their `password` column, keeps
|
|
41
|
+
"Remember me" tokens in `remember_token`, and uses `email` and `email_verified_at` for password resets and email
|
|
42
|
+
verification. A new app's `users` table has these columns, with a `password` column long enough for a hash.
|
|
43
|
+
|
|
44
|
+
```ts title="app/Models/User.ts"
|
|
45
|
+
import { AuthenticatableModel, Authorizable, HasApiTokens, Model, Notifiable } from '@orieljs/framework'
|
|
46
|
+
|
|
47
|
+
export class User extends HasApiTokens(Notifiable(Authorizable(AuthenticatableModel(Model)))) {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> [!NOTE]
|
|
51
|
+
> Laravel's `User` model extends `Illuminate\Foundation\Auth\User`, and adds traits such as `HasApiTokens` and
|
|
52
|
+
> `Notifiable`. Oriel's builds the same abilities from mixins: `AuthenticatableModel` for signing in, `Authorizable`
|
|
53
|
+
> for [authorization](authorization.md), `Notifiable` for [notifications](notifications.md) and `HasApiTokens` for
|
|
54
|
+
> [API tokens](#api-tokens).
|
|
55
|
+
|
|
56
|
+
## Authentication quickstart
|
|
57
|
+
|
|
58
|
+
### Retrieving the authenticated user
|
|
59
|
+
|
|
60
|
+
`Auth.user()` returns the signed-in user, or `undefined` for a guest. It is async, since the guard may have to read
|
|
61
|
+
the session or the database the first time it is asked:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const user = await Auth.user<User>()
|
|
65
|
+
|
|
66
|
+
const id = await Auth.id()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`request().user()` does the same for the current request, through the request's guard:
|
|
70
|
+
|
|
71
|
+
```ts title="app/Http/Controllers/FlightController.ts"
|
|
72
|
+
import { request } from '@orieljs/framework'
|
|
73
|
+
import type { User } from '../../Models/User'
|
|
74
|
+
import { Controller } from './Controller'
|
|
75
|
+
|
|
76
|
+
export class FlightController extends Controller {
|
|
77
|
+
/** Update the flight information for an existing flight. */
|
|
78
|
+
async update() {
|
|
79
|
+
const user = await request().user<User>()
|
|
80
|
+
|
|
81
|
+
// ...
|
|
82
|
+
|
|
83
|
+
return { updatedBy: user?.id }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The type you give is a cast, not a check. On a route behind the `auth` middleware, a user is there, so you can write
|
|
89
|
+
`(await request().user<User>())!`.
|
|
90
|
+
|
|
91
|
+
> [!WARNING]
|
|
92
|
+
> `request().user()` returns a promise, so `await` it every time. TypeScript flags `if (request().user())`, but not
|
|
93
|
+
> `!request().user()` or `request().user() === undefined`, which are always `false`.
|
|
94
|
+
|
|
95
|
+
> [!NOTE]
|
|
96
|
+
> Laravel's `Auth::user()` and `$request->user()` return the user at once. Oriel's return a promise: `await` them.
|
|
97
|
+
|
|
98
|
+
#### Determining if the current user is authenticated
|
|
99
|
+
|
|
100
|
+
`Auth.check()` tells you whether a user is signed in, and `Auth.guest()` whether no one is. Both are async:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
if (await Auth.check()) {
|
|
104
|
+
// The user is logged in...
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
To keep guests out of a route, use the `auth` middleware rather than checking in each action.
|
|
109
|
+
|
|
110
|
+
### Protecting routes
|
|
111
|
+
|
|
112
|
+
The `auth` middleware lets only signed-in users reach a route:
|
|
113
|
+
|
|
114
|
+
```ts title="routes/web.ts"
|
|
115
|
+
import { Route } from '@orieljs/framework'
|
|
116
|
+
|
|
117
|
+
Route.get('/flights', () => {
|
|
118
|
+
// Only authenticated users may access this route...
|
|
119
|
+
return 'Flights'
|
|
120
|
+
}).middleware('auth')
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Redirecting unauthenticated users
|
|
124
|
+
|
|
125
|
+
A guest who reaches a route behind `auth` is redirected to `/login`, and back to the page they wanted once they sign
|
|
126
|
+
in. A request that wants JSON gets a 401 instead, `{ "message": "Unauthenticated." }`. `redirectGuestsTo()` in
|
|
127
|
+
`bootstrap/app.ts` sends guests elsewhere (see
|
|
128
|
+
[Redirecting guests and signed-in users](middleware.md#redirecting-guests-and-signed-in-users)).
|
|
129
|
+
|
|
130
|
+
#### Redirecting authenticated users
|
|
131
|
+
|
|
132
|
+
The `guest` middleware does the opposite: it sends a signed-in user away from the pages meant for guests, such as the
|
|
133
|
+
login page. It sends them to your `dashboard` route, else your `home` route, else `/`; `redirectUsersTo()` in
|
|
134
|
+
`bootstrap/app.ts` chooses another place.
|
|
135
|
+
|
|
136
|
+
#### Specifying a guard
|
|
137
|
+
|
|
138
|
+
`auth` takes the guards to try, by name. The first one that has a user lets the request through, and becomes the
|
|
139
|
+
request's guard, so `Auth.user()` and `request().user()` read from it:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
Route.get('/flights', () => 'Flights').middleware('auth:token')
|
|
143
|
+
|
|
144
|
+
Route.get('/account', () => 'Account').middleware('auth:web,token')
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Login throttling
|
|
148
|
+
|
|
149
|
+
The kits' login stops a user after five failed attempts for the same email address and IP address, for a minute, and
|
|
150
|
+
dispatches the `Lockout` event. The limit is in their `LoginRequest` (in the Live kit, the `Login` component), written
|
|
151
|
+
with the `RateLimiter` (see [Rate limiting](cache.md#rate-limiting)).
|
|
152
|
+
|
|
153
|
+
## Manually authenticating users
|
|
154
|
+
|
|
155
|
+
You don't have to use the kits' code. `Auth.attempt()` signs a user in with their credentials:
|
|
156
|
+
|
|
157
|
+
```ts title="app/Http/Controllers/LoginController.ts"
|
|
158
|
+
import { Auth, back, redirect, request } from '@orieljs/framework'
|
|
159
|
+
import { Controller } from './Controller'
|
|
160
|
+
|
|
161
|
+
export class LoginController extends Controller {
|
|
162
|
+
/** Handle an authentication attempt. */
|
|
163
|
+
async authenticate() {
|
|
164
|
+
const credentials = await request().validate({
|
|
165
|
+
email: 'required|email',
|
|
166
|
+
password: 'required',
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (await Auth.attempt(credentials)) {
|
|
170
|
+
await request().session().regenerate()
|
|
171
|
+
|
|
172
|
+
return redirect().intended('dashboard')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return back()
|
|
176
|
+
.withErrors({ email: 'The provided credentials do not match our records.' })
|
|
177
|
+
.withInput(request().only('email'))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`attempt()` finds the user by every credential except those whose key contains `password`, here by `email`, then
|
|
183
|
+
checks the password against the user's hash. When it matches, it signs the user in and returns `true`; otherwise it
|
|
184
|
+
returns `false`. `redirect().intended()` sends the user to the page they wanted before `auth` sent them to the login
|
|
185
|
+
page, or to the path you give.
|
|
186
|
+
|
|
187
|
+
`attempt()` changes the session's id as it signs the user in, which protects against session fixation; the kits also
|
|
188
|
+
call `regenerate()`, as above.
|
|
189
|
+
|
|
190
|
+
#### Specifying additional conditions
|
|
191
|
+
|
|
192
|
+
Add any condition to the credentials. A list matches any of its values, and a function receives the query:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
declare const email: string
|
|
196
|
+
declare const password: string
|
|
197
|
+
// ---cut---
|
|
198
|
+
if (await Auth.attempt({ email, password, active: 1 })) {
|
|
199
|
+
// Authentication was successful...
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (await Auth.attempt({ email, password, role: ['admin', 'editor'] })) {
|
|
203
|
+
// ...
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (await Auth.attempt({ email, password, verified: (query: { whereNotNull(column: string): unknown }) => query.whereNotNull('email_verified_at') })) {
|
|
207
|
+
// ...
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Accessing specific guard instances
|
|
212
|
+
|
|
213
|
+
`Auth.guard()` returns a guard by name, with the same methods:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const user = await Auth.guard('token').user()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Remembering users
|
|
220
|
+
|
|
221
|
+
Pass `true` as `attempt()`'s second argument to keep the user signed in after their session ends, until they sign
|
|
222
|
+
out:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
declare const email: string
|
|
226
|
+
declare const password: string
|
|
227
|
+
declare const remember: boolean
|
|
228
|
+
// ---cut---
|
|
229
|
+
if (await Auth.attempt({ email, password }, remember)) {
|
|
230
|
+
// The user is being remembered...
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Oriel then sets a "Remember me" cookie, which lasts 400 days unless the guard's `remember` setting, in minutes, says
|
|
235
|
+
otherwise. `Auth.viaRemember()` tells you whether the current user was signed in by that cookie. Signing out
|
|
236
|
+
changes the user's `remember_token`, so the cookie stops working on every browser.
|
|
237
|
+
|
|
238
|
+
### Other authentication methods
|
|
239
|
+
|
|
240
|
+
#### Authenticate a user instance
|
|
241
|
+
|
|
242
|
+
`Auth.login()` signs in a user you already have, such as one who has registered. Its second argument remembers the user:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
declare const user: User
|
|
246
|
+
// ---cut---
|
|
247
|
+
await Auth.login(user)
|
|
248
|
+
|
|
249
|
+
await Auth.login(user, true)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Authenticate a user by ID
|
|
253
|
+
|
|
254
|
+
`Auth.loginUsingId()` signs in the user with that primary key, and returns them, or `false` when there is none:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
await Auth.loginUsingId(1)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Authenticate a user once
|
|
261
|
+
|
|
262
|
+
`Auth.once()` signs a user in for the current request only: nothing is written to the session, and no cookie is set:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
declare const credentials: { email: string; password: string }
|
|
266
|
+
// ---cut---
|
|
267
|
+
if (await Auth.once(credentials)) {
|
|
268
|
+
// ...
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`attempt()`, `once()`, `login()`, `loginUsingId()` and `logout()` need a guard that keeps a user, such as `web`, and
|
|
273
|
+
throw on the `token` guard.
|
|
274
|
+
|
|
275
|
+
> [!NOTE]
|
|
276
|
+
> Laravel also has HTTP Basic authentication (`auth.basic`). Oriel doesn't.
|
|
277
|
+
|
|
278
|
+
## Logging out
|
|
279
|
+
|
|
280
|
+
`Auth.logout()` signs the user out. Invalidate their session and make a new CSRF token too, then redirect them:
|
|
281
|
+
|
|
282
|
+
```ts title="app/Http/Controllers/LogoutController.ts"
|
|
283
|
+
import { Auth, redirect, request } from '@orieljs/framework'
|
|
284
|
+
import { Controller } from './Controller'
|
|
285
|
+
|
|
286
|
+
export class LogoutController extends Controller {
|
|
287
|
+
/** Log the user out of the application. */
|
|
288
|
+
async logout() {
|
|
289
|
+
await Auth.logout()
|
|
290
|
+
|
|
291
|
+
await request().session().invalidate()
|
|
292
|
+
|
|
293
|
+
request().session().regenerateToken()
|
|
294
|
+
|
|
295
|
+
return redirect('/')
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
> [!NOTE]
|
|
301
|
+
> Laravel's `logoutOtherDevices()` and its password confirmation pages (`password.confirm`) aren't in Oriel.
|
|
302
|
+
|
|
303
|
+
## API tokens
|
|
304
|
+
|
|
305
|
+
A client that isn't a browser, such as a mobile app or another service, signs in with an API token. Oriel's tokens are
|
|
306
|
+
Laravel Sanctum's personal access tokens, under the `token` guard: Sanctum's `auth:sanctum` is `auth:token` here. A new
|
|
307
|
+
app has the `personal_access_tokens` table, and its `User` model has `HasApiTokens`.
|
|
308
|
+
|
|
309
|
+
### Issuing tokens
|
|
310
|
+
|
|
311
|
+
`createToken()` makes a token for the user, with a name. The token's plain text is shown only once, so return it to the
|
|
312
|
+
client and keep nothing else. Here a user signed in in the browser makes one, from a web route:
|
|
313
|
+
|
|
314
|
+
```ts title="routes/web.ts"
|
|
315
|
+
import { Auth, request, Route } from '@orieljs/framework'
|
|
316
|
+
import type { User } from '../app/Models/User'
|
|
317
|
+
|
|
318
|
+
Route.post('/tokens/create', async () => {
|
|
319
|
+
const user = (await Auth.user<User>())!
|
|
320
|
+
const token = await user.createToken(request().string('token_name').toString())
|
|
321
|
+
|
|
322
|
+
return { token: token.plainTextToken }
|
|
323
|
+
}).middleware('auth')
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The client sends it with every request, in the `Authorization` header:
|
|
327
|
+
|
|
328
|
+
```text
|
|
329
|
+
Authorization: Bearer 1|Zb3qL9vXw2Kc7Ty4NfRm8HpAs6Ud1GeJo5WiYtBn
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The token is its row's id, a `|` and a random secret; the table keeps only the secret's SHA-256.
|
|
333
|
+
|
|
334
|
+
### Protecting routes
|
|
335
|
+
|
|
336
|
+
Put the routes behind `auth:token`. The API kit does, for its routes in `routes/api.ts`:
|
|
337
|
+
|
|
338
|
+
```ts title="routes/api.ts"
|
|
339
|
+
import { request, Route } from '@orieljs/framework'
|
|
340
|
+
|
|
341
|
+
Route.middleware('auth:token').group(() => {
|
|
342
|
+
Route.get('/user', () => request().user())
|
|
343
|
+
})
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
A request with no token, a wrong one or an expired one gets a 401, `{ "message": "Unauthenticated." }`, when it asks for
|
|
347
|
+
JSON, as API clients do; the API kit answers JSON under `/api` whatever the request asks for. Each request with a token
|
|
348
|
+
updates its `last_used_at`.
|
|
349
|
+
|
|
350
|
+
### Token abilities
|
|
351
|
+
|
|
352
|
+
A token can be limited to some abilities, which you list when you make it. A token made without a list has every
|
|
353
|
+
ability:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
declare const user: User
|
|
357
|
+
// ---cut---
|
|
358
|
+
const token = await user.createToken('token-name', ['server:update'])
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
In a request made with a token, `user.tokenCan()` tells you whether the token has an ability, and `tokenCant()`
|
|
362
|
+
whether it hasn't:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
const user = (await request().user<User>())!
|
|
366
|
+
|
|
367
|
+
if (user.tokenCan('server:update')) {
|
|
368
|
+
// ...
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Token expiration
|
|
373
|
+
|
|
374
|
+
A token can expire: give `createToken()` a date as its third argument. The `token` guard's `expiration`, in minutes,
|
|
375
|
+
ends every token that long after it was made; `null`, in a new app, leaves them to their own date.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
declare const user: User
|
|
379
|
+
// ---cut---
|
|
380
|
+
const token = await user.createToken('token-name', ['*'], new Date(Date.now() + 7 * 24 * 60 * 60 * 1000))
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Revoking tokens
|
|
384
|
+
|
|
385
|
+
`currentAccessToken()` is the token of the current request, and `tokens()` gives all of the user's tokens. Delete a
|
|
386
|
+
token to revoke it:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
const user = (await request().user<User>())!
|
|
390
|
+
|
|
391
|
+
// Revoke the token that was used to authenticate the current request...
|
|
392
|
+
await user.currentAccessToken()?.delete()
|
|
393
|
+
|
|
394
|
+
// Revoke all tokens...
|
|
395
|
+
for (const token of await user.tokens()) {
|
|
396
|
+
await token.delete()
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
> [!NOTE]
|
|
401
|
+
> Laravel Sanctum's `tokens()` is a relation, so `$user->tokens()->delete()` revokes them all with one query. Oriel's
|
|
402
|
+
> returns the tokens as an array: delete each one. Sanctum's SPA cookie authentication isn't in Oriel: a browser app
|
|
403
|
+
> served by your app signs in with the `web` guard, and a route can accept both with `auth:web,token`.
|
|
404
|
+
|
|
405
|
+
## Adding custom guards
|
|
406
|
+
|
|
407
|
+
`Auth.extend()` adds a guard driver, in a service provider's `boot()`. Its function receives the guard's name and its
|
|
408
|
+
entry of `config/auth.ts`, and returns an object with the `Guard` contract's methods:
|
|
409
|
+
|
|
410
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
411
|
+
// @filename: app/Services/Auth/JwtGuard.ts
|
|
412
|
+
import type { Guard } from '@orieljs/framework/contracts'
|
|
413
|
+
export declare class JwtGuard implements Guard {
|
|
414
|
+
constructor(name: string)
|
|
415
|
+
user: Guard['user']
|
|
416
|
+
id: Guard['id']
|
|
417
|
+
check: Guard['check']
|
|
418
|
+
guest: Guard['guest']
|
|
419
|
+
validate: Guard['validate']
|
|
420
|
+
hasUser: Guard['hasUser']
|
|
421
|
+
setUser: Guard['setUser']
|
|
422
|
+
}
|
|
423
|
+
// ---cut---
|
|
424
|
+
import { Auth, ServiceProvider } from '@orieljs/framework'
|
|
425
|
+
import { JwtGuard } from '../Services/Auth/JwtGuard'
|
|
426
|
+
|
|
427
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
428
|
+
override boot(): void {
|
|
429
|
+
Auth.extend('jwt', (name) => new JwtGuard(name))
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Then name the driver in a guard of `config/auth.ts`: `api: { driver: 'jwt', provider: 'users' }`.
|
|
435
|
+
|
|
436
|
+
## Adding custom user providers
|
|
437
|
+
|
|
438
|
+
`Auth.provider()` adds a user provider driver, for users kept somewhere your models don't reach. Its function
|
|
439
|
+
receives the provider's entry of `config/auth.ts` and returns an object with the `UserProvider` contract's methods:
|
|
440
|
+
`retrieveById()`, `retrieveByToken()`, `updateRememberToken()`, `retrieveByCredentials()` and
|
|
441
|
+
`validateCredentials()`:
|
|
442
|
+
|
|
443
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
444
|
+
// @filename: app/Extensions/MongoUserProvider.ts
|
|
445
|
+
import type { UserProvider } from '@orieljs/framework/contracts'
|
|
446
|
+
export declare class MongoUserProvider implements UserProvider {
|
|
447
|
+
constructor(connection: unknown)
|
|
448
|
+
retrieveById: UserProvider['retrieveById']
|
|
449
|
+
retrieveByToken: UserProvider['retrieveByToken']
|
|
450
|
+
updateRememberToken: UserProvider['updateRememberToken']
|
|
451
|
+
retrieveByCredentials: UserProvider['retrieveByCredentials']
|
|
452
|
+
validateCredentials: UserProvider['validateCredentials']
|
|
453
|
+
}
|
|
454
|
+
// ---cut---
|
|
455
|
+
import { Auth, ServiceProvider } from '@orieljs/framework'
|
|
456
|
+
import { MongoUserProvider } from '../Extensions/MongoUserProvider'
|
|
457
|
+
|
|
458
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
459
|
+
override boot(): void {
|
|
460
|
+
Auth.provider('mongo', (config) => new MongoUserProvider(config.connection))
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Then use it in `config/auth.ts`: `providers: { users: { driver: 'mongo', connection: 'mongodb' } }`.
|
|
466
|
+
|
|
467
|
+
## Automatic password rehashing
|
|
468
|
+
|
|
469
|
+
When a user signs in with `attempt()` and their password's hash was made with other settings than your current ones,
|
|
470
|
+
such as fewer bcrypt rounds, Oriel hashes the password again and saves it. Turn this off with `rehash_on_login: false`
|
|
471
|
+
in `config/hashing.ts` (see [Hashing](hashing.md)).
|
|
472
|
+
|
|
473
|
+
## Events
|
|
474
|
+
|
|
475
|
+
Signing in and out dispatches these events, which you can [listen](events.md) for:
|
|
476
|
+
|
|
477
|
+
| Event | When |
|
|
478
|
+
| --- | --- |
|
|
479
|
+
| `Attempting` | `attempt()` or `once()` is about to check credentials |
|
|
480
|
+
| `Validated` | The credentials matched |
|
|
481
|
+
| `Failed` | The credentials didn't match |
|
|
482
|
+
| `Login` | A user signed in with `attempt()`, `login()`, `loginUsingId()` or the "Remember me" cookie (`once()` dispatches none) |
|
|
483
|
+
| `Logout` | A user signed out |
|
|
484
|
+
| `Lockout` | The kits' login throttle stopped a user |
|
|
485
|
+
| `Registered` | The kits registered a user (it sends the verification mail) |
|
|
486
|
+
| `Verified` | A user verified their email address |
|
|
487
|
+
| `PasswordReset` | The kits reset a user's password |
|