@thinkingcat/auth-utils 1.0.6 → 1.0.7
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/dist/index.d.ts +9 -0
- package/dist/index.js +153 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -433,3 +433,12 @@ export declare function verifyAndRefreshTokenWithNextAuth(req: NextRequest, next
|
|
|
433
433
|
* @returns 미들웨어 설정 객체
|
|
434
434
|
*/
|
|
435
435
|
export declare function createMiddlewareConfig(config?: Partial<MiddlewareConfig>): Required<Omit<MiddlewareConfig, 'serviceId'>> & Pick<MiddlewareConfig, 'serviceId'>;
|
|
436
|
+
/**
|
|
437
|
+
* 통합 미들웨어 핸들러 함수
|
|
438
|
+
* 모든 인증, 권한, 구독 체크를 포함한 완전한 미들웨어 로직
|
|
439
|
+
* @param req NextRequest 객체
|
|
440
|
+
* @param config 미들웨어 설정
|
|
441
|
+
* @param getNextAuthToken NextAuth 토큰을 가져오는 함수
|
|
442
|
+
* @returns NextResponse 또는 null (다음 미들웨어로 진행)
|
|
443
|
+
*/
|
|
444
|
+
export declare function handleMiddleware(req: NextRequest, config: Required<Omit<MiddlewareConfig, 'serviceId'>> & Pick<MiddlewareConfig, 'serviceId'>, getNextAuthToken: (req: NextRequest) => Promise<JWT | null>): Promise<NextResponse | null>;
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ exports.isProtectedApiPath = isProtectedApiPath;
|
|
|
29
29
|
exports.checkAuthentication = checkAuthentication;
|
|
30
30
|
exports.verifyAndRefreshTokenWithNextAuth = verifyAndRefreshTokenWithNextAuth;
|
|
31
31
|
exports.createMiddlewareConfig = createMiddlewareConfig;
|
|
32
|
+
exports.handleMiddleware = handleMiddleware;
|
|
32
33
|
const jwt_1 = require("next-auth/jwt");
|
|
33
34
|
const jose_1 = require("jose");
|
|
34
35
|
const server_1 = require("next/server");
|
|
@@ -792,3 +793,155 @@ function createMiddlewareConfig(config) {
|
|
|
792
793
|
roleAccessConfig: config?.roleAccessConfig || defaultConfig.roleAccessConfig,
|
|
793
794
|
};
|
|
794
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* 통합 미들웨어 핸들러 함수
|
|
798
|
+
* 모든 인증, 권한, 구독 체크를 포함한 완전한 미들웨어 로직
|
|
799
|
+
* @param req NextRequest 객체
|
|
800
|
+
* @param config 미들웨어 설정
|
|
801
|
+
* @param getNextAuthToken NextAuth 토큰을 가져오는 함수
|
|
802
|
+
* @returns NextResponse 또는 null (다음 미들웨어로 진행)
|
|
803
|
+
*/
|
|
804
|
+
async function handleMiddleware(req, config, getNextAuthToken) {
|
|
805
|
+
try {
|
|
806
|
+
const pathname = req.nextUrl.pathname;
|
|
807
|
+
const secret = process.env.NEXTAUTH_SECRET;
|
|
808
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
809
|
+
const cookieDomain = process.env.COOKIE_DOMAIN;
|
|
810
|
+
const serviceId = config.serviceId || 'checkon';
|
|
811
|
+
const cookiePrefix = serviceId;
|
|
812
|
+
const token = await getNextAuthToken(req);
|
|
813
|
+
const effectiveRole = getEffectiveRole(token, serviceId);
|
|
814
|
+
// 1. API 요청 처리
|
|
815
|
+
if (pathname.startsWith('/api/')) {
|
|
816
|
+
if (config.authApiPaths.includes(pathname)) {
|
|
817
|
+
return server_1.NextResponse.next();
|
|
818
|
+
}
|
|
819
|
+
if (config.subscriptionExemptApiPaths.some((path) => pathname.startsWith(path))) {
|
|
820
|
+
return server_1.NextResponse.next();
|
|
821
|
+
}
|
|
822
|
+
const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
|
|
823
|
+
cookiePrefix,
|
|
824
|
+
isProduction,
|
|
825
|
+
cookieDomain,
|
|
826
|
+
text: serviceId,
|
|
827
|
+
});
|
|
828
|
+
if (authCheck.response) {
|
|
829
|
+
return authCheck.response;
|
|
830
|
+
}
|
|
831
|
+
if (!authCheck.isValid) {
|
|
832
|
+
const response = redirectToError(req, 'UNAUTHORIZED', '인증이 필요합니다.', config.errorPath);
|
|
833
|
+
return clearAuthCookies(response, cookiePrefix);
|
|
834
|
+
}
|
|
835
|
+
return server_1.NextResponse.next();
|
|
836
|
+
}
|
|
837
|
+
// 2. 루트 경로 처리 - SSO 토큰 처리 (인증 체크보다 먼저!)
|
|
838
|
+
if (pathname === '/') {
|
|
839
|
+
const tokenParam = req.nextUrl.searchParams.get('token');
|
|
840
|
+
if (tokenParam) {
|
|
841
|
+
try {
|
|
842
|
+
// 1. 토큰 검증
|
|
843
|
+
const tokenResult = await verifyToken(tokenParam, secret);
|
|
844
|
+
if (!tokenResult) {
|
|
845
|
+
throw new Error('Invalid token');
|
|
846
|
+
}
|
|
847
|
+
const { payload } = tokenResult;
|
|
848
|
+
// 2. 역할 추출
|
|
849
|
+
const defaultRole = Object.keys(config.rolePaths)[0] || 'ADMIN';
|
|
850
|
+
const tokenRole = extractRoleFromPayload(payload, serviceId, defaultRole);
|
|
851
|
+
// 3. Refresh token 가져오기 (서버 간 통신)
|
|
852
|
+
const userId = payload.sub || payload.userId || '';
|
|
853
|
+
const refreshToken = await getRefreshTokenFromSSO(userId, tokenParam) || '';
|
|
854
|
+
// 4. 자체 토큰 생성 및 쿠키 설정
|
|
855
|
+
const redirectPath = config.rolePaths[tokenRole] || config.rolePaths[defaultRole] || '/admin';
|
|
856
|
+
const response = await createAuthResponse(tokenParam, secret, {
|
|
857
|
+
refreshToken: refreshToken || undefined,
|
|
858
|
+
redirectPath,
|
|
859
|
+
text: serviceId,
|
|
860
|
+
cookiePrefix,
|
|
861
|
+
isProduction,
|
|
862
|
+
cookieDomain,
|
|
863
|
+
});
|
|
864
|
+
return response;
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
// 토큰 검증 실패 시 SSO 로그인으로 리다이렉트
|
|
868
|
+
return redirectToSSOLogin(req, serviceId);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// 토큰이 없고 이미 인증된 경우 역할별 대시보드로 리다이렉트
|
|
872
|
+
if (token && effectiveRole) {
|
|
873
|
+
return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
|
|
874
|
+
}
|
|
875
|
+
// 인증되지 않은 경우 SSO 로그인 페이지로 리다이렉트
|
|
876
|
+
return redirectToSSOLogin(req, serviceId);
|
|
877
|
+
}
|
|
878
|
+
// 3. 공개 경로 처리
|
|
879
|
+
if (config.publicPaths.some((path) => pathname === path || pathname.startsWith(path))) {
|
|
880
|
+
if (pathname === '/error' || pathname === '/verification') {
|
|
881
|
+
return server_1.NextResponse.next();
|
|
882
|
+
}
|
|
883
|
+
if (token && effectiveRole) {
|
|
884
|
+
return redirectToRoleDashboard(req, effectiveRole, config.rolePaths);
|
|
885
|
+
}
|
|
886
|
+
return server_1.NextResponse.next();
|
|
887
|
+
}
|
|
888
|
+
// 4. 인증 체크
|
|
889
|
+
const authCheck = await verifyAndRefreshTokenWithNextAuth(req, token, secret, {
|
|
890
|
+
cookiePrefix,
|
|
891
|
+
isProduction,
|
|
892
|
+
cookieDomain,
|
|
893
|
+
text: serviceId,
|
|
894
|
+
});
|
|
895
|
+
if (authCheck.response) {
|
|
896
|
+
return authCheck.response;
|
|
897
|
+
}
|
|
898
|
+
if (!authCheck.isValid) {
|
|
899
|
+
return redirectToSSOLogin(req, serviceId);
|
|
900
|
+
}
|
|
901
|
+
// 5. 토큰 확인 및 변환
|
|
902
|
+
let finalToken = token || await getNextAuthToken(req);
|
|
903
|
+
// verifyAndRefreshToken이 성공했는데 NextAuth 토큰이 없으면, 자체 토큰을 사용
|
|
904
|
+
if (!finalToken && authCheck.isValid) {
|
|
905
|
+
const accessToken = req.cookies.get(`${cookiePrefix}_access_token`)?.value;
|
|
906
|
+
if (accessToken) {
|
|
907
|
+
const tokenResult = await verifyToken(accessToken, secret);
|
|
908
|
+
if (tokenResult) {
|
|
909
|
+
const { payload } = tokenResult;
|
|
910
|
+
finalToken = createNextAuthJWT(payload, true); // academies 포함
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (!finalToken) {
|
|
915
|
+
return redirectToSSOLogin(req, serviceId);
|
|
916
|
+
}
|
|
917
|
+
// 6. 토큰 에러 체크
|
|
918
|
+
if (finalToken.error === "RefreshAccessTokenError") {
|
|
919
|
+
return redirectToSSOLogin(req, serviceId);
|
|
920
|
+
}
|
|
921
|
+
// 7. 토큰 유효성 체크
|
|
922
|
+
if (!finalToken.role || !finalToken.email) {
|
|
923
|
+
return redirectToSSOLogin(req, serviceId);
|
|
924
|
+
}
|
|
925
|
+
// 8. 역할 기반 접근 제어
|
|
926
|
+
const finalEffectiveRole = finalToken.role || getEffectiveRole(finalToken, serviceId) || effectiveRole || '';
|
|
927
|
+
if (config.roleAccessConfig && Object.keys(config.roleAccessConfig).length > 0 && finalEffectiveRole) {
|
|
928
|
+
const roleCheck = checkRoleAccess(pathname, finalEffectiveRole, config.roleAccessConfig);
|
|
929
|
+
if (!roleCheck.allowed) {
|
|
930
|
+
return redirectToError(req, 'ACCESS_DENIED', roleCheck.message || '접근 권한이 없습니다.', config.errorPath);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// 9. 구독 상태 확인 (시스템 관리자 제외)
|
|
934
|
+
if (finalEffectiveRole && requiresSubscription(pathname, finalEffectiveRole, config.subscriptionRequiredPaths, config.systemAdminRole)) {
|
|
935
|
+
const services = finalToken.services || [];
|
|
936
|
+
const subscriptionCheck = validateServiceSubscription(services, serviceId);
|
|
937
|
+
if (!subscriptionCheck.isValid) {
|
|
938
|
+
return server_1.NextResponse.redirect(subscriptionCheck.redirectUrl);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return null; // 다음 미들웨어로 진행
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
console.error('Middleware error:', error);
|
|
945
|
+
return redirectToError(req, 'INTERNAL_ERROR', '서버 오류가 발생했습니다.', config.errorPath);
|
|
946
|
+
}
|
|
947
|
+
}
|