access token๊ณผ session token์ ๋ง๋ค์ด auth_token์ ๋ณด์์ ๊ฐํ์์ผฐ๋ค. ์ด๋๋ค ์ ์ฅํ ๊น ๋์ ์ธ์ฆ์ ๋ณด
์ฅ์ :
๋จ์ :
์ฅ์ :
๋จ์ :
๋ณด์์ ์ต์ฐ์ ์ผ๋ก ๊ณ ๋ คํ ๋, ์ธ์
ํ ํฐ์ HttpOnly cookie์ ์ ์ฅ ํ๋ ๊ฒ์ด ๋ ์์ ํ๋ค.์ฟ ํค๋ XSS ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ํ ํฐ์ ๋ณดํธํ ์ ์์ผ๋ฉฐ, ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์์ฒญ์ ํฌํจ์์ผ์ฃผ๊ธฐ ๋๋ฌธ์ ๊ด๋ฆฌ๊ฐ ์ข๋ค. ์ฟ ํค ์์ฑ์ ๊ฐํํ๋ฉด ๋ ๋ณด์ ์งฑ
์ฟ ํค(Cookie)๋ ์น ๋ธ๋ผ์ฐ์ ์ ์๋ฒ ๊ฐ์ ์ํ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ ์ ๋ฌํ๊ธฐ ์ํ ์์ ๋ฐ์ดํฐ ์กฐ๊ฐ. ์ฟ ํค๋ ์ฃผ๋ก ์ฌ์ฉ์ ์ธ์ฆ, ์ธ์ ๊ด๋ฆฌ, ์ฌ์ฉ์ ์ค์ ์ ์ฅ ๋ฑ์ ์ฌ์ฉ๋๋ค. ๋ํ ์น์ฌ์ดํธ๋ค์ด ์ฟ ํค๋ฅผ ํ์ฉํ๋ผ๊ณ ๋์จ ๊ฒ์ ๋ณผ ์ ์๋ค. ๋ด์ ๋ณด๋ฅผ ๊ฐ์ธํํด์ ์ฌ์ฉํ ๋ผ๊ณ !!!
์ฟ ํค๋ ์ฌ์ฉ์๊ฐ ์น์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํ ๋๋ง๋ค ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ฒ์ ์๋์ผ๋ก ์ ์กํ๋ค.
์ฉ๋ ์ธ์ ๊ด๋ฆฌ: ๋ก๊ทธ์ธ ์ํ ์ ์ง, ์ฅ๋ฐ๊ตฌ๋ ์ ๋ณด ๋ฑ. ๊ฐ์ธํ: ์ฌ์ฉ์ ์ ํธ ์ค์ ์ ์ฅ. ์ถ์ ๋ฐ ๋ถ์: ์ฌ์ฉ์ ํ๋ ์ถ์ , ์น ๋ถ์ ๋๊ตฌ์ ์ฐ๋.
ํด๋ผ์ด์ธํธ ์ธก javascript ๊ฐ ์ ๊ทผ ํ ์ ์๋๋ก ์ค์ ๋ ์ฟ ํค์ด๋ค. ๋ณด์๊ฐํ๋ฅผ ์ํด ์ฌ์ฉ๋๊ณ ํฌ๋ก์ค ์ฌ์ดํธ ์คํฌ๋ฆฝํ (XXS) ๊ณต๊ฒฉ์ ํตํด ์ฟ ํค๊ฐ ํ์ทจ ๋๋ ๊ฒ์ ๋ฐฉ์งํ๋ค.
๋ณด์๊ฐํ! ์ธ์ ํ์ด์ฌํน ๋ฐฉ์ง!
httpOnly ์ฟ ํค๋ ํด๋ผ์ด์ธํธ ์ธก javascript์์ ์ค์ ํ ์ ์์! ์ฟ ํค๋ฅผ ์ค์ ํ๋ ค๋ฉด ๋ฐ๋์ server ์ธก์์ Set-Cookie ํค๋๋ฅผ ํตํด ์ค์ ํด์ผํจ
์ฐ๋ฆฐ socket์ ํตํด ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฐจ๋ฅผ ํ์ผ๋ ํ ๋ฒ ๋ ์๋ฒ์ http ํต์ ์ ํด์ผํ๋ค.
// workflow
Server->>Redis: ์ธ์
์ ์ฅ (sessionToken -> authToken ๋งคํ)
// ์์ฒญ์ด ํ ๋ฒ ๋ ํ์ํจ.
SocketServer->>Browser: sessionToken (.emit)
Browser->>Server: sessionToken์ผ๋ก ์ฟ ํค ์์ฑ ์์ฒญ
Server->>Browser: Set-Cookie ํค๋๋ก ์ธ์
ํ ํฐ ์ ๋ฌ
Note over Browser: ๋ธ๋ผ์ฐ์ ๋ Set-Cookie ํค๋๋ฅผ ํตํด HttpOnly ์ฟ ํค๋ก ์ธ์
ํ ํฐ ์ ์ฅ
Browser->>Server: ๋ณดํธ๋ ๋ฆฌ์์ค ์์ฒญ (์ฟ ํค ์๋ ํฌํจ)
ํด๋ผ์ด์ธํธ ์ธก์์ socket.on์ ์ฝ๋ฐฑํจ์๋ฅผ ๊ฐ์ ํ๋ค.
//Login.svelte (์ปดํฌ๋ํธ)
socket.on('webauthn:authenticate:response', handleAuthenticate)
const handleAuthenticate = async (response) => {
socket.off('webauthn:authenticate:response', handleAuthenticate)
if (response.success && response.sessionToken) {
try {
const res = await fetch('http://localhost:3000/auth/set-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ sessionToken: response.sessionToken }),
credentials: 'include'
})
if (res.ok) {
toast.success('๋ก๊ทธ์ธ ์ฑ๊ณต', {
description: 'Sunday, December 03, 2023 at 9:00 AM',
action: {
label: 'Undo',
onClick: () => console.info('Undo')
}
})
goto('/start')
}
} catch (error) {
console.error('์ธ์ฆ ์คํจ:', error)
}
}
if (dev) console.log('webauthn:authenticate:response : socket off ')
resolve(response)
}
์๋ต์ ํ์ธํ๊ณ sessionToken ๊ฐ์ ๊ฐ์ง๊ณ POST call ์ ํ๋ค. ์ฌ๊ธฐ์ header์ credentials: 'include'๋ฅผ ๋ฐ๋์ ํฌํจํ๋ค.
ํด๋ผ์ด์ธํธ ์ธก fetch ์์ฒญ ์ credentials: 'include' ์ค์ : ์ฟ ํค๊ฐ ์๋์ผ๋ก ํฌํจ๋์ด ์ธ์ฆ๋ ์์ฒญ์ ๋ณด๋ผ ์ ์์ต๋๋ค.
์ด์ ์๋ฒ์์ ์ด๋ฅผ ์ฟ ํค๋ก ๋ฑ๋กํด๋ณด์.
// node index.js
// ํ ํฐ ์ค์ ์ ์ํ ์๋ํฌ์ธํธ
app.post("/auth/set-token", async (req, res) => {
const { sessionToken } = req.body;
if (!sessionToken) {
return res.status(400).json({ error: "์ธ์
ํ ํฐ์ด ์์ต๋๋ค." });
}
// ์ ์ฅ๋ ์ค์ JWT ํ ํฐ ์กฐํ
const auth_token = await sessionStore.get(sessionToken);
console.log("๐ ~ app.post ~ auth_token:", auth_token);
if (!auth_token) {
return res.status(401).json({ error: "์ ํจํ์ง ์์ ์ธ์
" });
}
// ์ธ์
ํ ํฐ ์ฆ์ ์ญ์ (์ผํ์ฉ)
await sessionStore.delete(sessionToken);
// ์ค์ JWT๋ก ์ฟ ํค ์ค์
res.cookie("auth_token", auth_token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
path: "/",
maxAge: 3600000,
});
res.json({ success: true });
});
๋ฉ๋ชจ๋ฆฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅ๋ ์ค์ ์ฐ๋ฆฌ์ ํ ํฐ์ ์กฐํํ๋ค. ๊ทธ๋ฆฌ๊ณ res.cookie๋ก auth_token์ httpOnly cookie์ ์ ์ฅํ๊ณ ์ฑ๊ณต ๊ฐ์ ๋ฐํํ๋ค.
๊ทธ๋ฆฌ๊ณ ํด๋ผ์ด์ธํธ๋ ์ด ๊ฐ์ ๊ธฐ๋ค๋ฆฐ๋ค ํ์ธํด์ /start๋ก goto ์ํจ๋ค.
if (res.ok) {
toast.success('๋ก๊ทธ์ธ ์ฑ๊ณต', {
description: 'Sunday, December 03, 2025 at 9:00 AM',
action: {
label: 'Undo',
onClick: () => console.info('Undo')
}
})
goto('/start')
๊ทผ๋ฐ ์ด๋ป๊ฒ /start๋ก ์ ๊ทผ์ ๋ก๊ทธ์ธ์ด ๋์๋ค๊ณ ํ์ธํ ์ ์์๊น.
์ฐ๋ฆฌ๋ ์ฌ๊ธฐ์ server hook์ ์ฌ์ฉํด์ผํ๋ค.
server hooks์ ์๋ฒ ์ฌ์ด๋ ๋ก์ง์ ํ์ฅํ๊ณ ์ ์ดํ ์ ์๋ ๊ธฐ๋ฅ์ผ๋ก ์ฌ์ฉํ๋ฉด ์์ฒญ๊ณผ ์๋ต์ ๊ฐ๋ก์ฑ๊ณ , ์ธ์ฆ, ๊ถํ ๊ฒ์ฌ ๋ฑ ๋ค์ํ ์์ ์ ์ํํ ์ ์๋ค.
handle: ๋ชจ๋ ์์ฒญ์ ์ฒ๋ฆฌํ ๋ ํธ์ถ๋ฉ๋๋ค. handleError: ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํธ์ถ๋ฉ๋๋ค. externalFetch: ์ธ๋ถ ์์ฒญ์ ์ปค์คํฐ๋ง์ด์ง ํ ๋ ์ฌ์ฉ๋ฉ๋๋ค.
๋น๊ณต๊ฐ ๊ฒฝ๋ก๋ฅผ ์์ ํ๊ฒ ๋ณดํธ ํ ์ ์๋ค.
import { redirect } from '@sveltejs/kit'
// ๊ณต๊ฐ ์ ๊ทผ ๊ฐ๋ฅํ ๊ฒฝ๋ก
const PUBLIC_PATHS = ['/', '/about']
// ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง ์ ์ ์คํ๋๋ ํ
export const handle = async ({ event, resolve }) => {
const token = event.cookies.get('auth_token')
const path = event.url.pathname
// ๋น๊ณต๊ฐ ๊ฒฝ๋ก์ ๋ํ ์ธ์ฆ ๊ฒ์ฌ
if (!PUBLIC_PATHS.includes(path) && !token) {
throw redirect(302, '/')
}
// ์๋ต์ ์ธ์ฆ ์ํ ์ถ๊ฐ
const response = await resolve(event)
return response
}
์ด๋ ๊ฒ๋๋ฉด / ์ /about ์ ๊ฒฝ๋ก์ด์ธ์ session token์ด ํ์ํ๋ค.
webauthn ์ผ๋ก ๋ฑ๋ก๊ณผ ์ธ์ฆ์ ํ๋ค. ๋ฐ๊ธ๋ฐ์ auth_token์ ํ ํฐ ๋ถ๋ฆฌ ์ ๊ทผ์ ์ ํ์ฉํด ๋๋๋ค. httpOnly cookie์ ๋ฑ๋กํ๋ค.
์๋ฒ์ ์์ฒญ์ ๋ณด๋ด ๊ฒ์ฆํ์ฌ ์ฌ์ฉํ๋ค.
๋๋์ด ๋ก๊ทธ์ธ ๋ฐ private route ํ์ฑํ ์๋ฃ!