From 74e919aa717302dca65b865f6b3159e978e62332 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Tue, 14 Apr 2026 19:41:33 +0200 Subject: [PATCH] feat(deploy): ambrosio production stack (erosi.oier.ovh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-contained compose for a single-VPS deploy behind the host's Caddy. App + Supabase API share erosi.oier.ovh; Keycloak on auth.oier.ovh. - infra/docker-compose.erosi.yml — full stack bound to 127.0.0.1 only (db, auth, rest, realtime, storage, imgproxy, kong, meta, keycloak, app) - infra/caddy/erosi.Caddyfile — host-Caddy snippet with TLS + path routing (/auth /rest /storage /realtime /graphql → kong, rest → app) - infra/scripts/deploy-erosi.sh — rsync + first-run secret generation + build + migrations + Caddy snippet install - keycloak/realm-export.erosi.json — prod redirect URIs, registration on, client secret as __KEYCLOAK_CLIENT_SECRET__ placeholder (deploy sub'd) - .env.erosi.example — env template with placeholders Supporting fixes to make the deploy work on a clean Supabase-postgres volume: - infra/db-init/00-role-passwords.sh — rewrote as idempotent bootstrap: CREATE the Supabase service roles if missing, set passwords on pre-existing ones, enable pg_cron/pgcrypto/uuid-ossp, create auth/storage/graphql_public/ _realtime/realtime schemas, create empty supabase_realtime publication, set per-role search_path (auth→auth, storage→storage). Old version only ALTERed passwords and relied on the roles already existing — worked for a grandfathered dev volume, failed on a fresh prod init. - infra/db-init/00-role-passwords.sql — removed (folded into .sh). - apps/web/vite.config.ts — PWA strategy generateSW (was injectManifest). The plugin's injectManifest hardcodes the SW source filename to service-worker.js and ignores the filename: 'sw.ts' override, making the production build fail. generateSW's auto-generated precache-only SW is functionally equivalent to our 5-line src/sw.ts. Tested end-to-end on ambrosio: all 9 containers up, 11 migrations applied, https://erosi.oier.ovh returns 200, Keycloak OIDC discovery serves the correct issuer, /auth/v1/settings lists Keycloak as the sole external provider. --- .env.erosi.example | 32 + apps/web/vite.config.ts | 22 +- infra/caddy/erosi.Caddyfile | 46 + infra/db-init/00-role-passwords.sh | 161 ++- infra/db-init/00-role-passwords.sql | 11 - infra/docker-compose.erosi.yml | 234 ++++ infra/scripts/deploy-erosi.sh | 175 +++ keycloak/realm-export.erosi.json | 1851 +++++++++++++++++++++++++++ 8 files changed, 2494 insertions(+), 38 deletions(-) create mode 100644 .env.erosi.example create mode 100644 infra/caddy/erosi.Caddyfile delete mode 100644 infra/db-init/00-role-passwords.sql create mode 100644 infra/docker-compose.erosi.yml create mode 100755 infra/scripts/deploy-erosi.sh create mode 100644 keycloak/realm-export.erosi.json diff --git a/.env.erosi.example b/.env.erosi.example new file mode 100644 index 0000000..f5e21ab --- /dev/null +++ b/.env.erosi.example @@ -0,0 +1,32 @@ +# Production env for ambrosio — copy to .env on the server and fill in secrets. +# Generate JWT triplet: bash infra/scripts/rotate-jwt.sh + +# ── Domains (change if you migrate to new hostnames) ──────────────────────── +PUBLIC_APP_URL=https://erosi.oier.ovh +PUBLIC_SUPABASE_URL=https://erosi.oier.ovh +PUBLIC_KEYCLOAK_URL=https://auth.oier.ovh +PUBLIC_KEYCLOAK_REALM=colectivo +PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web + +# Keycloak sees this as its canonical hostname (KC_HOSTNAME). +KEYCLOAK_HOSTNAME=auth.oier.ovh + +# ── Postgres (single password for all internal roles) ─────────────────────── +POSTGRES_PASSWORD=CHANGEME-strong-random-password + +# ── Supabase JWT triplet (regenerate every 90 days) ───────────────────────── +# Produce all three with: bash infra/scripts/rotate-jwt.sh +SUPABASE_JWT_SECRET=CHANGEME-openssl-rand-base64-48 +PUBLIC_SUPABASE_ANON_KEY=CHANGEME-sign-with-above +SUPABASE_SERVICE_ROLE_KEY=CHANGEME-sign-with-above + +# ── Keycloak admin + client secret ────────────────────────────────────────── +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=CHANGEME-strong-random-password +# Must match the `secret` field of the colectivo-web client in realm-export.erosi.json. +KEYCLOAK_CLIENT_SECRET=CHANGEME-client-secret-32-chars + +# ── Realtime encryption ───────────────────────────────────────────────────── +# DB_ENC_KEY: exactly 16 characters. SECRET_KEY_BASE: ≥ 64 characters. +REALTIME_ENC_KEY=CHANGEME16charkey +REALTIME_SECRET_KEY_BASE=CHANGEME-openssl-rand-base64-64-at-minimum-so-phoenix-is-happy diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 9f1d961..a214446 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -11,14 +11,14 @@ export default defineConfig({ }), sveltekit(), SvelteKitPWA({ - // Fase 6.1: custom SW at src/sw.ts (NOT src/service-worker.ts — SvelteKit - // intercepts that filename and blocks Workbox imports; see gotcha #10 - // in CLAUDE.md). Minimal strategy: precache the built shell, no runtime - // caching of Supabase. The $lib/sync pending_ops queue already handles - // offline writes; SyncBanner surfaces offline state to the user. - strategies: 'injectManifest', - srcDir: 'src', - filename: 'sw.ts', + // `generateSW` strategy: the plugin auto-generates a Workbox SW that + // precaches the built shell. Equivalent behavior to our previous + // `src/sw.ts` (a 5-line precacheAndRoute wrapper) without the + // injectManifest path, which in @vite-pwa/sveltekit 0.6.8 hardcodes + // the SW source filename to `service-worker.js` and breaks on any + // custom filename. The $lib/sync pending_ops queue handles offline + // writes; SyncBanner surfaces offline state to the user. + strategies: 'generateSW', scope: '/', base: '/', manifest: { @@ -37,8 +37,10 @@ export default defineConfig({ { src: '/icons/icon-512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' } ] }, - injectManifest: { - globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'] + workbox: { + globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'], + // Supabase paths: never cache — always hit the network. + navigateFallbackDenylist: [/^\/auth\//, /^\/rest\//, /^\/realtime\//, /^\/storage\//] }, devOptions: { enabled: true, diff --git a/infra/caddy/erosi.Caddyfile b/infra/caddy/erosi.Caddyfile new file mode 100644 index 0000000..b34d2aa --- /dev/null +++ b/infra/caddy/erosi.Caddyfile @@ -0,0 +1,46 @@ +# Caddyfile snippet for ambrosio host Caddy. +# Install path: /etc/caddy/Caddyfile (append these blocks). +# Reload: sudo systemctl reload caddy +# +# Docker services bind to 127.0.0.1 only; this file is the public entrypoint. +# TLS certificates are issued automatically by Caddy via Let's Encrypt. + +# ── App + Supabase (single domain) ────────────────────────────────────────── +erosi.oier.ovh { + encode zstd gzip + + # Supabase API paths → Kong gateway. + # Realtime MUST come before /auth, /rest, /storage to match its WS upgrade path first. + @supabase path /auth/v1/* /rest/v1/* /storage/v1/* /realtime/v1/* /graphql/v1/* /pg/* + handle @supabase { + # Realtime WebSockets require a long read timeout; PostgREST benefits from + # flush_interval for streaming responses. Caddy handles WS upgrades natively. + reverse_proxy 127.0.0.1:8000 { + transport http { + read_timeout 3600s + } + header_up X-Real-IP {remote_host} + } + } + + # Everything else → SvelteKit Node server. + handle { + reverse_proxy 127.0.0.1:3000 { + header_up X-Real-IP {remote_host} + } + } +} + +# ── Keycloak ──────────────────────────────────────────────────────────────── +auth.oier.ovh { + encode zstd gzip + + # Keycloak 24 with KC_PROXY=edge uses the X-Forwarded-* headers that Caddy + # already sets automatically on every reverse_proxy (Proto, For, Host). + # Only X-Forwarded-Port is not default, so we add it explicitly — without + # it Keycloak builds redirect URIs with the internal port (8080). + reverse_proxy 127.0.0.1:8090 { + header_up X-Real-IP {remote_host} + header_up X-Forwarded-Port {server_port} + } +} diff --git a/infra/db-init/00-role-passwords.sh b/infra/db-init/00-role-passwords.sh index b6e9925..79795a9 100755 --- a/infra/db-init/00-role-passwords.sh +++ b/infra/db-init/00-role-passwords.sh @@ -1,32 +1,159 @@ #!/bin/bash # Runs once on first container start (docker-entrypoint-initdb.d). -# Sets Supabase internal role passwords to POSTGRES_PASSWORD and creates -# required schemas with correct grants. +# Two cases: +# 1. Fresh volume (e.g. prod on ambrosio): the supabase/postgres image's +# initdb only creates the `postgres` role. Create all Supabase internal +# roles here, then set their passwords. +# 2. Pre-existing volume carried over from an older build that already has +# the roles (e.g. long-lived dev): only ALTER their passwords. +# +# Idempotent: safe on both paths. set -e PASS="${POSTGRES_PASSWORD:-postgres}" -psql -v ON_ERROR_STOP=1 --username supabase_admin --dbname postgres <<-EOSQL - -- Set passwords for all Supabase internal roles. - -- The supabase/postgres image creates them with hardcoded defaults; - -- this overrides them so services connect with POSTGRES_PASSWORD. - ALTER ROLE authenticator WITH PASSWORD '${PASS}'; - ALTER ROLE supabase_auth_admin WITH PASSWORD '${PASS}'; - ALTER ROLE supabase_replication_admin WITH PASSWORD '${PASS}'; - ALTER ROLE supabase_storage_admin WITH PASSWORD '${PASS}'; - ALTER ROLE pgbouncer WITH PASSWORD '${PASS}'; +# Connect as `postgres` (always exists post-initdb) on the Unix socket (trusted). +psql -v ON_ERROR_STOP=1 --username postgres --dbname postgres <<-EOSQL + -- ── Role bootstrap (idempotent) ─────────────────────────────────────── + -- Template for most service roles: NOLOGIN OR LOGIN depending on use. + + DO \$\$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_admin') THEN + CREATE ROLE supabase_admin SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS LOGIN PASSWORD '${PASS}'; + ELSE + ALTER ROLE supabase_admin WITH PASSWORD '${PASS}'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN + CREATE ROLE anon NOLOGIN NOINHERIT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN + CREATE ROLE authenticated NOLOGIN NOINHERIT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN + CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticator') THEN + CREATE ROLE authenticator LOGIN NOINHERIT PASSWORD '${PASS}'; + ELSE + ALTER ROLE authenticator WITH PASSWORD '${PASS}'; + END IF; + -- authenticator must be able to SET ROLE to anon/authenticated/service_role. + GRANT anon, authenticated, service_role TO authenticator; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_auth_admin') THEN + CREATE ROLE supabase_auth_admin LOGIN NOINHERIT CREATEROLE PASSWORD '${PASS}'; + ELSE + ALTER ROLE supabase_auth_admin WITH PASSWORD '${PASS}'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_storage_admin') THEN + CREATE ROLE supabase_storage_admin LOGIN NOINHERIT CREATEROLE PASSWORD '${PASS}'; + ELSE + ALTER ROLE supabase_storage_admin WITH PASSWORD '${PASS}'; + END IF; + GRANT authenticator TO supabase_storage_admin; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_replication_admin') THEN + CREATE ROLE supabase_replication_admin LOGIN REPLICATION PASSWORD '${PASS}'; + ELSE + ALTER ROLE supabase_replication_admin WITH PASSWORD '${PASS}'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_realtime_admin') THEN + CREATE ROLE supabase_realtime_admin NOLOGIN NOINHERIT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'pgbouncer') THEN + CREATE ROLE pgbouncer LOGIN PASSWORD '${PASS}'; + ELSE + ALTER ROLE pgbouncer WITH PASSWORD '${PASS}'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_user') THEN + CREATE ROLE dashboard_user CREATEROLE CREATEDB REPLICATION; + END IF; + END \$\$; + + -- Grant postgres membership in service roles so seed inserts work. + GRANT anon, authenticated, service_role, supabase_auth_admin, supabase_storage_admin, supabase_realtime_admin TO postgres; + + -- Per-role search_path + timeouts (match dev to avoid migration drift). + -- GoTrue's migrations issue unqualified CREATE TYPE / CREATE TABLE expecting + -- these to land in the `auth` schema — with no default here they end up in + -- `public` and later migrations that reference `auth.factor_type` blow up. + ALTER ROLE supabase_auth_admin SET search_path TO auth; + ALTER ROLE supabase_auth_admin SET idle_in_transaction_session_timeout TO 60000; + ALTER ROLE supabase_storage_admin SET search_path TO storage; + ALTER ROLE anon SET statement_timeout TO '3s'; + ALTER ROLE authenticated SET statement_timeout TO '8s'; + ALTER ROLE authenticator SET statement_timeout TO '8s'; + ALTER ROLE authenticator SET lock_timeout TO '8s'; + + -- ── Extensions the migrations assume are enabled ───────────────────── + -- Migration 005 invokes cron.schedule(...); dev has pg_cron auto-enabled + -- via the image's supa-init scripts, prod does not. + CREATE EXTENSION IF NOT EXISTS pg_cron; + CREATE EXTENSION IF NOT EXISTS pgcrypto; + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + + -- ── Schemas the Supabase services expect ───────────────────────────── + CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin; + CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_storage_admin; + CREATE SCHEMA IF NOT EXISTS graphql_public; - -- Create the _realtime schema required by supabase/realtime CREATE SCHEMA IF NOT EXISTS _realtime; GRANT ALL ON SCHEMA _realtime TO supabase_replication_admin; ALTER ROLE supabase_replication_admin SET search_path TO _realtime; - -- Pre-create the `realtime` schema. Realtime's per-tenant migrations - -- expect it to exist before running (they don't CREATE SCHEMA themselves) - -- and without it every tenant connect fails with "schema realtime does - -- not exist" → PoolingReplicationError → no postgres_changes delivery. + -- Pre-create the `realtime` schema so Realtime's per-tenant migrations + -- don't fail on "schema realtime does not exist". CREATE SCHEMA IF NOT EXISTS realtime AUTHORIZATION supabase_admin; + + -- Empty publication that migration 007 will ALTER with specific tables. + -- (supabase/postgres image ships this pre-created in newer versions; + -- on v15.1.1.78 it does not.) + CREATE PUBLICATION supabase_realtime; + + -- PostgREST needs USAGE on public for anon + authenticated + service_role. + GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role; + + -- Default privileges for future tables in public (RLS policies still apply). + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon, authenticated, service_role; + + -- GoTrue runs its own migrations against `public.schema_migrations`. PG 15 + -- revokes CREATE from public by default, so grant it back to the service roles. + GRANT CREATE, USAGE ON SCHEMA public TO supabase_auth_admin, supabase_storage_admin; + + -- Storage migrations call statements that require database-level grants + -- (e.g. CREATE EXTENSION, ALTER DEFAULT PRIVILEGES). + GRANT ALL ON DATABASE postgres TO supabase_auth_admin, supabase_storage_admin; + + -- ── Keycloak database + role (separate DB, idempotent) ─────────────── + -- CREATE DATABASE cannot run in a transaction, so run it via \gexec. + SELECT 'CREATE DATABASE keycloak' + WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'keycloak') \gexec + + DO \$\$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'keycloak') THEN + CREATE ROLE keycloak LOGIN PASSWORD '${PASS}'; + ELSE + ALTER ROLE keycloak WITH PASSWORD '${PASS}'; + END IF; + END \$\$; + + GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak; EOSQL -echo "==> db-init: role passwords and schemas configured" +# Keycloak also needs CREATE on the keycloak database's public schema. +psql -v ON_ERROR_STOP=1 --username postgres --dbname keycloak <<-EOSQL + GRANT ALL ON SCHEMA public TO keycloak; +EOSQL + +echo "==> db-init: roles bootstrapped, passwords set, schemas created" diff --git a/infra/db-init/00-role-passwords.sql b/infra/db-init/00-role-passwords.sql deleted file mode 100644 index 7d41e73..0000000 --- a/infra/db-init/00-role-passwords.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Set passwords for Supabase internal roles to match POSTGRES_PASSWORD. --- The supabase/postgres image creates these roles with hardcoded default passwords; --- this script overrides them so all services can authenticate with POSTGRES_PASSWORD. --- Runs once on first container start (docker-entrypoint-initdb.d). --- Must run as supabase_admin (superuser) which this image uses for initdb scripts. - -ALTER ROLE authenticator WITH PASSWORD 'REPLACE_PASSWORD'; -ALTER ROLE supabase_auth_admin WITH PASSWORD 'REPLACE_PASSWORD'; -ALTER ROLE supabase_replication_admin WITH PASSWORD 'REPLACE_PASSWORD'; -ALTER ROLE supabase_storage_admin WITH PASSWORD 'REPLACE_PASSWORD'; -ALTER ROLE pgbouncer WITH PASSWORD 'REPLACE_PASSWORD'; diff --git a/infra/docker-compose.erosi.yml b/infra/docker-compose.erosi.yml new file mode 100644 index 0000000..cb6bb74 --- /dev/null +++ b/infra/docker-compose.erosi.yml @@ -0,0 +1,234 @@ +name: colectivo-erosi + +# Production stack for ambrosio (OVH VPS). +# Domains: erosi.oier.ovh (app + Supabase API) | auth.oier.ovh (Keycloak) +# +# TLS + reverse proxy is handled by the HOST Caddy (systemd service on ambrosio, +# config at /etc/caddy/Caddyfile) — NOT by a container. All services here bind +# to 127.0.0.1 so only Caddy can reach them. +# +# Bring up: docker compose -f infra/docker-compose.erosi.yml --env-file .env up -d --build +# Logs: docker compose -f infra/docker-compose.erosi.yml logs -f +# Tear down: docker compose -f infra/docker-compose.erosi.yml down + +services: + + # ── PostgreSQL 15 (Supabase image — internal roles preconfigured) ────────── + db: + image: supabase/postgres:15.1.1.78 + restart: always + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 10s + timeout: 5s + retries: 10 + environment: + POSTGRES_HOST: /var/run/postgresql + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + JWT_SECRET: ${SUPABASE_JWT_SECRET} + JWT_EXP: 3600 + volumes: + - db-data:/var/lib/postgresql/data + - ./db-init:/docker-entrypoint-initdb.d:ro + networks: [colectivo] + + # ── GoTrue (Supabase Auth) ───────────────────────────────────────────────── + auth: + image: supabase/gotrue:v2.158.1 + restart: always + depends_on: + db: { condition: service_healthy } + environment: + GOTRUE_API_HOST: "0.0.0.0" + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${PUBLIC_APP_URL} + GOTRUE_API_EXTERNAL_URL: ${PUBLIC_APP_URL} + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres + GOTRUE_SITE_URL: ${PUBLIC_APP_URL} + GOTRUE_URI_ALLOW_LIST: "${PUBLIC_APP_URL}/auth/callback,${PUBLIC_APP_URL}/*" + GOTRUE_JWT_SECRET: ${SUPABASE_JWT_SECRET} + GOTRUE_JWT_EXP: 3600 + GOTRUE_DISABLE_SIGNUP: "false" + GOTRUE_EXTERNAL_KEYCLOAK_ENABLED: "true" + GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID: ${PUBLIC_KEYCLOAK_CLIENT_ID} + GOTRUE_EXTERNAL_KEYCLOAK_SECRET: ${KEYCLOAK_CLIENT_SECRET} + GOTRUE_EXTERNAL_KEYCLOAK_SCOPES: "openid profile email" + # Public Keycloak base URL (browser sees this). The `/realms/colectivo` + # suffix is appended by GoTrue. + GOTRUE_EXTERNAL_KEYCLOAK_URL: ${PUBLIC_KEYCLOAK_URL}/realms/${PUBLIC_KEYCLOAK_REALM} + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI: ${PUBLIC_SUPABASE_URL}/auth/v1/callback + GOTRUE_MAILER_AUTOCONFIRM: "true" + networks: [colectivo] + + # ── PostgREST ────────────────────────────────────────────────────────────── + rest: + image: postgrest/postgrest:v12.2.0 + restart: always + depends_on: + db: { condition: service_healthy } + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres + PGRST_DB_SCHEMAS: public,storage,graphql_public + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${SUPABASE_JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${SUPABASE_JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: 3600 + networks: [colectivo] + + # ── Realtime (pinned to v2.76.5 — see CLAUDE.md note on handle_out/3) ────── + realtime: + image: supabase/realtime:v2.76.5 + restart: always + depends_on: + db: { condition: service_healthy } + environment: + PORT: 4000 + DB_HOST: db + DB_PORT: 5432 + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: postgres + DB_AFTER_CONNECT_QUERY: SET search_path TO _realtime + DB_ENC_KEY: ${REALTIME_ENC_KEY} + API_JWT_SECRET: ${SUPABASE_JWT_SECRET} + SELF_HOST_TENANT_NAME: realtime + SEED_SELF_HOST: "true" + RUN_JANITOR: "true" + APP_NAME: realtime + SECRET_KEY_BASE: ${REALTIME_SECRET_KEY_BASE} + ERL_AFLAGS: -proto_dist inet_tcp + DNS_NODES: "''" + RLIMIT_NOFILE: "10000" + METRICS_JWT_SECRET: ${SUPABASE_JWT_SECRET} + networks: [colectivo] + + # ── Storage ──────────────────────────────────────────────────────────────── + storage: + image: supabase/storage-api:v1.11.13 + restart: always + depends_on: + db: { condition: service_healthy } + rest: { condition: service_started } + environment: + ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} + SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${SUPABASE_JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: colectivo-erosi + REGION: local + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + volumes: + - storage-data:/var/lib/storage + networks: [colectivo] + + # ── ImgProxy ─────────────────────────────────────────────────────────────── + imgproxy: + image: darthsim/imgproxy:v3.8.0 + restart: always + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: "true" + volumes: + - storage-data:/var/lib/storage:ro + networks: [colectivo] + + # ── Kong (API Gateway) — bound to localhost only; host Caddy proxies it ──── + kong: + image: kong:2.8.1 + restart: always + entrypoint: ["/bin/bash", "/home/kong/kong-start.sh"] + ports: + - "127.0.0.1:8000:8000" + depends_on: + db: { condition: service_healthy } + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,rate-limiting + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} + SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} + volumes: + - ./kong.yml:/home/kong/temp.yml:ro + - ./kong-start.sh:/home/kong/kong-start.sh:ro + networks: [colectivo] + + # ── Keycloak 24 ──────────────────────────────────────────────────────────── + # Bound to localhost only; host Caddy proxies auth.oier.ovh → 127.0.0.1:8090. + # KC_PROXY=edge expects X-Forwarded-* from Caddy. + keycloak: + image: quay.io/keycloak/keycloak:24.0.5 + restart: always + ports: + - "127.0.0.1:8090:8080" + # Plain `start` (no --optimized) so Quarkus re-builds with the Postgres + # driver on first boot. --optimized skips the build phase and falls back + # to H2, producing "URL format error ... jdbc:h2" on startup. + command: start --import-realm + depends_on: + db: { condition: service_healthy } + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db:5432/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: ${POSTGRES_PASSWORD} + KC_HOSTNAME: ${KEYCLOAK_HOSTNAME} + KC_HOSTNAME_STRICT: "true" + KC_HOSTNAME_STRICT_HTTPS: "true" + KC_PROXY: edge + KC_HTTP_ENABLED: "true" + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_FEATURES: token-exchange + volumes: + - keycloak-data:/opt/keycloak/data + - ../keycloak/realm-export.erosi.json:/opt/keycloak/data/import/realm-export.json:ro + networks: [colectivo] + + # ── App (SvelteKit Node server) — built on the host from the repo ────────── + # Build args bake the PUBLIC_* values into the client bundle (SvelteKit reqs). + app: + image: colectivo-web:erosi + build: + context: .. + dockerfile: apps/web/Dockerfile + args: + PUBLIC_SUPABASE_URL: ${PUBLIC_SUPABASE_URL} + PUBLIC_SUPABASE_ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} + PUBLIC_KEYCLOAK_URL: ${PUBLIC_KEYCLOAK_URL} + PUBLIC_KEYCLOAK_REALM: ${PUBLIC_KEYCLOAK_REALM} + PUBLIC_KEYCLOAK_CLIENT_ID: ${PUBLIC_KEYCLOAK_CLIENT_ID} + PUBLIC_APP_URL: ${PUBLIC_APP_URL} + restart: always + ports: + - "127.0.0.1:3000:3000" + environment: + PUBLIC_SUPABASE_URL: ${PUBLIC_SUPABASE_URL} + PUBLIC_SUPABASE_ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} + PUBLIC_KEYCLOAK_URL: ${PUBLIC_KEYCLOAK_URL} + PUBLIC_KEYCLOAK_REALM: ${PUBLIC_KEYCLOAK_REALM} + PUBLIC_KEYCLOAK_CLIENT_ID: ${PUBLIC_KEYCLOAK_CLIENT_ID} + PUBLIC_APP_URL: ${PUBLIC_APP_URL} + NODE_ENV: production + networks: [colectivo] + +networks: + colectivo: + driver: bridge + +volumes: + db-data: + storage-data: + keycloak-data: diff --git a/infra/scripts/deploy-erosi.sh b/infra/scripts/deploy-erosi.sh new file mode 100755 index 0000000..3dd52ba --- /dev/null +++ b/infra/scripts/deploy-erosi.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Deploy the full stack to ambrosio. +# +# Usage: infra/scripts/deploy-erosi.sh +# Env: DEPLOY_HOST=ambrosio (override if needed) +# DEPLOY_PATH=/opt/colectivo +# +# First run: +# 1. rsyncs repo → ambrosio:/opt/colectivo/ +# 2. if .env doesn't exist on server, generates secrets and writes it +# 3. patches realm-export.erosi.json with the generated Keycloak client secret +# 4. docker compose build + up -d +# 5. applies DB migrations +# 6. installs the Caddyfile snippet into host Caddy and reloads +# +# Subsequent runs: rsync + rebuild app + rolling restart. + +set -euo pipefail + +DEPLOY_HOST="${DEPLOY_HOST:-ambrosio}" +DEPLOY_PATH="${DEPLOY_PATH:-/opt/colectivo}" +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + +echo "==> Deploying to $DEPLOY_HOST:$DEPLOY_PATH" + +# ── 1. Ensure target dir exists ──────────────────────────────────────────── +ssh "$DEPLOY_HOST" "sudo mkdir -p $DEPLOY_PATH && sudo chown \$(id -u):\$(id -g) $DEPLOY_PATH" + +# ── 2. rsync repo (excluding node_modules / build artefacts / local env) ── +rsync -az --delete \ + --exclude '.git' \ + --exclude 'node_modules' \ + --exclude '.svelte-kit' \ + --exclude 'apps/web/build' \ + --exclude 'apps/web/.vite' \ + --exclude '**/*.log' \ + --exclude '**/.env' \ + --exclude '**/.env.development' \ + --exclude '**/.env.local' \ + --exclude 'lighthouse-report.html' \ + --exclude 'coverage' \ + --exclude 'test-results' \ + --exclude 'playwright-report' \ + "$REPO_ROOT/" "$DEPLOY_HOST:$DEPLOY_PATH/" + +# ── 3. Bootstrap secrets on first run ────────────────────────────────────── +ssh "$DEPLOY_HOST" bash -s << 'REMOTE' +set -euo pipefail +cd /opt/colectivo + +if [ ! -f .env ]; then + echo "--- First deploy: generating secrets" + + # JWT triplet (anon + service_role) + JWT_OUT=$(bash infra/scripts/rotate-jwt.sh) + SUPABASE_JWT_SECRET=$(echo "$JWT_OUT" | awk -F= '/^SUPABASE_JWT_SECRET=/{print substr($0, index($0,$2))}') + PUBLIC_SUPABASE_ANON_KEY=$(echo "$JWT_OUT" | awk -F= '/^PUBLIC_SUPABASE_ANON_KEY=/{print substr($0, index($0,$2))}') + SUPABASE_SERVICE_ROLE_KEY=$(echo "$JWT_OUT" | awk -F= '/^SUPABASE_SERVICE_ROLE_KEY=/{print substr($0, index($0,$2))}') + + # Other passwords + keys + POSTGRES_PASSWORD=$(openssl rand -hex 24) + KEYCLOAK_ADMIN_PASSWORD=$(openssl rand -hex 24) + KEYCLOAK_CLIENT_SECRET=$(openssl rand -hex 24) + REALTIME_ENC_KEY=$(openssl rand -hex 8) # 16 chars + REALTIME_SECRET_KEY_BASE=$(openssl rand -hex 48) # 96 chars + + cat > .env < /tmp/Caddyfile.new + +{ + echo "" + echo "$MARKER_START" + cat infra/caddy/erosi.Caddyfile + echo "$MARKER_END" +} >> /tmp/Caddyfile.new + +sudo mv /tmp/Caddyfile.new /etc/caddy/Caddyfile +sudo caddy validate --config /etc/caddy/Caddyfile +sudo systemctl reload caddy +echo "--- Caddy reloaded" +REMOTE + +echo "==> Deploy complete." +echo " App: https://erosi.oier.ovh" +echo " Keycloak: https://auth.oier.ovh" +echo " Admin pw: ssh $DEPLOY_HOST 'grep KEYCLOAK_ADMIN_PASSWORD $DEPLOY_PATH/.env'" diff --git a/keycloak/realm-export.erosi.json b/keycloak/realm-export.erosi.json new file mode 100644 index 0000000..a366b85 --- /dev/null +++ b/keycloak/realm-export.erosi.json @@ -0,0 +1,1851 @@ +{ + "id": "colectivo", + "realm": "colectivo", + "displayName": "Colectivo", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 3600, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "bruteForceProtected": true, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRole": { + "id": "53a87905-0c43-41e0-b088-95e62fc2ab47", + "name": "default-roles-colectivo", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "colectivo" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "e602a4ed-1b0e-4c5e-ab62-1dcb67f0430f", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/colectivo/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/colectivo/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "60e4a8dc-4fa2-4c08-a9ac-2d1f8df16115", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/colectivo/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/colectivo/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "2a601995-c699-42c2-bcf7-fe224e3a6a81", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "3f5ddb14-1cb6-4c0d-95d3-837c920ed0df", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "3b31a13d-5f66-4a68-ae17-f19de2db62e2", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "70c159b1-dd13-48f2-97db-6e08f174f1ff", + "clientId": "colectivo-web", + "name": "Colectivo Web App", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "__KEYCLOAK_CLIENT_SECRET__", + "redirectUris": [ + "https://erosi.oier.ovh/*", + "https://erosi.oier.ovh/auth/v1/callback" + ], + "webOrigins": [ + "https://erosi.oier.ovh" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "openid", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "29f4d79d-acab-4d0d-91ac-06fd7357ec55", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "65550f90-9861-4e94-82e2-a64ab6414af1", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/colectivo/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/colectivo/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "ef110cf5-dc7c-4030-a81f-0eb862f225df", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "43cc5c5a-a0f1-440f-af21-f9b787254cb4", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "07e3ec5d-0309-4d8a-b692-a77ac16ab296", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "1763586b-d803-499b-a8cc-19e99bcba57e", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "55a32b72-4e90-48ab-bdfa-ee5819351647", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "5a099bf0-f566-4eff-bbec-b45f30c39181", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "5fdaa43d-2441-420d-82e4-870141519bda", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "f63b50c9-1772-422f-8aff-4258d07a2d12", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "ff677dc7-b368-41c9-acc4-5b14914a298d", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "9ca466fd-f7f6-453b-bfd0-1e36313c1aee", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "6b10f0f7-b05b-4842-9c66-c87b59131afd", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "b9df0edd-dcc9-4628-9f82-cc5e1a778543", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "a3319ff1-8563-4659-a139-72bbb46ae5ae", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "c28b7ef9-d114-460e-8221-7a23897c46e2", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "5dd4e760-1dc8-43a1-b4ed-0fccc0a491a7", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "297ce0d1-0c90-4f27-a414-9f6a98a37924", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "4cadb013-a54a-474c-a83b-97e2f042b767", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "4aba17be-7a4a-4150-ac0e-22c32704b4c1", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "47d263d9-f7de-4a18-8f92-7babc062d4ba", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "12d9f262-622d-4f53-876a-3dcb2f0ea938", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "dd5600b1-0924-46a7-aae9-d1905b4429d1", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "910bbb61-9266-492d-8279-c1c30e16256c", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "f3027af3-7c0e-48f0-9094-625b4d125d6f", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "db9b568b-752b-444a-a70d-6fb62bea48f6", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "3e5fce11-0894-4b37-ad1f-31132d98a4c9", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "316b595a-120d-4bb7-bccd-448ef501cedd", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "ceeb2310-3124-4236-8730-e38fc9c1bff9", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "89bd1531-da9e-4f95-afc1-4b67a4412a02", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "1ac6db43-af2c-4fa4-8ff2-71467fcac544", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "df83ea60-efb9-4a8d-a461-52acfb2e0309", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "f9167181-c4af-461d-873e-1a90c68f178c", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "aa176105-9d4a-41f2-890f-c8f4f9ac9274", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "4dd3096f-beac-4ba8-8266-ceb826243aed", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "07454787-bb63-44ea-987b-09c02b6b70ed", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "5e5c0c52-b29d-418b-ac94-c1b1f6052330", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "5fa0a8a5-e7e6-4eb0-b795-0a584914be8b", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "28f2db5c-96dc-4c6c-a725-012446420e38", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "a5aafb14-f527-460c-b759-fbc4e9d484f5", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "a09a9441-c626-4a26-8723-475d2d4b2356", + "name": "openid", + "description": "OpenID Connect scope \u2014 ensures access token includes openid scope claim for userinfo endpoint", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "a5263f5f-0b80-4b29-b0ba-9a0d80e69533", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "d77dd0b8-d310-44a4-a38e-e2d9db83e8ce", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "5b08a208-a7d9-49aa-9a51-fc3825b83868", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-full-name-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "bbb169ee-0347-4522-b141-05a66abb6f91", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "b048beb3-c7cf-4153-887d-12d5ef09d3df", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "dbf10af0-c5c9-462c-9e8d-9f1b9a6bfcae", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "0a42adf2-e54e-4ca9-9ab5-c23278a2486d", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "8a195ea3-5818-42b1-82ad-cef49d834bb4", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "90f1e5e6-3334-4d2d-8341-4eb79932f43e", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "67819849-b4cf-4df6-9f83-08c0b4a202c2", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "310dd178-0fdb-4f3c-96ae-80026762eabd", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "da8480d9-f2e9-4dc6-a4c3-9a6439c3fa9b", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "01a21375-2f08-4b27-8555-d87f4036eee1", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "fbaa51a0-b2f3-4306-be95-a3d10932a33b", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ba72b4d0-3c5a-436c-8957-b77eb7724a92", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "57ad8f14-7aed-4f44-9c22-d22c73764d42", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9d70edb3-3acc-4098-b403-931de571d8e2", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "c29275cd-a589-4625-a432-648297ebe2e1", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "6db1ccd0-781c-4b27-870b-3ed016ea599f", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "058822b3-f060-4660-a8de-86e0c9cc361c", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "4d582d7d-69fe-435b-b450-7612f98562d5", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "310dcaca-cb8c-4e1f-b647-4b16c9b00b14", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "7762bf8c-52fe-4d01-bfaf-f68f8ac2ed57", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "e0ae3351-a961-46b5-a4da-4e7e95b5eff2", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "da6cdb3d-0f03-4991-a93a-9971e795f6f3", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "060880c1-4b76-47ca-b355-b56d81ad7bd0", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "d0c67220-d44e-4432-9f74-8ec7b2bcbfc4", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "61250053-362b-4568-a82f-94937f164b16", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "239c37dd-0bdb-4fad-91b0-1a688221c17a", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "d1f850c5-ea71-4732-9e01-3ab59a246af0", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "21e84058-848d-4a35-9565-6342f5973908", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "5edcc002-5926-4513-88dc-5b3483f14e16", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "24.0.5", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file