Skip to content

Instantly share code, notes, and snippets.

@Phathdt
Last active January 18, 2025 15:11
Show Gist options
  • Select an option

  • Save Phathdt/e915a99817adbe4835ce9cc95ca913cf to your computer and use it in GitHub Desktop.

Select an option

Save Phathdt/e915a99817adbe4835ce9cc95ca913cf to your computer and use it in GitHub Desktop.

Next.js and NestJS Authentication with Clerk

1. System Architecture

  • Frontend: Next.js with Clerk (port 3000)
  • Backend: NestJS (port 4000)
  • Authentication: Clerk + Token verification
  • Database: Prisma as ORM

2. Authentication Flow

sequenceDiagram
    participant Browser
    participant NextClient as Next.js Client
    participant NextServer as Next.js Server
    participant Clerk
    participant NestJS
    participant Prisma

    %% Flow 1: Login Flow
    rect rgb(200, 220, 240)
    Note over Browser,NestJS: Flow 1: Login Process
    Browser->>NextServer: Access /sign-in
    NextServer->>Browser: Return Clerk sign-in page
    Browser->>Clerk: Submit credentials
    Clerk->>Clerk: Validate credentials
    Clerk-->>Browser: Set session cookie
    Clerk-->>Browser: Redirect to /products
    end

    %% Flow 2: Protected Page Access
    rect rgb(220, 240, 200)
    Note over Browser,NestJS: Flow 2: Protected Page Access (e.g., Cart)
    Browser->>+NextServer: Access /cart
    NextServer->>Clerk: Verify session & get token
    Clerk-->>NextServer: Return token
    NextServer->>+NestJS: Fetch cart data with token
    Note over NextServer,NestJS: Authorization: Bearer {token}
    NestJS->>NestJS: Validate token via ClerkAuthGuard
    NestJS->>Prisma: Query cart data
    Prisma-->>NestJS: Return cart data
    NestJS-->>-NextServer: Return cart data
    NextServer-->>-Browser: Return rendered page
    end

    %% Flow 3: Client-side API Calls
    rect rgb(240, 220, 200)
    Note over Browser,NestJS: Flow 3: Client-side API Calls (e.g., Add to Cart)
    Browser->>NextClient: Click "Add to Cart"
    NextClient->>Clerk: Get token via useAuthToken
    Clerk-->>NextClient: Return token
    NextClient->>+NestJS: POST /cart/items with token
    Note over NextClient,NestJS: Authorization: Bearer {token}
    NestJS->>NestJS: Validate token via ClerkAuthGuard
    NestJS->>Prisma: Update cart in database
    Prisma-->>NestJS: Return updated cart
    NestJS-->>-NextClient: Return updated cart
    NextClient->>Browser: Update UI with toast notification
    end
Loading

3. Frontend Setup (Next.js)

Key Dependencies

{
  "@clerk/nextjs": "^6.9.12",
  "next": "15.1.4",
  "react": "^19.0.0"
}

Environment Variables

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_****
CLERK_SECRET_KEY=sk_test_****

Clerk Provider Setup

// app/layout.tsx
import { ClerkProvider, SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/nextjs'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <SignedOut>
            <SignInButton />
          </SignedOut>
          <SignedIn>
            <UserButton />
          </SignedIn>
          {children}
          <ToastProvider />
        </body>
      </html>
    </ClerkProvider>
  )
}

Authentication Middleware

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isPublicRoute = createRouteMatcher([
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/',
  '/products',
])

export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect()
  }
})

Auth Token Hook

// hooks/use-auth-token.ts
export const useAuthToken = () => {
  const { getToken, isSignedIn } = useAuth()

  const getAuthToken = async () => {
    if (!isSignedIn) return null
    return getToken()
  }

  return {
    getAuthToken,
    isSignedIn,
  }
}

Protected Route Example

// app/cart/page.tsx
async function getCart(): Promise<Cart> {
  const { getToken } = await auth()
  const token = await getToken()

  if (!token) {
    redirect('/sign-in')
  }

  const response = await fetch('http://localhost:4000/cart', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
    cache: 'no-store',
  })

  return response.json()
}

4. Backend Setup (NestJS)

Key Dependencies

{
  "@clerk/backend": "latest",
  "@nestjs/common": "latest",
  "@nestjs/passport": "latest"
}

Clerk Auth Guard

// libs/auth/src/clerk-auth.guard.ts
@Injectable()
export class ClerkAuthGuard extends AuthGuard('clerk') {
  constructor(private reflector: Reflector) {
    super()
  }

  override canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ])

    if (isPublic) {
      return true
    }

    return super.canActivate(context)
  }
}

Clerk Strategy

// libs/auth/src/clerk.strategy.ts
@Injectable()
export class ClerkStrategy extends PassportStrategy(Strategy, 'clerk') {
  constructor(
    @Inject('ClerkClient')
    private readonly clerkClient: ClerkClient,
    private readonly configService: ConfigService
  ) {
    super()
  }

  async validate(req: Request): Promise<User> {
    const token = req.headers.authorization?.split(' ').pop()

    if (!token) {
      throw new UnauthorizedException('No token provided')
    }

    try {
      const tokenPayload = await verifyToken(token, {
        secretKey: this.configService.get('CLERK_SECRET_KEY'),
      })

      const user = await this.clerkClient.users.getUser(tokenPayload.sub)
      return user
    } catch (error) {
      throw new UnauthorizedException('Invalid token')
    }
  }
}

Protected Controller Example

// apps/api/src/controllers/cart.controller.ts
@Controller('cart')
@UseGuards(ClerkAuthGuard)
export class CartController {
  constructor(private readonly cartService: CartService) {}

  @Get()
  async getMyCart(@CurrentUser() user: User) {
    return this.cartService.findUserCart(user.id)
  }

  @Post('items')
  async addToCart(@CurrentUser() user: User, @Body() data: { productId: number; quantity: number }) {
    return this.cartService.addItem(user.id, data.productId, data.quantity)
  }
}

5. Security Considerations

Token Management

  • Clerk handles token lifecycle management
  • Tokens are stored securely in httpOnly cookies
  • Auto refresh token mechanism
  • No client-side token storage

Security Best Practices

  • All protected routes are guarded by Clerk middleware
  • Backend validates tokens on every request
  • Public decorator for explicitly marking public routes
  • CORS enabled with proper configuration
  • Environment variables for sensitive data
  • Error handling with proper HTTP status codes

CORS Configuration

// apps/api/src/main.ts
app.enableCors({
  origin: '*',
  credentials: true,
})

6. API Examples

Frontend API Calls

// lib/cart.ts
export async function addToCart(token: string | null, productId: number, quantity: number) {
  if (!token) {
    throw new Error('Not authenticated')
  }

  const response = await fetch(`${API_URL}/cart/items`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ productId, quantity }),
  })

  return response.json()
}

Client Component Example

// app/products/page.tsx
export default function ProductsPage() {
  const { getAuthToken, isSignedIn } = useAuthToken()
  const { toast } = useToast()

  const handleAddToCart = async (productId: number) => {
    if (!isSignedIn) {
      toast({
        title: 'Please sign in',
        description: 'You need to be signed in to add items to your cart',
        variant: 'destructive',
      })
      return
    }

    try {
      const token = await getAuthToken()
      await addToCart(token, productId, 1)
      toast({
        title: 'Added to cart',
        description: 'Item has been added to your cart',
      })
    } catch (error) {
      toast({
        title: 'Error',
        description: 'Failed to add item to cart',
        variant: 'destructive',
      })
    }
  }
}

Authentication trong Next.js và NestJS

1. Kiến trúc Hệ thống

  • Frontend: Next.js (port 3001)
  • Backend: NestJS (port 3000)
  • Authentication: Cookie-based + JWT

2. Flow Authentication

sequenceDiagram
    Client->>+Next.js: Login Request
    Next.js->>+NestJS: POST /auth/login
    NestJS->>NestJS: Verify credentials
    NestJS->>NestJS: Generate JWT
    NestJS-->>-Next.js: Set httpOnly cookie
    Next.js-->>-Client: Redirect to dashboard

    Note over Client,NestJS: Subsequent Requests
    Client->>NestJS: Request with cookie
    NestJS->>NestJS: Verify JWT from cookie
    NestJS-->>Client: Protected resource
Loading

3. Setup Backend (NestJS)

CORS và Cookie Setup

// main.ts
app.enableCors({
  origin: 'http://localhost:3001',
  credentials: true
});
app.use(cookieParser());

Auth Guard

@Injectable()
class AuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const token = request.cookies['token'];

    if (!token) throw new UnauthorizedException();

    try {
      request.user = await this.jwtService.verify(token);
      return true;
    } catch {
      throw new UnauthorizedException();
    }
  }
}

Login Endpoint

@Post('login')
async login(@Res({ passthrough: true }) response: Response) {
  const token = await this.jwtService.sign(payload);

  response.cookie('token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });
}

4. Setup Frontend (Next.js)

API Calls

// utils/api.ts
const api = {
  fetch: (url: string, options = {}) => {
    return fetch(`http://localhost:3000${url}`, {
      ...options,
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
  }
};

Middleware

// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('token');

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

export const config = {
  matcher: ['/dashboard', '/cart']
}

5. Lưu ý Quan trọng

Security

  • Luôn sử dụng httpOnly cookies
  • Implement rate limiting
  • Set proper CORS policies
  • Validate JWT ở mọi request

API Calls

  • Đảm bảo credentials: 'include' trong mọi request
  • Handle token expiration
  • Implement refresh token flow nếu cần
  • Proper error handling

6. Environment Setup

# .env.local (Next.js)
NEXT_PUBLIC_API_URL=http://localhost:3000

# .env (NestJS)
JWT_SECRET=your_secure_secret_here

7. Packages cần cài đặt

# NestJS
npm install @nestjs/jwt cookie-parser
npm install -D @types/cookie-parser

# Next.js
npm install js-cookie
npm install -D @types/js-cookie

8. Protected Routes Example

// NestJS
@Controller('cart')
@UseGuards(AuthGuard)
export class CartController {
  @Get()
  getCart(@Request() req) {
    return this.cartService.findByUserId(req.user.id);
  }
}

// Next.js
export default async function CartPage() {
  const cart = await api.fetch('/cart');
  return <CartItems items={cart.items} />;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment