-
Notifications
You must be signed in to change notification settings - Fork 0
[API] Auth BFF Route Handler 구현 #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import {cookies} from 'next/headers'; | ||
| import {NextResponse} from 'next/server'; | ||
|
|
||
| import {ApiError, normalizeApiError} from '@/src/shared/api/api-error'; | ||
| import {AUTH_COOKIE_NAMES, AUTH_COOKIE_PATHS} from '@/src/shared/auth'; | ||
|
|
||
| interface AuthTokenPair { | ||
| accessToken?: string; | ||
| refreshToken?: string; | ||
| } | ||
|
|
||
| const AUTH_COOKIE_OPTIONS = { | ||
| httpOnly: true, | ||
| sameSite: 'lax', | ||
| secure: process.env.NODE_ENV === 'production', | ||
| } as const; | ||
|
|
||
| const getCookieStore = () => { | ||
| return cookies(); | ||
| }; | ||
|
|
||
| const createMissingTokenError = () => { | ||
| return new ApiError({ | ||
| message: '인증 토큰 응답이 올바르지 않습니다.', | ||
| status: 502, | ||
| type: 'server', | ||
| }); | ||
| }; | ||
|
|
||
| export const setAuthCookies = async ({ | ||
| accessToken, | ||
| refreshToken, | ||
| }: AuthTokenPair) => { | ||
| if (!accessToken || !refreshToken) { | ||
| throw createMissingTokenError(); | ||
| } | ||
|
|
||
| const cookieStore = await getCookieStore(); | ||
|
|
||
| cookieStore.set(AUTH_COOKIE_NAMES.accessToken, accessToken, { | ||
| ...AUTH_COOKIE_OPTIONS, | ||
| path: AUTH_COOKIE_PATHS.accessToken, | ||
| }); | ||
| cookieStore.set(AUTH_COOKIE_NAMES.refreshToken, refreshToken, { | ||
| ...AUTH_COOKIE_OPTIONS, | ||
| path: AUTH_COOKIE_PATHS.refreshToken, | ||
| }); | ||
| }; | ||
|
|
||
| export const clearAuthCookies = async () => { | ||
| const cookieStore = await getCookieStore(); | ||
|
|
||
| cookieStore.set(AUTH_COOKIE_NAMES.accessToken, '', { | ||
| ...AUTH_COOKIE_OPTIONS, | ||
| maxAge: 0, | ||
| path: AUTH_COOKIE_PATHS.accessToken, | ||
| }); | ||
| cookieStore.set(AUTH_COOKIE_NAMES.refreshToken, '', { | ||
| ...AUTH_COOKIE_OPTIONS, | ||
| maxAge: 0, | ||
| path: AUTH_COOKIE_PATHS.refreshToken, | ||
| }); | ||
| }; | ||
|
|
||
| export const getRefreshToken = async () => { | ||
| return (await getCookieStore()).get(AUTH_COOKIE_NAMES.refreshToken)?.value; | ||
| }; | ||
|
|
||
| export const createApiErrorResponse = (error: unknown) => { | ||
| const apiError = normalizeApiError(error); | ||
|
|
||
| return NextResponse.json( | ||
| { | ||
| code: apiError.code, | ||
| message: apiError.message, | ||
| }, | ||
| { | ||
| status: apiError.status ?? 500, | ||
| } | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import {NextResponse} from 'next/server'; | ||
|
|
||
| import {API_ENDPOINTS, type AdminApiTypes} from '@/src/shared/api'; | ||
| import {ApiError} from '@/src/shared/api/api-error'; | ||
| import {createServerApi} from '@/src/shared/api/server-client'; | ||
|
|
||
| import { | ||
| createApiErrorResponse, | ||
| getRefreshToken, | ||
| setAuthCookies, | ||
| } from '../_lib/auth-route'; | ||
|
|
||
| const createMissingRefreshTokenError = () => { | ||
| return new ApiError({ | ||
| message: '리프레시 토큰이 없습니다.', | ||
| status: 401, | ||
| type: 'auth', | ||
| }); | ||
| }; | ||
|
|
||
| export async function POST() { | ||
| try { | ||
| const refreshToken = await getRefreshToken(); | ||
|
|
||
| if (!refreshToken) { | ||
| throw createMissingRefreshTokenError(); | ||
| } | ||
|
|
||
| const serverApi = await createServerApi({accessToken: refreshToken}); | ||
| const {data} = await serverApi.get<AdminApiTypes.GetAccessTokenResponse>( | ||
| API_ENDPOINTS.auth.accessToken | ||
| ); | ||
|
|
||
| await setAuthCookies(data); | ||
|
|
||
| return new NextResponse(null, {status: 204}); | ||
| } catch (error) { | ||
| return createApiErrorResponse(error); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import {NextResponse} from 'next/server'; | ||
|
|
||
| import {API_ENDPOINTS, type AdminApiTypes} from '@/src/shared/api'; | ||
| import {createServerApi} from '@/src/shared/api/server-client'; | ||
|
|
||
| import {createApiErrorResponse, setAuthCookies} from '../_lib/auth-route'; | ||
|
|
||
| export async function POST(request: Request) { | ||
| try { | ||
| const body = (await request.json()) as AdminApiTypes.PostLoginRequest; | ||
| const serverApi = await createServerApi({includeAccessToken: false}); | ||
| const {data} = await serverApi.post<AdminApiTypes.PostLoginResponse>( | ||
| API_ENDPOINTS.auth.login, | ||
| body | ||
| ); | ||
|
Comment on lines
+12
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n '"axios"|"follow-redirects"|getHttpsAdminApiBaseUrl' package.json src/shared/api/server-client.ts
pnpm exec node --input-type=module <<'NODE'
import http from 'node:http';
import axios from 'axios';
let authorization = '';
let body = '';
const sink = http.createServer((request, response) => {
const chunks = [];
request.on('data', (chunk) => chunks.push(chunk));
request.on('end', () => {
authorization = request.headers.authorization ?? '';
body = Buffer.concat(chunks).toString();
response.end('ok');
});
});
await new Promise((resolve) => sink.listen(0, '127.0.0.1', resolve));
const sinkPort = sink.address().port;
const redirector = http.createServer((request, response) => {
response.writeHead(307, {
Location: `http://127.0.0.1:${sinkPort}/sink`,
});
response.end();
});
await new Promise((resolve) => redirector.listen(0, '127.0.0.1', resolve));
const redirectorPort = redirector.address().port;
try {
await axios.post(
`http://127.0.0.1:${redirectorPort}/login`,
{id: 'probe', password: 'probe'},
{headers: {Authorization: 'Bearer probe'}}
);
} catch {}
await new Promise((resolve) => redirector.close(resolve));
await new Promise((resolve) => sink.close(resolve));
if (authorization || body.includes('probe')) {
console.error('FAIL: credentials crossed the redirect hop');
process.exit(1);
}
console.log('PASS: credentials were not forwarded across the redirect hop');
NODERepository: Room-In-Us/Room-In-Us-Front-Admin Length of output: 1923 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/room-in-us-room-in-us-front-admin-91f4490a \
-type f \( -name 'AGENTS.md' -o -name 'SKILL.md' -o -name '*.md' \) \
-print | sort | head -40
printf '%s\n' '--- relevant files ---'
cat -n src/shared/api/server-client.ts
printf '%s\n' '--- login route ---'
cat -n src/app/api/auth/login/route.ts
printf '%s\n' '--- access-token route ---'
cat -n src/app/api/auth/access-token/route.ts
printf '%s\n' '--- axios lock entries ---'
rg -n -C 3 '(^|/)(axios|follow-redirects)@|axios:|follow-redirects:' pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || trueRepository: Room-In-Us/Room-In-Us-Front-Admin Length of output: 10094 🌐 Web query:
💡 Result: In Axios 1.16 and its underlying Citations:
Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information Reachability: External · Exploitability: Moderate 인증 요청의 redirect를 비활성화하세요.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| await setAuthCookies(data); | ||
|
|
||
| return NextResponse.json({ | ||
| adminId: data.adminId, | ||
| }); | ||
| } catch (error) { | ||
| return createApiErrorResponse(error); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import {NextResponse} from 'next/server'; | ||
|
|
||
| import {clearAuthCookies} from '../_lib/auth-route'; | ||
|
|
||
| export async function POST() { | ||
| await clearAuthCookies(); | ||
|
|
||
| return new NextResponse(null, {status: 204}); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
잘못된 요청 본문을 500으로 반환하지 마세요.
Line 10에서
request.json()이 형식이 잘못된 JSON에 대해 예외를 발생시키면, Line 22-23의 catch 경로가 이를 알 수 없는 오류로 정규화합니다.createApiErrorResponse는 상태 코드가 없는 오류에500을 사용하므로 잘못된 클라이언트 요청이 서버 오류로 처리됩니다. 또한as AdminApiTypes.PostLoginRequest는 런타임 검증을 수행하지 않습니다. 본문을unknown으로 파싱하고 JSON 형식과id·password문자열 필드를 검증한 뒤, 실패 시400을 반환하세요.As per path instructions: strict type checking에 따라 request body를 명시적이고 타입 안전하게 처리해야 합니다.
Also applies to: 22-23
🤖 Prompt for AI Agents
Source: Path instructions