Authentication

By Syncfusion®, Inc. · Last updated

Authentication vs authorization, how login works, session-based login and logout actions, hashing passwords, protecting pages and endpoints in middleware, why path matching is unsafe, roles and ownership, 401 vs 403, and auth libraries.

Authentication

  • Authentication = checking who someone is. Usually with an email and a password.
  • Authorization = checking what they may do. For example, only admins may delete posts.
  • In this lesson, you will put together everything from this chapter:

    • actions for the login form,
    • sessions to remember the user,
    • middleware to protect pages.

Simple lesson: authentication asks "who are you?", authorization asks "are you allowed?".

A Real-World Example

  • Asha wants a private dashboard where she drafts posts.
  • Only she should see it. Ravi, her co-writer, may edit only his own drafts.
  • She adds a login page, stores the logged-in user in the session, and lets middleware guard /dashboard.

Press ▶ Run. Try /dashboard first: you are sent to the login page. Log in with asha@example.com and astro123.

What happened:

  • Middleware read the user from the session. With no user, it redirected /dashboard to /login.
  • The login action checked the password hash, regenerated the session, and stored the user.
  • Log out destroyed the session, so /dashboard is closed again.

What You Will Learn

In this lesson, you will learn:

  1. The difference between authentication and authorization.
  2. How a login works: identity, session and cookie.
  3. How to store passwords safely.
  4. How to build login and logout actions.
  5. How to protect pages and endpoints in middleware.
  6. How to check roles and ownership.
  7. When to answer 401, and when 403.
  8. When to use an auth library instead.

Authentication Basics

How Login Works

Animation: four steps. The visitor sends email and password. The server checks the hash. The server stores the user in a session. The browser keeps only the session cookie for later requests.

1. Send email + password

2. Check the password hash

3. regenerate() + store user

4. Cookie with session ID only

On every later request, middleware reads the session and knows who is visiting.

The password is checked once. After that, the session remembers who you are.

  1. Identity: the visitor proves who they are, usually with an email and a password.
  2. Session: the server remembers them, with the user stored in Astro.session.
  3. Cookie: the browser keeps only the session ID and sends it with every request.

Choosing Between Sessions and Auth Libraries

  • Build it yourself (this lesson) to learn how login works, or for a very small private site.
  • Use an auth library for real products. It also handles password resets, email checks, "Log in with Google" and many attacks you might forget.

Simple lesson: prove identity once, remember it in the session, and read it on every request.

Session-Based Login

Hashing and Storing Credentials Safely

  • Never store passwords as plain text. If the database leaks, every account leaks.
  • Hash = a one-way scramble of the password. You can check a password against it, but you cannot turn it back.
  • Salt = a random value added before hashing, so two people with the same password get different hashes.
  • Use a slow hash made for passwords: Argon2, bcrypt, scrypt or PBKDF2. Slow hashes make guessing millions of passwords very expensive.

Press ▶ Run. The same password with different salts gives different hashes. Only the right password matches.

Creating Login and Logout Actions

  • Login action:

    1. check the email and password against the stored hash,
    2. on failure, throw new ActionError({ code: "UNAUTHORIZED" }) with a vague message such as "Wrong email or password",
    3. on success, await context.session.regenerate(), then context.session.set("user", user).
  • Logout action: context.session.destroy().
  • Store only what pages need in the session: an ID, a name, a role. Never the password or its hash.

Press ▶ Run. Try the wrong password, then the right one. Read the session lines in the Terminal.

Say "Wrong email or password", not "No account with that email". A precise message tells attackers which emails have accounts.

Simple lesson: hash passwords with a slow, salted hash. Regenerate the session on login and destroy it on logout.

Protecting Routes

Checking the User in Middleware

  • Read the user once, in middleware: context.locals.user = await context.session?.get("user").
  • Then every page, endpoint and action can use locals.user.

Redirecting Unauthenticated Visitors

  • If a protected page has no user, return context.redirect("/login").
  • Add where they wanted to go, so you can send them back after login: /login?next=/dashboard.

Making the Logged-In User Available to Pages

  • Pages read Astro.locals.user. They do not need to touch the session.

[TS] Typing the Logged-In User

declare namespace App {
  interface Locals {
    user?: { id: string; name: string; role: "admin" | "writer" };
  }
  interface SessionData {
    user: { id: string; name: string; role: "admin" | "writer" };
  }
}

Protecting Endpoints

  • An endpoint is just as public as a page. Check context.locals.user there too.
  • Answer 401 when nobody is logged in.

Why Matching context.url.pathname Strings Is Unsafe

  • A rule like pathname === "/admin" misses /admin/, /Admin and /admin/settings.
  • Encoded addresses, such as /%61dmin, can also slip past simple string checks.
  • Safer:

    • protect a whole prefix, normalised first,
    • and also check the user inside each private page, endpoint and action.
  • Middleware is the first gate, not the only one.

Press ▶ Run. Try /api/me.json before and after logging in. The page under /dashboard/settings is protected by the prefix rule.

/login itself must stay public, or nobody could ever log in. Check that your protected list does not cover it.

Simple lesson: read the user in middleware, guard whole prefixes, and check again inside private endpoints.

Authorization

Checking Roles and Permissions

  • Role = a label for what a user may do: "admin", "writer", "reader".
  • Check it before the action: if (user.role !== "admin") throw new ActionError({ code: "FORBIDDEN" }).

Checking Resource Ownership

  • Ownership = "this item belongs to this user".
  • Ravi may edit drafts, but only his drafts: compare draft.authorId === user.id.
  • Roles say what kind of thing you may do. Ownership says on which items.

401 Unauthorized vs 403 Forbidden

  • 401 Unauthorized = "I do not know who you are". Log in, then try again.
  • 403 Forbidden = "I know who you are, and you may not do this". Logging in again will not help.

Press ▶ Run. Log in as Ravi, then try to edit both drafts and open the admin page.

Simple lesson: roles decide what you may do, ownership decides which items. 401 means "log in", 403 means "not allowed".

Auth Libraries and Services

Real products usually use a library or a service:

  • Better Auth = an open-source TypeScript library. Email and password, social logins, two-factor login. Has an Astro guide.
  • Clerk = a hosted service with ready-made sign-in pages and an official Astro SDK.
  • Lucia = now a learning resource with guides for building session auth yourself, like this lesson.
  • Scalekit = a hosted service focused on business logins, such as company single sign-on.

Security code is easy to get almost right. For anything with real users, prefer a well-tested library or service, and keep your own checks in middleware and handlers.

Simple lesson: learn the pieces yourself, then use a tested library for real users.

TL;DR

  1. Authentication = who you are. Authorization = what you may do.
  2. Login = check identity → store user in the session → cookie holds only the ID.
  3. Passwords = slow, salted hashes (Argon2, bcrypt, scrypt, PBKDF2). Never plain text.
  4. Login action = verify → regenerate() → set("user", ...). Logout = destroy().
  5. Middleware = locals.user = await session.get("user"), then redirect if a protected prefix has no user.
  6. Path checks = normalise and protect prefixes, and check again in endpoints and actions.
  7. Roles and ownership = check both before changing data.
  8. 401 = not logged in. 403 = logged in, not allowed.
  9. Libraries = Better Auth, Clerk, Lucia guides, Scalekit.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Plain-text passwordsThe database is privateStore slow, salted hashesA leak exposes every password
No regenerate() on loginThe old ID is fineRegenerate on loginSession fixation
Only pathname === "/admin"One exact check covers itNormalise, protect prefixes, check again inside/admin/ or /Admin slips through
Hiding links instead of checkingNo link, no accessCheck on the serverAnyone can type the address
403 for "not logged in"401 and 403 are the same401 = log in, 403 = not allowedConfusing errors for users and apps
Telling which emails existClear errors help"Wrong email or password"Attackers learn valid accounts

Check Yourself

Exercises

What to Remember

  • Authentication proves who someone is. Authorization decides what they may do.
  • Store slow, salted password hashes. Regenerate the session on login, destroy it on logout.
  • Read the user in middleware into locals, protect whole prefixes, and check again in endpoints and actions.
  • Check roles and ownership. Answer 401 for "log in" and 403 for "not allowed".
  • Use a tested auth library for real users.

Checkpoint: Asha's site now has endpoints, middleware, cookies, sessions, actions and a working login. That is a full-stack site.

Next: Fresh Data at Scale, where server islands and live collections keep busy pages fast.