깨달은 점 1번 난 개발에 대해 아직 아무것도 모르는구나 😇
'/' route이 외엔 로그인 없이 접근불가✋🏾
Webauthn을 통해 받아온 auth_token(공개 키 기반 인증 메커니즘) 을 어디에 저장해야 효율적으로 client-side에서 private route를 구현할 수 있을지 고민해보고 결론을 낸다. 아 참 여기선 어떻게 보안을 강화할지 생각해본다.
지금생각해보면 너무 당연한 행위들인데 시도해본 내가 레전드
문제가 된 건 socket 통신 중에 .on method에서 auth_token을 발급받게 되었다.
사실 authenticate를 restAPI로 했으면 쉬웠을 것이다.
그냥 그대로 쿠키를 구워줬으면 됐을 일이기 때문에 200에 success를 return 하면 되는건데... socket 통신에선 쿠키를 못 굽는다.
서버간 통신에서는 브라우저 컨텍스트가 없어서 쿠키 저장 불가능 (socket 통신은 연결된 뒤 http 헤더 사용 불가) socket에는 set-Cookie 헤더를 지원하지 않음 소켓 연결을 위한 핸드쉐이크 이후론 http 헤더 안씀
아무튼 socket에선 안됨
아무리 webauthN에서 제공하는 공개키라도 클라이언트에 공개하는건 찝찝하다. 공개키는 서버 측에서 안전하게 보관하는게 좋다.
그러면 어떻게 할 것인가를 고민해봤을 때 token 화를 하면되지 않을까했다. 암호화를 하자!
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번 방식을 사용해보겠다.
서버에서 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에 상응하는 값을 전달할 수 있었다.
근데 이제 어떻게 저장하고 로그인을 인증할 것인가?