Skip to content

How We Built Secure Guest Access Without Login at YouViCo

TL;DR

YouViCo’s guest access model uses secure, expiring tokens instead of login credentials. Reviewers click a link, gain access to video projects, and leave comments—no account creation needed. Architecture: JWT tokens (signed, expiring), permission scopes (read-only vs. comment vs. edit), role-based access control (RBAC), and real-time revocation. This reduces collaboration friction while maintaining enterprise security.

The Problem

Collaboration tools often have painful access patterns:

YouViCo’s insight: Most reviewers don’t need accounts. They need:

  1. Access to one video project
  2. Permission to comment (or just view)
  3. A session that expires in a few days

Why build accounts for that?

Architecture: The Three Layers

Our guest access stack:

Layer 1: Token Generation (Server-side)

Layer 2: Token Validation (API Gateway)

Layer 3: Permission Enforcement (Service Level)

Layer 1: Token Generation

When a project owner wants to invite external reviewers, they click “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}`;
  }
}

Key properties:

Layer 2: Token Validation (API Gateway)

Every request with a guest token hits the API gateway:

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;
    }
  }
}

Why Redis for revocation? Allows instant token revocation without waiting for JWT expiration.

Layer 3: Permission Enforcement

Each service enforces the guest’s permissions:

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;
  }
}

Permission Model

Guests get role-based permissions. Typical roles:

RolePermissionsUse Case
ViewerviewStakeholder review (read-only)
Commenterview, commentClient feedback (can comment but not resolve)
Approverview, comment, resolveCreative director (full review authority)

Each permission is enforced at the service level.

Real-Time Revocation

What if you need to revoke access immediately (e.g., client relationship ended)?

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' };
  }
}

Result: Guest loses access within seconds. No session cleanup needed.

URL Encoding & Security

Guest links are shared via email, Slack, text. They need to be:

  1. Short (shareable)
  2. Secure (no tampering)
  3. Expiring (limited lifetime)

We use:

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

Security note: We strongly recommend HTTPS-only links. Tokens are bearer tokens; they should never travel over HTTP.

Analytics: Tracking Guest Access

Guests are anonymous by default, but we track their activity for analytics:

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;

This helps project owners understand: “How many external reviewers? How engaged are they?”

Lessons Learned

  1. Tokens > Passwords for one-time access. Passwords imply ongoing identity; tokens imply temporary permission.

  2. Expiration is security: A token that expires in 7 days is safer than one that expires in 1 year. Balance convenience vs. security.

  3. Redis revocation is instant: Don’t rely on JWT expiration alone for sensitive projects. Always have a revocation layer.

  4. Scope is critical: A guest token should only work for one project, one IP range (optional), one user agent (optional). Overly broad tokens are security risks.

  5. Audit logging: Track all guest access. If a security incident happens, you can see exactly what guests accessed.

Guest access transformed YouViCo from “account friction” to “one click to collaborate.” Adoption increased 3x when we launched this feature.

Ready to streamline your video collaboration?

Get started for free