immich-app / immich

High performance self-hosted photo and video management solution.
https://immich.app
GNU Affero General Public License v3.0
47.33k stars 2.4k forks source link

Facebook OAuth login "email" property not recognized #10928

Closed qrkourier closed 2 months ago

qrkourier commented 3 months ago

The bug

I'll use this issue to track progress investigating why Facebook social login did not work. I used Auth0's dev keys and picked the optional "email" scope when creating the Facebook social connection for my Auth0 application. The same Auth0 application is functioning for Google, Microsoft, Apple, and Amazon OAuth.

The OS that Immich Server is running on

Debian GNU/Linux 12 (bookworm)

Version of Immich Server

v1.107.2

Version of Immich Mobile App

NA

Platform with the issue

Your docker-compose.yml content

#
# WARNING: Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
#

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ${IMMICH_IMAGE:-ghcr.io/immich-app/immich-server}:${IMMICH_VERSION:-release}
    # extends:
    #   file: hwaccel.transcoding.yml
    #   service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
    volumes:
      - /mnt/ssd/immich-content:/usr/src/app/upload
      - /mnt/hdd/SyncThing/FamilyShare:/mnt/FamilyShare
      - /mnt/hdd/SyncThing/Pictures:/mnt/Pictures
      - /mnt/hdd/SyncThing/Videos:/mnt/Videos
      - /etc/localtime:/etc/localtime:ro
    labels:
      restic-compose-backup.volumes: true
      restic-compose-backup.volumes.include: "immich-content"
    env_file:
      - .env
    ports:
      - 2283:3001
    depends_on:
      - redis
      - database
    restart: always

  immich-machine-learning:
    container_name: immich_machine_learning
    # For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
    # Example tag: ${IMMICH_VERSION:-release}-cuda
    image: ${IMMICH_IMAGE:-ghcr.io/immich-app/immich-machine-learning}:${IMMICH_VERSION:-release}
    # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
    #   file: hwaccel.ml.yml
    #   service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always

  redis:
    container_name: immich_redis
    image: docker.io/redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - postgres:/var/lib/postgresql/data
    labels:
      restic-compose-backup.postgres: true
    healthcheck:
      test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
      interval: 5m
      start_interval: 30s
      start_period: 5m
    command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
    restart: always

  backup:
    image: docker.io/qrkourier/restic-compose-backup
    env_file:
      - .env.backup
    volumes:
      # Map in docker socket
      - /var/run/docker.sock:/var/run/docker.sock
      # Map restic cache
      - restic_cache:/cache

volumes:
  model-cache:
  postgres:
  library:
  restic_cache:

Your .env content

# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables

# The location where your uploaded files are stored
# UPLOAD_LOCATION=library
# The location where your database files are stored
# DB_DATA_LOCATION=postgres

# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
# TZ=Etc/UTC

# The Immich version to use. You can pin this to a specific version like "v1.71.0"
IMMICH_VERSION="v1.107.2"

# Connection secret for postgres. You should change it to a random password
DB_PASSWORD="********************************"

# The values below this line do not need to be changed
###################################################################################
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

IMMICH_API_KEY="********************************"

Reproduction steps

1. In Auth0, create an application for Immich
1. In Auth0, add Facebook social connection, pick "email" to include in scope, and enable the Immich application
1. In Immich, enable OAuth for the Auth0 application
1. Attempt log in to Immich with OAuth, picking Facebook from the Auth0 consent screen

Relevant log output

immich_server            |     at async OAuthController.finishOAuth (/usr/src/app/dist/controllers/oauth.controller.js:39:22)~789fpyso] Failed to finish oauth
immich_server            | [Nest] 17  - 07/06/2024, 1:30:21 PM   ERROR [Api:QueryFailedError: null value in column "email" of relation "users" violates not-null constraint
immich_server            |     at PostgresQueryRunner.query (/usr/src/app/node_modules/typeorm/driver/postgres/PostgresQueryRunner.js:219:19)
immich_server            |     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
immich_server            |     at async InsertQueryBuilder.execute (/usr/src/app/node_modules/typeorm/query-builder/InsertQueryBuilder.js:106:33)
immich_server            |     at async SubjectExecutor.executeInsertOperations (/usr/src/app/node_modules/typeorm/persistence/SubjectExecutor.js:260:42)
immich_server            |     at async SubjectExecutor.execute (/usr/src/app/node_modules/typeorm/persistence/SubjectExecutor.js:92:9)
immich_server            |     at async EntityPersistExecutor.execute (/usr/src/app/node_modules/typeorm/persistence/EntityPersistExecutor.js:140:21)
immich_server            |     at async UserRepository.save (/usr/src/app/dist/repositories/user.repository.js:135:24)
immich_server            |     at async AuthService.callback (/usr/src/app/dist/services/auth.service.js:184:20)
immich_server            |     at async OAuthController.finishOAuth (/usr/src/app/dist/controllers/oauth.controller.js:39:22)~789fpyso] QueryFailedError: null value in column "email" of relation "users" violates not-null constraint

Additional information

The login flow includes a request to https://www.facebook.com/dialog/oauth with query param scope: email,public_profile and I believe this confirms that Auth0 is correctly requesting access to the FB account's primary email address, and I believe this is essential for Immich to link Immich accounts to OAuth accounts.

I confirmed the problem with FB OAuth is not caused by the FB account email matching an existing OAuth-linked Immich account, and I understand an Immich account can only be linked to a single OAuth account.

I did not test with my own FB Developer Account that I created today, and I determined that I will need a separate public Immich instance configured exclusively for FB OAuth to obtain a FB app review, which is several more steps than I can do today.

It's possible this problem is unique to Auth0 as transitive IdP on behalf of FB, but may also be affecting all FB OAuth login flows for Immich. That's probably the first question to answer as we investigate.

### Tasks
michelheusschen commented 3 months ago

Immich requires an email address, but it looks like facebook doesn't include that in the response. If you set the log level to debug, you should see the userinfo response to verify that https://github.com/immich-app/immich/blob/59cdbdc4924668314d7faac7fdacaa4685600874/server/src/services/auth.service.ts#L194-L195

bo0tzz commented 3 months ago

Maybe we should check and error out if profile.email is empty?

qrkourier commented 3 months ago
immich_server  | [Nest] 17  - 07/07/2024, 12:03:15 PM   DEBUG [Api:AuthService~jylssnym] Logging in with OAuth: {"sub":"facebook|10161652594816163","given_name":"Kenneth","family_name":"Bingham","nickname":"Kenneth Bingham","name":"Kenneth Bingham","picture":"https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=10161652594816163&height=50&width=50&ext=1722945790&hash=AbZEp7pd34gCHSxUsRz-iaex","updated_at":"2024-07-07T12:03:10.919Z","email_verified":true}
immich_server  | [Nest] 17  - 07/07/2024, 12:03:15 PM     LOG [Api:AuthService~jylssnym] Registering new user: undefined/facebook|10161652594816163
immich_server  | [Nest] 17  - 07/07/2024, 12:03:15 PM   ERROR [Api:QueryFailedError: null value in column "email" of relation "users" violates not-null constraint

The log above contains this JSON:

{
  "sub": "facebook|10161652594816163",
  "given_name": "Kenneth",
  "family_name": "Bingham",
  "nickname": "Kenneth Bingham",
  "name": "Kenneth Bingham",
  "picture": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=10161652594816163&height=50&width=50&ext=1722945790&hash=AbZEp7pd34gCHSxUsRz-iaex",
  "updated_at": "2024-07-07T12:03:10.919Z",
  "email_verified": true
}

This confirms that:

  1. Immich expects the callback to contain "email"
  2. Auth0's social connection to FB with optional "email" scope configured, possibly all FB OAuth, doesn't provide "email"

While it's probably best for Immich to explicitly report an empty "email" property before attempting to create the user in the database, I'd also like to get FB login working. Still, the FB developer process appears prohibitive in comparison to my motivation to enable FB login for Immich, so I'll likely disable it for myself if this isn't a relatively convenient fix.

I stumbled around fruitlessly in Meta's developer documentation for a while. I never found a specification for a web-initiated OpenID Connect callback, or how to enclose the verified email address. ChatGPT suggests that FB's OAuth2 isn't fully OIDC-compliant, and that it's necessary to exchange a "code" for a "token" with which to query Meta's GraphQL API for the additional, allowed fields from the scope, including "email."

If that's accurate, then we'd need to determine whether Immich or Auth0 would need to handle that GraphQL query.

qrkourier commented 3 months ago

I recreated the Facebook social connection in Auth0 with the mandatory "email" scope to ensure that any Auth0 users were deleted and re-created with the expected scope.

Then, I found supporting evidence in Auth0 docs for ChatGPT's claim that it's necessary to make a follow-up API request to obtain the Facebook user profile info.

Access Facebook's API Once a user successfully authenticates, Facebook will include an Access Token in the user profile it returns to Auth0. You can use this token to call Facebook's API.

To get the Facebook Access Token, you must retrieve the full user's profile using the Auth0 Management API and extract the Access Token from the response. For detailed steps, see Call an Identity Provider's API.

This documentation points toward the following cause for empty email: Auth0 obscures the full profile from the application, citing "security" reasons, so it would be necessary for Immich to specifically implement Auth0's mgmt API to fetch the Facebook access token (JWT?) and parse out the missing profile info.

Based on this, I don't expect the problem will manifest when Facebook OAuth is configured directly in Immich, only when using Auth0 as an intermediary/transitive IdP with certain providers, including Facebook and GitHub, at least.

michelheusschen commented 2 months ago

I followed your reproduction steps, but also created an app on Facebook and it somehow works fine for me. The JSON also includes the email property:

{
  "sub": "",
  "given_name": "",
  "family_name": "",
  "nickname": "",
  "name": "",
  "picture": "",
  "updated_at": "",
  "email": "",
  "email_verified": true
}
qrkourier commented 2 months ago

That's encouraging. Thank you. I'm still stumped. Here's what I've tried.

I verified the "email" scope is enabled in the Auth0 social connection for Facebook. Still, when I authenticate with Facebook using a test account, the "email" scope is not requested, only "Name and profile picture."

There may be something different about my Facebook privacy settings and there may be a configuration problem with my Auth0 tenant or the social connection settings for Facebook.

I tried setting my Facebook email visibility to public in my main account and a test account. The "email" scope is never requested in the Facebook consent screen.

I created a new Auth0 tenant which also provides a pristine Facebook social connector. Still, with either my regular or test Facebook account, the "email" scope is never requested in the consent screen presented by Facebook to the user confirming they wish to share "Name and profile picture" with the Auth0 app. Additionally, the callback never contains the "email" property, which is expected in light of it having not been requested.

To reproduce your success, I may need to create everything fresh including a Facebook account, Auth0 tenant w/ FB connection, and Immich server.

A little digging yields more evidence that others have encountered this issue with FB not returning the email via Auth0. Auth0 recommends apps like Immich implement "progressive profiling" to collect missing information after authentication, which appears to be their term for the API interactions I was describing in an earlier post to this thread to obtain the FB token from Auth0 for querying FB to obtain the full profile object.

I hope this information assists the next person. I'm not sure if it's important enough for me to investigate FB via Auth0 specifically for Immich, so I'll close the issue.