Astro กับ Auth.js: Authentication ใน Astro

#astro13 เม.ย. 2569

Astro กับ Auth.js: Authentication ใน Astro

Auth.js (เดิมชื่อ NextAuth.js) เป็น authentication library ที่ได้รับความนิยมสูงสุดใน JavaScript ecosystem ปัจจุบันรองรับ Astro อย่างเป็นทางการ ทำให้การเพิ่ม authentication ให้กับ Astro project เป็นเรื่องง่ายและปลอดภัย

ทำไมต้องใช้ Auth.js?

  • Multiple Providers: รองรับ OAuth providers มากกว่า 50 ตัว
  • Session Management: จัดการ session อย่างปลอดภัย
  • Database Adapters: รองรับ database หลายประเภท
  • TypeScript: Type-safe ตั้งแต่ต้น
  • Security: จัดการ CSRF, JWT, และ security best practices

การติดตั้ง

npm install auth-astro @auth/core

แก้ไข astro.config.mjs:

import { defineConfig } from 'astro/config';
import auth from 'auth-astro';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
  integrations: [auth()],
});

กำหนด Auth Configuration

สร้าง auth.config.ts ที่ root:

import GitHub from '@auth/core/providers/github';
import Google from '@auth/core/providers/google';
import Credentials from '@auth/core/providers/credentials';
import { defineConfig } from 'auth-astro';

export default defineConfig({
  providers: [
    GitHub({
      clientId: import.meta.env.GITHUB_CLIENT_ID,
      clientSecret: import.meta.env.GITHUB_CLIENT_SECRET,
    }),
    Google({
      clientId: import.meta.env.GOOGLE_CLIENT_ID,
      clientSecret: import.meta.env.GOOGLE_CLIENT_SECRET,
    }),
    Credentials({
      name: 'Email & Password',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        // ตรวจสอบ credentials กับ database
        const user = await verifyUser(
          credentials.email as string,
          credentials.password as string
        );
        return user ?? null;
      },
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      if (session.user && token.sub) {
        session.user.id = token.sub;
      }
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.role = (user as any).role;
      }
      return token;
    },
  },
  pages: {
    signIn: '/auth/signin',
    signOut: '/auth/signout',
    error: '/auth/error',
  },
});

async function verifyUser(email: string, password: string) {
  // implement your user verification logic
  return null;
}

Environment Variables

# .env
AUTH_SECRET=your-secret-key-here
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

Sign In Page

สร้าง src/pages/auth/signin.astro:

---
import { getSession } from 'auth-astro/server';

const session = await getSession(Astro.request);

if (session) {
  return Astro.redirect('/');
}
---

<html lang="th">
  <head>
    <title>เข้าสู่ระบบ</title>
  </head>
  <body>
    <div class="auth-container">
      <h1>เข้าสู่ระบบ</h1>
      
      <!-- OAuth Providers -->
      <div class="providers">
        <button id="github-signin" class="btn-github">
          เข้าสู่ระบบด้วย GitHub
        </button>
        <button id="google-signin" class="btn-google">
          เข้าสู่ระบบด้วย Google
        </button>
      </div>
      
      <!-- Credentials Form -->
      <form id="credentials-form">
        <input type="email" name="email" placeholder="อีเมล" required />
        <input type="password" name="password" placeholder="รหัสผ่าน" required />
        <button type="submit">เข้าสู่ระบบ</button>
      </form>
    </div>

    <script>
      import { signIn } from 'auth-astro/client';

      document.getElementById('github-signin')?.addEventListener('click', () => {
        signIn('github', { callbackUrl: '/' });
      });

      document.getElementById('google-signin')?.addEventListener('click', () => {
        signIn('google', { callbackUrl: '/' });
      });

      document.getElementById('credentials-form')?.addEventListener('submit', async (e) => {
        e.preventDefault();
        const form = e.target as HTMLFormElement;
        const email = (form.elements.namedItem('email') as HTMLInputElement).value;
        const password = (form.elements.namedItem('password') as HTMLInputElement).value;
        
        await signIn('credentials', { email, password, callbackUrl: '/' });
      });
    </script>
  </body>
</html>

Protected Pages

---
// src/pages/dashboard.astro
import { getSession } from 'auth-astro/server';

const session = await getSession(Astro.request);

if (!session) {
  return Astro.redirect('/auth/signin');
}
---

<html lang="th">
  <body>
    <h1>ยินดีต้อนรับ, {session.user?.name}!</h1>
    <p>อีเมล: {session.user?.email}</p>
    <img src={session.user?.image ?? ''} alt="Profile" />
    
    <button id="signout">ออกจากระบบ</button>
    
    <script>
      import { signOut } from 'auth-astro/client';
      document.getElementById('signout')?.addEventListener('click', () => {
        signOut({ callbackUrl: '/' });
      });
    </script>
  </body>
</html>

Middleware สำหรับ Route Protection

สร้าง src/middleware.ts:

import { defineMiddleware } from 'astro:middleware';
import { getSession } from 'auth-astro/server';

const protectedRoutes = ['/dashboard', '/profile', '/admin'];
const adminRoutes = ['/admin'];

export const onRequest = defineMiddleware(async (context, next) => {
  const { pathname } = context.url;

  const isProtected = protectedRoutes.some((route) =>
    pathname.startsWith(route)
  );

  if (isProtected) {
    const session = await getSession(context.request);

    if (!session) {
      return context.redirect(`/auth/signin?callbackUrl=${pathname}`);
    }

    const isAdmin = adminRoutes.some((route) => pathname.startsWith(route));
    if (isAdmin && (session.user as any)?.role !== 'admin') {
      return context.redirect('/403');
    }

    context.locals.session = session;
    context.locals.user = session.user;
  }

  return next();
});

Database Adapter

ใช้ Prisma adapter:

npm install @auth/prisma-adapter
// auth.config.ts
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from './src/lib/prisma';

export default defineConfig({
  adapter: PrismaAdapter(prisma),
  providers: [...],
  session: { strategy: 'database' },
});

สรุป

Auth.js ทำให้การเพิ่ม authentication ใน Astro เป็นเรื่องง่ายและปลอดภัย ด้วย OAuth providers ที่หลากหลาย, session management ที่ดี, และ middleware support ทำให้สามารถสร้าง authentication system ที่สมบูรณ์ได้อย่างรวดเร็ว