fix(WEB): FCM 토큰 등록을 위한 is_authenticated 쿠키 추가
- HttpOnly 쿠키(access_token)는 JavaScript에서 읽을 수 없어 FCM 초기화 실패 - non-HttpOnly is_authenticated 쿠키 추가로 클라이언트에서 인증 상태 확인 가능 - login/logout/refresh/proxy 라우트에서 쿠키 설정/삭제 처리 - hasAuthToken()이 is_authenticated 쿠키 확인하도록 변경
This commit is contained in:
@@ -170,12 +170,23 @@ export async function POST(request: NextRequest) {
|
||||
'Max-Age=604800', // TODO: 테스트용 10초, 원래 604800 (7 days)
|
||||
].join('; ');
|
||||
|
||||
// ✅ FCM 등에서 인증 상태 확인용 (non-HttpOnly - JavaScript 접근 가능)
|
||||
const isAuthenticatedCookie = [
|
||||
'is_authenticated=true',
|
||||
// HttpOnly 제외 - JavaScript에서 접근 가능해야 함
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
].join('; ');
|
||||
|
||||
console.log('✅ Login successful - Access & Refresh tokens stored in HttpOnly cookies');
|
||||
|
||||
const response = NextResponse.json(responseData, { status: 200 });
|
||||
|
||||
response.headers.append('Set-Cookie', accessTokenCookie);
|
||||
response.headers.append('Set-Cookie', refreshTokenCookie);
|
||||
response.headers.append('Set-Cookie', isAuthenticatedCookie);
|
||||
|
||||
return response;
|
||||
|
||||
|
||||
@@ -70,6 +70,15 @@ export async function POST(request: NextRequest) {
|
||||
'Max-Age=0', // Delete immediately
|
||||
].join('; ');
|
||||
|
||||
// ✅ is_authenticated 쿠키도 삭제 (FCM 인증 상태 플래그)
|
||||
const clearIsAuthenticated = [
|
||||
'is_authenticated=',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
'Max-Age=0',
|
||||
].join('; ');
|
||||
|
||||
console.log('✅ Logout complete - Access & Refresh tokens cleared');
|
||||
|
||||
const response = NextResponse.json(
|
||||
@@ -79,6 +88,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
response.headers.append('Set-Cookie', clearAccessToken);
|
||||
response.headers.append('Set-Cookie', clearRefreshToken);
|
||||
response.headers.append('Set-Cookie', clearIsAuthenticated);
|
||||
|
||||
return response;
|
||||
|
||||
|
||||
@@ -60,9 +60,12 @@ export async function POST(request: NextRequest) {
|
||||
// Refresh token is invalid or expired
|
||||
console.warn('⚠️ Token refresh failed - user needs to re-login');
|
||||
|
||||
// Clear both tokens
|
||||
const clearAccessToken = 'access_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0';
|
||||
const clearRefreshToken = 'refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0';
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// Clear all tokens
|
||||
const clearAccessToken = `access_token=; HttpOnly; ${isProduction ? 'Secure; ' : ''}SameSite=Lax; Path=/; Max-Age=0`;
|
||||
const clearRefreshToken = `refresh_token=; HttpOnly; ${isProduction ? 'Secure; ' : ''}SameSite=Lax; Path=/; Max-Age=0`;
|
||||
const clearIsAuthenticated = `is_authenticated=; ${isProduction ? 'Secure; ' : ''}SameSite=Lax; Path=/; Max-Age=0`;
|
||||
|
||||
const failResponse = NextResponse.json(
|
||||
{ error: 'Token refresh failed', needsReauth: true },
|
||||
@@ -71,6 +74,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
failResponse.headers.append('Set-Cookie', clearAccessToken);
|
||||
failResponse.headers.append('Set-Cookie', clearRefreshToken);
|
||||
failResponse.headers.append('Set-Cookie', clearIsAuthenticated);
|
||||
|
||||
return failResponse;
|
||||
}
|
||||
@@ -86,11 +90,13 @@ export async function POST(request: NextRequest) {
|
||||
};
|
||||
|
||||
// Set new HttpOnly cookies
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const accessTokenCookie = [
|
||||
`access_token=${data.access_token}`,
|
||||
'HttpOnly',
|
||||
'Secure',
|
||||
'SameSite=Strict',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
].join('; ');
|
||||
@@ -98,18 +104,28 @@ export async function POST(request: NextRequest) {
|
||||
const refreshTokenCookie = [
|
||||
`refresh_token=${data.refresh_token}`,
|
||||
'HttpOnly',
|
||||
'Secure',
|
||||
'SameSite=Strict',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
'Max-Age=604800', // 7 days
|
||||
].join('; ');
|
||||
|
||||
// ✅ FCM 등에서 인증 상태 확인용 (non-HttpOnly)
|
||||
const isAuthenticatedCookie = [
|
||||
'is_authenticated=true',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
].join('; ');
|
||||
|
||||
console.log('✅ Token refresh successful - New tokens stored');
|
||||
|
||||
const successResponse = NextResponse.json(responseData, { status: 200 });
|
||||
|
||||
successResponse.headers.append('Set-Cookie', accessTokenCookie);
|
||||
successResponse.headers.append('Set-Cookie', refreshTokenCookie);
|
||||
successResponse.headers.append('Set-Cookie', isAuthenticatedCookie);
|
||||
|
||||
return successResponse;
|
||||
|
||||
|
||||
@@ -79,6 +79,15 @@ function createTokenCookies(tokens: { accessToken?: string; refreshToken?: strin
|
||||
'Path=/',
|
||||
`Max-Age=${tokens.expiresIn || 7200}`,
|
||||
].join('; '));
|
||||
|
||||
// ✅ FCM 등에서 인증 상태 확인용 (non-HttpOnly)
|
||||
cookies.push([
|
||||
'is_authenticated=true',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${tokens.expiresIn || 7200}`,
|
||||
].join('; '));
|
||||
}
|
||||
|
||||
if (tokens.refreshToken) {
|
||||
@@ -104,6 +113,7 @@ function createClearTokenCookies(): string[] {
|
||||
return [
|
||||
`access_token=; HttpOnly${secureFlag}; SameSite=Lax; Path=/; Max-Age=0`,
|
||||
`refresh_token=; HttpOnly${secureFlag}; SameSite=Lax; Path=/; Max-Age=0`,
|
||||
`is_authenticated=${secureFlag}; SameSite=Lax; Path=/; Max-Age=0`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,16 @@ export const getMultipartHeaders = (): HeadersInit => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 토큰 존재 여부 확인
|
||||
* 인증 상태 확인
|
||||
*
|
||||
* ⚠️ 중요: access_token은 HttpOnly 쿠키로 JavaScript에서 읽을 수 없음
|
||||
* - is_authenticated 쿠키는 non-HttpOnly로 JavaScript에서 접근 가능
|
||||
* - 로그인/토큰갱신 시 함께 설정되어 인증 상태를 나타냄
|
||||
* - FCM 토큰 등록 등 클라이언트에서 인증 상태 확인이 필요한 경우 사용
|
||||
*/
|
||||
export const hasAuthToken = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
// ✅ access_token 쿠키 존재 여부 확인
|
||||
const token = document.cookie.split('; ').find(row => row.startsWith('access_token='))?.split('=')[1];
|
||||
return !!token;
|
||||
// ✅ is_authenticated 쿠키로 인증 상태 확인 (non-HttpOnly)
|
||||
const isAuth = document.cookie.split('; ').find(row => row.startsWith('is_authenticated='))?.split('=')[1];
|
||||
return isAuth === 'true';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user