GitHub

token์„ ์ฟ ํ‚ค๐Ÿช์— ์ €์žฅํ•˜๋ฉฐ ๊นจ๋‹ฌ์€ ์  | socketIO๋ฅผ ์ด์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋‘๋”์ง€ ๊ฒŒ์ž„ ๊ตฌํ˜„ํ•˜๊ธฐ (6)

hojun lee ยท 02/06/2025
์ปค๋ฒ„์ด๋ฏธ์ง€

๋ชฉํ‘œ

access token๊ณผ session token์„ ๋งŒ๋“ค์–ด auth_token์˜ ๋ณด์•ˆ์„ ๊ฐ•ํ™”์‹œ์ผฐ๋‹ค. ์–ด๋””๋‹ค ์ €์žฅํ• ๊นŒ ๋‚˜์˜ ์ธ์ฆ์ •๋ณด

Local Storage VS HttpOnly ์ฟ ํ‚ค

  1. Local Storage

์žฅ์ :

  • ๊ฐ„ํŽธํ•œ ์ ‘๊ทผ:ย JavaScript๋ฅผ ํ†ตํ•ด ์‰ฝ๊ฒŒ ์ฝ๊ณ  ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์šฉ๋Ÿ‰:ย ์ฟ ํ‚ค๋ณด๋‹ค ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์œ ์—ฐ์„ฑ:ย ๋‹ค์–‘ํ•œ ํด๋ผ์ด์–ธํŠธ ์ธก ๋กœ์ง์—์„œ ์ž์œ ๋กญ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ :

  • XSS ๊ณต๊ฒฉ ์ทจ์•ฝ์„ฑ:ย ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋  ๊ฒฝ์šฐ, Local Storage์— ์ €์žฅ๋œ ํ† ํฐ์„ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ž๋™ ์ „์†ก ๋ถ€์žฌ:ย ๋ชจ๋“  HTTP ์š”์ฒญ์— ์ž๋™์œผ๋กœ ํฌํ•จ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋ณ„๋„์˜ ๋กœ์ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
  1. HttpOnly ์ฟ ํ‚ค

์žฅ์ :

  • ๋ณด์•ˆ ๊ฐ•ํ™”:ย HttpOnlyย ์†์„ฑ์œผ๋กœ ์ธํ•ด JavaScript์—์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์–ด XSS ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ๋ณดํ˜ธ๋ฉ๋‹ˆ๋‹ค.
  • ์ž๋™ ์ „์†ก:ย ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ๋ชจ๋“  ํ•ด๋‹น ๋„๋ฉ”์ธ์— ๋Œ€ํ•œ HTTP ์š”์ฒญ์— ์ฟ ํ‚ค๋ฅผ ํฌํ•จ์‹œํ‚ต๋‹ˆ๋‹ค.
  • CSRF ๋ฐฉ์–ด:ย SameSiteย ์†์„ฑ์„ ์„ค์ •ํ•˜๋ฉด CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ :

  • ์ฟ ํ‚ค ๊ด€๋ฆฌ ์ œ์•ฝ:ย JavaScript์—์„œ ์ง์ ‘ ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์–ด, ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ์˜ ์œ ์—ฐํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.
  • ๋ณด์•ˆ ์„ค์ • ํ•„์š”:ย Secure,ย SameSiteย ๋“ฑ์˜ ์†์„ฑ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •ํ•ด์•ผ ์ตœ๋Œ€ ๋ณด์•ˆ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ณด์•ˆ์„ ๊ณ ๋ คํ•  ๋•Œ

๋ณด์•ˆ์„ ์ตœ์šฐ์„ ์œผ๋กœ ๊ณ ๋ คํ•  ๋•Œ, ์„ธ์…˜ ํ† ํฐ์€ HttpOnly cookie์— ์ €์žฅ ํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•˜๋‹ค.์ฟ ํ‚ค๋Š” XSS ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ํ† ํฐ์„ ๋ณดํ˜ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์š”์ฒญ์— ํฌํ•จ์‹œ์ผœ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ๊ด€๋ฆฌ๊ฐ€ ์ข‹๋‹ค. ์ฟ ํ‚ค ์†์„ฑ์„ ๊ฐ•ํ™”ํ•˜๋ฉด ๋” ๋ณด์•ˆ ์งฑ

์ฟ ํ‚ค๐Ÿช๋ž€ ๋ฌด์—‡์ผ๊นŒ

์ฟ ํ‚ค(Cookie)๋Š” ์›น ๋ธŒ๋ผ์šฐ์ €์™€ ์„œ๋ฒ„ ๊ฐ„์— ์ƒํƒœ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ  ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ์ž‘์€ ๋ฐ์ดํ„ฐ ์กฐ๊ฐ. ์ฟ ํ‚ค๋Š” ์ฃผ๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ, ์„ธ์…˜๊ด€๋ฆฌ, ์‚ฌ์šฉ์ž ์„ค์ • ์ €์žฅ ๋“ฑ์— ์‚ฌ์šฉ๋œ๋‹ค. ๋Œ€ํ˜• ์›น์‚ฌ์ดํŠธ๋“ค์ด ์ฟ ํ‚ค๋ฅผ ํ—ˆ์šฉํ•˜๋ผ๊ณ  ๋‚˜์˜จ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ๋‚ด์ •๋ณด๋ฅผ ๊ฐœ์ธํ™”ํ•ด์„œ ์‚ฌ์šฉํ• ๋ผ๊ณ !!!

์ฟ ํ‚ค๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์›น์‚ฌ์ดํŠธ๋ฅผ ๋ฐฉ๋ฌธํ•  ๋•Œ๋งˆ๋‹ค ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์„œ๋ฒ„์— ์ž๋™์œผ๋กœ ์ „์†กํ•œ๋‹ค.

์šฉ๋„ ์„ธ์…˜ ๊ด€๋ฆฌ: ๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด ๋“ฑ. ๊ฐœ์ธํ™”: ์‚ฌ์šฉ์ž ์„ ํ˜ธ ์„ค์ • ์ €์žฅ. ์ถ”์  ๋ฐ ๋ถ„์„: ์‚ฌ์šฉ์ž ํ–‰๋™ ์ถ”์ , ์›น ๋ถ„์„ ๋„๊ตฌ์™€ ์—ฐ๋™.

httpOnly ์ฟ ํ‚ค, ๋” ๋ง› ์ข‹์€

ํด๋ผ์ด์–ธํŠธ ์ธก 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 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 ํ™œ์„ฑํ™” ์™„๋ฃŒ!