1.4.6 • Published 6 months ago

authlite v1.4.6

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

authlite.js

Lite authentication system for Next.js. authlite.js is designed to simplify the authentication process fitting personal project needs, but can be used by anyone who needs a simple layer of abstraction to their application. Currently maintained for Next.js v15.

Installation

npm install authlite

Releases

VersionDescription
v1.3Minor tweaks
v1.2Basic device fingerprinting
v1.1Security tweaks
v1.0Initial lib

Usage

1. Create .env with:

JWT_SECRET="..."
TOKEN_SECRET="..."

2. Wrap root layout with AuthProvider

import { AuthProvider } from 'authlite';
...
<AuthProvider>
    {children}
</AuthProvider>

3. Create middleware.ts at the root of your project

3.1 No protected routes

import { AuthMiddleware, Csp } from 'authlite';

const allowedOrigins = ['http://localhost:3000/'];

export default AuthMiddleware(allowedOrigins, Csp.NONE);

export const config = {
    matcher: [
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
    ],
}
ParametersOptionsDescription
allowedOriginsan array of strings with allowed originsCORS configuration
cspCsp.STRICT, Csp.RELAXED, Csp.NONECSP configuration

Add any additional allowed origin urls if needed. For csp, see docs for the setup. STRICT adds nonces, RELAXED doesn't and NONE doesn't have any policy. Either STRICT and RELAXED are configured only for production(npm run build && npm run start) so always test in production as well. If STRICT is selected you have to mark every page as async and have some async operation inside to avoid errors. If using next/image, it will produce errors due to width and height being injected, but it doesn't cause any problems.

3.2 Protected routes

import { AuthMiddleware, Csp, protect } from 'authlite';

const allowedOrigins = ['http://localhost:3000/'];

const isProtectedRoute = [
    '/profile(.*)',
    '/dashboard(.*)'
];

const redirectUrl = '/login';

export default AuthMiddleware(
    allowedOrigins, 
    Csp.NONE, 
    (request, response) => {
        return protect(
            request, 
            response,
            isProtectedRoute, 
            redirectUrl
        );
    }
);

export const config = {
    matcher: [
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
    ],
}

This setup protects all routes in /profile and /dashboard. Other useful configurations are:

  • To protect all routes except for login and register:
const isProtectedRoute = [
    '/((?!login|register)(.*))'
];

const redirectUrl = '/login';
  • To protect all routes except for all /auth routes:
const isProtectedRoute = [
    '^/(?!auth)(.*)'  
];

const redirectUrl = '/auth/login';
  • To add searchParam redirect when you need to redirect the user back to the protected route after login:
protect(
    request, 
    response, 
    isProtectedRoute, 
    redirectUrl, 
    true
);

A typical STRICT csp setup would look like this:

page.tsx
import { headers } from "next/headers";
import Component from "./component";
import Image from "next/image";

export default async function Home() {
  // Or simply await headers(); if not planning to use nonce
  // On any other protected action to avoid csp errors
  const headersList = await headers();
  const nonce = headersList.get('X-Nonce') || "";
  return (
    <div>
      <p className="red-box bg-green-500 size-20">
        Hello World
      </p>
      <style nonce={nonce}>
        {
          `
          .red-box {
            background-color: red;
          }
          `
        }
      </style>
      {/* Image will produce error in production */}
      <Image 
      src={'./next.svg'}
      alt="next logo"
      width={100}
      height={100}
      />
      <Component nonce={nonce}/>
    </div>
  );
}
component.tsx
"use client";

import Script from "next/script";

export const Component = ({nonce}: { nonce: string }) =>  {
    const handleClick = async () => {
        console.log('Hello World');
    }
    return (
        <div>
            <button onClick={handleClick}>
                click me
            </button>
            <Script nonce={nonce} id="123">
                {`console.log('Hello World!');`}
            </Script>
        </div>
        
    )
}

Root layout can be async too.

4. Login / Logout

4.1 Server Side

"use server";

import { createSession } from 'authlite';
import { UserType } from '...';

export const loginAction = async (...) => {
    ...
    const user: UserType = {...}
    
    const success = await createSession(user);
    return success;
}

4.2 Client Side

"use client";

import { useAuth } from 'authlite';
import { useRouter } from "next/navigation";
    
    ...
    const { onLogin, onLogout } = useAuth();
    const router = useRouter();

    const handleLogin = async () => {
        ...
        await onLogin();
        ...
        router.push('...');
    };

    const handleLogout = async () => {
        ...
        await onLogout();
        ...
        router.replace('...');
    }

5. Access Session

5.1 Server Side

5.1.1 Authenticate Session
"use server";

import { authenticateSession } from 'authlite';
import { UserType } from '...';

export const protectedAction = async () => {
    ...
    const { session } = await authenticateSession<UserType>();
    ...
}
5.1.2 Only Access Session
"use server";

import { getSession } from 'authlite';
import { UserType } from '...';

export const protectedAction = async () => {
    ...
    const { session } = await getSession<UserType>();
    ...
}

5.2 Client Side

"use client";
import { useAuth } from 'authlite';
import { UserType } from '...'

...
const { session } = useAuth<UserType>();
...

6. Device fingerprint (GDPR)

In the client component's login submit call await generateFingerprint() and add it to the user object. At every protected action generate it again, include it in a hidden form field and validate it server side against the session fingerprint. If it's not validated, call await deleteSession() and redirect to the login route, maybe with searchParam redirect in the url like in middleware as well.

7. Csrf Token validation

client-component.tsx

"use client";

import { createCsrfToken } from 'authlite';
import { protectedAction } from '...';
...
const handleSubmit = async () => {
    ...
    const { clientToken } = await createCsrfToken();
    ...
    await protectedAction(clientToken);
}

protected-action.ts

"use server";

export const protectedAction = async (clientToken: string) => {
    try {
        ...
        const { serverToken } = await getCsrfToken();
        const isValid = await validateCsrfToken(clientToken, serverToken);
        ...
    }
    catch (error) {
        console.error('Error validating csrf-token', error);
    }
}

8. Api routes

You can call the api from the client or from a server action. If server is preferred, you have to add the full domain before /api. If planning to use a server accessing function inside a route handler like createSession(), manipulate the response cookies, or accessing the cookieStore, api has to be called from the client.

8.1 Login

client-component.tsx
"use client";

import { useAuth } from 'authlite';

    ...
    const { onLogin } = useAuth();
    const handleSubmit = async (...) => {
        try {
            ...
            // Your fetch
            const response = await fetch('/api/...', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
            });

            // Get response
            const result = await response.json();

            ...
            // Update session status if response is ok
            await onLogin();
            ...
        } 
        catch (error) {
            console.error('Error validating jwt or making API request:', error);
        }
    }
route.ts
import { NextResponse } from "next/server";
import { createSession } from 'authlite';
import { UserType } from '...'

export const POST = async () => {
    ...
    const user: UserType = {...}
    const success = await createSession(user);
    if (success) {
        return NextResponse.json(
            { success: true, message: 'Session created successfully.' });
    } 
    
    return NextResponse.json(
        { success: false, message: 'Failed to create session.' },
        { status: 403 }
    );
}

8.2 Validate session

client-component.tsx
"use client";

import { getJwt } from 'authlite';

...
const handleSubmit = async (...) => {
    try {
        ...
        // Get jwt
        const { jwt } = await getJwt();
        if (!jwt) throw new Error('Invalid jwt');

        // Your fetch
        const response = await fetch('/api/...', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${jwt}`
            },
        });

        // Get response
        const result = await response.json();
        ...
    } 
    catch (error) {
        console.error('Error validating jwt or making API request:', error);
    }
  }

route.ts

import { NextRequest, NextResponse } from "next/server";
import { verifyJwt } from 'authlite';

export const POST = async (request: NextRequest) => {
    ...
    // Your secret
    const jwtSecret = process.env.JWT_SECRET as string;
    const JWT_SECRET = new TextEncoder().encode(jwtSecret);

    // Get headers
    const headers = new Headers(request.headers);
    const jwtHeader = headers.get('Authorization') || "";

    // Get token
    const token = jwtHeader.split(' ')[1];

    // Verify the token
    const verifiedToken = await verifyJwt(token, JWT_SECRET);
    if (verifiedToken) {
        return NextResponse.json(
            { success: true, message: 'Jwt validated successfully.' });
    }
    
    return NextResponse.json(
        { success: false, message: 'Invalid Jwt.' },
        { status: 403 }
    );
}

8.3 Validate csrf-token

client-component.tsx
"use client";

import { createCsrfToken, getCsrfToken } from 'authlite';

    const handleSubmit = async (...) => {
        try {
            ...
            // Create the csrf token
            const { clientToken } = await createCsrfToken();

            // Get the server csrf token
            const { serverToken } = await getCsrfToken();
            
            // Data for the POST
            const data = {
                csrfToken: clientToken
            }
            // Your fetch
            const response = await fetch('/api/...', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-Csrf-Token': serverToken || ""
                },
                body: JSON.stringify(data)
            });

            // Get response
            const result = await response.json();
            ...
        }
        catch (error) {
            console.error('Error validating CSRF or making API request:', error);
        }
route.ts
import { NextRequest, NextResponse } from "next/server";
import { validateCsrfToken } from 'authlite';

export const POST = async (request: NextRequest) => {
    // Get headers
    const headers = new Headers(request.headers);
    const serverToken = headers.get('X-Csrf-Token') || "";

    // Get request body
    const body = await request.json();
    const clientToken = body.csrfToken;

    // Verify the tokens
    const isValidCsrfToken = await validateCsrfToken(clientToken, serverToken);
    if (isValidCsrfToken) {
        return NextResponse.json(
            { success: true, message: 'CSRF token validated successfully.' });
    }
    
    return NextResponse.json(
        { success: false, message: 'Invalid Csrf Token' },
        { status: 403 }
    );
}

Security

Use in production at your own risk. If a session cookie is stolen, it will infinitely produce new sessions, unless JWT_SECRET has changed. Consider changing JWT_SECRET and TOKEN_SECRET frequently to invalidate sessions. Consider calling await generateFingerprint()(GDPR) at login and add it to the user object and validate it at every protected action. Consider having only your domain as allowedOrigins in CORS configuration. Consider having STRICT CSP policy. Consider including csrf token in hidden form fields for protected actions.

OAuth

For GitHub , Google etc. providers don't forget to call server or client side on callback page await createSession(user) with your user object and on client side await onLogin().

1.4.6

6 months ago

1.4.5

7 months ago

1.4.4

7 months ago

1.3.7

7 months ago

1.4.3

7 months ago

1.4.2

7 months ago

1.4.1

7 months ago

1.3.6

7 months ago

1.3.5

8 months ago

1.3.4

8 months ago

1.3.3

8 months ago

1.2.4

8 months ago

1.3.2

8 months ago

1.2.3

8 months ago

1.3.1

8 months ago

1.1.4

8 months ago

1.2.2

8 months ago

1.1.3

8 months ago

1.2.1

8 months ago

1.1.2

8 months ago

1.1.1

8 months ago

1.0.9

8 months ago

1.0.8

8 months ago

1.0.7

8 months ago

1.0.6

8 months ago

1.0.11

8 months ago

1.0.10

8 months ago

1.0.5

8 months ago

1.0.4

8 months ago

1.0.3

8 months ago

1.0.2

8 months ago

1.0.1

8 months ago