Skip to content

로그인 없이 안전한 게스트 액세스를 구축한 방법

TL;DR

YouViCo의 게스트 액세스 모델은 로그인 자격 증명 대신 안전하게 서명되고 만료되는 토큰을 사용합니다. 리뷰어는 링크를 클릭해 영상 프로젝트에 접근하고 댓글을 남깁니다. 계정을 만들 필요가 없습니다. 아키텍처는 JWT 토큰(서명, 만료), 권한 스코프(읽기 전용 vs. 댓글 vs. 편집), RBAC(역할 기반 접근 제어), 그리고 실시간 폐기로 구성됩니다. 이 구조 덕분에 협업 마찰을 줄이면서도 엔터프라이즈 수준의 보안을 유지할 수 있습니다.

문제

협업 도구의 접근 방식은 종종 고통스럽습니다.

YouViCo의 통찰은 분명했습니다. 대부분의 리뷰어는 계정이 필요 없습니다. 그들에게 필요한 건 다음 세 가지뿐입니다.

  1. 하나의 영상 프로젝트에 대한 접근 권한
  2. 댓글(또는 단순 보기) 권한
  3. 며칠 안에 만료되는 세션

이걸 위해 굳이 계정을 만들 필요가 있을까요?

아키텍처: 세 개의 레이어

게스트 액세스 스택은 다음과 같습니다.

Layer 1: Token Generation (Server-side)

Layer 2: Token Validation (API Gateway)

Layer 3: Permission Enforcement (Service Level)

Layer 1: 토큰 생성

프로젝트 소유자가 외부 리뷰어를 초대하고 싶을 때 “Generate Guest Link”를 클릭합니다.

interface GuestAccessRequest {
  projectId: string;
  expiresIn: number;           // hours until token expires
  permissions: Permission[];    // ['view', 'comment', 'resolve-comments']
  maxUses?: number;             // optional: one-time link
}

class GuestAccessService {
  generateToken(request: GuestAccessRequest): string {
    const payload = {
      sub: `guest:${uuid()}`,    // Subject: anonymous guest
      projectId: request.projectId,
      permissions: request.permissions,
      iat: now(),
      exp: now() + (request.expiresIn * 3600), // Expiration time
      maxUses: request.maxUses || unlimited
    };
    
    // Sign with server private key
    const token = jwt.sign(payload, PRIVATE_KEY, {
      algorithm: 'RS256'
    });
    
    // Generate shareable link
    return `https://youvico.com/project/${request.projectId}?token=${token}`;
  }
}

핵심 속성:

Layer 2: 토큰 검증 (API Gateway)

게스트 토큰이 포함된 모든 요청은 API 게이트웨이에서 검증됩니다.

class GuestTokenMiddleware {
  async validateToken(req: Request): Promise<GuestContext | null> {
    const token = extractFromQuery(req, 'token') || extractFromHeader(req, 'Authorization');
    
    if (!token) return null;
    
    try {
      // Verify signature and expiration
      const decoded = jwt.verify(token, PUBLIC_KEY);
      
      // Check if token has been revoked
      const isRevoked = await redis.get(`revoked:${decoded.jti}`);
      if (isRevoked) {
        throw new Error('Token revoked');
      }
      
      // Check usage limit
      if (decoded.maxUses) {
        const usageCount = await redis.incr(`token:usage:${decoded.jti}`);
        if (usageCount > decoded.maxUses) {
          throw new Error('Token usage limit exceeded');
        }
      }
      
      return {
        guestId: decoded.sub,
        projectId: decoded.projectId,
        permissions: decoded.permissions,
        expiresAt: new Date(decoded.exp * 1000)
      };
    } catch (err) {
      return null;
    }
  }
}

폐기 처리에 Redis를 쓰는 이유는 무엇일까요? JWT의 만료 시점을 기다릴 필요 없이 즉시 토큰을 무효화할 수 있기 때문입니다.

Layer 3: 권한 적용

각 서비스는 게스트의 권한을 직접 검사합니다.

class CommentService {
  async createComment(guestContext: GuestContext, input: CreateCommentInput) {
    // Verify guest has 'comment' permission
    if (!guestContext.permissions.includes('comment')) {
      throw new Error('Insufficient permissions');
    }
    
    // Verify guest is accessing their own project
    const project = await getProject(input.projectId);
    if (project.id !== guestContext.projectId) {
      throw new Error('Unauthorized project access');
    }
    
    // Create comment with guest attribution
    return await Comment.create({
      ...input,
      authorId: guestContext.guestId,
      isGuest: true  // Track as guest for analytics
    });
  }
}

class VideoService {
  async getVideo(guestContext: GuestContext, videoId: string) {
    // 'view' permission is implicit - if token is valid, guest can view
    // (no need to check 'view' explicitly, presence of token = access)
    
    const video = await getVideo(videoId);
    
    // Verify guest can access this video (it's in their project)
    if (video.projectId !== guestContext.projectId) {
      throw new Error('Unauthorized access');
    }
    
    return video;
  }
}

권한 모델

게스트에게는 역할 기반 권한이 부여됩니다. 대표적인 역할은 다음과 같습니다.

역할권한사용 사례
Viewerview이해관계자 리뷰 (읽기 전용)
Commenterview, comment클라이언트 피드백 (댓글은 달지만 해소(resolve)는 못 함)
Approverview, comment, resolve크리에이티브 디렉터 (리뷰 전체 권한)

각 권한은 서비스 레이어에서 강제됩니다.

실시간 폐기

지금 즉시 접근 권한을 회수해야 한다면 어떻게 해야 할까요(예: 클라이언트 관계 종료)?

class GuestAccessController {
  async revokeToken(projectId: string, tokenJti: string) {
    // Set revocation flag in Redis with expiration = token's original expiration
    const token = await Token.findByJti(tokenJti);
    const expirationSeconds = Math.floor((token.expiresAt - now()) / 1000);
    
    await redis.setex(
      `revoked:${tokenJti}`,
      expirationSeconds,
      'true'
    );
    
    // Revoked token becomes useless immediately
    // No API calls will succeed with this token
    
    return { success: true, message: 'Token revoked' };
  }
}

결과: 게스트는 몇 초 안에 접근 권한을 잃습니다. 별도의 세션 정리도 필요 없습니다.

URL 인코딩과 보안

게스트 링크는 이메일, Slack, 문자 메시지를 통해 공유됩니다. 그래서 다음 조건을 만족해야 합니다.

  1. 짧을 것 (공유 가능)
  2. 안전할 것 (변조 불가)
  3. 만료될 것 (수명 제한)

우리는 다음과 같이 사용합니다.

https://youvico.com/project/abc123?token=eyJhbGc...

1. Project ID in URL path: Transparent, easy to understand
2. Token in query string: Cryptographically signed, can't be modified
3. Token signed with RS256: Client verifies with public key, can't forge

보안 참고: HTTPS 전용 링크를 강력히 권장합니다. 토큰은 bearer 토큰이므로 절대 HTTP로 전송되어서는 안 됩니다.

분석: 게스트 액세스 추적

게스트는 기본적으로 익명이지만, 분석을 위해 활동은 추적합니다.

SELECT 
  token_id,
  project_id,
  permissions,
  created_at,
  expires_at,
  comments_created,
  videos_viewed,
  last_accessed
FROM guest_access_logs
WHERE project_id = 'proj_xyz'
ORDER BY created_at DESC;

이를 통해 프로젝트 소유자는 다음과 같은 질문에 답할 수 있습니다. “외부 리뷰어가 몇 명인가? 얼마나 활발히 참여하고 있는가?”

배운 점

  1. 일회성 접근에는 비밀번호보다 토큰이 낫습니다. 비밀번호는 지속적인 정체성을 전제로 하지만, 토큰은 한시적 권한을 전제로 합니다.

  2. 만료가 곧 보안입니다. 7일 안에 만료되는 토큰은 1년짜리 토큰보다 안전합니다. 편의성과 보안 사이의 균형을 잡아야 합니다.

  3. Redis 기반 폐기는 즉시 동작합니다. 민감한 프로젝트라면 JWT 만료에만 기대지 말고 반드시 폐기 레이어를 두세요.

  4. 스코프가 핵심입니다. 게스트 토큰은 하나의 프로젝트, (선택적으로) 하나의 IP 대역, (선택적으로) 하나의 User-Agent에서만 동작해야 합니다. 지나치게 넓은 스코프는 보안 리스크입니다.

  5. 감사 로깅을 남기세요. 모든 게스트 액세스를 추적해야 합니다. 보안 사고가 발생했을 때, 게스트가 무엇에 접근했는지 정확히 파악할 수 있습니다.

게스트 액세스는 YouViCo를 “계정 마찰”에서 “원클릭 협업”으로 바꿔놓았습니다. 이 기능을 출시한 뒤 채택률은 3배 증가했습니다.

영상 협업을 간소화할 준비가 되셨나요?

무료로 시작하기