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

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-----\nOd 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.10Najważ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.localhostWystarczy 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)
endJeś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 -dJeśli wszystko poszło dobrze, aplikacja działa. Otwórz https://auth.oblivio.localhost w przeglądarce — powinna pojawić się 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”.

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

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.

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ę.