{"record":{"id":"3dfa9207a9f1698e","repo":"immich-app/immich","slug":"forbidden-3dfa92","errorCode":null,"errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"http","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"server/src/services/shared-link.service.ts","lineNumber":29,"sourceCode":"  SharedLinkResponseDto,\n  SharedLinkSearchDto,\n} from 'src/dtos/shared-link.dto';\nimport { Permission, SharedLinkType } from 'src/enum';\nimport { BaseService } from 'src/services/base.service';\nimport { findOrFail, getExternalDomain, OpenGraphTags } from 'src/utils/misc';\n\n@Injectable()\nexport class SharedLinkService extends BaseService {\n  async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {\n    return this.sharedLinkRepository\n      .getAll({ userId: auth.user.id, id, albumId })\n\n      .then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));\n  }\n\n  async login(auth: AuthDto, dto: SharedLinkLoginDto) {\n    if (!auth.sharedLink) {\n      throw new ForbiddenException();\n    }\n\n    const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);\n    const { id, password } = sharedLink;\n\n    if (!password) {\n      throw new BadRequestException('Shared link is not password protected');\n    }\n\n    if (password !== dto.password) {\n      throw new UnauthorizedException('Invalid password');\n    }\n\n    return {\n      sharedLink: mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }),\n      token: this.asToken({ id, password }),\n    };\n  }","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/shared-link.service.ts#L11-L47","documentation":"SharedLinkService.login() requires that the incoming request carry a shared-link context (auth.sharedLink), normally supplied via the x-immich-share-key or shared-link token header. Without it, a bare ForbiddenException (HTTP 403) is thrown at shared-link.service.ts:29 before any password logic runs.","triggerScenarios":"Calling the shared-link login endpoint (auth flow for a password-protected shared link) without identifying which shared link you are logging into — i.e. missing the shared-link token/key header.","commonSituations":"Frontend routing directly to the password prompt without first resolving the share key, a reverse proxy stripping custom share headers, or a client using the wrong auth header for public shared-link access.","solutions":["Attach the shared-link identification header (x-immich-share-key / the share token) so auth.sharedLink is populated before calling login.","Ensure the public/shared-link auth middleware runs on this route and isn't bypassed.","Verify any reverse proxy forwards custom x-immich-* headers unchanged."],"exampleFix":"// before\nfetch('/shared-links/me/login', { body: { password } });\n// after\nfetch('/shared-links/me/login', {\n  headers: { 'x-immich-share-key': sharedLinkKey },\n  body: { password },\n});","handlingStrategy":"validation","validationCode":"if (!shareKey) {\n  throw new Error('Shared-link key header is required to log in.');\n}\nawait sharedLinkApi.login({ shareKey, password });","typeGuard":"const hasShareContext = (auth: AuthDto): boolean => !!auth.sharedLink;","tryCatchPattern":"try {\n  await sharedLinkApi.login(dto);\n} catch (e) {\n  if (e instanceof ForbiddenException) {\n    promptForShareKey();\n  } else throw e;\n}","preventionTips":["Always send the share identification header on public shared-link endpoints.","Ensure reverse proxies forward x-immich-* headers."],"tags":["shared-link","auth","forbidden"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}