GitHub

auth_token을 JWT로 변신시켜야 하는 이유 | socketIO를 이용한 실시간 두더지 게임 구현하기 (5)

hojun lee · 02/06/2025
커버이미지

깨달은 점 1번 난 개발에 대해 아직 아무것도 모르는구나 😇

목표

'/' route이 외엔 로그인 없이 접근불가✋🏾

Webauthn을 통해 받아온 auth_token(공개 키 기반 인증 메커니즘) 을 어디에 저장해야 효율적으로 client-side에서 private route를 구현할 수 있을지 고민해보고 결론을 낸다. 아 참 여기선 어떻게 보안을 강화할지 생각해본다.

배운 점

  1. socket 통신으론 http cookie를 저장할 수 없다.
  2. server에서 server의 통신으론 http cookie에 저장 할 수 없다.
  3. node 서버에서 svelteKit /api 로 호출해도 브라우저 쿠키가 설정되지 않는다.

지금생각해보면 너무 당연한 행위들인데 시도해본 내가 레전드


auth_token을 client로 보내도 되나!?

문제가 된 건 socket 통신 중에 .on method에서 auth_token을 발급받게 되었다.

사실 authenticate를 restAPI로 했으면 쉬웠을 것이다.

그냥 그대로 쿠키를 구워줬으면 됐을 일이기 때문에 200에 success를 return 하면 되는건데... socket 통신에선 쿠키를 못 굽는다.

서버간 통신에서는 브라우저 컨텍스트가 없어서 쿠키 저장 불가능 (socket 통신은 연결된 뒤 http 헤더 사용 불가) socket에는 set-Cookie 헤더를 지원하지 않음 소켓 연결을 위한 핸드쉐이크 이후론 http 헤더 안씀

아무튼 socket에선 안됨

아무리 webauthN에서 제공하는 공개키라도 클라이언트에 공개하는건 찝찝하다. 공개키는 서버 측에서 안전하게 보관하는게 좋다.

그러면 어떻게 할 것인가를 고민해봤을 때 token 화를 하면되지 않을까했다. 암호화를 하자!

JWT와 세션토큰

1. JWT

JSON Web Token 은 두 당사자 간에 보안 정보를 전송하기 위한 컴팩트하고 URL 안전한 방법. JWT는 주로 사용자의 인증 및 권한 부여에 사용되며, 다음과 같은 세부분으로 구성됨

헤더 (Header): 토큰의 유형(JWT)과 해싱 알고리즘(HS256, RS256 등)을 지정합니다. 페이로드 (Payload): 주체(예: 사용자 ID, 역할 등)에 대한 클레임(Claims)을 포함합니다. 서명 (Signature): 토큰이 변조되지 않았음을 보장하기 위해 사용됩니다.

예시 JWT:

eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3Mzk5Njc4NjMsImlzcyI6Imh0dHBzOi8vbXlkYi1ob3p1bmxlZS5jYy04MC5pLmF3cy5lZGdlZGItY2xvdWQ6NTY1Ni9kYi9tYWluL2V4dC9hdXRoIiwic3ViIjoiNTI1ZDAzMmUtZDNkYy0xMWVmLTgwMGEtMzcyOTYwOTQxNzViIn0.HjUbeWakSl-TkEkhVbOdazlaGqqrYaQJNbNWZ0Vsd3k

장점 : 무상태(stateless) : 서버에서 세션을 저장할 필요 없음

이전에 많이 사용되었던 **JWT(JSON Web Token)**는 클라이언트와 서버 간의 인증 상태를 유지하는 데 널리 활용되고 있습니다. 그러나 최근에는 액세스 토큰과 세션 토큰을 분리하여 사용하는 방식이 주목받고 있습니다.

JWT 대신 액세스 토큰과 세션 토큰을 분리하여 사용하는 방식은 보안성과 세션 관리 측면에서 다음과 같은 장점을 가집니다:

보안 강화: 세션 토큰을 HttpOnly 쿠키에 저장하여 클라이언트 측 스크립트로부터 보호할 수 있습니다.

유연한 세션 관리: 서버에서 세션 토큰을 중앙 관리하므로, 토큰 해지 및 갱신이 용이합니다.

보안 공격 대응: 세션 탈취 시 서버에서 즉시 무효화할 수 있어 보안 사고를 최소화할 수 있습니다.

그러나, 서버 측에서 세션 상태를 관리해야 하므로 추가적인 인프라 (예: Redis)가 필요하고, 서버 간 세션 동기화가 필요할 수 있다는 점을 고려해야 합니다.

그래서 2번 방식을 사용해보겠다.

2. 액세스 토큰과 세션 토큰

서버에서 auth_token을 가지고 액세스 토큰과 세션토큰을 만듦 redis 등 메모리 storage를 사용해 상태를 동기화해야함

// node/server_sockets.js

// webauthn = 개인키와 공개키의 쌍
// 개인키는 컴퓨터나 개인기기에 저장되어있음
//공개 키 기반 인증 메커니즘
const { auth_token } = await tokenResponse.json();

// 액세스 토큰과 세션 토큰으로 분리
const sessionToken = crypto.randomUUID();
// Redis나 서버 메모리에 저장
await sessionStore.set(sessionToken, auth_token);

해당 토큰 방식은 메모리 스토어가 꼭 필요하다. 개발용은 memory store를 직접구현하고, product 일땐 꼭 redis등 메모리 스토어를 사용하도록

import { createClient } from "redis"; // Redis 사용 시

// 1. 메모리 스토어 구현 (개발용, 프로덕션에서는 Redis 권장)
class MemoryStore {
    constructor() {
        this.sessions = new Map();
    }

    async set(key, value, ttl = 3600) {
        this.sessions.set(key, {
            value,
            expires: Date.now() + ttl * 1000,
        });
    }

    async get(key) {
        const session = this.sessions.get(key);
        if (!session) return null;

        if (Date.now() > session.expires) {
            this.sessions.delete(key);
            return null;
        }

        return session.value;
    }

    async delete(key) {
        return this.sessions.delete(key);
    }
}

// 2. Redis 스토어 구현 (프로덕션용)
class RedisStore {
    constructor() {
        this.client = createClient({
            url: process.env.REDIS_URL,
        });
        this.client.connect().catch(console.error);
    }

    async set(key, value, ttl = 3600) {
        await this.client.set(key, value, { EX: ttl });
    }

    async get(key) {
        return await this.client.get(key);
    }

    async delete(key) {
        await this.client.del(key);
    }
}

// 환경에 따라 적절한 스토어 선택
const sessionStore =
    process.env.NODE_ENV === "production"
        ? new RedisStore()
        : new MemoryStore();

export default sessionStore;

이렇게 하면 우린 client에 auth_token을 직접 보내지 않을 수 있다.

    User->>Browser: WebAuthn 인증 (생체 인식/보안 키 사용)
    Browser->>Server: 인증 응답 (공개 키 서명 등)
    Server->>Redis: 세션 저장 (sessionToken -> authToken 매핑)
    Server->>Browser: 세션 토큰 전달 (Session Token)

서버에서 sessionToken에 접근가능한 accessToken을 만들어서 암호를 제공해 주는것이다!

 // 클라이언트에는 세션 토큰만 전달
socket.emit("webauthn:authenticate:response", {
    success: true,
    message: "인증 성공",
    sessionToken,
});

드디어 우린 socket의 http header 한계를 뛰어넘어 안전하게 브라우저에 token에 상응하는 값을 전달할 수 있었다.

근데 이제 어떻게 저장하고 로그인을 인증할 것인가?