JSPM

  • Created
  • Published
  • Downloads 12
  • Score
    100M100P100Q108559F
  • License MIT

Authentication utilities for ThinkingCat SSO services with conditional logging

Package Exports

  • @thinkingcat/auth-utils

Readme

@thinkingcat/auth-utils

ThinkingCat SSO 서비스를 위한 인증 유틸리티 패키지입니다. JWT 토큰 검증, NextAuth 세션 생성, 쿠키 설정, 역할 기반 접근 제어, 미들웨어 헬퍼 등의 광범위한 인증 기능을 제공합니다.

📑 목차 (Table of Contents)

📦 설치 (Installation)

npm 사용

npm install @thinkingcat/auth-utils

yarn 사용

yarn add @thinkingcat/auth-utils

pnpm 사용

pnpm add @thinkingcat/auth-utils

📋 요구사항 (Requirements)

  • Next.js: >= 13.0.0
  • Node.js: >= 18.0.0
  • TypeScript: 권장 (타입 지원)

🐛 디버깅 (Debugging)

이 패키지는 조건부 로깅 시스템을 사용합니다. 기본적으로 프로덕션 환경에서는 로그가 출력되지 않으며, 개발 환경에서만 로그가 출력됩니다.

디버그 로그 활성화

환경 변수 AUTH_UTILS_DEBUG=true를 설정하여 모든 환경에서 디버그 로그를 활성화할 수 있습니다:

# .env.local 또는 환경 변수 설정
AUTH_UTILS_DEBUG=true

로그 출력 조건

  • NODE_ENV === 'development': 자동으로 로그 출력
  • AUTH_UTILS_DEBUG === 'true': 모든 환경에서 로그 출력
  • 그 외: 로그 출력 안 함 (성능 최적화)

디버그 로그 예시

// 개발 환경 또는 AUTH_UTILS_DEBUG=true일 때만 출력됨
[handleMiddleware] Processing: /admin
[verifyAndRefreshToken] Checking refresh: { hasRefreshToken: true, forceRefresh: false }
[createAuthResponse] JWT created: { hasId: true, hasEmail: true, hasRole: true }

이 기능으로 프로덕션 환경에서 불필요한 로그 출력을 방지하여 성능을 최적화합니다.

⚙️ Next.js 설정 (Next.js Configuration)

npm에 배포된 패키지를 사용하는 경우 (권장)

npm에 배포된 패키지를 사용할 때는 별도의 설정이 필요하지 않습니다. 바로 사용할 수 있습니다.

npm install @thinkingcat/auth-utils
{
  "dependencies": {
    "@thinkingcat/auth-utils": "^1.0.17"
  }
}

설정 불필요: npm에 배포된 패키지는 이미 빌드되어 있으므로 next.config.ts에 추가 설정이 필요 없습니다.

로컬 패키지를 사용하는 경우 (개발 중)

로컬 패키지(file:../packages/auth-token-utils)를 사용하는 경우, Next.js가 모듈을 찾지 못하는 오류가 발생할 수 있습니다.

해결 방법 1: transpilePackages 추가 (권장)

next.config.ts에 다음 설정을 추가하세요:

/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ["@thinkingcat/auth-utils"],
  // ... 기타 설정
};

module.exports = nextConfig;

해결 방법 2: npm에 배포된 버전 사용 (가장 안정적)

로컬 패키지 대신 npm에 배포된 버전을 사용하면 설정이 필요 없습니다.

🚀 빠른 시작 (Quick Start)

1. 기본 import

import {
  // 토큰 검증 및 생성
  verifyToken,
  extractRoleFromPayload,
  createNextAuthJWT,
  encodeNextAuthToken,

  // 쿠키 관리
  setCustomTokens,
  setNextAuthToken,
  clearAuthCookies,

  // 리다이렉트 및 응답
  createRedirectHTML,
  createAuthResponse,
  redirectToError,
  redirectToSSOLogin,
  redirectToRoleDashboard,

  // 역할 및 접근 제어
  getEffectiveRole,
  hasRole,
  hasAnyRole,
  checkRoleAccess,
  requiresSubscription,

  // 경로 체크
  isPublicPath,
  isApiPath,
  isProtectedApiPath,

  // 토큰 유효성 검사
  isTokenExpired,
  isValidToken,

  // SSO 통합
  refreshSSOToken,
  getRefreshTokenFromSSO,
  verifyTokenFromSSO,
  validateServiceSubscription,

  // 미들웨어
  createMiddlewareConfig,
  handleMiddleware,
  verifyAndRefreshTokenWithNextAuth,

  // NextAuth 설정 및 콜백
  createNextAuthBaseConfig,
  createNextAuthCookies,
  handleJWTCallback,
  createInitialJWTToken,
  createEmptySession,
  mapTokenToSession,
  getJWTFromCustomTokenCookie,

  // 라이센스
  checkLicenseKey,

  // 타입
  ServiceInfo,
  ResponseLike,
  JWTPayload,
  MiddlewareConfig,
  MiddlewareOptions,
} from "@thinkingcat/auth-utils";

2. 라이센스 키 사용

라이센스 키는 필수입니다. 모든 함수 호출 시 licenseKey 파라미터를 전달해야 합니다.

// 예시: handleMiddleware 사용 시
const response = await handleMiddleware(req, middlewareConfig, {
  secret: process.env.NEXTAUTH_SECRET!,
  isProduction: process.env.NODE_ENV === "production",
  ssoBaseURL: process.env.SSO_BASE_URL!,
  licenseKey: process.env.LICENSE_KEY!, // 필수
});

중요:

  • 라이센스 키는 모든 함수 호출 시 필수 파라미터입니다.
  • 라이센스 키가 없거나 유효하지 않으면 함수가 에러를 발생시킵니다.
  • 라이센스 키는 SHA-256 해시로 변환되어 모듈 내부의 유효한 키 목록과 비교됩니다.
  • 다른 환경 변수(NEXTAUTH_SECRET, SSO_BASE_URL 등)는 이 패키지를 사용하는 애플리케이션에서 관리합니다.

📚 주요 기능 (Features)

이 패키지는 다음 기능을 제공합니다:

  1. 토큰 검증: JWT access token 검증 및 디코딩
  2. 역할 추출: payload에서 서비스별 역할 추출
  3. NextAuth JWT 생성: NextAuth 호환 JWT 객체 생성
  4. 세션 토큰 인코딩: NextAuth 세션 토큰 생성
  5. 쿠키 설정: 자체 토큰 및 NextAuth 토큰 쿠키 설정
  6. 리다이렉트 HTML: 클라이언트 리다이렉트용 HTML 생성
  7. 완전한 인증 응답: 모든 인증 단계를 한 번에 처리
  8. 미들웨어 헬퍼: 에러 리다이렉트, 쿠키 삭제, SSO 로그인 리다이렉트 등
  9. 역할 기반 접근 제어: 경로별 역할 검증 및 접근 제어
  10. 구독 검증: 서비스 구독 상태 확인
  11. 토큰 유효성 검사: 토큰 만료 및 유효성 확인
  12. 경로 체크: 공개 경로, API 경로, 보호된 경로 확인
  13. 통합 인증 체크: NextAuth와 자체 토큰을 모두 확인하는 통합 함수
  14. 미들웨어 통합: 완전한 미들웨어 핸들러 함수 제공
  15. 설정 관리: 미들웨어 설정 생성 및 관리 함수

🔧 API 레퍼런스

토큰 검증 및 생성

verifyToken(accessToken: string, secret: string, licenseKey: string)

JWT access token을 검증하고 디코딩합니다.

파라미터:

  • accessToken: 검증할 JWT 토큰
  • secret: JWT 서명에 사용할 secret key
  • licenseKey: 라이센스 키 (필수)

반환값:

  • 성공: { payload: JWTPayload }
  • 실패: null

사용 예시:

const secret = process.env.NEXTAUTH_SECRET!;
const licenseKey = process.env.LICENSE_KEY!;
const result = await verifyToken(accessToken, secret, licenseKey);

if (result) {
  const { payload } = result;
  console.log("User email:", payload.email);
  console.log("User ID:", payload.sub || payload.id);
} else {
  console.log("Invalid token");
}

extractRoleFromPayload(payload: JWTPayload, serviceId: string, defaultRole?: string)

payload에서 특정 서비스의 역할을 추출합니다.

파라미터:

  • payload: JWT payload 객체 (JWTPayload 타입)
  • serviceId: 서비스 ID (필수)
  • defaultRole: 기본 역할 (기본값: 'ADMIN')

반환값:

  • 추출된 역할 문자열

사용 예시:

const role = extractRoleFromPayload(payload, "myservice", "ADMIN");
// 'ADMIN', 'TEACHER', 'STUDENT' 등

createNextAuthJWT(payload: JWTPayload, serviceId: string)

NextAuth와 호환되는 JWT 객체를 생성합니다.

파라미터:

  • payload: 원본 JWT payload (JWTPayload 타입)
  • serviceId: 서비스 ID (필수)

반환값:

  • NextAuth JWT 객체

사용 예시:

const jwt = createNextAuthJWT(payload, "myservice");

encodeNextAuthToken(jwt: JWT, secret: string, maxAge?: number)

NextAuth JWT 객체를 인코딩된 세션 토큰으로 변환합니다.

파라미터:

  • jwt: NextAuth JWT 객체
  • secret: JWT 서명에 사용할 secret key
  • maxAge: 토큰 유효 기간 (초, 기본값: 30일)

반환값:

  • 인코딩된 세션 토큰 문자열

사용 예시:

const secret = process.env.NEXTAUTH_SECRET!;
const jwt = createNextAuthJWT(payload, "myservice");
const sessionToken = await encodeNextAuthToken(jwt, secret);

쿠키 관리

setCustomTokens(response: ResponseLike, accessToken: string, optionsOrRefreshToken?, options?)

자체 토큰(access token, refresh token)만 쿠키에 설정합니다.

파라미터:

  • response: NextResponse 객체 (또는 ResponseLike 인터페이스를 구현한 객체)
  • accessToken: access token 문자열
  • optionsOrRefreshToken:
    • 문자열인 경우: refresh token
    • 객체인 경우: 옵션 객체 { refreshToken?, cookiePrefix?, isProduction? }
  • options: 옵션 객체 (refreshToken이 문자열로 전달된 경우)

옵션:

  • cookiePrefix: 쿠키 이름 접두사 (필수)
  • isProduction: 프로덕션 환경 여부 (기본값: false)
  • refreshToken: refresh token (옵션 객체 내부에 포함 가능)

사용 예시:

// 방법 1: refreshToken이 있는 경우
setCustomTokens(response, accessToken, refreshToken, {
  cookiePrefix: "myservice",
  isProduction: process.env.NODE_ENV === "production",
});

// 방법 2: refreshToken이 없는 경우
setCustomTokens(response, accessToken, {
  cookiePrefix: "myservice",
  isProduction: process.env.NODE_ENV === "production",
});

설정되는 쿠키:

  • {cookiePrefix}_access_token: 15분 유효
  • {cookiePrefix}_refresh_token: 30일 유효 (있는 경우)

setNextAuthToken(response: ResponseLike, sessionToken: string, options?)

NextAuth 세션 토큰만 쿠키에 설정합니다.

파라미터:

  • response: NextResponse 객체
  • sessionToken: NextAuth 세션 토큰
  • options: 옵션 객체

옵션:

  • isProduction: 프로덕션 환경 여부 (기본값: false)
  • cookieDomain: 쿠키 도메인 (선택사항)

사용 예시:

setNextAuthToken(response, sessionToken, {
  isProduction: process.env.NODE_ENV === "production",
  cookieDomain: process.env.COOKIE_DOMAIN,
});

설정되는 쿠키:

  • 개발: next-auth.session-token
  • 프로덕션: __Secure-next-auth.session-token
  • 유효 기간: 30일

clearAuthCookies(response: NextResponse, cookiePrefix: string)

인증 쿠키를 삭제합니다.

파라미터:

  • response: NextResponse 객체
  • cookiePrefix: 쿠키 이름 접두사 (필수)

사용 예시:

const response = NextResponse.redirect("/login");
clearAuthCookies(response, "myservice");
return response;

역할 및 접근 제어

getEffectiveRole(payload: JWTPayload, serviceId: string, defaultRole?: string)

payload에서 유효한 역할을 가져옵니다.

파라미터:

  • payload: JWT payload 객체
  • serviceId: 서비스 ID (필수)
  • defaultRole: 기본 역할 (기본값: 'ADMIN')

반환값:

  • 역할 문자열

hasRole(payload: JWTPayload, serviceId: string, role: string)

payload에 특정 역할이 있는지 확인합니다.

파라미터:

  • payload: JWT payload 객체
  • serviceId: 서비스 ID (필수)
  • role: 확인할 역할

반환값:

  • boolean

hasAnyRole(payload: JWTPayload, serviceId: string, roles: string[])

payload에 여러 역할 중 하나라도 있는지 확인합니다.

파라미터:

  • payload: JWT payload 객체
  • serviceId: 서비스 ID (필수)
  • roles: 확인할 역할 배열

반환값:

  • boolean

checkRoleAccess(pathname: string, role: string, config: RoleAccessConfig[])

경로에 대한 역할 접근 권한을 확인합니다.

파라미터:

  • pathname: 경로
  • role: 사용자 역할
  • config: 역할 접근 설정 배열

반환값:

  • { allowed: boolean; message?: string }

requiresSubscription(pathname: string, role: string, subscriptionRequiredPaths: string[], systemAdminRole?: string)

경로가 구독이 필요한지 확인합니다.

파라미터:

  • pathname: 경로
  • role: 사용자 역할
  • subscriptionRequiredPaths: 구독이 필요한 경로 배열
  • systemAdminRole: 시스템 관리자 역할 (선택사항)

반환값:

  • boolean

SSO 통합

refreshSSOToken(refreshToken: string, options: { ssoBaseURL: string; authServiceKey?: string })

SSO 서버에서 refresh token을 사용하여 새로운 access token을 발급받습니다.

파라미터:

  • refreshToken: refresh token
  • options.ssoBaseURL: SSO 서버 기본 URL (필수)
  • options.authServiceKey: 인증 서비스 키 (선택사항)

반환값:

  • SSORefreshTokenResponse

사용 예시:

const result = await refreshSSOToken(refreshToken, {
  ssoBaseURL: process.env.SSO_BASE_URL!,
  authServiceKey: process.env.AUTH_SERVICE_SECRET_KEY,
});

getRefreshTokenFromSSO(userId: string, accessToken: string, options: { ssoBaseURL: string; authServiceKey?: string })

SSO 서버에서 사용자의 refresh token을 가져옵니다.

파라미터:

  • userId: 사용자 ID
  • accessToken: access token
  • options.ssoBaseURL: SSO 서버 기본 URL (필수)
  • options.authServiceKey: 인증 서비스 키 (선택사항)

반환값:

  • string | null

verifyTokenFromSSO(accessToken: string, options: { ssoBaseURL: string; authServiceKey?: string })

SSO 서버에서 토큰을 검증합니다.

파라미터:

  • accessToken: access token
  • options.ssoBaseURL: SSO 서버 기본 URL (필수)
  • options.authServiceKey: 인증 서비스 키 (선택사항)

반환값:

  • Promise<{ isValid: boolean; payload?: JWTPayload }>

validateServiceSubscription(services: ServiceInfo[], serviceId: string, ssoBaseURL: string)

서비스 구독 상태를 확인합니다.

파라미터:

  • services: 서비스 정보 배열
  • serviceId: 서비스 ID
  • ssoBaseURL: SSO 서버 기본 URL (필수)

반환값:

  • { isValid: boolean; redirectUrl?: string }

NextAuth 설정 및 콜백

createNextAuthBaseConfig(options)

NextAuth 기본 설정을 생성합니다.

파라미터:

  • options.secret: NextAuth secret (필수)
  • options.isProduction: 프로덕션 환경 여부 (기본값: false)
  • options.cookieDomain: 쿠키 도메인 (선택)
  • options.signInPath: 로그인 페이지 경로 (기본값: '/login')
  • options.errorPath: 에러 페이지 경로 (기본값: '/login')
  • options.nextAuthUrl: NextAuth URL (선택)
  • options.sessionMaxAge: 세션 최대 유지 시간 (초, 기본값: 30일)
  • options.jwtMaxAge: JWT 최대 유지 시간 (초, 기본값: 30일)

반환값:

  • NextAuth 기본 설정 객체

사용 예시:

import { createNextAuthBaseConfig } from "@thinkingcat/auth-utils";

const baseConfig = createNextAuthBaseConfig({
  secret: process.env.NEXTAUTH_SECRET!,
  isProduction: process.env.NODE_ENV === "production",
  cookieDomain: process.env.COOKIE_DOMAIN,
  signInPath: "/login",
  errorPath: "/login",
  nextAuthUrl: process.env.NEXTAUTH_URL,
});

export const authOptions: NextAuthOptions = {
  ...baseConfig,
  // ... 기타 설정
};

createNextAuthCookies(options)

NextAuth 쿠키 설정을 생성합니다.

파라미터:

  • options.isProduction: 프로덕션 환경 여부 (기본값: false)
  • options.cookieDomain: 쿠키 도메인 (선택)

반환값:

  • NextAuth 쿠키 설정 객체

handleJWTCallback(token, user?, account?, options?)

JWT 콜백을 위한 통합 헬퍼 함수입니다. 초기 로그인, 토큰 갱신, 커스텀 토큰 읽기를 모두 처리합니다.

파라미터:

  • token: 기존 JWT 토큰
  • user: 사용자 정보 (초기 로그인 시)
  • account: 계정 정보 (초기 로그인 시)
  • options.secret: NextAuth secret (커스텀 토큰 읽기용)
  • options.licenseKey: 라이센스 키 (커스텀 토큰 읽기용)
  • options.serviceId: 서비스 ID (커스텀 토큰 읽기용)
  • options.cookieName: 커스텀 토큰 쿠키 이름 (기본값: '{serviceId}_access_token')
  • options.debug: 디버깅 로그 출력 여부 (기본값: false)

반환값:

  • 업데이트된 JWT 토큰

사용 예시:

import { handleJWTCallback } from "@thinkingcat/auth-utils";

async jwt({ token, user, account }) {
  return handleJWTCallback(
    token,
    user ? {
      id: user.id,
      email: user.email,
      role: user.role,
      // ... 기타 사용자 정보
    } : null,
    account,
    {
      secret: process.env.NEXTAUTH_SECRET!,
      licenseKey: process.env.LICENSE_KEY!,
      serviceId: 'myservice',
      cookieName: 'myservice_access_token',
      debug: true,
    }
  );
}

createInitialJWTToken(token, user, account?)

JWT 콜백에서 초기 로그인 시 토큰 생성 헬퍼입니다.

파라미터:

  • token: 기존 토큰
  • user: 사용자 정보
  • account: 계정 정보 (선택)

반환값:

  • 업데이트된 JWT 토큰

createEmptySession(session)

Session 콜백에서 빈 세션 반환 헬퍼입니다.

파라미터:

  • session: 기존 세션

반환값:

  • 빈 세션 객체

mapTokenToSession(session, token)

Session 콜백에서 토큰 정보를 세션에 매핑하는 헬퍼입니다.

파라미터:

  • session: 기존 세션
  • token: JWT 토큰

반환값:

  • 업데이트된 세션

getJWTFromCustomTokenCookie(cookieName, secret, serviceId, licenseKey)

쿠키에서 커스텀 토큰을 읽어서 NextAuth JWT로 변환하는 헬퍼 함수입니다.

파라미터:

  • cookieName: 쿠키 이름 (예: 'checkon_access_token')
  • secret: JWT 서명에 사용할 secret key
  • serviceId: 서비스 ID (필수)
  • licenseKey: 라이센스 키 (필수)

반환값:

  • NextAuth JWT 객체 또는 null

미들웨어

createMiddlewareConfig(config: Partial<MiddlewareConfig> & { serviceId: string }, defaults?)

미들웨어 설정을 생성합니다.

파라미터:

  • config: 미들웨어 설정 객체 (serviceId 필수)
  • defaults: 기본 설정값 (선택사항)

반환값:

  • 완전한 미들웨어 설정 객체

사용 예시:

import { createMiddlewareConfig } from "@thinkingcat/auth-utils";

const middlewareConfig = createMiddlewareConfig({
  serviceId: "myservice",
  publicPaths: ["/login", "/register"],
  roleAccessConfig: [
    {
      paths: ["/admin"],
      role: "ADMIN",
      message: "관리자만 접근할 수 있습니다.",
    },
  ],
});

handleMiddleware(req: NextRequest, config: MiddlewareConfig, options: MiddlewareOptions)

통합 미들웨어 핸들러 함수입니다. 모든 인증, 권한, 구독 체크를 포함합니다.

파라미터:

  • req: NextRequest 객체
  • config: 미들웨어 설정 (createMiddlewareConfig로 생성)
  • options: 미들웨어 실행 옵션

옵션:

  • secret: NextAuth Secret (필수)
  • isProduction: 프로덕션 환경 여부 (필수)
  • cookieDomain: 쿠키 도메인 (선택사항)
  • getNextAuthToken: NextAuth 토큰을 가져오는 함수 (선택사항)
  • ssoBaseURL: SSO 서버 기본 URL (필수)
  • authServiceKey: 인증 서비스 키 (선택사항)

반환값:

  • NextResponse 또는 null (다음 미들웨어로 진행)

사용 예시:

import { withAuth } from "next-auth/middleware";
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import {
  createMiddlewareConfig,
  handleMiddleware,
} from "@thinkingcat/auth-utils";

const middlewareConfig = createMiddlewareConfig({
  serviceId: "myservice",
  publicPaths: ["/login", "/register"],
  roleAccessConfig: [
    {
      paths: ["/admin"],
      role: "ADMIN",
      message: "관리자만 접근할 수 있습니다.",
    },
  ],
});

export default withAuth(
  async function middleware(req: NextRequest) {
    const response = await handleMiddleware(req, middlewareConfig, {
      secret: process.env.NEXTAUTH_SECRET!,
      isProduction: process.env.NODE_ENV === "production",
      cookieDomain: process.env.COOKIE_DOMAIN,
      ssoBaseURL: process.env.SSO_BASE_URL!,
      getNextAuthToken: async (req: NextRequest) => {
        return await getToken({ req, secret: process.env.NEXTAUTH_SECRET! });
      },
      authServiceKey: process.env.AUTH_SERVICE_SECRET_KEY,
    });
    return response || NextResponse.next();
  },
  {
    callbacks: {
      authorized: () => true,
    },
  }
);

verifyAndRefreshTokenWithNextAuth(req: NextRequest, secret: string, cookiePrefix: string, serviceId: string, options: { ssoBaseURL: string; authServiceKey?: string; getNextAuthToken?: (req: NextRequest) => Promise<JWT | null> })

NextAuth 토큰을 먼저 확인하고, 없으면 자체 토큰을 확인하고 필요시 리프레시합니다.

파라미터:

  • req: NextRequest 객체
  • secret: JWT 서명에 사용할 secret key
  • cookiePrefix: 쿠키 이름 접두사
  • serviceId: 서비스 ID (필수)
  • options: 옵션 객체

반환값:

  • Promise<{ isValid: boolean; payload?: JWTPayload; error?: string }>

경로 체크

isPublicPath(pathname: string, publicPaths: string[])

경로가 공개 경로인지 확인합니다.

파라미터:

  • pathname: 경로
  • publicPaths: 공개 경로 배열

반환값:

  • boolean

isApiPath(pathname: string)

경로가 API 경로인지 확인합니다.

파라미터:

  • pathname: 경로

반환값:

  • boolean

isProtectedApiPath(pathname: string, authApiPaths: string[])

경로가 보호된 API 경로인지 확인합니다.

파라미터:

  • pathname: 경로
  • authApiPaths: 인증이 필요한 API 경로 배열

반환값:

  • boolean

토큰 유효성 검사

isTokenExpired(token: JWT | null)

토큰이 만료되었는지 확인합니다.

파라미터:

  • token: JWT 객체 또는 null

반환값:

  • boolean

isValidToken(token: JWT | null)

토큰이 유효한지 확인합니다.

파라미터:

  • token: JWT 객체 또는 null

반환값:

  • boolean

라이센스 검증

checkLicenseKey(licenseKey: string)

라이센스 키를 검증합니다.

파라미터:

  • licenseKey: 라이센스 키 (필수)

반환값:

  • 없음 (유효하지 않으면 에러 발생)

사용 예시:

import { checkLicenseKey } from "@thinkingcat/auth-utils";

try {
  checkLicenseKey(process.env.LICENSE_KEY!);
  console.log("라이센스 키가 유효합니다");
} catch (error) {
  console.error("라이센스 키 검증 실패:", error);
}

리다이렉트 및 응답

createRedirectHTML(redirectPath: string, text: string)

클라이언트 리다이렉트용 HTML을 생성합니다.

파라미터:

  • redirectPath: 리다이렉트할 경로
  • text: 표시할 텍스트 (필수)

반환값:

  • HTML 문자열

사용 예시:

const html = createRedirectHTML("/dashboard", "myservice");
return new NextResponse(html, {
  status: 200,
  headers: { "Content-Type": "text/html" },
});

createAuthResponse(accessToken: string, secret: string, options)

완전한 인증 세션을 생성합니다. 토큰 검증부터 쿠키 설정까지 모든 과정을 처리합니다.

파라미터:

  • accessToken: access token
  • secret: JWT 서명에 사용할 secret key
  • options: 옵션 객체

옵션:

  • refreshToken: refresh token (선택)
  • redirectPath: 리다이렉트할 경로 (선택)
  • text: 리다이렉트 HTML에 표시할 텍스트 (선택사항, serviceId가 기본값)
  • cookiePrefix: 쿠키 이름 접두사 (선택사항, serviceId가 기본값)
  • isProduction: 프로덕션 환경 여부 (기본값: false)
  • cookieDomain: 쿠키 도메인 (선택)
  • serviceId: 서비스 ID (필수)
  • licenseKey: 라이센스 키 (필수)

반환값:

  • NextResponse 객체

사용 예시:

const secret = process.env.NEXTAUTH_SECRET!;
const response = await createAuthResponse(accessToken, secret, {
  refreshToken: refreshToken,
  redirectPath: "/dashboard",
  text: "myservice",
  cookiePrefix: "myservice",
  isProduction: process.env.NODE_ENV === "production",
  cookieDomain: process.env.COOKIE_DOMAIN,
  serviceId: "myservice",
  licenseKey: process.env.LICENSE_KEY!, // 필수
});

redirectToError(req: NextRequest, errorType: string, message: string, errorPath?: string)

에러 페이지로 리다이렉트합니다.

파라미터:

  • req: NextRequest 객체
  • errorType: 에러 타입
  • message: 에러 메시지
  • errorPath: 에러 페이지 경로 (기본값: '/error')

반환값:

  • NextResponse 리다이렉트 응답

redirectToSSOLogin(req: NextRequest, serviceId: string, ssoBaseURL: string)

SSO 로그인 페이지로 리다이렉트합니다.

파라미터:

  • req: NextRequest 객체
  • serviceId: 서비스 ID
  • ssoBaseURL: SSO 서버 기본 URL (필수)

반환값:

  • NextResponse 리다이렉트 응답

redirectToRoleDashboard(req: NextRequest, role: string, rolePaths: Record<string, string>, defaultPath?: string)

역할별 대시보드 경로로 리다이렉트합니다.

파라미터:

  • req: NextRequest 객체
  • role: 사용자 역할
  • rolePaths: 역할별 경로 매핑
  • defaultPath: 기본 경로 (기본값: '/admin')

반환값:

  • NextResponse 리다이렉트 응답

💡 사용 시나리오

시나리오 1: 자체 토큰만 사용하는 서비스

SSO에서 받은 토큰을 자체 쿠키에만 저장하는 경우:

import { NextRequest, NextResponse } from "next/server";
import {
  verifyToken,
  setCustomTokens,
  createRedirectHTML,
} from "@thinkingcat/auth-utils";

export async function GET(req: NextRequest) {
  const tokenParam = req.nextUrl.searchParams.get("token");
  if (!tokenParam) {
    return NextResponse.redirect("/login");
  }

  const secret = process.env.NEXTAUTH_SECRET!;
  const licenseKey = process.env.LICENSE_KEY!;
  const tokenResult = await verifyToken(tokenParam, secret, licenseKey);

  if (!tokenResult) {
    return NextResponse.redirect("/login");
  }

  const html = createRedirectHTML("/dashboard", "myservice");
  const response = new NextResponse(html, {
    status: 200,
    headers: { "Content-Type": "text/html" },
  });

  setCustomTokens(response, tokenParam, {
    cookiePrefix: "myservice",
    isProduction: process.env.NODE_ENV === "production",
  });

  return response;
}

시나리오 2: NextAuth만 사용하는 서비스

NextAuth 세션만 사용하는 경우:

import { NextRequest, NextResponse } from "next/server";
import {
  verifyToken,
  createNextAuthJWT,
  encodeNextAuthToken,
  setNextAuthToken,
} from "@thinkingcat/auth-utils";

export async function GET(req: NextRequest) {
  const tokenParam = req.nextUrl.searchParams.get("token");
  if (!tokenParam) {
    return NextResponse.redirect("/login");
  }

  const secret = process.env.NEXTAUTH_SECRET!;
  const licenseKey = process.env.LICENSE_KEY!;
  const tokenResult = await verifyToken(tokenParam, secret, licenseKey);

  if (!tokenResult) {
    return NextResponse.redirect("/login");
  }

  const { payload } = tokenResult;
  const jwt = createNextAuthJWT(payload, "myservice");
  const sessionToken = await encodeNextAuthToken(jwt, secret);

  const response = NextResponse.redirect("/dashboard");
  setNextAuthToken(response, sessionToken, {
    isProduction: process.env.NODE_ENV === "production",
    cookieDomain: process.env.COOKIE_DOMAIN,
  });

  return response;
}

시나리오 3: 자체 토큰 + NextAuth 모두 사용

자체 토큰과 NextAuth 세션을 모두 사용하는 경우:

import { NextRequest, NextResponse } from "next/server";
import {
  verifyToken,
  extractRoleFromPayload,
  createNextAuthJWT,
  encodeNextAuthToken,
  setCustomTokens,
  setNextAuthToken,
  createRedirectHTML,
} from "@thinkingcat/auth-utils";

export async function GET(req: NextRequest) {
  const tokenParam = req.nextUrl.searchParams.get("token");
  if (!tokenParam) {
    return NextResponse.redirect("/login");
  }

  const secret = process.env.NEXTAUTH_SECRET!;
  const licenseKey = process.env.LICENSE_KEY!;
  const isProduction = process.env.NODE_ENV === "production";

  const tokenResult = await verifyToken(tokenParam, secret, licenseKey);
  if (!tokenResult) {
    return NextResponse.redirect("/login");
  }

  const { payload } = tokenResult;
  const role = extractRoleFromPayload(payload, "myservice", "ADMIN");

  const jwt = createNextAuthJWT(payload, "myservice");
  const sessionToken = await encodeNextAuthToken(jwt, secret);

  const redirectPath = role === "ADMIN" ? "/admin" : "/dashboard";
  const html = createRedirectHTML(redirectPath, "myservice");

  const response = new NextResponse(html, {
    status: 200,
    headers: { "Content-Type": "text/html" },
  });

  setCustomTokens(response, tokenParam, {
    cookiePrefix: "myservice",
    isProduction,
  });

  setNextAuthToken(response, sessionToken, {
    isProduction,
    cookieDomain: process.env.COOKIE_DOMAIN,
  });

  return response;
}

시나리오 4: 미들웨어에서 통합 사용

Next.js Middleware에서 통합 미들웨어 핸들러 사용:

import { withAuth } from "next-auth/middleware";
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import {
  createMiddlewareConfig,
  handleMiddleware,
} from "@thinkingcat/auth-utils";

const middlewareConfig = createMiddlewareConfig({
  serviceId: "myservice",
  publicPaths: ["/login", "/register", "/api/public"],
  subscriptionRequiredPaths: ["/premium"],
  roleAccessConfig: [
    {
      paths: ["/admin"],
      role: "ADMIN",
      message: "관리자만 접근할 수 있습니다.",
    },
  ],
  rolePaths: {
    ADMIN: "/admin",
    USER: "/dashboard",
  },
});

export default withAuth(
  async function middleware(req: NextRequest) {
    const response = await handleMiddleware(req, middlewareConfig, {
      secret: process.env.NEXTAUTH_SECRET!,
      isProduction: process.env.NODE_ENV === "production",
      cookieDomain: process.env.COOKIE_DOMAIN,
      ssoBaseURL: process.env.SSO_BASE_URL!,
      getNextAuthToken: async (req: NextRequest) => {
        return await getToken({ req, secret: process.env.NEXTAUTH_SECRET! });
      },
      authServiceKey: process.env.AUTH_SERVICE_SECRET_KEY,
    });
    return response || NextResponse.next();
  },
  {
    callbacks: {
      authorized: () => true,
    },
  }
);

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

🔒 보안 고려사항

⚠️ 중요 사항

  1. Secret Key 관리

    • NEXTAUTH_SECRET은 반드시 환경 변수로 관리하세요
    • 절대 코드에 하드코딩하지 마세요
    • 프로덕션과 개발 환경에서 다른 secret을 사용하세요
  2. 쿠키 보안

    • 프로덕션 환경에서는 isProduction: true를 설정하세요
    • secure: true로 설정되어 HTTPS에서만 전송됩니다
    • httpOnly: true로 설정되어 JavaScript에서 접근할 수 없습니다
  3. 토큰 검증

    • 모든 토큰은 사용 전에 반드시 verifyToken으로 검증하세요
    • 검증 실패 시 적절한 에러 처리를 하세요
  4. 환경 변수

    • 이 패키지는 환경 변수를 직접 읽지 않습니다
    • 모든 값은 함수 파라미터로 전달해야 합니다
    • 하드코딩된 값(서비스 ID, URL 등)이 없도록 주의하세요

📝 타입 정의

JWTPayload

JWT 토큰의 payload 타입입니다.

interface JWTPayload {
  // 표준 JWT 필드
  sub?: string; // Subject (사용자 ID)
  id?: string; // 사용자 ID
  email: string; // 이메일
  name: string; // 이름
  role?: string; // 역할
  iat?: number; // Issued At
  exp?: number; // Expiration Time

  // 서비스 정보
  services?: ServiceInfo[];

  // 인증 상태
  phoneVerified?: boolean;
  emailVerified?: boolean;
  smsVerified?: boolean;

  // 선택적 필드
  phone?: string;
  isPasswordReset?: boolean;
  decryptedEmail?: string;
  decryptedPhone?: string;
  emailHash?: string;
  maskedEmail?: string;
  phoneHash?: string;
  maskedPhone?: string;
  refreshToken?: string;
  accessToken?: string;
  accessTokenExpires?: number;
  serviceId?: string;

  // 기타 필드
  [key: string]: unknown;
}

ServiceInfo

interface ServiceInfo {
  serviceId: string;
  role: string;
  joinedAt: string;
  lastAccessAt?: string;
  expiredAt?: string;
  status: string;
}

MiddlewareConfig

interface MiddlewareConfig {
  serviceId: string;
  publicPaths: string[];
  subscriptionRequiredPaths: string[];
  subscriptionExemptApiPaths: string[];
  authApiPaths: string[];
  roleAccessConfig: RoleAccessConfig[];
  rolePaths: Record<string, string>;
  systemAdminRole: string;
  errorPath: string;
}

MiddlewareOptions

interface MiddlewareOptions {
  secret: string;
  isProduction: boolean;
  cookieDomain?: string;
  getNextAuthToken?: (req: NextRequest) => Promise<JWT | null>;
  ssoBaseURL: string;
  authServiceKey?: string;
  licenseKey: string;
}

RoleAccessConfig

interface RoleAccessConfig {
  paths: string[];
  role: string;
  allowedRoles?: string[];
  message?: string;
}

SSO API 응답 타입

SSORefreshTokenResponse

interface SSORefreshTokenResponse {
  success: boolean;
  accessToken?: string;
  refreshToken?: string;
  user?: {
    id: string;
    email: string;
    name: string;
  };
  error?: string;
}

SSOGetRefreshTokenResponse

interface SSOGetRefreshTokenResponse {
  success: boolean;
  refreshToken?: string;
  error?: string;
}

ResponseLike

interface ResponseLike {
  cookies: {
    delete(name: string): void;
    set(
      name: string,
      value: string,
      options?: {
        httpOnly?: boolean;
        secure?: boolean;
        sameSite?: "strict" | "lax" | "none";
        maxAge?: number;
        path?: string;
        domain?: string;
      }
    ): void;
  };
}

🐛 문제 해결 (Troubleshooting)

문제 1: "Cannot find module '@thinkingcat/auth-utils'"

해결 방법:

# 패키지 재설치
npm install @thinkingcat/auth-utils

# 또는 node_modules 삭제 후 재설치
rm -rf node_modules package-lock.json
npm install

문제 2: 타입 오류 "NextResponse is not assignable to ResponseLike"

해결 방법: ResponseLike 인터페이스는 NextResponse와 호환됩니다. 타입 단언이 필요하지 않습니다. 만약 오류가 발생한다면 패키지 버전을 확인하세요.

문제 3: 토큰 검증 실패

확인 사항:

  • NEXTAUTH_SECRET이 올바르게 설정되었는지 확인
  • 토큰이 만료되지 않았는지 확인
  • 토큰 형식이 올바른지 확인

문제 4: 쿠키가 설정되지 않음

확인 사항:

  • isProduction 옵션이 올바르게 설정되었는지 확인
  • 브라우저 개발자 도구에서 쿠키 설정 확인
  • 도메인 및 경로 설정 확인

문제 5: "ssoBaseURL is required" 에러

해결 방법: 모든 SSO 관련 함수는 ssoBaseURL을 필수 파라미터로 받습니다. 환경 변수에서 값을 가져와서 전달하세요:

const ssoBaseURL = process.env.SSO_BASE_URL!;
if (!ssoBaseURL) {
  throw new Error("SSO_BASE_URL environment variable is required");
}

문제 6: "cookiePrefix is required" 에러

해결 방법: setCustomTokensclearAuthCookies 함수는 cookiePrefix를 필수 파라미터로 받습니다. 서비스 ID를 사용하거나 명시적으로 전달하세요:

const cookiePrefix = "myservice"; // 서비스별로 변경
setCustomTokens(response, accessToken, {
  cookiePrefix, // 필수
  isProduction: true,
});

문제 7: "License key is required" 또는 "Invalid license key" 에러

해결 방법: 라이센스 키는 모든 함수 호출 시 필수입니다. 환경 변수에서 라이센스 키를 가져와서 전달하세요:

const licenseKey = process.env.LICENSE_KEY;
if (!licenseKey) {
  throw new Error("LICENSE_KEY environment variable is required");
}

// 함수 호출 시 licenseKey 전달
const response = await handleMiddleware(req, middlewareConfig, {
  secret: process.env.NEXTAUTH_SECRET!,
  licenseKey, // 필수
  ssoBaseURL: process.env.SSO_BASE_URL!,
  // ... 기타 옵션
});

📦 패키지 정보

  • 패키지명: @thinkingcat/auth-utils
  • 버전: 1.0.17
  • 라이선스: MIT
  • 저장소: npm registry

📝 변경 이력 (Changelog)

v1.0.17 (2024-11-15)

새로운 기능:

  • 조건부 로깅 시스템 추가 (debugLog, debugError)
  • 환경 변수 AUTH_UTILS_DEBUG 지원
  • 프로덕션 환경에서 로그 출력 최적화

개선 사항:

  • 코드 구조 개선 (15개 기능별 섹션으로 그룹화)
  • 중복 코드 제거 (deleteNextAuthSessionCookie, clearAllAuthCookies 헬퍼 추가)
  • 로그 메시지 간소화 및 가독성 향상
  • 불필요한 주석 제거

성능 최적화:

  • 프로덕션 환경에서 로그 출력 비활성화로 성능 향상
  • 조건부 로깅으로 런타임 오버헤드 감소

🤝 기여 (Contributing)

이슈나 개선 사항이 있으면 GitHub 이슈를 등록해주세요.

📄 라이선스 (License)

MIT License


Made with ❤️ by ThinkingCat