05 — Identita: EGO-SSO (Ecoidentita) × REMP × CMS
Pojmenování: Systém se oficiálně jmenuje EGO-SSO (viz Implementace EGO-SSO pro klienty). V kódu a dokumentaci projektu bývá označován také jako Ecoidentita nebo ECO Identita — jedná se o tentýž systém. Implementace do Folio CMS je vedena v CMS-353, větev
CMS-353_Ego_identita.
Problém
Tři systémy mají vlastní uživatelskou databázi:
- Folio CMS —
Folio::User(PostgreSQL), autentizace přes Devise + EGO-SSO callback flow - REMP CRM —
crm_users(MySQL), vlastní auth, API tokeny - EGO-SSO (Ecoidentita) — SSO provider, master identita
Potřebujeme zajistit, aby čtenář měl jednu identitu napříč všemi systémy, bez duplikace a s konzistentním stavem.
SSO providery v Economia CMS
| Typ uživatele | SSO provider | Poznámka |
|---|---|---|
| Čtenář (koncový zákazník) | EGO-SSO (Ecoidentita) | SSO redirect + userinfo flow, ucet.centrum.cz |
| Admin / redaktor (zaměstnanec) | Google SSO | Google Workspace Economia |
Ke kolizi SSO providerů nedochází — každý typ uživatele má právě jednoho providera. Mailer Economia::DeviseMailer#omniauth_conflict existuje v kódu, ale v praxi se neuplatní.
Stávající implementace EGO-SSO (z větve CMS-353_Ego_identita)
Důležité: EGO-SSO není klasický OAuth2 Authorization Code flow. Je to proprietární SSO řešení provozované na doméně
centrum.cz(produkce) /mailkafe.cz(dev).
Architektura
┌──────────────────────┐
│ EGO-SSO │
│ ucet.centrum.cz │
│ master user: uuid │
└──────────┬───────────┘
│
┌──────────┴───────────┐
│ │
▼ ▼
┌───────────────┐ ┌──────────────┐
│ Folio CMS │ │ REMP CRM │
│ Folio::User │ │ crm_users │
│ id: 42 │ │ id: 99 │
│ eco_id: uuid │◄───►│ ext_id: uuid│
│ remp_id: 99 │ │ │
└───────────────┘ └──────────────┘
Klíčové komponenty v CMS
| Komponenta | Soubor | Popis |
|---|---|---|
| EgoIdentitaAuthenticator | app/services/economia/ego_identita_authenticator.rb | Service: sign-in URL, sign-out URL, user-info volání |
| OmniauthCallbacksController | app/controllers/economia/folio/users/omniauth_callbacks_controller.rb | Zpracování EGO callback → OmniAuth hash → Devise sign-in |
| SessionsController | app/controllers/economia/folio/users/sessions_controller.rb | Sign-in redirect na EGO, sign-out s EGO kolečkem |
Princip
- EGO
uuidje kanonický identifikátor (v kóduuidz userinfo response) - Folio ukládá
eco_user_id(= EGO uuid) aremp_user_id(REMP CRM PK) - REMP CRM ukládá
ext_id= EGO uuid - Při prvním přihlášení se uživatel vytvoří v obou systémech
Flow: Přihlášení čtenáře (produkce, aktualne.cz)
Rekonstruováno z implementace v CMS-353_Ego_identita:
1. Čtenář klikne "Přihlásit se"
│ → SessionsController#egoidentita_sign_in
│ → uloží referrer do session
│
2. Redirect na EGO-SSO authorize
│ https://ucet.centrum.cz/auth/authorize
│ ?client_id=<EGO_IDENTITA_CLIENT_ID>
│ &redirect_uri=https://www.hn.cz/users/auth/egoidentita/callback
│ &state=not_used_yet
│
3. Čtenář se přihlásí na ucet.centrum.cz
│
4. Řetězec redirectů (nastavuje cookies):
│ a) ucet.centrum.cz → Set-Cookie: EGO_SESS_ID (domain=.centrum.cz)
│ b) sso.centrum.cz → Set-Cookie: EGO_APROFILE (domain=.centrum.cz)
│ c) sso.hn.cz → Set-Cookie: EGO_APROFILE (domain=.hn.cz) ← KLÍČOVÝ KROK
│ d) www.hn.cz/users/auth/egoidentita/callback
│
5. OmniauthCallbacksController#egoidentita
│ → EgoIdentitaAuthenticator.user_info(cookies)
│ → POST https://userinfo.centrum.cz/userinfo
│ {scope, profile: EGO_APROFILE, session: EGO_SESS_ID,
│ client_id, client_secret}
│
6. UserInfo response:
│ {uuid, email, firstname, lastname, avatar, subscriber, ego_id}
│
7. Build OmniAuth hash → bind_user_and_redirect
│ → find_or_create Folio::User (eco_user_id: uuid)
│ → Devise sign_in
│
8. (NOVÉ pro REMP) Background job: SyncUserToRempJob
│ POST REMP CRM /api/v1/users/register
│ {email, ext_id: uuid, first_name, last_name}
│
9. Session established, redirect na uloženou stránku
Krok 4c — Cross-domain cookie propagace
Toto je klíčový mechanismus. EGO-SSO propaguje cookies na cílovou doménu přes SSO redirect:
- EGO SSO server zná seznam registrovaných domén (
hn.cz,aktualne.cz,zena.cz…) - Po úspěšném přihlášení provede redirect přes
sso.<domena>pro každou doménu - Na
sso.hn.cz(Cloudflare Worker nebo jiný edge endpoint) se nastaví cookieEGO_APROFILEsdomain=.hn.cz - Folio CMS na
www.hn.czpak cookie přečte v Rails controlleru
⚠️ Otevřená otázka pro HN.cz: Je sso.hn.cz endpoint nasazen? Na aktualne.cz je sso.aktualne.cz funkční. Pro HN.cz je potřeba ověřit s Economia DevOps, zda:
- existuje CF Worker / endpoint na
sso.hn.cz - je nakonfigurován pro propagaci EGO cookies
- callback URL
https://www.hn.cz/users/auth/egoidentita/callbackje registrován v EGO-SSO
Flow: Odhlášení
1. Čtenář klikne "Odhlásit se"
│ → SessionsController#destroy
│
2. Pokud has_egoidentita_authentication? && EGO cookies přítomny:
│ → Redirect na EGO logout
│ https://ucet.centrum.cz/logout
│ ?url=https://www.hn.cz/users/sign_out
│
3. EGO SSO smaže cookies přes redirect kolečko
│ (obdobně jako při přihlášení, přes sso.hn.cz)
│
4. Finální redirect na /users/sign_out
│ → Devise sign_out
│ → SessionsController#after_sign_out → redirect na původní stránku
Flow: Změna e-mailu
1. Čtenář změní e-mail → EGO-SSO je source of truth
2. Při dalším přihlášení userinfo vrátí nový e-mail
3. Folio::User aktualizuje e-mail z userinfo response
4. Background job: sync nový e-mail do REMP CRM
EGO-SSO je source of truth pro e-mail, jméno, příjmení. CMS se synchronizuje při každém přihlášení z userinfo response.
Datový model
Folio::User (rozšíření)
# Stávající pole (z Devise / OmniAuth)
# email, encrypted_password, first_name, last_name, ...
# Nová pole pro REMP integraci
add_column :folio_users, :eco_user_id, :string, index: { unique: true }
add_column :folio_users, :remp_user_id, :integer, index: { unique: true }
add_column :folio_users, :remp_synced_at, :datetime
REMP CRM User
-- V REMP CRM (MySQL)
-- Stávající tabulka crm_users
-- Klíčová pole:
-- id (auto-increment)
-- email (unique)
-- ext_id (VARCHAR, indexed) -- = eco_user_id
Mapování polí
| Folio::User | Ecoidentita | REMP CRM User | Poznámka |
|---|---|---|---|
id | — | — | Interní Folio PK |
eco_user_id | uuid / uid z userinfo | ext_id | Kanonický identifier |
remp_user_id | — | id | Pro přímé API volání |
email | email | email | Sync needed |
first_name | given_name | first_name | Sync needed |
last_name | family_name | last_name | Sync needed |
REMP SSO vs. EGO-SSO
REMP SSO modul (/Sso) poskytuje:
- Google OAuth login pro admin uživatele REMP nástrojů
- JWT session management
- API token management (
/api/auth/*)
REMP SSO NENÍ identity provider pro čtenáře. Čtenáři se autentizují přes EGO-SSO → Folio CMS → REMP CRM API.
REMP admin uživatelé (redaktoři, marketéři) se přihlašují:
- Do Campaign admin → přes REMP SSO (Google OAuth)
- Do Mailer admin → přes REMP SSO
- Do Beam admin → přes REMP SSO
Doporučení: Pro budoucí zjednodušení zvážit napojení REMP SSO na EGO-SSO (jako další SSO provider), aby se admin uživatelé nemuseli přihlašovat zvlášť.
Cookies a session management
Skutečné cookies (z implementace CMS-353)
EGO-SSO používá dvě klíčové cookies:
| Cookie | Doména (produkce) | Popis |
|---|---|---|
EGO_SESS_ID | .centrum.cz | Session ID, nastavuje ucet.centrum.cz, httpOnly |
EGO_APROFILE | .centrum.cz a .hn.cz | Profilový token, propagován přes SSO redirect kolečko |
Mechanismus cross-domain propagace
EGO-SSO řeší sdílení identity přes domény redirectovým řetězcem:
ucet.centrum.cz
→ Set-Cookie: EGO_SESS_ID (domain=.centrum.cz, httpOnly, secure)
sso.centrum.cz
→ Set-Cookie: EGO_APROFILE (domain=.centrum.cz)
sso.hn.cz ← Cloudflare Worker / edge endpoint
→ Set-Cookie: EGO_APROFILE (domain=.hn.cz)
www.hn.cz/users/auth/egoidentita/callback
→ Rails čte EGO_APROFILE + EGO_SESS_ID
→ POST na userinfo.centrum.cz/userinfo
sso.hn.cz je endpoint (pravděpodobně Cloudflare Worker), který přijme token z SSO redirect řetězce a nastaví EGO_APROFILE cookie na doméně .hn.cz. Bez tohoto endpointu by CMS na www.hn.cz cookie nepřečetl.
Přehled všech cookies na hn.cz
Doména: .hn.cz
├── EGO_APROFILE → EGO-SSO profilový token (nastavuje sso.hn.cz)
├── _hn_session → Folio CMS Rails session cookie (Devise)
└── remplib_* → remplib.js tracking cookies (pro Campaign/Beam)
Doména: .centrum.cz (čitelná pouze userinfo endpointem)
├── EGO_SESS_ID → EGO-SSO session (httpOnly, secure)
├── EGO_APROFILE → EGO-SSO profilový token
Doména: remp.economia.cz (admin)
├── laravel_session → REMP SSO admin session cookie
│ JWT stav je v server-side session `jwt.user`, `jwt.token`
Prostředí
| Prostředí | Auth URL | UserInfo URL | Cookie doména |
|---|---|---|---|
| Produkce | ucet.centrum.cz | userinfo.centrum.cz | .centrum.cz |
| Dev/Test | ucet.mailkafe.cz | userinfo.mailkafe.cz | .mailkafe.cz |
| Demo | ucet.mailkafe.cz | userinfo.mailkafe.cz | .ecodevel.cz |
⚠️ Bezpečnostní poznámky
stateparametr je aktuálně hardcoded jako"not_used_yet"— CSRF ochrana není implementována- Cookies
EGO_SESS_IDaEGO_APROFILEnastavuje SSO server, nikoli Rails aplikace
remplib.js na frontendu hn.cz potřebuje identifikovat uživatele pro Campaign targeting:
var rempConfig = {
userId: "<%= current_user&.remp_user_id %>",
userSubscribed: <%= user_has_active_subscription? %>,
cookieDomain: ".hn.cz",
campaign: {
url: "https://campaign.remp.economia.cz"
}
};
remplib.campaign.init(rempConfig);
Edge cases
1. Uživatel existuje v REMP CRM, ale ne ve Folio
Scénář: Legacy uživatel importovaný přímo do REMP CRM.
Řešení: Při prvním přihlášení přes Ecoidentita, Folio hledá uživatele v CRM dle ext_id (eco_user_id). Pokud nalezen, propojí. Pokud ne, vytvoří.
def sync_user_to_remp(folio_user)
response = Remp::CrmClient.find_user_by_ext_id(folio_user.eco_user_id)
if response.success?
folio_user.update!(remp_user_id: response.body["user"]["id"])
else
result = Remp::CrmClient.register_user(
email: folio_user.email,
ext_id: folio_user.eco_user_id
)
folio_user.update!(remp_user_id: result.body["user"]["id"])
end
end
2. E-mail conflict
Scénář: Uživatel v CRM má jiný e-mail než v Ecoidentita (legacy stav).
Řešení: Ecoidentita je source of truth. Při linku aktualizujeme e-mail v CRM. Log warning.
3. REMP CRM nedostupný při loginu
Scénář: CRM API timeout při user sync.
Řešení: Login proběhne (UX nesmí trpět). Sync se zopakuje přes retry mechanismus Sidekiq jobu. remp_user_id zůstane nil → entitlement check fallback na “locked”.
4. Anonymní uživatel
Scénář: Nepřihlášený čtenář.
Řešení: Žádný REMP user. remplib.js pracuje s anonymous segment. Paywall je vždy zamknutý. Campaign cílí dle anonymous pravidel.
Bezpečnost
| Aspekt | Opatření |
|---|---|
| API tokeny | Uloženy v Rails credentials / env vars, nikdy v kódu |
| Webhook signature | HMAC-SHA256 verifikace pro REMP → CMS webhooky |
| CORS | remplib.js volání Campaign – CORS headers nakonfigurovat |
| PII data | Pouze email, jméno. Žádné platební údaje do REMP CRM |
| GDPR | User deletion musí propagovat do REMP CRM (right to be forgotten) |
| Rate limiting | REMP API volání rate-limitovat na straně CMS |
GDPR: Právo na výmaz
Při požadavku na smazání uživatele (GDPR Art. 17):
- Folio CMS: Soft-delete
Folio::User - REMP CRM:
DELETE /api/v1/users/{remp_user_id}(nebo anonymizace) - REMP Mailer:
POST /api/v1/users/delete→UserDeleteApiHandler - Ecoidentita: Delete user (řeší Ecoidentita)
- Log deletion request pro audit trail