Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when implementing authentication - login/signup forms, session management, protected routes, or role-based access control. NOT when non-auth UI, plain data fetching, or unrelated backend logic. Triggers: "login page", "signup form", "auth setup", "protected route", "role-based access", "Better Auth", "NextAuth".
.claude/skills/aiskillstore-auth-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 258% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 211% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 75% | 0% |
Expert guidance for authentication implementation using Better Auth/NextAuth v5, including login/signup forms, session management with Zustand, protected routes via middleware, and role-based access control for ERP systems.
This skill triggers when users request:
typescript// app/auth/[...auth]/route.ts import { auth } from '@/lib/auth'; import { toNextJsHandler } from 'better-auth/next-js'; export const { GET, POST } = toNextJsHandler(auth); // lib/auth.ts import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; export const auth = betterAuth({ database: prismaAdapter(prisma), emailAndPassword: { enabled: true, requireEmailVerification: true, }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 24 hours }, });
Requirements:
tsximport { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { signIn } from '@/lib/auth/client'; const loginSchema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(8, 'Password must be at least 8 characters'), }); type LoginFormData = z.infer<typeof loginSchema>; export default function LoginForm() { const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<LoginFormData>({ resolver: zodResolver(loginSchema), }); const onSubmit = async (data: LoginFormData) => { await signIn.email({ email: data.email, password: data.password, }); }; return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div> <label className="block text-sm font-medium mb-2">Email</label> <input {...register('email')} type="email" className="w-full px-4 py-2 rounded-lg border" /> {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>} </div> <div> <label className="block text-sm font-medium mb-2">Password</label> <input {...register('password')} type="password" className="w-full px-4 py-2 rounded-lg border" /> {errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>} </div> <button type="submit" disabled={isSubmitting} className="w-full py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50" > {isSubmitting ? 'Signing in...' : 'Sign In'} </button> </form> ); }
Requirements:
typescriptimport { create } from 'zustand'; import { authClient } from '@/lib/auth/client'; interface AuthState { user: User | null; session: Session | null; isLoading: boolean; isAuthenticated: boolean; role: string | null; hydrateAuth: () => Promise<void>; signIn: (email: string, password: string) => Promise<void>; signOut: () => Promise<void>; refresh: () => Promise<void>; } export const useAuthStore = create<AuthState>((set, get) => ({ user: null, session: null, isLoading: true, isAuthenticated: false, role: null, hydrateAuth: async () => { set({ isLoading: true }); try { const session = await authClient.getSession(); set({ user: session.user, session: session, isAuthenticated: !!session, role: session.user?.role || null, isLoading: false, }); } catch (error) { set({ isLoading: false, isAuthenticated: false }); } }, signIn: async (email: string, password: string) => { await authClient.signIn.email({ email, password }); await get().hydrateAuth(); }, signOut: async () => { await authClient.signOut(); set({ user: null, session: null, isAuthenticated: false, role: null, }); }, refresh: async () => { await get().hydrateAuth(); }, })); export const useAuth = () => { const auth = useAuthStore(); useEffect(() => { if (!auth.user && !auth.isLoading) { auth.hydrateAuth(); } }, []); return auth; };
Requirements:
typescriptimport { authMiddleware } from 'better-auth/next-js'; import { NextResponse } from 'next/server'; export default authMiddleware({ pathPrefix: '/dashboard', callback: async (request) => { const { user } = request; // Redirect to login if not authenticated if (!user) { return NextResponse.redirect(new URL('/auth/login', request.url)); } // Role-based access control const path = request.nextUrl.pathname; if (path.startsWith('/dashboard/admin') && user.role !== 'admin') { return NextResponse.redirect(new URL('/dashboard', request.url)); } if (path.startsWith('/dashboard/teacher') && user.role !== 'teacher' && user.role !== 'admin') { return NextResponse.redirect(new URL('/dashboard', request.url)); } return NextResponse.next(); }, }); export const config = { matcher: ['/dashboard/:path*'], };
tsximport { useRouter } from 'next/navigation'; import { useAuth } from '@/hooks/useAuth'; export function withAuth<P extends object>( WrappedComponent: React.ComponentType<P>, allowedRoles?: string[] ) { return function AuthGuard(props: P) { const { isAuthenticated, user, isLoading } = useAuth(); const router = useRouter(); useEffect(() => { if (!isLoading && !isAuthenticated) { router.push('/auth/login'); } if (!isLoading && isAuthenticated && allowedRoles) { if (!allowedRoles.includes(user?.role || '')) { router.push('/unauthorized'); } } }, [isAuthenticated, isLoading, user, router]); if (isLoading) { return <LoadingSkeleton />; } if (!isAuthenticated || (allowedRoles && !allowedRoles.includes(user?.role || ''))) { return null; } return <WrappedComponent {...props} />; }; }
Requirements:
typescriptexport const ROLES = { ADMIN: 'admin', TEACHER: 'teacher', STUDENT: 'student', PARENT: 'parent', } as const; export const PERMISSIONS = { // Admin permissions MANAGE_USERS: 'manage:users', MANAGE_COURSES: 'manage:courses', VIEW_ALL_DATA: 'view:all_data', // Teacher permissions VIEW_CLASS: 'view:class', MANAGE_STUDENTS: 'manage:students', CREATE_ASSIGNMENTS: 'create:assignments', // Student permissions VIEW_OWN_DATA: 'view:own_data', SUBMIT_ASSIGNMENTS: 'submit:assignments', } as const; export const hasRole = (user: User | null, role: string): boolean => { return user?.role === role || user?.role === ROLES.ADMIN; }; export const hasPermission = (user: User | null, permission: string): boolean => { if (!user) return false; if (user.role === ROLES.ADMIN) return true; const rolePermissions = { [ROLES.ADMIN]: Object.values(PERMISSIONS), [ROLES.TEACHER]: [ PERMISSIONS.VIEW_CLASS, PERMISSIONS.MANAGE_STUDENTS, PERMISSIONS.CREATE_ASSIGNMENTS, ], [ROLES.STUDENT]: [ PERMISSIONS.VIEW_OWN_DATA, PERMISSIONS.SUBMIT_ASSIGNMENTS, ], [ROLES.PARENT]: [ PERMISSIONS.VIEW_OWN_DATA, ], }; return rolePermissions[user.role]?.includes(permission) ?? false; }; export const PermissionGuard = ({ permission, fallback = null, children, }: { permission: string; fallback?: React.ReactNode; children: React.ReactNode; }) => { const { user } = useAuth(); if (!hasPermission(user, permission)) { return <>{fallback}</>; } return <>{children}</>; };
Requirements:
lib/auth.ts - Better Auth setupapp/auth/[...auth]/route.ts - Auth API routesapp/auth/login/page.tsx - Login formapp/auth/signup/page.tsx - Signup formapp/auth/forgot-password/page.tsx - Password resetlib/auth-store.ts - Zustand auth storehooks/useAuth.ts - Auth hookmiddleware.ts - Route protectioncomponents/AuthGuard.tsx - Auth guard HOCBefore completing any auth implementation:
tsx'use client'; import { useState } from 'react'; import { signIn } from '@/lib/auth/client'; import { useRouter } from 'next/navigation'; export default function LoginPage() { const router = useRouter(); const [error, setError] = useState(''); const handleGoogleSignIn = async () => { try { await signIn.social({ provider: 'google', callbackURL: '/dashboard', }); } catch (err) { setError('Failed to sign in with Google'); } }; const handleEmailSignIn = async (email: string, password: string) => { try { await signIn.email({ email, password, callbackURL: '/dashboard', }); router.push('/dashboard'); } catch (err) { setError('Invalid email or password'); } }; return ( <div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow-md"> <h1 className="text-3xl font-bold text-center">Sign In</h1> {error && ( <div className="p-3 bg-red-100 text-red-700 rounded-lg"> {error} </div> )} <button onClick={handleGoogleSignIn} className="w-full flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50" > Sign in with Google </button> <div className="relative"> <div className="absolute inset-0 flex items-center"> <div className="w-full border-t border-gray-300" /> </div> <div className="relative flex justify-center text-sm"> <span className="px-2 bg-white text-gray-500">Or continue with email</span> </div> </div> <LoginForm onSubmit={handleEmailSignIn} /> <p className="text-center text-sm text-gray-600"> Don't have an account?{' '} <a href="/auth/signup" className="text-blue-600 hover:underline"> Sign up </a> </p> </div> </div> ); }
tsx'use client'; import { useState } from 'react'; import { signUp } from '@/lib/auth/client'; export default function SignupPage() { const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const handleSignup = async (data: SignupFormData) => { try { await signUp.email({ email: data.email, password: data.password, name: data.name, }); setSuccess(true); } catch (err) { setError('Failed to create account'); } }; if (success) { return ( <div className="min-h-screen flex items-center justify-center"> <div className="text-center"> <h1 className="text-2xl font-bold mb-4">Check your email</h1> <p className="text-gray-600"> We've sent a verification link to your email address </p> </div> </div> ); } return <SignupForm onSubmit={handleSignup} />; }
tsximport { withAuth } from '@/components/AuthGuard'; import { ROLES } from '@/lib/permissions'; function AdminDashboard() { const { user } = useAuth(); return ( <div className="p-6"> <h1 className="text-2xl font-bold mb-6">Admin Dashboard</h1> <p>Welcome, {user?.name}</p> </div> ); } export default withAuth(AdminDashboard, [ROLES.ADMIN]);
tsxexport const PasswordStrength = ({ password }: { password: string }) => { const getStrength = (pwd: string) => { let strength = 0; if (pwd.length >= 8) strength++; if (/[a-z]/.test(pwd)) strength++; if (/[A-Z]/.test(pwd)) strength++; if (/[0-9]/.test(pwd)) strength++; if (/[^a-zA-Z0-9]/.test(pwd)) strength++; return strength; }; const strength = getStrength(password); const colors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500', 'bg-green-600']; const labels = ['Weak', 'Fair', 'Good', 'Strong', 'Very Strong']; return ( <div className="mt-2"> <div className="flex gap-1"> {[1, 2, 3, 4, 5].map((i) => ( <div key={i} className={`h-2 flex-1 rounded ${i <= strength ? colors[strength - 1] : 'bg-gray-200'}`} /> ))} </div> {password && ( <p className={`text-sm mt-1 ${colors[strength - 1]?.replace('bg-', 'text-')}`}> Password strength: {labels[strength - 1]} </p> )} </div> ); };
typescriptimport { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(5, '10 s'), // 5 requests per 10 seconds }); export async function checkRateLimit(identifier: string) { const { success } = await ratelimit.limit(identifier); return success; }
typescriptexport async function logAuthEvent(event: { type: 'login' | 'logout' | 'signup' | 'password_reset'; userId?: string; ip?: string; userAgent?: string; }) { await prisma.authEvent.create({ data: { ...event, timestamp: new Date(), }, }); }
bash# .env.local AUTH_SECRET=your-super-secret-random-string BETTER_AUTH_URL=http://localhost:3000 # Google OAuth GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret # Database DATABASE_URL=your-database-url
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | fail→fail | 32,200 | 24,365 | -24% | 1 | 1 | 0% | 3,252 | 9,492 | +192% | 0 | 0 | — |
case-01 | fail→fail | 41,535 | 38,292 | -8% | 1 | 1 | 0% | 8,292 | 13,518 | +63% | 0 | 0 | — |
case-02 | fail→fail | 55,461 | 26,831 | -52% | 1 | 1 | 0% | 6,884 | 10,753 | +56% | 0 | 0 | — |
case-03 | fail→fail | 40,795 | 35,227 | -14% | 1 | 1 | 0% | 8,270 | 13,496 | +63% | 0 | 0 | — |
case-04 | fail→pass | 25,627 | 24,048 | -6% | 1 | 1 | 0% | 4,240 | 9,224 | +118% | 0 | 0 | — |
case-05 | fail→pass | 16,619 | 16,580 | -0% | 1 | 1 | 0% | 2,026 | 7,253 | +258% | 0 | 0 | — |
case-07 | fail→pass | 13,674 | 21,824 | +60% | 1 | 1 | 0% | 2,886 | 8,976 | +211% | 0 | 0 | — |
case-08 | pass→pass | 11,464 | 14,396 | +26% | 1 | 1 | 0% | 1,264 | 6,881 | +444% | 0 | 0 | — |
case-09 | fail→fail | 21,356 | 20,782 | -3% | 1 | 1 | 0% | 3,189 | 8,484 | +166% | 0 | 0 | — |
case-10 | fail→pass | 26,360 | 16,371 | -38% | 1 | 1 | 0% | 4,072 | 8,535 | +110% | 0 | 0 | — |
case-11 | pass→pass | 20,762 | 22,507 | +8% | 1 | 1 | 0% | 3,236 | 9,041 | +179% | 0 | 0 | — |
case-12 | fail→pass | 27,546 | 17,427 | -37% | 1 | 1 | 0% | 4,465 | 7,817 | +75% | 0 | 0 | — |
case-13 | pass→pass | 14,036 | 25,329 | +80% | 1 | 1 | 0% | 1,700 | 8,685 | +411% | 0 | 0 | — |
case-14 | pass→pass | 20,309 | 17,669 | -13% | 1 | 1 | 0% | 3,312 | 7,956 | +140% | 0 | 0 | — |
case-15 | fail→pass | 22,686 | 25,064 | +10% | 1 | 1 | 0% | 3,734 | 10,998 | +195% | 0 | 0 | — |
case-16 | fail→pass | 25,618 | 25,538 | -0% | 1 | 1 | 0% | 4,409 | 9,777 | +122% | 0 | 0 | — |
case-22 | pass→pass | 22,681 | 20,119 | -11% | 1 | 1 | 0% | 3,449 | 8,660 | +151% | 0 | 0 | — |
case-17 | pass→pass | 22,075 | 22,258 | +1% | 1 | 1 | 0% | 3,712 | 8,610 | +132% | 0 | 0 | — |
case-18 | pass→pass | 20,807 | 15,441 | -26% | 1 | 1 | 0% | 2,817 | 7,956 | +182% | 0 | 0 | — |
case-19 | pass→pass | 32,282 | 16,047 | -50% | 1 | 1 | 0% | 2,784 | 8,631 | +210% | 0 | 0 | — |
case-20 | fail→pass | 12,984 | 14,155 | +9% | 1 | 1 | 0% | 1,689 | 7,306 | +333% | 0 | 0 | — |
case-21 | pass→pass | 21,567 | 13,569 | -37% | 1 | 1 | 0% | 2,407 | 8,045 | +234% | 0 | 0 | — |
case-23 | pass→pass | 18,943 | 39,250 | +107% | 1 | 1 | 0% | 2,740 | 8,326 | +204% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +35 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.