{"record":{"id":"aa6a247f3df70711","repo":"immich-app/immich","slug":"unexpected-profile-response-no-sub","errorCode":null,"errorMessage":"Unexpected profile response, no `sub`","messagePattern":"Unexpected profile response, no `sub`","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/repositories/oauth.repository.ts","lineNumber":104,"sourceCode":"    codeVerifier: string,\n  ): Promise<{ profile: OAuthProfile; sid?: string; idToken?: string }> {\n    const client = await this.getClient(config);\n    const pkceCodeVerifier = client.serverMetadata().supportsPKCE() ? codeVerifier : undefined;\n\n    try {\n      const tokens = await authorizationCodeGrant(client, new URL(url), { expectedState, pkceCodeVerifier });\n\n      let profile: OAuthProfile;\n      const tokenClaims = tokens.claims();\n      if (tokenClaims && 'email' in tokenClaims) {\n        this.logger.debug('Using ID token claims instead of userinfo endpoint');\n        profile = tokenClaims as OAuthProfile;\n      } else {\n        profile = await fetchUserInfo(client, tokens.access_token, skipSubjectCheck);\n      }\n\n      if (!profile.sub) {\n        throw new Error('Unexpected profile response, no `sub`');\n      }\n\n      let sid: string | undefined;\n      if (tokens.id_token) {\n        const claims = tokens.claims();\n        if (typeof claims?.sid === 'string') {\n          sid = claims.sid;\n        }\n      }\n\n      return { profile, sid, idToken: tokens.id_token };\n    } catch (error: Error | any) {\n      if (error.message.includes('unexpected JWT alg received')) {\n        this.logger.warn(\n          [\n            'Algorithm mismatch. Make sure the signing algorithm is set correctly in the OAuth settings.',\n            'Or, that you have specified a signing key in your OAuth provider.',\n          ].join(' '),","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/repositories/oauth.repository.ts#L86-L122","documentation":"After resolving the OIDC profile (either directly from ID token claims or by fetching the userinfo endpoint), getProfileAndOAuthSid requires the standard `sub` (subject) claim to identify the user. If the profile object lacks `sub`, the response cannot be trusted/mapped to a user, so it throws. This guards against malformed or non-conformant OIDC providers.","triggerScenarios":"OAuth callback calls getProfileAndOAuthSid(); token claims or the userinfo endpoint return a profile object whose `sub` field is missing, null, or empty; a non-standard provider (e.g. misconfigured scopes omitting the subject) is used.","commonSituations":"Provider userinfo endpoint returns an error-shaped or partial payload (e.g. only email when scope excludes openid); custom OAuth2 (non-OIDC) provider that does not issue a `sub` claim; scope misconfiguration dropping required claims; provider API version change altering the response shape.","solutions":["Ensure the OAuth client requests the `openid` scope so an ID token with `sub` is issued","Check the provider's userinfo response includes `sub` (test with curl against the userinfo endpoint)","If using a non-OIDC provider, map its unique user identifier to `sub` before use","Verify the userinfo fetch (skipSubjectCheck path) targets the correct userinfo_endpoint from discovery"],"exampleFix":"// before\n// scope: 'email profile'\n// after\n// scope: 'openid email profile'  // ensures ID token with `sub`","handlingStrategy":"try-catch","validationCode":"const res = await fetch(userInfoEndpoint, { headers: { Authorization: `Bearer ${accessToken}` } });\nconst profile = await res.json();\nif (!profile?.sub) throw new Error(`Provider userinfo lacks 'sub': ${JSON.stringify(profile).slice(0, 200)}`);","typeGuard":"const isOAuthProfile = (p: unknown): p is { sub: string } =>\n  typeof p === 'object' && p !== null && 'sub' in p && typeof (p as any).sub === 'string' && (p as any).sub.length > 0;","tryCatchPattern":"try {\n  const { profile } = await oauthRepo.getProfileAndOAuthSid(/* ... */);\n  loginUser(profile);\n} catch (e) {\n  if (e.message.includes('no `sub`')) {\n    logger.error('OIDC provider did not return a subject claim; check openid scope / provider config', e);\n    redirectToErrorPage('login-provider-misconfigured');\n  } else throw e;\n}","preventionTips":["Always include the `openid` scope in the authorization request","Prefer ID-token claims over userinfo when both are available","Test new providers against the userinfo endpoint before wiring them in","Validate the discovery metadata advertises a userinfo_endpoint"],"tags":["oauth","oidc","profile","validation"],"backgroundTag":"unexpected-response-shape","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}