Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Security

Authentication

Sign your users in with a session in the browser, or with API tokens, and keep guests out of the routes that need a user.

Introduction#

Oriel's authentication is built from guards and providers. A guard decides how a request's user is found: the session guard reads it from the session, and the token guard from an API token in the Authorization header. A provider decides how users are read from storage: the eloquent provider reads them with your User model.

Both are set in config/auth.ts:

config/auth.ts
import { env } from '@orieljs/framework'import { User } from '../app/Models/User'export default {  defaults: { guard: env('AUTH_GUARD', 'web'), passwords: env('AUTH_PASSWORD_BROKER', 'users') },  guards: {    web: { driver: 'session', provider: 'users' },    token: { driver: 'token', provider: 'users', expiration: null },  },  providers: { users: { driver: 'eloquent', model: User } },  passwords: { users: { provider: 'users', table: 'password_reset_tokens', expire: 60, throttle: 60 } },  password_timeout: 10800,}

The web guard is the default. The provider names the model class itself, which the config file imports.

Starter kits#

Every starter kit comes with authentication: the kits with pages have registration, login, email verification, password reset and the settings pages, and the API kit has its token endpoints. Their code is in your app, so this page explains what it calls.

Database considerations#

A new app's User model is built on AuthenticatableModel, which signs users in by their password column, keeps "Remember me" tokens in remember_token, and uses email and email_verified_at for password resets and email verification. A new app's users table has these columns, with a password column long enough for a hash.

app/Models/User.ts
import { AuthenticatableModel, Authorizable, HasApiTokens, Model, Notifiable } from '@orieljs/framework'export class User extends HasApiTokens(Notifiable(Authorizable(AuthenticatableModel(Model)))) {}

Authentication quickstart#

Retrieving the authenticated user#

Auth.user() returns the signed-in user, or undefined for a guest. It is async, since the guard may have to read the session or the database the first time it is asked:

const user = await Auth.user<User>()const id = await Auth.id()

request().user() does the same for the current request, through the request's guard:

app/Http/Controllers/FlightController.ts
import { request } from '@orieljs/framework'import type { User } from '../../Models/User'import { Controller } from './Controller'export class FlightController extends Controller {  /** Update the flight information for an existing flight. */  async update() {    const user = await request().user<User>()    // ...    return { updatedBy: user?.id }  }}

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 (await request().user<User>())!.

Determining if the current user is authenticated#

Auth.check() tells you whether a user is signed in, and Auth.guest() whether no one is. Both are async:

if (await Auth.check()) {  // The user is logged in...}

To keep guests out of a route, use the auth middleware rather than checking in each action.

Protecting routes#

The auth middleware lets only signed-in users reach a route:

routes/web.ts
import { Route } from '@orieljs/framework'Route.get('/flights', () => {  // Only authenticated users may access this route...  return 'Flights'}).middleware('auth')

Redirecting unauthenticated users#

A guest who reaches a route behind auth is redirected to /login, and back to the page they wanted once they sign in. A request that wants JSON gets a 401 instead, { "message": "Unauthenticated." }. redirectGuestsTo() in bootstrap/app.ts sends guests elsewhere (see Redirecting guests and signed-in users).

Redirecting authenticated users#

The guest middleware does the opposite: it sends a signed-in user away from the pages meant for guests, such as the login page. It sends them to your dashboard route, else your home route, else /; redirectUsersTo() in bootstrap/app.ts chooses another place.

Specifying a guard#

auth takes the guards to try, by name. The first one that has a user lets the request through, and becomes the request's guard, so Auth.user() and request().user() read from it:

Route.get('/flights', () => 'Flights').middleware('auth:token')Route.get('/account', () => 'Account').middleware('auth:web,token')

Login throttling#

The kits' login stops a user after five failed attempts for the same email address and IP address, for a minute, and dispatches the Lockout event. The limit is in their LoginRequest (in the Live kit, the Login component), written with the RateLimiter (see Rate limiting).

Manually authenticating users#

You don't have to use the kits' code. Auth.attempt() signs a user in with their credentials:

app/Http/Controllers/LoginController.ts
import { Auth, back, redirect, request } from '@orieljs/framework'import { Controller } from './Controller'export class LoginController extends Controller {  /** Handle an authentication attempt. */  async authenticate() {    const credentials = await request().validate({      email: 'required|email',      password: 'required',    })    if (await Auth.attempt(credentials)) {      await request().session().regenerate()      return redirect().intended('dashboard')    }    return back()      .withErrors({ email: 'The provided credentials do not match our records.' })      .withInput(request().only('email'))  }}

attempt() finds the user by every credential except those whose key contains password, here by email, then checks the password against the user's hash. When it matches, it signs the user in and returns true; otherwise it returns false. redirect().intended() sends the user to the page they wanted before auth sent them to the login page, or to the path you give.

attempt() changes the session's id as it signs the user in, which protects against session fixation; the kits also call regenerate(), as above.

Specifying additional conditions#

Add any condition to the credentials. A list matches any of its values, and a function receives the query:

if (await Auth.attempt({ email, password, active: 1 })) {  // Authentication was successful...}if (await Auth.attempt({ email, password, role: ['admin', 'editor'] })) {  // ...}if (await Auth.attempt({ email, password, verified: (query: { whereNotNull(column: string): unknown }) => query.whereNotNull('email_verified_at') })) {  // ...}

Accessing specific guard instances#

Auth.guard() returns a guard by name, with the same methods:

const user = await Auth.guard('token').user()

Remembering users#

Pass true as attempt()'s second argument to keep the user signed in after their session ends, until they sign out:

if (await Auth.attempt({ email, password }, remember)) {  // The user is being remembered...}

Oriel then sets a "Remember me" cookie, which lasts 400 days unless the guard's remember setting, in minutes, says otherwise. Auth.viaRemember() tells you whether the current user was signed in by that cookie. Signing out changes the user's remember_token, so the cookie stops working on every browser.

Other authentication methods#

Authenticate a user instance#

Auth.login() signs in a user you already have, such as one who has registered. Its second argument remembers the user:

await Auth.login(user)await Auth.login(user, true)

Authenticate a user by ID#

Auth.loginUsingId() signs in the user with that primary key, and returns them, or false when there is none:

await Auth.loginUsingId(1)

Authenticate a user once#

Auth.once() signs a user in for the current request only: nothing is written to the session, and no cookie is set:

if (await Auth.once(credentials)) {  // ...}

attempt(), once(), login(), loginUsingId() and logout() need a guard that keeps a user, such as web, and throw on the token guard.

Logging out#

Auth.logout() signs the user out. Invalidate their session and make a new CSRF token too, then redirect them:

app/Http/Controllers/LogoutController.ts
import { Auth, redirect, request } from '@orieljs/framework'import { Controller } from './Controller'export class LogoutController extends Controller {  /** Log the user out of the application. */  async logout() {    await Auth.logout()    await request().session().invalidate()    request().session().regenerateToken()    return redirect('/')  }}

API tokens#

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 Laravel Sanctum's personal access tokens, under the token guard: Sanctum's auth:sanctum is auth:token here. A new app has the personal_access_tokens table, and its User model has HasApiTokens.

Issuing tokens#

createToken() makes a token for the user, with a name. The token's plain text is shown only once, so return it to the client and keep nothing else. Here a user signed in in the browser makes one, from a web route:

routes/web.ts
import { Auth, request, Route } from '@orieljs/framework'import type { User } from '../app/Models/User'Route.post('/tokens/create', async () => {  const user = (await Auth.user<User>())!  const token = await user.createToken(request().string('token_name').toString())  return { token: token.plainTextToken }}).middleware('auth')

The client sends it with every request, in the Authorization header:

Authorization: Bearer 1|Zb3qL9vXw2Kc7Ty4NfRm8HpAs6Ud1GeJo5WiYtBn

The token is its row's id, a | and a random secret; the table keeps only the secret's SHA-256.

Protecting routes#

Put the routes behind auth:token. The API kit does, for its routes in routes/api.ts:

routes/api.ts
import { request, Route } from '@orieljs/framework'Route.middleware('auth:token').group(() => {  Route.get('/user', () => request().user())})

A request with no token, a wrong one or an expired one gets a 401, { "message": "Unauthenticated." }, when it asks for JSON, as API clients do; the API kit answers JSON under /api whatever the request asks for. Each request with a token updates its last_used_at.

Token abilities#

A token can be limited to some abilities, which you list when you make it. A token made without a list has every ability:

const token = await user.createToken('token-name', ['server:update'])

In a request made with a token, user.tokenCan() tells you whether the token has an ability, and tokenCant() whether it hasn't:

const user = (await request().user<User>())!if (user.tokenCan('server:update')) {  // ...}

Token expiration#

A token can expire: give createToken() a date as its third argument. The token guard's expiration, in minutes, ends every token that long after it was made; null, in a new app, leaves them to their own date.

const token = await user.createToken('token-name', ['*'], new Date(Date.now() + 7 * 24 * 60 * 60 * 1000))

Revoking tokens#

currentAccessToken() is the token of the current request, and tokens() gives all of the user's tokens. Delete a token to revoke it:

const user = (await request().user<User>())!// Revoke the token that was used to authenticate the current request...await user.currentAccessToken()?.delete()// Revoke all tokens...for (const token of await user.tokens()) {  await token.delete()}

Adding custom guards#

Auth.extend() adds a guard driver, in a service provider's boot(). Its function receives the guard's name and its entry of config/auth.ts, and returns an object with the Guard contract's methods:

app/Providers/AppServiceProvider.ts
import { Auth, ServiceProvider } from '@orieljs/framework'import { JwtGuard } from '../Services/Auth/JwtGuard'export class AppServiceProvider extends ServiceProvider {  override boot(): void {    Auth.extend('jwt', (name) => new JwtGuard(name))  }}

Then name the driver in a guard of config/auth.ts: api: { driver: 'jwt', provider: 'users' }.

Adding custom user providers#

Auth.provider() adds a user provider driver, for users kept somewhere your models don't reach. Its function receives the provider's entry of config/auth.ts and returns an object with the UserProvider contract's methods: retrieveById(), retrieveByToken(), updateRememberToken(), retrieveByCredentials() and validateCredentials():

app/Providers/AppServiceProvider.ts
import { Auth, ServiceProvider } from '@orieljs/framework'import { MongoUserProvider } from '../Extensions/MongoUserProvider'export class AppServiceProvider extends ServiceProvider {  override boot(): void {    Auth.provider('mongo', (config) => new MongoUserProvider(config.connection))  }}

Then use it in config/auth.ts: providers: { users: { driver: 'mongo', connection: 'mongodb' } }.

Automatic password rehashing#

When a user signs in with attempt() and their password's hash was made with other settings than your current ones, such as fewer bcrypt rounds, Oriel hashes the password again and saves it. Turn this off with rehash_on_login: false in config/hashing.ts (see Hashing).

Events#

Signing in and out dispatches these events, which you can listen for:

EventWhen
Attemptingattempt() or once() is about to check credentials
ValidatedThe credentials matched
FailedThe credentials didn't match
LoginA user signed in with attempt(), login(), loginUsingId() or the "Remember me" cookie (once() dispatches none)
LogoutA user signed out
LockoutThe kits' login throttle stopped a user
RegisteredThe kits registered a user (it sends the verification mail)
VerifiedA user verified their email address
PasswordResetThe kits reset a user's password

Privacy policy ·

esc