AuthJS
DB
- Tener una base de datos disponible, como Astro DB (
db) con Turso - Configurar y relacionar tablas si es necesario, como
RolesyUsers
1. AuthJS
astro/clientpara tener información del lado del clienteastro/serverpara sesiones del lado del server
1. Instalar
bun astro add auth-astro
2. Variables
El AUTH_SECRET debe tener un mínimo de 32 caracteres
# AuthJS
AUTH_TRUST_HOST=true # Si no se despliega en Vercel
AUTH_SECRET=<min-32-characters>
#GITHUB_CLIENT_ID=abc
#GITHUB_CLIENT_SECRET=abc
}),
3. Archivo de configuración
- Crear archivo de configuración en el root del proyecto (en
/, no/src) - Se configuran los métodos de autenticación y, si es necesario, en los callbacks definir la información personalizada de user del objeto
session
Si se utiliza credentials (correo y contraseña) entonces se debe crear antes el proceso de registro para poder validar después el correo y la contraseña.
import type { AdapterUser } from "@auth/core/adapters";
import Credentials from "@auth/core/providers/credentials";
import { db, eq, User } from "astro:db";
import { defineConfig } from "auth-astro";
import bcrypt from "bcryptjs";
export default defineConfig({
providers: [
// Email and password provider
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
authorize: async ({ email, password }) => {
// Check if email and password are correct
const [user] = await db
.select()
.from(User)
.where(eq(User.email, `${email}`));
if (!user) throw new Error("Invalid email or password");
if (!bcrypt.compareSync(password as string, user.password)) {
throw new Error("Invalid email or password");
}
// Return object with user session data
// Add custom info (auth.d.ts)
const { password: _, ...rest } = user;
return rest;
},
}),
],
callbacks: {
// The user data is the same as the Credentials.authorize return
// The data in token es the data returned
jwt: ({ token, user }) => {
// Add user to token for session can access it
if (user) token.user = user;
return token;
},
session: ({ session, token }) => {
// Add user data to session
session.user = token.user as AdapterUser;
return session;
},
},
});
- Por defecto, solo devuelve el email del usuario que inició sesión, este se puede extraer de
session.user.email - Al devolver un objeto con los datos del usuario, ej.
return user, estos se devuelven en el payload del token pero estos datos no se pueden ver o extraer fácilmente (como se haría con el email) - Para extraer los datos personalizados, se debe especificar en los
callbacksy ya estará disponible en el objeto desession, para tener tipado estricto se debe agregarauth.d.ts
import { getSession } from "auth-astro/server";
const session = await getSession(Astro.request);
4. Op. Archivo de definición
- Archivo de definición si se agrega información extra a la sesión y se requiere el tipado estricto
- Si no se agrega más información a la sesión, únicamente estará disponible el email del usuario en
session.user - Si se agrega información, entonces únicamente estará disponible esa información personalizada y el ID (no el email)
Ejemplo agregando solo el rol
import { DefaultUser } from "@auth/core/types";
declare module "@auth/core/types" {
interface User extends DefaultUser {
role: string;
}
interface Session extends DefaultSession {
user: User;
}
}
Ejemplo agregando más campos
import { DefaultUser } from "@auth/core/types";
declare module "@auth/core/types" {
interface User extends DefaultUser {
role?: string;
customFieldOne?: boolean;
customFieldTwo?: string;
}
interface Session extends DefaultSession {
user: User;
}
}
2. Op. Register
Action
import { defineAction } from "astro:actions";
import { db, eq, User } from "astro:db";
import { z } from "astro:schema";
import bcrypt from "bcryptjs";
import { v4 as UUID } from "uuid";
export const register = defineAction({
accept: "form",
input: z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
password: z.string().min(6).max(50),
}),
handler: async ({ name, email, password }) => {
// Check if email is already registered
const [user] = await db.select().from(User).where(eq(User.email, email));
if (user) throw new Error("Email already registered");
// Create user
await db.insert(User).values({
id: UUID(),
name,
email,
password: bcrypt.hashSync(password),
role: "user",
});
return true;
},
});
Form
<form>
<!-- Labels and inputs -->
<button id="btn-submit" class="w-full disabled:bg-primary/50">
Register
</button>
</form>
<script>
import { actions } from "astro:actions";
// Get form and submit button
const form = document.querySelector("form") as HTMLFormElement;
const btnSubmit = document.getElementById("btn-submit") as HTMLButtonElement;
form.addEventListener("submit", async (e) => {
e.preventDefault();
btnSubmit.setAttribute("disabled", "disabled");
// Register user
const formData = new FormData(form);
const { error } = await actions.register(formData);
if (error) {
alert(error.message);
btnSubmit.removeAttribute("disabled");
return;
}
// If no error, redirect to login page
window.location.replace("/auth/login");
});
</script>
3. Login
- Client side
- La importación del
sign inpermite iniciar sesión con cualquier proveedor - No es necesario definir un
actionporque se está utilizando la configuración deauth.config.ts
<form action="/login" method="POST">
<!-- Data... -->
<button id="btn-submit" class="w-full disabled:bg-primary/50">
Login
</button>
</form>
<script>
const { signIn } = await import("auth-astro/client");
const form = document.querySelector("form") as HTMLFormElement;
const btnSubmit = document.getElementById("btn-submit") as HTMLButtonElement;
form.addEventListener("submit", async (e) => {
e.preventDefault();
btnSubmit.setAttribute("disabled", "disabled");
const formData = new FormData(form);
const res = await signIn("credentials", {
email: formData.get("email") as string,
password: formData.get("password") as string,
redirect: false,
});
if (res) {
alert("Email or password is incorrect");
btnSubmit.removeAttribute("disabled");
return;
}
window.location.replace("/dashboard");
});
</script>
4. Logout
<li id="logout">Logout</li>
<script>
const { signOut } = await import("auth-astro/client");
const logoutElem = document.getElementById("logout") as HTMLLIElement;
logoutElem?.addEventListener("click", async () => {
await signOut();
window.location.href = "/auth/login";
});
</script>
5. Middleware
Se extrae la información del usuario de session (AuthJS) como roles, nombre, email, etc.. Si no viene toda la información que se necesita (ej. isAdmin, removed) entonces se configura el return en el archivo de configuración auth.config.ts
El middleware también permite configurar y compartir información específica de la solicitud entre puntos finales y páginas mediante el objeto de Astro.locals que está disponible en todos los componentes de Astro y puntos finales de API.
Entonces, con los datos extraídos de session se pueden asignar los campos de Astro.locals y así utilizar Astro.locals para extraer la información del usuario en cada página o endpoint y no tener que utilizar session otra vez, únicamente en el middleware.
import { defineMiddleware } from "astro:middleware";
import { getSession } from "auth-astro/server";
const publicRoutes = ["/auth/login", "/auth/register"];
export const onRequest = defineMiddleware(
async ({ url, locals, redirect, request }, next) => {
// Verify if user is logged in
const session = await getSession(request);
const isLoggedIn = !!session;
const user = session?.user;
// Set values to Astro Locals
locals.isLoggedIn = isLoggedIn;
locals.user = null;
locals.isAdmin = false;
if (user) {
locals.user = {
name: user.name!,
email: user.email!,
};
locals.isAdmin = user.role === "admin";
}
// Protect the login and register routes when user is logged in
if (isLoggedIn && publicRoutes.includes(url.pathname)) {
return redirect("/dashboard");
}
// Redirect to login if user is not logged in
if (!isLoggedIn && url.pathname.startsWith("/dashboard")) {
return redirect("/auth/login");
}
return next();
}
);
env.d.ts
env.d.tsse utiliza para agregar campos personalizados aAstro.locals
interface User {
name: string;
email: string;
}
declare namespace App {
interface Locals {
isLoggedIn: boolean;
isAdmin: boolean;
user: User | null;
}
}
Ej. Extrayendo la información del usuario de Astro.locals
---
const { isLoggedIn, isAdmin } = Astro.locals;
---
<nav>
<ul>
<li><a href="/">Home</a></li>
{
isAdmin && (
<li><a href="/admin/dashboard">Admin</a></li>
)
}
{
!isLoggedIn ? (
<li><a href="/auth/login">Ingresar</a></li>
) : (
<li id="logout"><a href="#">Salir</a></li>
)
}
</ul>
</nav>
Ej. Extrayendo la información del usuario de session
---
import MainLayout from "@/layouts/MainLayout.astro";
import { getSession } from "auth-astro/server";
// Get session without middleware and Astro.locals
const session = await getSession(Astro.request);
const { user } = session ?? {};
---
<pre>
<code>
{ JSON.stringify(user) }
</code>
</pre>