@thinkingcat/auth-utils 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.d.ts +136 -1
- package/dist/index.js +204 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ import {
|
|
|
139
139
|
createMiddlewareConfig,
|
|
140
140
|
handleMiddleware,
|
|
141
141
|
verifyAndRefreshTokenWithNextAuth,
|
|
142
|
-
|
|
142
|
+
|
|
143
143
|
// 라이센스
|
|
144
144
|
checkLicenseKey,
|
|
145
145
|
|
|
@@ -166,7 +166,8 @@ const response = await handleMiddleware(req, middlewareConfig, {
|
|
|
166
166
|
});
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
**중요**:
|
|
169
|
+
**중요**:
|
|
170
|
+
|
|
170
171
|
- 라이센스 키는 모든 함수 호출 시 필수 파라미터입니다.
|
|
171
172
|
- 라이센스 키가 없거나 유효하지 않으면 함수가 에러를 발생시킵니다.
|
|
172
173
|
- 라이센스 키는 SHA-256 해시로 변환되어 모듈 내부의 유효한 키 목록과 비교됩니다.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JWT } from "next-auth/jwt";
|
|
2
|
+
import type { Session } from "next-auth";
|
|
2
3
|
import { NextResponse, NextRequest } from 'next/server';
|
|
3
4
|
export interface ResponseLike {
|
|
4
5
|
cookies: {
|
|
@@ -325,7 +326,141 @@ export interface MiddlewareOptions {
|
|
|
325
326
|
/** 라이센스 키 (필수) */
|
|
326
327
|
licenseKey: string;
|
|
327
328
|
}
|
|
328
|
-
|
|
329
|
+
/**
|
|
330
|
+
* NextAuth 쿠키 설정 생성
|
|
331
|
+
* @param options 옵션
|
|
332
|
+
* @param options.isProduction 프로덕션 환경 여부
|
|
333
|
+
* @param options.cookieDomain 쿠키 도메인 (선택)
|
|
334
|
+
* @returns NextAuth 쿠키 설정 객체
|
|
335
|
+
*/
|
|
336
|
+
export declare function createNextAuthCookies(options: {
|
|
337
|
+
isProduction?: boolean;
|
|
338
|
+
cookieDomain?: string;
|
|
339
|
+
}): {
|
|
340
|
+
sessionToken: {
|
|
341
|
+
name: string;
|
|
342
|
+
options: {
|
|
343
|
+
httpOnly: boolean;
|
|
344
|
+
sameSite: 'lax' | 'none';
|
|
345
|
+
path: string;
|
|
346
|
+
secure: boolean;
|
|
347
|
+
domain?: string;
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
callbackUrl: {
|
|
351
|
+
name: string;
|
|
352
|
+
options: {
|
|
353
|
+
sameSite: 'lax' | 'none';
|
|
354
|
+
path: string;
|
|
355
|
+
secure: boolean;
|
|
356
|
+
domain?: string;
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
csrfToken: {
|
|
360
|
+
name: string;
|
|
361
|
+
options: {
|
|
362
|
+
httpOnly: boolean;
|
|
363
|
+
sameSite: 'lax' | 'none';
|
|
364
|
+
path: string;
|
|
365
|
+
secure: boolean;
|
|
366
|
+
domain?: string;
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* NextAuth 기본 설정 생성
|
|
372
|
+
* @param options 옵션
|
|
373
|
+
* @param options.secret NextAuth secret
|
|
374
|
+
* @param options.isProduction 프로덕션 환경 여부
|
|
375
|
+
* @param options.cookieDomain 쿠키 도메인 (선택)
|
|
376
|
+
* @param options.signInPath 로그인 페이지 경로 (기본값: '/login')
|
|
377
|
+
* @param options.errorPath 에러 페이지 경로 (기본값: '/login')
|
|
378
|
+
* @param options.nextAuthUrl NextAuth URL (선택)
|
|
379
|
+
* @param options.sessionMaxAge 세션 최대 유지 시간 (초, 기본값: 30일)
|
|
380
|
+
* @param options.jwtMaxAge JWT 최대 유지 시간 (초, 기본값: 30일)
|
|
381
|
+
* @returns NextAuth 기본 설정 객체
|
|
382
|
+
*/
|
|
383
|
+
export declare function createNextAuthBaseConfig(options: {
|
|
384
|
+
secret: string;
|
|
385
|
+
isProduction?: boolean;
|
|
386
|
+
cookieDomain?: string;
|
|
387
|
+
signInPath?: string;
|
|
388
|
+
errorPath?: string;
|
|
389
|
+
nextAuthUrl?: string;
|
|
390
|
+
sessionMaxAge?: number;
|
|
391
|
+
jwtMaxAge?: number;
|
|
392
|
+
}): {
|
|
393
|
+
session: {
|
|
394
|
+
strategy: 'jwt';
|
|
395
|
+
maxAge: number;
|
|
396
|
+
};
|
|
397
|
+
jwt: {
|
|
398
|
+
maxAge: number;
|
|
399
|
+
};
|
|
400
|
+
providers: never[];
|
|
401
|
+
url?: string;
|
|
402
|
+
pages: {
|
|
403
|
+
signIn: string;
|
|
404
|
+
error: string;
|
|
405
|
+
};
|
|
406
|
+
cookies: ReturnType<typeof createNextAuthCookies>;
|
|
407
|
+
secret: string;
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* JWT 콜백에서 초기 로그인 시 토큰 생성 헬퍼
|
|
411
|
+
* @param token 기존 토큰
|
|
412
|
+
* @param user 사용자 정보
|
|
413
|
+
* @param account 계정 정보
|
|
414
|
+
* @returns 업데이트된 JWT 토큰
|
|
415
|
+
*/
|
|
416
|
+
export declare function createInitialJWTToken(token: JWT, user: {
|
|
417
|
+
id: string;
|
|
418
|
+
email?: string | null;
|
|
419
|
+
emailHash?: string | null;
|
|
420
|
+
maskedEmail?: string | null;
|
|
421
|
+
phoneHash?: string | null;
|
|
422
|
+
maskedPhone?: string | null;
|
|
423
|
+
role?: string;
|
|
424
|
+
phone?: string | null;
|
|
425
|
+
decryptedEmail?: string | null;
|
|
426
|
+
decryptedPhone?: string | null;
|
|
427
|
+
refreshToken?: string | null;
|
|
428
|
+
}, account?: {
|
|
429
|
+
serviceId?: string;
|
|
430
|
+
} | null): JWT;
|
|
431
|
+
/**
|
|
432
|
+
* Session 콜백에서 빈 세션 반환 헬퍼
|
|
433
|
+
* @param session 기존 세션
|
|
434
|
+
* @returns 빈 세션 객체
|
|
435
|
+
*/
|
|
436
|
+
export declare function createEmptySession(session: Session): Session;
|
|
437
|
+
/**
|
|
438
|
+
* Session 콜백에서 토큰 정보를 세션에 매핑하는 헬퍼
|
|
439
|
+
* @param session 기존 세션
|
|
440
|
+
* @param token JWT 토큰
|
|
441
|
+
* @param options 옵션
|
|
442
|
+
* @param options.primaryAcademy 기본 학원 정보 (선택)
|
|
443
|
+
* @returns 업데이트된 세션
|
|
444
|
+
*/
|
|
445
|
+
export declare function mapTokenToSession(session: Session, token: JWT, options?: {
|
|
446
|
+
primaryAcademy?: {
|
|
447
|
+
id: string;
|
|
448
|
+
name: string;
|
|
449
|
+
role: string;
|
|
450
|
+
} | null;
|
|
451
|
+
}): Session;
|
|
452
|
+
/**
|
|
453
|
+
* 쿠키에서 커스텀 토큰을 읽어서 NextAuth JWT로 변환하는 헬퍼 함수
|
|
454
|
+
* NextAuth JWT 콜백에서 사용
|
|
455
|
+
* @param cookieName 쿠키 이름 (예: 'checkon_access_token')
|
|
456
|
+
* @param secret JWT 서명에 사용할 secret key
|
|
457
|
+
* @param serviceId 서비스 ID (필수)
|
|
458
|
+
* @param licenseKey 라이센스 키 (필수)
|
|
459
|
+
* @param includeAcademies academies 정보 포함 여부 (기본값: false)
|
|
460
|
+
* @returns NextAuth JWT 객체 또는 null
|
|
461
|
+
*/
|
|
462
|
+
export declare function getJWTFromCustomTokenCookie(cookieName: string, secret: string, serviceId: string, licenseKey: string, includeAcademies?: boolean): Promise<JWT | null>;
|
|
463
|
+
export declare function checkLicenseKey(licenseKey: string): Promise<void>;
|
|
329
464
|
export declare function checkRoleAccess(pathname: string, role: string, roleConfig: RoleAccessConfig): {
|
|
330
465
|
allowed: boolean;
|
|
331
466
|
message?: string;
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,12 @@ exports.redirectToError = redirectToError;
|
|
|
49
49
|
exports.clearAuthCookies = clearAuthCookies;
|
|
50
50
|
exports.getEffectiveRole = getEffectiveRole;
|
|
51
51
|
exports.requiresSubscription = requiresSubscription;
|
|
52
|
+
exports.createNextAuthCookies = createNextAuthCookies;
|
|
53
|
+
exports.createNextAuthBaseConfig = createNextAuthBaseConfig;
|
|
54
|
+
exports.createInitialJWTToken = createInitialJWTToken;
|
|
55
|
+
exports.createEmptySession = createEmptySession;
|
|
56
|
+
exports.mapTokenToSession = mapTokenToSession;
|
|
57
|
+
exports.getJWTFromCustomTokenCookie = getJWTFromCustomTokenCookie;
|
|
52
58
|
exports.checkLicenseKey = checkLicenseKey;
|
|
53
59
|
exports.checkRoleAccess = checkRoleAccess;
|
|
54
60
|
exports.redirectToSSOLogin = redirectToSSOLogin;
|
|
@@ -67,7 +73,14 @@ exports.handleMiddleware = handleMiddleware;
|
|
|
67
73
|
const jwt_1 = require("next-auth/jwt");
|
|
68
74
|
const jose_1 = require("jose");
|
|
69
75
|
const server_1 = require("next/server");
|
|
70
|
-
|
|
76
|
+
// Edge Runtime 호환을 위해 Web Crypto API 사용
|
|
77
|
+
async function createHashSHA256(data) {
|
|
78
|
+
const encoder = new TextEncoder();
|
|
79
|
+
const dataBuffer = encoder.encode(data);
|
|
80
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
|
|
81
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
82
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
83
|
+
}
|
|
71
84
|
/**
|
|
72
85
|
* 토큰 검증 및 디코딩
|
|
73
86
|
* @param accessToken JWT access token
|
|
@@ -331,7 +344,7 @@ function createRedirectHTML(redirectPath, text) {
|
|
|
331
344
|
* @returns NextResponse 객체
|
|
332
345
|
*/
|
|
333
346
|
async function createAuthResponse(accessToken, secret, options) {
|
|
334
|
-
checkLicenseKey(options.licenseKey);
|
|
347
|
+
await checkLicenseKey(options.licenseKey);
|
|
335
348
|
const { refreshToken, redirectPath, text, cookiePrefix, isProduction = false, cookieDomain, serviceId, } = options;
|
|
336
349
|
// 1. 토큰 검증
|
|
337
350
|
const tokenResult = await verifyToken(accessToken, secret);
|
|
@@ -600,11 +613,197 @@ function requiresSubscription(pathname, role, subscriptionRequiredPaths, systemA
|
|
|
600
613
|
const VALID_LICENSE_KEY_HASHES = new Set([
|
|
601
614
|
'73bce4f3b64804c255cdab450d759a8b53038f9edb59ae42d9988b08dfd007e2',
|
|
602
615
|
]);
|
|
603
|
-
|
|
616
|
+
/**
|
|
617
|
+
* NextAuth 쿠키 설정 생성
|
|
618
|
+
* @param options 옵션
|
|
619
|
+
* @param options.isProduction 프로덕션 환경 여부
|
|
620
|
+
* @param options.cookieDomain 쿠키 도메인 (선택)
|
|
621
|
+
* @returns NextAuth 쿠키 설정 객체
|
|
622
|
+
*/
|
|
623
|
+
function createNextAuthCookies(options) {
|
|
624
|
+
const { isProduction = false, cookieDomain } = options;
|
|
625
|
+
const isSecure = isProduction;
|
|
626
|
+
return {
|
|
627
|
+
sessionToken: {
|
|
628
|
+
name: isSecure ? `__Secure-next-auth.session-token` : `next-auth.session-token`,
|
|
629
|
+
options: {
|
|
630
|
+
httpOnly: true,
|
|
631
|
+
sameSite: isSecure ? 'none' : 'lax',
|
|
632
|
+
path: '/',
|
|
633
|
+
secure: isSecure,
|
|
634
|
+
...(cookieDomain && { domain: cookieDomain }),
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
callbackUrl: {
|
|
638
|
+
name: isSecure ? `__Secure-next-auth.callback-url` : `next-auth.callback-url`,
|
|
639
|
+
options: {
|
|
640
|
+
sameSite: isSecure ? 'none' : 'lax',
|
|
641
|
+
path: '/',
|
|
642
|
+
secure: isSecure,
|
|
643
|
+
...(cookieDomain && { domain: cookieDomain }),
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
csrfToken: {
|
|
647
|
+
name: isSecure ? `__Secure-next-auth.csrf-token` : `next-auth.csrf-token`,
|
|
648
|
+
options: {
|
|
649
|
+
httpOnly: true,
|
|
650
|
+
sameSite: isSecure ? 'none' : 'lax',
|
|
651
|
+
path: '/',
|
|
652
|
+
secure: isSecure,
|
|
653
|
+
...(cookieDomain && { domain: cookieDomain }),
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* NextAuth 기본 설정 생성
|
|
660
|
+
* @param options 옵션
|
|
661
|
+
* @param options.secret NextAuth secret
|
|
662
|
+
* @param options.isProduction 프로덕션 환경 여부
|
|
663
|
+
* @param options.cookieDomain 쿠키 도메인 (선택)
|
|
664
|
+
* @param options.signInPath 로그인 페이지 경로 (기본값: '/login')
|
|
665
|
+
* @param options.errorPath 에러 페이지 경로 (기본값: '/login')
|
|
666
|
+
* @param options.nextAuthUrl NextAuth URL (선택)
|
|
667
|
+
* @param options.sessionMaxAge 세션 최대 유지 시간 (초, 기본값: 30일)
|
|
668
|
+
* @param options.jwtMaxAge JWT 최대 유지 시간 (초, 기본값: 30일)
|
|
669
|
+
* @returns NextAuth 기본 설정 객체
|
|
670
|
+
*/
|
|
671
|
+
function createNextAuthBaseConfig(options) {
|
|
672
|
+
const { secret, isProduction = false, cookieDomain, signInPath = '/login', errorPath = '/login', nextAuthUrl, sessionMaxAge = 30 * 24 * 60 * 60, // 30일
|
|
673
|
+
jwtMaxAge = 30 * 24 * 60 * 60, // 30일
|
|
674
|
+
} = options;
|
|
675
|
+
return {
|
|
676
|
+
session: {
|
|
677
|
+
strategy: 'jwt',
|
|
678
|
+
maxAge: sessionMaxAge,
|
|
679
|
+
},
|
|
680
|
+
jwt: {
|
|
681
|
+
maxAge: jwtMaxAge,
|
|
682
|
+
},
|
|
683
|
+
providers: [],
|
|
684
|
+
...(nextAuthUrl && { url: nextAuthUrl }),
|
|
685
|
+
pages: {
|
|
686
|
+
signIn: signInPath,
|
|
687
|
+
error: errorPath,
|
|
688
|
+
},
|
|
689
|
+
cookies: createNextAuthCookies({ isProduction, cookieDomain }),
|
|
690
|
+
secret,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* JWT 콜백에서 초기 로그인 시 토큰 생성 헬퍼
|
|
695
|
+
* @param token 기존 토큰
|
|
696
|
+
* @param user 사용자 정보
|
|
697
|
+
* @param account 계정 정보
|
|
698
|
+
* @returns 업데이트된 JWT 토큰
|
|
699
|
+
*/
|
|
700
|
+
function createInitialJWTToken(token, user, account) {
|
|
701
|
+
return {
|
|
702
|
+
...token,
|
|
703
|
+
id: user.id,
|
|
704
|
+
email: user.email ?? undefined,
|
|
705
|
+
emailHash: user.emailHash ?? undefined,
|
|
706
|
+
maskedEmail: user.maskedEmail ?? undefined,
|
|
707
|
+
phoneHash: user.phoneHash ?? undefined,
|
|
708
|
+
maskedPhone: user.maskedPhone ?? undefined,
|
|
709
|
+
role: user.role,
|
|
710
|
+
phone: user.phone ?? undefined,
|
|
711
|
+
decryptedEmail: user.decryptedEmail ?? undefined,
|
|
712
|
+
decryptedPhone: user.decryptedPhone ?? undefined,
|
|
713
|
+
refreshToken: user.refreshToken ?? undefined,
|
|
714
|
+
accessTokenExpires: Date.now() + (15 * 60 * 1000), // 15분
|
|
715
|
+
serviceId: account?.serviceId ?? undefined,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Session 콜백에서 빈 세션 반환 헬퍼
|
|
720
|
+
* @param session 기존 세션
|
|
721
|
+
* @returns 빈 세션 객체
|
|
722
|
+
*/
|
|
723
|
+
function createEmptySession(session) {
|
|
724
|
+
return {
|
|
725
|
+
...session,
|
|
726
|
+
user: {
|
|
727
|
+
...session.user,
|
|
728
|
+
id: '',
|
|
729
|
+
email: null,
|
|
730
|
+
role: 'GUEST',
|
|
731
|
+
},
|
|
732
|
+
expires: new Date().toISOString(),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Session 콜백에서 토큰 정보를 세션에 매핑하는 헬퍼
|
|
737
|
+
* @param session 기존 세션
|
|
738
|
+
* @param token JWT 토큰
|
|
739
|
+
* @param options 옵션
|
|
740
|
+
* @param options.primaryAcademy 기본 학원 정보 (선택)
|
|
741
|
+
* @returns 업데이트된 세션
|
|
742
|
+
*/
|
|
743
|
+
function mapTokenToSession(session, token, options) {
|
|
744
|
+
if (!session.user) {
|
|
745
|
+
return session;
|
|
746
|
+
}
|
|
747
|
+
const { primaryAcademy } = options || {};
|
|
748
|
+
const user = session.user;
|
|
749
|
+
user.id = token.id;
|
|
750
|
+
user.email = token.email;
|
|
751
|
+
user.name = token.name;
|
|
752
|
+
user.role = primaryAcademy?.role || token.role;
|
|
753
|
+
user.academyId = primaryAcademy?.id;
|
|
754
|
+
user.academyName = primaryAcademy?.name;
|
|
755
|
+
if (token.academies && Array.isArray(token.academies)) {
|
|
756
|
+
user.academies = token.academies.map((ua) => ({
|
|
757
|
+
id: ua.id,
|
|
758
|
+
name: ua.name,
|
|
759
|
+
}));
|
|
760
|
+
}
|
|
761
|
+
user.smsVerified = token.smsVerified;
|
|
762
|
+
user.emailVerified = token.emailVerified;
|
|
763
|
+
user.phone = token.phone;
|
|
764
|
+
user.phoneVerified = token.phoneVerified;
|
|
765
|
+
user.isPasswordReset = token.isPasswordReset || false;
|
|
766
|
+
user.decryptedEmail = token.decryptedEmail;
|
|
767
|
+
user.decryptedPhone = token.decryptedPhone;
|
|
768
|
+
return session;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* 쿠키에서 커스텀 토큰을 읽어서 NextAuth JWT로 변환하는 헬퍼 함수
|
|
772
|
+
* NextAuth JWT 콜백에서 사용
|
|
773
|
+
* @param cookieName 쿠키 이름 (예: 'checkon_access_token')
|
|
774
|
+
* @param secret JWT 서명에 사용할 secret key
|
|
775
|
+
* @param serviceId 서비스 ID (필수)
|
|
776
|
+
* @param licenseKey 라이센스 키 (필수)
|
|
777
|
+
* @param includeAcademies academies 정보 포함 여부 (기본값: false)
|
|
778
|
+
* @returns NextAuth JWT 객체 또는 null
|
|
779
|
+
*/
|
|
780
|
+
async function getJWTFromCustomTokenCookie(cookieName, secret, serviceId, licenseKey, includeAcademies = false) {
|
|
781
|
+
try {
|
|
782
|
+
const { cookies } = await Promise.resolve().then(() => __importStar(require('next/headers')));
|
|
783
|
+
const cookieStore = await cookies();
|
|
784
|
+
const accessToken = cookieStore.get(cookieName)?.value;
|
|
785
|
+
if (!accessToken) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
await checkLicenseKey(licenseKey);
|
|
789
|
+
const tokenResult = await verifyToken(accessToken, secret);
|
|
790
|
+
if (!tokenResult) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
const { payload } = tokenResult;
|
|
794
|
+
const jwt = createNextAuthJWT(payload, serviceId, includeAcademies);
|
|
795
|
+
return jwt;
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
console.error(`[getJWTFromCustomTokenCookie] Failed to read ${cookieName}:`, error);
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function checkLicenseKey(licenseKey) {
|
|
604
803
|
if (!licenseKey || licenseKey.length < 10) {
|
|
605
804
|
throw new Error('License key is required');
|
|
606
805
|
}
|
|
607
|
-
const keyHash =
|
|
806
|
+
const keyHash = await createHashSHA256(licenseKey);
|
|
608
807
|
if (!VALID_LICENSE_KEY_HASHES.has(keyHash)) {
|
|
609
808
|
throw new Error('Invalid license key');
|
|
610
809
|
}
|
|
@@ -858,7 +1057,7 @@ async function handleMiddleware(req, config, options) {
|
|
|
858
1057
|
try {
|
|
859
1058
|
const pathname = req.nextUrl.pathname;
|
|
860
1059
|
const { secret, isProduction, cookieDomain, getNextAuthToken, licenseKey } = options;
|
|
861
|
-
checkLicenseKey(licenseKey);
|
|
1060
|
+
await checkLicenseKey(licenseKey);
|
|
862
1061
|
if (!config.serviceId) {
|
|
863
1062
|
throw new Error('serviceId is required in middleware config');
|
|
864
1063
|
}
|