umap-project / umap

uMap lets you create maps with OpenStreetMap layers in a minute and embed them in your site.
https://umap-project.org
Other
1.18k stars 226 forks source link

Login session not working with third-party openid #1767

Closed terion-name closed 6 months ago

terion-name commented 6 months ago

Describe the bug Trying to setup umap with third-party OIDC provider.

Installation:

version: '3'

services:
  db:
    healthcheck:
        test: ["CMD-SHELL", "pg_isready -U postgres"]
        interval: 2s
    image: postgis/postgis:14-3.3-alpine
    environment:
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-secret}
      - PGUSER=${POSTGRES_USER:-postgres}
      - POSTGRES_DB=${POSTGRES_DB:-umap}
    volumes:
      - ./data/db:/var/lib/postgresql/data

  app:
    depends_on:
      migrate:
        condition: service_completed_successfully
    image: umap/umap:2.1.3
    ports:
      - "${PORT-8001}:8000"
    environment: &app_env
      - DATABASE_URL=postgis://${POSTGRES_USER:-postgres}@db:5432/${POSTGRES_DB:-umap}
      - POSTGRES_HOST=${POSTGRES_HOST:-db}
      - POSTGRES_DB=${POSTGRES_DB:-umap}
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-secret}
      - SECRET_KEY=some-long-and-weirdly-unrandom-secret-key
      - SITE_URL=https://localhost:${PORT-8001}/
      - STATIC_ROOT=/srv/umap/static
      - MEDIA_ROOT=/srv/umap/uploads
      - UMAP_ALLOW_ANONYMOUS=True
      - DEBUG=1
      - UMAP_SETTINGS=/srv/umap/umap/local_settings.py
      - SOCIAL_AUTH_OPEN_ID_CONNECT_ENDPOINT_URL=${SOCIAL_AUTH_OPEN_ID_CONNECT_ENDPOINT_URL}
      - SOCIAL_AUTH_OPEN_ID_CONNECT_AUTHORIZATION_URL=${SOCIAL_AUTH_OPEN_ID_CONNECT_AUTHORIZATION_URL}
      - SOCIAL_AUTH_OAUTH_KEY=${SOCIAL_AUTH_OAUTH_KEY}
      - SOCIAL_AUTH_OAUTH_SECRET=${SOCIAL_AUTH_OAUTH_SECRET}
    volumes:
      - ./data/umap:/srv/umap/uploads
      - ./local_settings.py:/srv/umap/umap/local_settings.py

  migrate:
    depends_on:
      db:
        condition: service_healthy
    image: umap/umap:2.1.3
    environment: *app_env
    command: umap migrate && umap collectstatic
    volumes:
      - ./data/umap:/srv/umap/uploads
      - ./local_settings.py:/srv/umap/umap/local_settings.py

local_settings.py:

import environ
import os
import logging
from social_core.backends.open_id_connect import OpenIdConnectAuth
from social_core.utils import cache

env = environ.Env()

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        "HOST": os.environ.get("POSTGRES_HOST", "db"),
        "NAME": os.environ.get("POSTGRES_DB", "umap"),
        "USER": os.environ.get("POSTGRES_USER", "postgres"),
        "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "secret"),
        "PORT": 5432,
        "DISABLE_SERVER_SIDE_CURSORS": True,
    }
}

ALLOWED_HOSTS = ["*"]

logger = logging.getLogger(__name__)

# this is needed because generic OpenIdConnectAuth has no option for endpoint and is required to be extended as stated in docs
class EndpointOpenIdConnectAuth(OpenIdConnectAuth):
    OIDC_ENDPOINT = env('SOCIAL_AUTH_OPEN_ID_CONNECT_ENDPOINT_URL', default='')

    @cache(ttl=86400)
    def oidc_config(self):
        response =  self.get_json(self.OIDC_ENDPOINT +
                                  '/.well-known/openid-configuration')
        return response

OIDC_BACKEND_CLASS = EndpointOpenIdConnectAuth

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
)

SOCIAL_AUTH_AUTHORIZATION_URL = env(
    "SOCIAL_AUTH_OPEN_ID_CONNECT_AUTHORIZATION_URL", default=""
)

SOCIAL_AUTH_ENDPOINT_URL = env(
    "SOCIAL_AUTH_OPEN_ID_CONNECT_ENDPOINT_URL", default=""
)

SOCIAL_AUTH_KEY = env(
    "SOCIAL_AUTH_OAUTH_KEY", default=""
)

SOCIAL_AUTH_SECRET = env(
    "SOCIAL_AUTH_OAUTH_SECRET", default=""
)
if SOCIAL_AUTH_KEY and SOCIAL_AUTH_SECRET:
    AUTHENTICATION_BACKENDS += (
        "umap.settings.OIDC_BACKEND_CLASS",
    )

As the result:

  1. Provider appears on login screen
  2. Auth flow succedes
  3. User is created in database
  4. Social association is created in database
  5. Session is created in database
  6. Session cookie is set and contains correct session key

... But user is still not logged in. And it is weird, because everything looks fine and no errors in logs. If I enable generic auth — it works ok.

Decrypted session data of oidc-connected user is:

{
'oidc_state': 'sHOkEHBvuKsRR6PH0M87DZxYmCvn8GBn', 
'_auth_user_id': '1', 
'_auth_user_backend': 'config.EndpointOpenIdConnectAuth', 
'_auth_user_hash': 'd5dab....c5d6', 
'social_auth_last_login_backend': 'oidc'
}

To compare it with generic auth userdata:

{
'_auth_user_id': '2', 
'_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', 
'_auth_user_hash': '3007....64a8'
}

Looked for everything — no idea why

To Reproduce Take compose and settings above, add to .env vars for any third-party OIDC (okta, keycloak, authentik, etc), try to log in

Expected behavior User is logged in

Screenshots If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

yohanboniface commented 6 months ago

Weird! We use OIDC in the ANCT server, see our OIDC class here (overridden to be able to control some params):

https://github.com/umap-project/umap-dsfr/blob/main/umap_dsfr/moncomptepro.py

terion-name commented 6 months ago

@yohanboniface we've figured out already. To keep things simple and keep everything in one settings file to be supplied to docker container, this will work like this:

import environ, os
from django.utils.module_loading import import_string

env = environ.Env()

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        "HOST": os.environ.get("POSTGRES_HOST", "db"),
        "NAME": os.environ.get("POSTGRES_DB", "umap"),
        "USER": os.environ.get("POSTGRES_USER", "postgres"),
        "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "secret"),
        "PORT": 5432,
        "DISABLE_SERVER_SIDE_CURSORS": True,
    }
}

OPEN_ID_CONNECT_BACKEND = import_string('social_core.backends.open_id_connect.OpenIdConnectAuth')

SOCIAL_AUTH_KEY = env('SOCIAL_AUTH_KEY', default='')
SOCIAL_AUTH_SECRET = env('SOCIAL_AUTH_SECRET', default='')

OPEN_ID_CONNECT_BACKEND.OIDC_ENDPOINT = env('SOCIAL_AUTH_OPEN_ID_CONNECT_ENDPOINT_URL', default='')
OPEN_ID_CONNECT_BACKEND.ID_TOKEN_ISSUER = env('SOCIAL_AUTH_OPEN_ID_CONNECT_ID_TOKEN_ISSUER', default='')
OPEN_ID_CONNECT_BACKEND.ACCESS_TOKEN_URL = env('SOCIAL_AUTH_OPEN_ID_CONNECT_ACCESS_TOKEN_URL', default='')
OPEN_ID_CONNECT_BACKEND.AUTHORIZATION_URL = env('SOCIAL_AUTH_OPEN_ID_CONNECT_AUTHORIZATION_URL', default='')
OPEN_ID_CONNECT_BACKEND.REVOKE_TOKEN_URL = env('SOCIAL_AUTH_OPEN_ID_CONNECT_REVOKE_TOKEN_URL', default='')
OPEN_ID_CONNECT_BACKEND.USERINFO_URL = env('SOCIAL_AUTH_OPEN_ID_CONNECT_USERINFO_URL', default='')
OPEN_ID_CONNECT_BACKEND.JWKS_URI = env('SOCIAL_AUTH_OPEN_ID_CONNECT_JWKS_URI', default='')

SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
    'social_core.backends.open_id_connect.OpenIdConnectAuth',
)

AUTHENTICATION_BACKENDS = (
    'social_core.backends.open_id_connect.OpenIdConnectAuth',
    'django.contrib.auth.backends.ModelBackend',
)
yohanboniface commented 6 months ago

Thanks for providing the working settings, it may help someone else!