immich-app/immich · error

Unexpected profile response, no `sub`

Error message

Unexpected profile response, no `sub`

What it means

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.

Solutions

  1. Ensure the OAuth client requests the `openid` scope so an ID token with `sub` is issued
  2. Check the provider's userinfo response includes `sub` (test with curl against the userinfo endpoint)
  3. If using a non-OIDC provider, map its unique user identifier to `sub` before use
  4. Verify the userinfo fetch (skipSubjectCheck path) targets the correct userinfo_endpoint from discovery

Example fix

// before
// scope: 'email profile'
// after
// scope: 'openid email profile'  // ensures ID token with `sub`
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(userInfoEndpoint, { headers: { Authorization: `Bearer ${accessToken}` } });
const profile = await res.json();
if (!profile?.sub) throw new Error(`Provider userinfo lacks 'sub': ${JSON.stringify(profile).slice(0, 200)}`);

Type guard

const isOAuthProfile = (p: unknown): p is { sub: string } =>
  typeof p === 'object' && p !== null && 'sub' in p && typeof (p as any).sub === 'string' && (p as any).sub.length > 0;

Try / catch

try {
  const { profile } = await oauthRepo.getProfileAndOAuthSid(/* ... */);
  loginUser(profile);
} catch (e) {
  if (e.message.includes('no `sub`')) {
    logger.error('OIDC provider did not return a subject claim; check openid scope / provider config', e);
    redirectToErrorPage('login-provider-misconfigured');
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/aa6a247f3df70711. Report an issue: GitHub.

Appendix: source

Thrown at server/src/repositories/oauth.repository.ts:104

    codeVerifier: string,
  ): Promise<{ profile: OAuthProfile; sid?: string; idToken?: string }> {
    const client = await this.getClient(config);
    const pkceCodeVerifier = client.serverMetadata().supportsPKCE() ? codeVerifier : undefined;

    try {
      const tokens = await authorizationCodeGrant(client, new URL(url), { expectedState, pkceCodeVerifier });

      let profile: OAuthProfile;
      const tokenClaims = tokens.claims();
      if (tokenClaims && 'email' in tokenClaims) {
        this.logger.debug('Using ID token claims instead of userinfo endpoint');
        profile = tokenClaims as OAuthProfile;
      } else {
        profile = await fetchUserInfo(client, tokens.access_token, skipSubjectCheck);
      }

      if (!profile.sub) {
        throw new Error('Unexpected profile response, no `sub`');
      }

      let sid: string | undefined;
      if (tokens.id_token) {
        const claims = tokens.claims();
        if (typeof claims?.sid === 'string') {
          sid = claims.sid;
        }
      }

      return { profile, sid, idToken: tokens.id_token };
    } catch (error: Error | any) {
      if (error.message.includes('unexpected JWT alg received')) {
        this.logger.warn(
          [
            'Algorithm mismatch. Make sure the signing algorithm is set correctly in the OAuth settings.',
            'Or, that you have specified a signing key in your OAuth provider.',
          ].join(' '),

View on GitHub (pinned to e55ac299a4)