Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Security

Email verification

Ask new users to confirm their email address with a signed link before they reach the parts of your app that need it. The starter kits with pages do this already.

Introduction#

When a user registers, Oriel mails them a link. Opening it, while signed in, marks their address as verified, and the verified middleware keeps unverified users out of the routes you give it. The kits with pages use it for the dashboard (see Email verification in the kits).

Model preparation#

A model built on AuthenticatableModel, as a new app's User is, can verify its email:

  • hasVerifiedEmail() tells you whether email_verified_at is set;
  • markEmailAsVerified() sets it to now and saves the model;
  • getEmailForVerification() returns the email attribute;
  • sendEmailVerificationNotification() mails the user the verification link.

Database preparation#

The users table needs an email_verified_at column, a nullable timestamp, which a new app's first migration has.

Routing#

Verification takes three routes, which the Inertia kits define in routes/auth.ts, behind the auth middleware: the notice that asks the user to verify, the link's handler, and a route that sends the link again.

The email verification notice#

The notice is the page a user sees when verified stops them. It is the verification.notice route, /verify-email in the kits:

routes/auth.ts
import { inertia, Route } from '@orieljs/framework'Route.middleware('auth').group(() => {  Route.get('verify-email', () => inertia('Auth/VerifyEmail')).name('verification.notice')})

The kits' notice sends a user who has verified on to the dashboard, and shows a "Resend verification email" button.

The email verification handler#

The link in the mail opens the verification.verify route, /verify-email/{id}/{hash}: the user's id, and the SHA-1 of their email address. The link is signed, and works for 60 minutes. Its route checks the signature with the signed middleware, and EmailVerificationRequest checks the id and hash against the signed-in user:

routes/auth.ts
import { EmailVerificationRequest, redirect, Route } from '@orieljs/framework'Route.get('verify-email/{id}/{hash}', async () => {  await EmailVerificationRequest.validate()  await new EmailVerificationRequest().fulfill()  return redirect().intended('/dashboard')})  .middleware(['auth', 'signed', 'throttle:6,1'])  .name('verification.verify')

validate() answers 403 when the link is for another user. fulfill() marks the email as verified and dispatches the Verified event, unless it was verified already. A link opened after it expired, or changed, gets a 403 from signed. The user must be signed in, so a user who opens the link in another browser signs in first, and is then sent back to it.

The kits' handler is VerifyEmailController, which does the same and redirects to the dashboard with ?verified=1.

Resending the verification email#

A user who lost the mail can ask for it again: the kits' verification.send route sends it, at most six times a minute:

routes/auth.ts
import { back, request, Route } from '@orieljs/framework'import type { User } from '../app/Models/User'Route.post('email/verification-notification', async () => {  const user = (await request().user<User>())!  await user.sendEmailVerificationNotification()  return back().with('status', 'verification-link-sent')})  .middleware(['auth', 'throttle:6,1'])  .name('verification.send')

In the Live kit, the VerifyEmail component's action sends it, with no rate limit of its own.

Protecting routes#

The verified middleware lets only verified users through:

routes/web.ts
import { Route, view } from '@orieljs/framework'Route.get('profile', () => view('profile')).middleware(['auth', 'verified'])

An unverified user is redirected to the verification.notice route, and to the page they wanted once they have verified. A request that wants JSON gets a 403, "Your email address is not verified.", instead. verified:<route> redirects to another named route, and with no verification.notice route the redirect goes to /email/verify. A guest gets the same redirect, so put auth first, as above.

Customization#

Verification email customization#

The mail is the VerifyEmail notification's: "Verify Email Address", a line asking the user to click the button, the button, and a closing line. It is sent in the styled template of mail notifications, with a plain-text part. VerifyEmail.toMailUsing() builds the mail yourself, in a service provider's boot(). Its function receives the user and the signed link:

app/Providers/AppServiceProvider.ts
import { MailMessage, ServiceProvider, VerifyEmail } from '@orieljs/framework'export class AppServiceProvider extends ServiceProvider {  override boot(): void {    VerifyEmail.toMailUsing((notifiable, url) =>      new MailMessage()        .subject('Verify Email Address')        .line('Click the button below to verify your email address.')        .action('Verify Email Address', url),    )  }}

VerifyEmail.createUrlUsing() builds the link instead, for example for a client app on another domain, and sendEmailVerificationUsing() replaces the whole sending, with a function that receives the user.

The link works for auth.verification.expire minutes, 60 unless your config/auth.ts sets it:

const auth = {  verification: { expire: 120 },}

Events#

Registered, which the kits dispatch when a user signs up, sends the verification mail: Oriel listens for it with SendEmailVerificationNotification, which mails a user who hasn't verified. fulfill() and the kits' handler dispatch Verified once an address is verified. Listen for either, as for any event.

Privacy policy ·

esc