immich-app/immich · error · UnauthorizedException

Invalid share slug

Error message

Invalid share slug

What it means

Thrown by AuthService.validateSharedLinkSlug when a share link cannot be resolved or is no longer usable. The slug is looked up via sharedLinkRepository.getBySlug, then isValidSharedLink asserts the link exists, has a non-null user, and has not passed its expiresAt. If any of those fail the request is treated as unauthenticated.

Source

Thrown at server/src/services/auth.service.ts:511

  async validateSharedLinkKey(key: string | string[]): Promise<AuthDto> {
    key = Array.isArray(key) ? key[0] : key;

    const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
    const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
    if (!this.isValidSharedLink(sharedLink)) {
      throw new UnauthorizedException('Invalid share key');
    }

    return { user: sharedLink.user, sharedLink };
  }

  async validateSharedLinkSlug(slug: string | string[]): Promise<AuthDto> {
    slug = Array.isArray(slug) ? slug[0] : slug;

    const sharedLink = await this.sharedLinkRepository.getBySlug(slug);
    if (!this.isValidSharedLink(sharedLink)) {
      throw new UnauthorizedException('Invalid share slug');
    }

    return { user: sharedLink.user, sharedLink };
  }

  private isValidSharedLink(
    sharedLink?: AuthSharedLink & { user: AuthUser | null },
  ): sharedLink is AuthSharedLink & { user: AuthUser } {
    return !!sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date());
  }

  private async validateApiKey(key: string): Promise<AuthDto> {
    const hashed = this.cryptoRepository.hashSha256(key);
    const apiKey = await this.apiKeyRepository.getKey(hashed);
    if (apiKey?.user) {
      return {
        user: apiKey.user,
        apiKey,

View on GitHub (pinned to 199723261c)

Solutions

  1. Verify the exact slug value against what is stored/generated by the server (no extra slashes, query params, or whitespace).
  2. Check whether the shared link has expired (expiresAt) and regenerate it if needed.
  3. Confirm the owning user still exists; recreate the share from a valid account if the user was deleted.
  4. Catch UnauthorizedException at the controller edge and surface a 401 with a 'link invalid or expired' message to the client.

Example fix

// before: blindly trust an unvalidated slug from a URL
const auth = await authService.validateSharedLinkSlug(req.query.slug);

// after: normalize + presence-check, then handle the auth failure
const raw = Array.isArray(req.query.slug) ? req.query.slug[0] : req.query.slug;
if (!raw || typeof raw !== 'string' || raw.trim().length === 0) {
  throw new UnauthorizedException('Missing share slug');
}
let auth: AuthDto;
try {
  auth = await authService.validateSharedLinkSlug(raw.trim());
} catch (e) {
  if (e instanceof UnauthorizedException) {
    throw new UnauthorizedException('Share link is invalid or expired');
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const slug = Array.isArray(req.query.slug) ? req.query.slug[0] : req.query.slug;
if (!slug || typeof slug !== 'string' || slug.trim().length === 0) {
  throw new UnauthorizedException('A share slug is required');
}

Type guard

function isShareSlug(v: unknown): v is string {
  return typeof v === 'string' && v.trim().length > 0 && !/[/\\]/.test(v);
}

Try / catch

try {
  const auth = await authService.validateSharedLinkSlug(slug);
} catch (e) {
  if (e instanceof UnauthorizedException) {
    // 401 to client: link unknown, expired, or orphaned
    throw new UnauthorizedException('Share link is invalid or expired');
  }
  throw e;
}

Prevention

When it happens

Trigger: A request authenticated by a share-link slug (e.g. a public-album / shared-asset route that accepts ?slug= or a path segment) where the slug is unknown, was deleted, belongs to a link whose user was removed, or whose expiresAt is in the past.

Common situations: Stale bookmarks to a shared album whose link was regenerated or expired; copy-paste of a slug with a trailing space or missing segment; the owning user account was deleted leaving sharedLink.user null; clock skew where a link intended to be live is treated as expired.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/3334301cb923d4df. Report an issue: GitHub.