Przejdź do treści
Carrotly Technologies

Zabezpieczanie CouchDB za pomocą Keycloak za reverse proxy Nginx

Single Sign-On dla CouchDB z Keycloak jako dostawcą tożsamości i Nginx jako reverse proxy — zarządzanie użytkownikami przeniesione z bazy do porządnego IAM.

Tomasz Kisiel 16 min czytania
Zabezpieczanie CouchDB za pomocą Keycloak za reverse proxy Nginx

CouchDB to solidna baza dokumentowa, ale jej wbudowane uwierzytelnianie często nie wystarcza w projektach, które korzystają już z centralnego dostawcy tożsamości. Ten artykuł pokazuje, jak zabezpieczyć instancję CouchDB przez Single Sign-On, z Keycloak jako dostawcą tożsamości i Nginx jako reverse proxy — tak, by całkowicie wyjąć zarządzanie użytkownikami z bazy danych i powierzyć je dedykowanemu rozwiązaniu IAM klasy produkcyjnej.

Wprowadzenie

Aby zasymulować środowisko produkcyjne, używamy Dockera i kontenerów Bitnami — szybko i łatwo je uruchomić. Architektura składa się z:

  • Keycloak — otwartoźródłowe rozwiązanie do zarządzania tożsamością i dostępem, z zarządzaniem użytkownikami i precyzyjną autoryzacją.
  • Keycloak Config CLI — narzędzie, które utrzymuje docelową konfigurację realmu na podstawie pliku JSON/YAML.
  • PostgreSQL — wydajna, otwartoźródłowa relacyjno-obiektowa baza danych, używana jako magazyn danych Keycloak.
  • CouchDB — otwartoźródłowa baza dokumentowa, do której dostęp zabezpieczymy protokołem OpenID Connect oferowanym przez Keycloak.
  • Nginx — serwer WWW, który działa też jako reverse proxy i load balancer. Technicznie użyjemy dystrybucji OpenResty z kompilatorem Lua just-in-time, ale będziemy nazywać go po prostu Nginx.

Poniższy diagram pokazuje, jak współdziałają poszczególne elementy.

Diagram architektury CouchDB, Keycloak i Nginx

Przygotowanie środowiska

Przejdźmy od razu do pliku docker-compose i omówmy usługi jedna po drugiej, zaczynając od Keycloak.

version: "3.9"

services:
  keycloak:
    image: "bitnami/keycloak:24.0.3"
    environment:
      KEYCLOAK_HTTP_PORT: "8080"
      KEYCLOAK_CREATE_ADMIN_USER: "true"
      KEYCLOAK_ADMIN: "admin"
      KEYCLOAK_PROXY: "edge"
      KEYCLOAK_ADMIN_PASSWORD: "admin"
      KEYCLOAK_DATABASE_HOST: "postgres"
      KEYCLOAK_DATABASE_USER: "postgres"
      KEYCLOAK_DATABASE_PASSWORD: "postgres"
      KEYCLOAK_DATABASE_NAME: "postgres"
      KEYCLOAK_DATABASE_PORT: "5432"
    depends_on:
      - "postgres"
    networks:
      - "cks-network"

networks:
  cks-network:
    driver: "bridge"

Bazujemy na jednym z kontenerów Bitnami — dostępne zmienne środowiskowe są opisane na ich GitHubie. Ustawiamy domyślne konto administratora i dane dostępowe do bazy. Ustawiamy też tryb proxy na “edge”, co oznacza, że Keycloak komunikuje się przez HTTP, a nie HTTPS. To w porządku, bo SSL obsłuży za nas reverse proxy Nginx.

Kontener zależy od PostgreSQL, z którego Keycloak korzysta jako magazynu danych, i należy do sieci cks-network — tak jak każda kolejna usługa.

Dalej mamy Keycloak Config CLI.

services:
  keycloak-config-cli:
    image: "bitnami/keycloak-config-cli:5.12.0"
    environment:
      KEYCLOAK_URL: "http://keycloak:8080"
      KEYCLOAK_USER: "admin"
      KEYCLOAK_PASSWORD: "admin"
      IMPORT_FILES_LOCATIONS: "/config/*"
    depends_on:
      - "keycloak"
    volumes:
      - "./keycloak/master.yaml:/config/master.yaml"
    networks:
      - "cks-network"

Znów kontener Bitnami. Ustawiamy zmienne dostępu do Keycloak i katalog z konfiguracją, podpinamy go do naszej sieci i uzależniamy od Keycloak. Montujemy też wolumin z plikiem YAML z konfiguracją realmu, który napiszemy w dalszej części artykułu.

Następnie PostgreSQL, który istnieje tu wyłącznie na potrzeby Keycloak.

services:
  postgres:
    image: "bitnami/postgresql:15.6.0"
    environment:
      POSTGRESQL_USERNAME: "postgres"
      POSTGRESQL_PASSWORD: "postgres"
      POSTGRESQL_DATABASE: "postgres"
    volumes:
      - "cks-postgres-data:/bitnami/postgresql"
    networks:
      - "cks-network"

volumes:
  cks-postgres-data:
    driver: "local"

Proste: kontener Bitnami z domyślnymi danymi dostępowymi, wolumin chroniący dane przy restarcie i ta sama sieć co wcześniej.

Mając pierwszą bazę, czas na drugą — CouchDB.

services:
  couchdb:
    image: "bitnami/couchdb:3.3.3"
    environment:
      COUCHDB_USER: "admin"
      COUCHDB_PASSWORD: "admin"
      COUCHDB_SECRET: "top-secret"
      COUCHDB_BIND_ADDRESS: "0.0.0.0"
      COUCHDB_PORT_NUMBER: "5984"
    volumes:
      - "cks-couchdb-data:/bitnami/couchdb"
      - "./couchdb/10-config.ini:/opt/bitnami/couchdb/etc/local.d/10-config.ini"
    networks:
      - "cks-network"

volumes:
  cks-couchdb-data:
    driver: "local"

Kolejny kontener Bitnami. W zmiennych środowiskowych są domyślne dane administratora, sekret do szyfrowania ciasteczek i konfiguracja startowa. Mamy trwały wolumin na dane i drugi z plikiem konfiguracyjnym, który szczegółowo omówimy później.

Na koniec Nginx — nasze reverse proxy.

services:
  nginx:
    build: "./nginx"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "./nginx/certs:/opt/bitnami/openresty/nginx/conf/bitnami/certs:ro"
      - "./nginx/server_blocks:/opt/bitnami/openresty/nginx/conf/server_blocks:ro"
      - "./nginx/lua:/opt/bitnami/openresty/lua:ro"
    depends_on:
      - "couchdb"
      - "keycloak"
    networks:
      - "cks-network"

Ta usługa również korzysta z obrazu Bitnami, ale doinstalujemy do niej kilka pakietów — o tym w osobnej sekcji. Nginx jako jedyny wystawia porty, zależy od CouchDB i Keycloak i dzieli sieć z pozostałymi kontenerami. Woluminy zawierają certyfikaty SSL, definicje bloków server i skrypty Lua.

Infrastruktura gotowa — możemy skonfigurować poszczególne usługi.

Konfiguracja Keycloak

Cały realm moglibyśmy skonfigurować ręcznie w konsoli administracyjnej, ale trudno to udokumentować — zrzutów ekranu jest mnóstwo, a interfejs się zmienia. Keycloak Config CLI pozwala trzymać konfigurację w pliku YAML pod kontrolą wersji, łatwym do pokazania i odtworzenia.

Nasza aplikacja ma jeden realm o nazwie “master”. Początkowa konfiguracja wygląda tak:

realm: "master"
attributes:
  frontendUrl: "https://auth.oblivio.localhost"

Definiuje ona nazwę realmu i adres frontendu. Później dodamy blok server w Nginx, który przekieruje tę subdomenę do kontenera Keycloak. Nazwa “oblivio” nie ma szczególnego znaczenia — to po prostu robocza nazwa projektu.

Dalej grupy. Mamy jedną główną grupę dla użytkowników CouchDB z dwiema podgrupami: administratorów i zwykłych użytkowników. Później mapper atrybutów doda odpowiednie role do tokena dostępowego, aby CouchDB mógł rozpoznać role użytkownika.

groups:
  - name: "couchdb"
    path: "/couchdb"
    subGroups:
      - name: "admins"
        path: "/couchdb/admins"
        attributes:
          _couchdb.roles:
            - "_admin"
      - name: "users"
        path: "/couchdb/users"
        attributes:
          _couchdb.roles:
            - "_user"

Atrybut nazywa się _couchdb.roles — to domyślna właściwość, z której CouchDB odczytuje role z tokena dostępowego, choć w razie potrzeby można ją zmienić.

Następnie konfiguracja klienta. Mamy jednego klienta, którego Nginx użyje do autoryzacji dostępu do CouchDB.

clients:
  - clientId: "couchdb-proxy"
    name: "CouchDB Proxy"
    publicClient: "false"
    clientAuthenticatorType: "client-secret"
    secret: "32scbZbgGNSaVOAAuZHgYeTjdQrkfwTh"
    redirectUris:
      - "https://couchdb.oblivio.localhost/*"
    standardFlowEnabled: "true"
    directAccessGrantsEnabled: "false"
    optionalClientScopes:
      - "couchdb"
      - "profile"
      - "email"

Klient jest poufny i ma sekret, który Nginx może bezpiecznie przechowywać. Dopuszcza wyłącznie przepływ authorization code z OAuth2 i tylko adresy przekierowań w subdomenie, pod którą będzie dostępny CouchDB.

Klient ma trzy opcjonalne zakresy. Email i profile są dostępne w Keycloak domyślnie, a zakres CouchDB jest własny i definiujemy go tak:

clientScopes:
  - name: "couchdb"
    description: "CouchDB"
    protocol: "openid-connect"
    protocolMappers:
      - name: "couchdb-roles"
        protocol: "openid-connect"
        protocolMapper: "oidc-usermodel-attribute-mapper"
        config:
          user.attribute: "_couchdb.roles"
          claim.name: "_couchdb\\.roles"
          jsonType.label: "String"
          userinfo.token.claim: "true"
          access.token.claim: "true"
          id.token.claim: "false"
          multivalued: "true"
          aggregate.attrs: "true"

Zakres zawiera mapper protokołu OpenID Connect, który określa, gdzie trafi atrybut z grupy. Zwróć uwagę na dwa backslashe w nazwie claimu — bez nich kropka oznaczałaby, że “roles” to obiekt wewnątrz “_couchdb”, a nie o to nam chodzi. Ponieważ użytkownik może mieć kilka ról, claim jest wielowartościowy i agregowany, a na końcu wskazujemy, w których tokenach ma się pojawić.

Ostatni element konfiguracji to klucz RSA, którym podpisywane są generowane tokeny.

components:
  org.keycloak.keys.KeyProvider:
    - name: "rsa"
      providerId: "rsa"
      config:
        active: ["true"]
        enabled: ["true"]
        priority: ["1000"]
        algorithm: ["RS256"]
        kid: ["xvAsHaF2w0M1y9GG6bmFannhp9aFLKvHQRaAAb8gUYc"]
        privateKey: ["-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDM98i2/CFRiFYNlUtJ5ppUNyZUOa2+7SMnya3tzfrPOEVma6AJAMJ9YR2CL6SIkyz6q5RqnhQSXTzvPO9OasKuXLWtxpjVZRawCXoCciyaJTLe8qcmb6SOCOsjRSiGB1PaivJ/7NbCDiP6r8BxX4TXsYfdGW2EDBot+klxG6a+FObCA7KJ1bp/yPbgpP+mNyj7P8lG22E3USRjE3g8ag/J8b3UK+Azu1yBmdYAEPG1qz8q46tgF9qJiDo7QNDroRDLxoclypMsHJ3AIbJh5lquAl4uTALYMLI2foKJqXlc+JZ9tdTzxYg04R7SKuAcizdjZ2VccJNpGySGs5i8XguTAgMBAAECggEAOjpiQOmbpYfvumghPVtPmIEaWG8SVt0TUahPyvDrQZcg0BnfGu+mUOwX7/YM7eexrXy06x0BYr4uI2DSMxrNN6+KxVVX8beIHHZ0vOEmnpvWudOBfL/WpasO8bQh8QF/5uP2RDVKRVKzEfJ/3zVdjdEXYc5peEvf3BPwbTuHwRO4F69hgCZi8saBNXBinnOwQ8MSsUeA4RsC7+WcxygufBhNqjkqrYbpznkaZI5nrVdw7mb5E9KcOxbg0BUWnz141gPuUpu2O0iFiiAZoSlDtIwKCtdcvc2UJMYbXK9ORsIscRwP6b8T6O3Uhq1zkXQyjtLWbrfcpoNGGJ2udRFDEQKBgQD/UUVugyEOP98+S5e/2d2W+rk96HX7CKMQwj/ZA9NETugGAiD7fuUpbu3NzrAN7tsTXGOyArBEfdcXwjhvg52WhO9eOB/mcaiXEWVTk89ZrbcvZknDljKWM5zXOMvVZXAK6ci53jfVn5RLA2RIjK5mds1pLXIWgDHPrXFI7Nkb/wKBgQDNhA4/aaAyko8NiAWvk0hYZYQCFSH3YBl28lt90zzDjoZaQd/s8mpaRO4TY+KwFGBEznlFa28e1g0YzpZh13+V1ss4WT9NV/3tux2rblJWa2kmYbA/PeQDLHVfC/fq46T90uUW0wRIkc+nVTKG94Oo+tPUERjlmSJbzsjUDJLgbQKBgCXW9LRhSNfkzYBdEbuEXZwPwr6TIlE3QXutXmsabwhTrX2eeSbs8qfGYgY7mMon2V4wNjJexaMRB3zk8xpL5mI1h4huRwQPWk4xbNQLNxLydRDYVxxeuVabhaY8K7GP3CAx7+bkMWA+y2qmsQkzmHFlMCJjcuI0060U5pJJUBAfAoGALI30iMrdcBlV6hkTIn1Lsd5QQCNUucybuK3SJ/Ujt0Gu3uJpKXVkmS1Yb9u3yXShaklZATPJY2YEcNxYvd16S4HFjPHMR3hMFL38MK46K4IdybRkAVHpnMaGq5Rsqv+vRVfzUn9s7k6uNhjCW4BNitTWF6OdQilwyXaLE22magECgYAx1tGChvGQM3rYyJDA22ZNU4b+olc2bBJ0v45EX0unjGseuzPTKQRaGp8LqgByXcMuZqCCidsvlfrrz16hGHnsqPQFSV4ZL4D1pOKshmWhscLtF10FeC8z0QoJCNFaPuRkMCXyx+X+XjGsdfKRO/84z/7FfCI0t2QvvGWSnRpKUw==\n-----END PRIVATE KEY-----\n"]
        certificate: ["-----BEGIN CERTIFICATE-----\nMIIDhzCCAm+gAwIBAgIUVv9/pUd6omHb9BhGmfZb/jVPmz4wDQYJKoZIhvcNAQELBQAwUzELMAkGA1UEBhMCUEwxEzARBgNVBAgMClNvbWUtU3RhdGUxHTAbBgNVBAcMFEdyZWF0ZXIgUG9sYW5kV2Fyc2F3MRAwDgYDVQQKDAdPYmxpdmlvMB4XDTI0MDUxNzE0NTkxNloXDTI2MDUxNzE0NTkxNlowUzELMAkGA1UEBhMCUEwxEzARBgNVBAgMClNvbWUtU3RhdGUxHTAbBgNVBAcMFEdyZWF0ZXIgUG9sYW5kV2Fyc2F3MRAwDgYDVQQKDAdPYmxpdmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzPfItvwhUYhWDZVLSeaaVDcmVDmtvu0jJ8mt7c36zzhFZmugCQDCfWEdgi+kiJMs+quUap4UEl087zzvTmrCrly1rcaY1WUWsAl6AnIsmiUy3vKnJm+kjgjrI0UohgdT2oryf+zWwg4j+q/AcV+E17GH3RlthAwaLfpJcRumvhTmwgOyidW6f8j24KT/pjco+z/JRtthN1EkYxN4PGoPyfG91CvgM7tcgZnWABDxtas/KuOrYBfaiYg6O0DQ66EQy8aHJcqTLBydwCGyYeZargJeLkwC2DCyNn6Cial5XPiWfbXU88WINOEe0irgHIs3Y2dlXHCTaRskhrOYvF4LkwIDAQABo1MwUTAdBgNVHQ4EFgQU9poNBVHTqAYUxc5c4naQhd2kOOswHwYDVR0jBBgwFoAU9poNBVHTqAYUxc5c4naQhd2kOOswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANcDxRDTyi1VLSjA4DFm/s0aSYSRtiGJoYxCSjxW+IthzMDmV6kuI7c/n+O5gIOTBQ2gCF9evbVbFcF/nYq4zKo5WvCfrZ8Hekvjdm5TOSKMRGWaoydOsVsRPlvNN2q+iVFzmymPixWRblLzbYG1T0lRn6tLn2BKH0qkNUUg68ljA8qYgvulYo5FzSLB1KgZRjDyyDS5+IT/vr/M2H/4h1eCPdD2JROfxf4+3OKBXg5N2Y6DJ/mwNqe+8WGOLmaPDV6GaBVR8BcryYBohrEwYwouhqvNYsk5c1wLBS+k4T1PHC53I/9oGrdhX9jDQiHvQ2CzTp5e9rscbVr71nv03ug==\n-----END CERTIFICATE-----\n"]

Wpisanie klucza prywatnego i sekretu na sztywno może budzić niepokój — i słusznie. To proof of concept, więc upraszczamy, ale w prawdziwym wdrożeniu te wartości powinny trafić do porządnego systemu zarządzania sekretami.

To cała konfiguracja realmu. Użytkownika testowego dodamy później — teraz CouchDB.

Przygotowanie CouchDB do integracji SSO

Konfiguracja CouchDB jest krótsza, więc pokazujemy ją w całości z krótkim opisem.

[couchdb]
uuid = 5f1a34cf3b35423690c2474a7527e2ff

[chttpd]
authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
require_valid_user = false

[jwt_auth]
required_claims = exp, iat
roles_claim_path = _couchdb\.roles

[jwt_keys]
rsa:xvAsHaF2w0M1y9GG6bmFannhp9aFLKvHQRaAAb8gUYc = -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzPfItvwhUYhWDZVLSeaaVDcmVDmtvu0jJ8mt7c36zzhFZmugCQDCfWEdgi+kiJMs+quUap4UEl087zzvTmrCrly1rcaY1WUWsAl6AnIsmiUy3vKnJm+kjgjrI0UohgdT2oryf+zWwg4j+q/AcV+E17GH3RlthAwaLfpJcRumvhTmwgOyidW6f8j24KT/pjco+z/JRtthN1EkYxN4PGoPyfG91CvgM7tcgZnWABDxtas/KuOrYBfaiYg6O0DQ66EQy8aHJcqTLBydwCGyYeZargJeLkwC2DCyNn6Cial5XPiWfbXU88WINOEe0irgHIs3Y2dlXHCTaRskhrOYvF4LkwIDAQAB\n-----END PUBLIC KEY-----\n

Od góry: UUID jednoznacznie identyfikuje instancję. Dalej lista mechanizmów uwierzytelniania, do której dodajemy obsługę JWT, bo domyślnie nie jest włączona. Ponieważ użytkownicy nie są już przechowywani w CouchDB, wyłączamy też ich walidację.

Następnie konfiguracja JWT: wymagane claimy i ścieżka do claimu z rolami. Backslash pełni tu tę samą funkcję co podwójny backslash w konfiguracji Keycloak. Na końcu klucz publiczny wyprowadzony z klucza podpisującego, dzięki któremu CouchDB wie, że może ufać tokenom z Keycloak.

Nginx jako reverse proxy

Prawie gotowe, ale przed testami musimy skonfigurować reverse proxy. Ta część jest dłuższa, więc dzielimy ją na trzy: budowę obrazu Dockera, konfigurację bloków server i skrypt uwierzytelniania w Lua.

Budowa obrazu Dockera

Obraz OpenResty od Bitnami jest dobry, ale brakuje w nim kilku pakietów Lua potrzebnych do integracji z Keycloak.

FROM bitnami/openresty:1.25.3-1

RUN opm get zmartzone/lua-resty-openidc
RUN opm get ledgetech/lua-resty-http
RUN opm get bungle/lua-resty-session=3.10

Najważniejszy jest zmartzone/lua-resty-openidc, który implementuje OpenID Connect Relying Party — z niego skorzystamy przy autoryzacji w Keycloak. Pozostałe dwa pakiety to jego zależności. W chwili pisania rozwiązanie nie działa z bungle/lua-resty-session nowszym niż 3.10, więc przypinamy tę wersję.

Konfiguracja bloków server

Zaczynamy prosto.

server {
    server_name _;

    listen 80;
    listen [::]:80;

    return 301 https://$host$request_uri;
}

Ten blok nasłuchuje na porcie 80 dla wszystkich domen i przekierowuje żądania HTTP na HTTPS. Później wygenerujemy samopodpisane certyfikaty, żeby HTTPS działał na localhost.

Dalej blok server dla Keycloak.

server {
    server_name auth.oblivio.localhost;

    listen 443 ssl;
    listen [::]:443 ssl;

    http2 on;

    ssl_certificate bitnami/certs/server.crt;
    ssl_certificate_key bitnami/certs/server.key;

    location / {
        proxy_pass http://keycloak:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Nasłuchuje na porcie 443 dla IPv4 i IPv6 i obsługuje auth.oblivio.localhost — adres frontendu, który wcześniej ustawiliśmy w Keycloak. Wskazuje certyfikat i klucz SSL, które wygenerujemy na końcu tej sekcji, i przekazuje wszystkie żądania do kontenera Keycloak.

Ostatni blok jest dla CouchDB.

server {
    server_name couchdb.oblivio.localhost;

    listen 443 ssl;
    listen [::]:443 ssl;

    resolver 127.0.0.11 valid=10s;

    http2 on;

    ssl_certificate bitnami/certs/server.crt;
    ssl_certificate_key bitnami/certs/server.key;

    location / {
        access_by_lua_file /opt/bitnami/openresty/lua/access.lua;
        proxy_pass http://couchdb:5984;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Podobna konfiguracja: subdomena (couchdb.oblivio.localhost), porty i ścieżki certyfikatów. Różni się dyrektywa resolver, wskazująca wewnętrzny DNS Dockera — potrzebny do zapytań, które lua-resty-openidc wysyła podczas uwierzytelniania — oraz access_by_lua_file, wskazująca skrypt Lua, który napiszemy za chwilę.

Zanim przejdziemy do Lua, wygenerujmy samopodpisane certyfikaty narzędziem mkcert. Zainstaluje ono też certyfikat w odpowiednich katalogach systemowych, więc przeglądarka będzie mu ufać.

mkcert -cert-file nginx/certs/server.crt -key-file nginx/certs/server.key oblivio.localhost \*.oblivio.localhost

Wystarczy wskazać, gdzie zapisać certyfikat i klucz oraz jakie domeny mają być nim objęte. Proste, prawda?

Skrypt uwierzytelniania w Lua

Zaczynamy od opcji dla lua-resty-openidc dopasowanych do naszego przypadku.

local opts = {
    redirect_uri = "/callback",
    discovery = {
        issuer = "https://auth.oblivio.localhost/realms/master",
        authorization_endpoint = "https://auth.oblivio.localhost/realms/master/protocol/openid-connect/auth",
        end_session_endpoint = "https://auth.oblivio.localhost/realms/master/protocol/openid-connect/logout",
        token_endpoint = "http://keycloak:8080/realms/master/protocol/openid-connect/token",
        jwks_uri = "http://keycloak:8080/realms/master/protocol/openid-connect/certs",
        userinfo_endpoint = "http://keycloak:8080/realms/master/protocol/openid-connect/userinfo",
        revocation_endpoint = "http://keycloak:8080/realms/master/protocol/openid-connect/revoke",
        introspection_endpoint = "http://keycloak:8080/realms/master/protocol/openid-connect/token/introspect"
    },
    client_id = "couchdb-proxy",
    client_secret = "32scbZbgGNSaVOAAuZHgYeTjdQrkfwTh",
    scope = "openid couchdb",
    renew_access_token_on_expiry = true,
    access_token_expires_in = 60,
    accept_none_alg = false,
    accept_unsupported_alg = false,
    session_contents = {
        id_token = true,
        access_token = true,
        refresh_token = true
    }
}

Jest ich sporo. Najpierw callback, na który użytkownik wraca po udanym logowaniu — ścieżka względna w bieżącej subdomenie. Potem endpointy dostawcy OpenID Connect. Część wskazuje publiczną subdomenę, a część bezpośrednio kontener: jeśli adresu używa przeglądarka, podajemy subdomenę, a jeśli biblioteka wywołuje go wewnętrznie — adres kontenera.

Dalej wybieramy klienta, jego sekret i zakresy — w tym couchdb, który dodaje role CouchDB z grup Keycloak do tokena dostępowego. Potem ustawienia tokenów: czas ważności, odnawianie po wygaśnięciu i to, czy akceptujemy nieobsługiwane algorytmy. Na końcu określamy, co trafia do sesji: ID token, access token i refresh token.

Mając opcje, wywołujemy funkcję uwierzytelniania i pozwalamy bibliotece wykonać całą pracę.

local res, err = require("resty.openidc").authenticate(opts)

if err then
    ngx.status = 500
    ngx.say(err)
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

if res then
    ngx.req.set_header("Authorization", "Bearer " .. res.access_token)
end

Jeśli wywołanie się nie powiedzie, kończymy żądanie błędem serwera. W przeciwnym razie dodajemy nagłówek autoryzacji typu Bearer z tokenem dostępowym z Keycloak, aby CouchDB mógł zweryfikować tożsamość użytkownika.

Uruchomienie i testy

Uff — konfiguracja za nami. Możemy przetestować rozwiązanie. Uruchom całą aplikację poleceniem:

docker compose up -d

Jeśli wszystko poszło dobrze, aplikacja działa. Otwórz https://auth.oblivio.localhost w przeglądarce — powinna pojawić się strona logowania Keycloak.

Strona logowania Keycloak

Zaloguj się jako domyślny administrator, używając danych ze zmiennych środowiskowych Dockera. Sprawdź, czy wszystko z naszego pliku YAML jest na miejscu, a następnie utwórz użytkownika do testu logowania w CouchDB.

Przejdź do zakładki “Users” i kliknij “Add user”. Włącz “Email verified”, uzupełnij dane i dodaj użytkownika do grupy /couchdb/admins przyciskiem “Join Groups”.

Tworzenie użytkownika w Keycloak

Kliknij “Create” i otwórz zakładkę “Credentials”. Naciśnij “Set password”, ustaw hasło i dla uproszczenia wyłącz opcję “Temporary”.

Ustawianie hasła użytkownika w Keycloak

Użytkownik gotowy. Otwórz kartę prywatną i wejdź na https://couchdb.oblivio.localhost. Ponieważ nie jesteś jeszcze zalogowany, zostaniesz przekierowany na https://auth.oblivio.localhost, gdzie podasz ustawione przed chwilą dane.

Logowanie do CouchDB przez Keycloak

Po kliknięciu “Sign In” trafisz na stronę powitalną CouchDB — dokument JSON z podstawowymi informacjami o instancji. Stamtąd możesz otworzyć /_session, by zobaczyć zalogowanego użytkownika, albo /_utils, czyli Fauxton — panel administracyjny CouchDB. /_session zwróci JSON podobny do tego:

{
  "ok": true,
  "userCtx": {
    "name": "287e5d17-4937-48b7-a6fe-cc2029c1cf68",
    "roles": ["_admin"]
  },
  "info": {
    "authentication_handlers": ["proxy", "jwt", "cookie", "default"],
    "authenticated": "jwt"
  }
}

Jesteś zalogowany jako użytkownik o podanym UUID — identyfikatorze nadanym przez Keycloak — z rolą _admin. Po dołączeniu do drugiej grupy zobaczysz rolę _user. Na końcu znajdziesz dostępne mechanizmy uwierzytelniania i ten, który został użyty.

Podsumowanie

Łącząc Keycloak, Nginx, Lua i kilka plików konfiguracyjnych, uzyskujemy dostęp do CouchDB przez Single Sign-On bez zarządzania użytkownikami w samej bazie. Droga była długa, ale efektem jest czysta, rozszerzalna konfiguracja gotowa do utwardzenia pod produkcję.

Czytaj dalej

  • Inżynieria · 5 min czytania

    Pozbądź się ciasno powiązanych modułów i zależności cyklicznych w NestJS

    Jak zdarzenia pomagają modułom NestJS trzymać się swoich granic — i pozbyć się zależności cyklicznych bez rozsiewania forwardRef() po całym kodzie.

Zbudujmy razem Twój produkt

Opowiedz nam, co budujesz — wystarczą luźne notatki. W 30-minutowej rozmowie dostaniesz szczery feedback, wstępny harmonogram i widełki budżetowe. Bez presji i bez sprzedażowego gadania.

  • ✓ Odpowiedź w ciągu 1 dnia roboczego
  • ✓ NDA na życzenie
  • ✓ Bezpłatna wycena
Napisz do nas