@onaio/akuko-embed-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ # Path D — Keycloak Identity Brokering
2
+
3
+ **Use this recipe when:** your users authenticate via an identity provider other than Akuko's Keycloak realm. Examples:
4
+ - Another Keycloak realm (e.g., `onadata-realm`, either on the same Keycloak instance as Akuko or a different one)
5
+ - Auth0 / Azure AD / Microsoft Entra ID / Okta / Google Workspace / AWS Cognito
6
+ - Your own OIDC-compliant identity server
7
+ - A SAML 2.0 enterprise IdP (ADFS, Ping, etc.)
8
+
9
+ Path D works cross-domain by design (uses OIDC redirects, not cookie sharing), so your app and Akuko can live on entirely separate domains.
10
+
11
+ > **Ops engineers:** for an automated reference of these exact Keycloak steps (idempotent `kcadm.sh` commands you can adapt to your realm), see [`../examples/keycloak-path-d-setup.sh`](../examples/keycloak-path-d-setup.sh) in this package.
12
+
13
+ ## What you don't need to change
14
+
15
+ - **No code changes in Akuko** — Path D is entirely Keycloak configuration.
16
+ - **No code changes in your host app** — your existing OIDC/SAML login flow keeps working unchanged.
17
+ - **No need to share cookies, domains, or Keycloak instances.**
18
+
19
+ ## One-time setup (Akuko side, per client)
20
+
21
+ An admin with access to Akuko's Keycloak realm (`akuko-realm`) registers your IdP as a brokered provider.
22
+
23
+ ### Step 1: Add the Identity Provider
24
+
25
+ 1. In `akuko-realm` admin console → Identity Providers → Add Provider → **OpenID Connect v1.0** (or **SAML v2.0** for SAML clients).
26
+ 2. **Alias** — a short identifier, e.g., `onadata`, `acme-auth0`, `customer-azure`.
27
+ 3. **Redirect URI** — copy this; you'll give it to the upstream IdP in Step 2.
28
+
29
+ ### Step 2: Register the discovery
30
+
31
+ **For OIDC providers:**
32
+ - Paste your IdP's issuer URL (`https://your-idp.example/realms/your-realm` or `https://your-tenant.auth0.com/` etc.).
33
+ - Keycloak auto-discovers endpoints from `/.well-known/openid-configuration`.
34
+ - Client ID and Client Secret — create an OIDC client on your IdP with the redirect URI from Step 1; supply the credentials here.
35
+
36
+ **For SAML providers:**
37
+ - Paste the upstream IdP's SAML metadata URL (or upload the metadata XML).
38
+ - Keycloak configures request signing and response validation from the metadata.
39
+
40
+ ### Step 3: Configure claim mappers
41
+
42
+ For OIDC (similar idea for SAML with attribute mappers):
43
+
44
+ **Email** (essential):
45
+ - Add an "Attribute Importer" mapper.
46
+ - Claim: `email` (or the equivalent attribute in SAML).
47
+ - User attribute: `email`.
48
+ - Sync mode: `Force` (refresh on every login).
49
+
50
+ Without this, users will log in but Akuko won't know their email, and RLS rules won't match.
51
+
52
+ **Groups** (optional, enables group-based RLS):
53
+ - Add a "Claim to Group" mapper (or "Attribute to Group" for SAML).
54
+ - Claim: `groups` (Auth0, Azure AD, Keycloak realms all support this with configuration).
55
+ - Configure how upstream group names translate to akuko-realm groups — either by name match or explicit mapping.
56
+
57
+ If your IdP doesn't emit groups, skip this — user-level RLS rules still work.
58
+
59
+ ### Step 4: Enable auto-link on email (essential for pre-invited users)
60
+
61
+ 1. Authentication → Flows → duplicate the "First Broker Login" flow so you can edit it.
62
+ 2. In the copy, find the **"Detect Existing Broker User"** step.
63
+ 3. Set its requirement to **REQUIRED** (or at least ALTERNATIVE that auto-links on a confirmed email match, depending on your security posture).
64
+ 4. Attach the copy to your brokered IdP → "First Login Flow" setting.
65
+
66
+ Effect: when a user arrives via brokering and an `akuko-realm` user with the same email already exists (e.g., because an Akuko admin pre-invited them), Keycloak links the brokered identity to that existing account instead of creating a duplicate.
67
+
68
+ Without this step, every invited user will become a separate user in `akuko-realm` from their brokered identity, breaking RLS matching.
69
+
70
+ ### Step 5: Tell the SDK which IdP alias to use
71
+
72
+ The IdP alias you configured in Step 1 ("Alias" field — e.g., `onadata`, `acme-auth0`) is what your host code passes to the SDK as `idpHint`. The SDK appends it to the iframe URL; Akuko's embed page forwards it to Keycloak as `kc_idp_hint`.
73
+
74
+ Example (React):
75
+
76
+ ```tsx
77
+ <AkukoDashboard
78
+ dashboardId="..."
79
+ idpHint="onadata" // matches the alias from Step 1
80
+ />
81
+ ```
82
+
83
+ Example (vanilla):
84
+
85
+ ```js
86
+ import { AkukoDashboard } from "@onaio/akuko-embed-sdk";
87
+
88
+ new AkukoDashboard({
89
+ container: '#mount',
90
+ dashboardId: '...',
91
+ idpHint: 'onadata',
92
+ });
93
+ ```
94
+
95
+ With `idpHint` set, a first-time user never sees a broker-selection UI — they go straight from their host app → Akuko iframe → onadata-realm → back → dashboard. Silent federation.
96
+
97
+ If your host has multiple user populations (some come from Auth0, some from Azure), pass a different `idpHint` value per user based on your own context.
98
+
99
+ ### Step 6: Test
100
+
101
+ 1. Open a private/incognito browser window.
102
+ 2. Hit an Akuko dashboard URL where your test user should have RLS rules.
103
+ 3. Expect: a redirect flash to your IdP → immediate return to Akuko (your IdP has an active session, or prompts for login).
104
+ 4. Verify the dashboard renders with filters applied.
105
+
106
+ ## Per-client configuration checklist
107
+
108
+ When onboarding a new integrating client, confirm all these:
109
+
110
+ - [ ] OIDC or SAML provider registered in `akuko-realm`
111
+ - [ ] Email claim mapper configured and set to `Force` sync mode
112
+ - [ ] (Optional) Groups claim mapper configured and tested
113
+ - [ ] "Detect Existing Broker User" enabled on the First Broker Login flow
114
+ - [ ] Test login from upstream IdP succeeds and lands in Akuko as the expected user
115
+ - [ ] (If pre-invited) invited user account is linked, not duplicated
116
+ - [ ] (If group rules exist) test user with group membership sees group-scoped data
117
+
118
+ ## Example: onadata + Akuko (same Keycloak, different realms)
119
+
120
+ Given `onadata-realm` and `akuko-realm` on the same Keycloak instance at `https://keycloak.example.internal`:
121
+
122
+ - **Issuer URL:** `https://keycloak.example.internal/realms/onadata`
123
+ - **Client ID / Secret:** create an OIDC client in `onadata-realm` named e.g. `akuko-broker-client`; supply credentials.
124
+ - **Email claim:** `email` (Keycloak emits this by default if the realm's OIDC client has `email` in its Default Client Scopes).
125
+ - **Groups claim:** requires `onadata-realm` to emit a `groups` claim — configure the client's protocol mapper if not present.
126
+ - **Auto-link:** enable per Step 4.
127
+
128
+ After this, an onadata user signs in to their onadata app normally, and when they open an Akuko dashboard from there they're authenticated silently (one redirect flash, no password prompt).
129
+
130
+ For an automated reference of these exact Keycloak steps (creating the IdP, the email Attribute Importer, and the auto-link First Broker Login flow), see [`../examples/keycloak-path-d-setup.sh`](../examples/keycloak-path-d-setup.sh) in this package and adapt it to your realm.
131
+
132
+ ## Example: external SaaS using Auth0
133
+
134
+ Given a customer runs their app on Auth0 at `https://customer.auth0.com`:
135
+
136
+ - **Issuer URL:** `https://customer.auth0.com/`
137
+ - **Client ID / Secret:** create an Application in Auth0 of type "Regular Web Application"; get the credentials; add Akuko's redirect URI to Auth0's allowed callback URLs.
138
+ - **Email claim:** `email` (Auth0 emits by default with the `openid email` scopes).
139
+ - **Groups claim:** if their Auth0 emits groups via a custom rule or the Authorization extension, map the `groups` claim.
140
+ - **Auto-link:** enable per Step 4.
141
+
142
+ Same flow as onadata from there.
143
+
144
+ ## Troubleshooting
145
+
146
+ | Symptom | Likely cause |
147
+ |---|---|
148
+ | User lands on Akuko but RLS rules don't apply | Email claim mapper not configured or not in `Force` sync mode; Akuko sees empty email |
149
+ | Brokered user shows up as a new user every login | "Detect Existing Broker User" not enabled on First Broker Login flow |
150
+ | User gets a password prompt instead of silent redirect | Upstream IdP session expired or cookies blocked; not a brokering bug |
151
+ | Group rules don't apply for brokered users | Groups claim not emitted by upstream, or claim mapper missing/misconfigured |
152
+ | Infinite redirect loop | Client credentials wrong, or redirect URI mismatch in upstream IdP's allow-list |
153
+
154
+ ## Scope boundary
155
+
156
+ Path D is the responsibility of whoever administers Akuko's Keycloak. Individual dashboard owners don't need to understand any of this — once the broker is set up for a given client, that client's users show up as regular users in Akuko's Share modal (searchable by email), and RLS configuration for them works identically to native users.
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Reference only — adapt to your Keycloak. This is a sanitized, parameterized
4
+ # walkthrough of the Path D identity-brokering setup described in
5
+ # ../docs/sso-path-d-brokering.md, expressed as idempotent `kcadm.sh` calls.
6
+ #
7
+ # It configures Akuko's Keycloak realm to trust an UPSTREAM OIDC identity
8
+ # provider (your IdP) so users who authenticate there can open embedded Akuko
9
+ # dashboards via silent federation (the SDK's `idpHint` option). It:
10
+ # 1. authenticates to the Akuko Keycloak admin API
11
+ # 2. clones the "First Broker Login" flow with the verification pages disabled
12
+ # 3. creates an OIDC Identity Provider aliased "$IDP_ALIAS"
13
+ # 4. attaches an email Attribute Importer mapper (Force sync)
14
+ # 5. adds the idp-auto-link authenticator so brokered users auto-link to an
15
+ # existing Akuko user by matching email — no confirm/verify page (those
16
+ # can't render inside the cross-origin analytics iframe)
17
+ #
18
+ # Do NOT run this as-is. Fill in the placeholders below for your environment.
19
+ # Akuko's own local development wires an equivalent broker automatically via a
20
+ # make target, so this script is only a reference for external operators.
21
+ #
22
+ set -euo pipefail
23
+
24
+ # ── Configure these for your environment ───────────────────────────────────
25
+ # Akuko Keycloak admin API + realm:
26
+ KC_ADMIN_URL="${KC_ADMIN_URL:?set to your Akuko Keycloak base URL, e.g. https://keycloak.example.com}"
27
+ KC_ADMIN_USER="${KC_ADMIN_USER:?Keycloak admin username}"
28
+ KC_ADMIN_PASS="${KC_ADMIN_PASS:?Keycloak admin password}"
29
+ AKUKO_REALM="${AKUKO_REALM:-akuko}"
30
+
31
+ # The brokered IdP, as the SDK's `idpHint` value:
32
+ IDP_ALIAS="${IDP_ALIAS:?short alias for the brokered IdP, e.g. onadata}"
33
+
34
+ # Upstream OIDC provider (your IdP). The issuer is required; the per-endpoint
35
+ # URLs default to standard Keycloak conventions derived from it. For a NON-
36
+ # Keycloak IdP (Auth0, Azure AD, Okta, …) override each from your IdP's
37
+ # /.well-known/openid-configuration.
38
+ UPSTREAM_ISSUER_URL="${UPSTREAM_ISSUER_URL:?upstream IdP issuer, e.g. https://idp.example.com/realms/your-realm}"
39
+ UPSTREAM_AUTH_URL="${UPSTREAM_AUTH_URL:-$UPSTREAM_ISSUER_URL/protocol/openid-connect/auth}"
40
+ UPSTREAM_TOKEN_URL="${UPSTREAM_TOKEN_URL:-$UPSTREAM_ISSUER_URL/protocol/openid-connect/token}"
41
+ UPSTREAM_USERINFO_URL="${UPSTREAM_USERINFO_URL:-$UPSTREAM_ISSUER_URL/protocol/openid-connect/userinfo}"
42
+ UPSTREAM_JWKS_URL="${UPSTREAM_JWKS_URL:-$UPSTREAM_ISSUER_URL/protocol/openid-connect/certs}"
43
+ UPSTREAM_LOGOUT_URL="${UPSTREAM_LOGOUT_URL:-$UPSTREAM_ISSUER_URL/protocol/openid-connect/logout}"
44
+
45
+ # OIDC client you created on the upstream IdP, with redirect URI
46
+ # $KC_ADMIN_URL/realms/$AKUKO_REALM/broker/$IDP_ALIAS/endpoint
47
+ BROKER_CLIENT_ID="${BROKER_CLIENT_ID:?OIDC client id registered on the upstream IdP}"
48
+ BROKER_CLIENT_SECRET="${BROKER_CLIENT_SECRET:?the secret for that OIDC client}"
49
+
50
+ # kcadm.sh must be on PATH (ships in the Keycloak distribution under bin/).
51
+ KCADM="${KCADM:-kcadm.sh}"
52
+ FBL_FLOW="first broker login - ${IDP_ALIAS}"
53
+
54
+ # kcadm flow paths need spaces percent-encoded.
55
+ enc_spaces() { printf '%s' "$1" | sed 's/ /%20/g'; }
56
+
57
+ # ── Step 0: authenticate ───────────────────────────────────────────────────
58
+ "$KCADM" config credentials --server "$KC_ADMIN_URL" \
59
+ --realm master --user "$KC_ADMIN_USER" --password "$KC_ADMIN_PASS"
60
+
61
+ # ── Step 1: clone "First Broker Login" (before IdP create — Keycloak validates
62
+ # firstBrokerLoginFlowAlias against existing flows at create time) ──────────
63
+ if "$KCADM" get authentication/flows -r "$AKUKO_REALM" \
64
+ --fields alias --format csv --noquotes 2>/dev/null | grep -qx "$FBL_FLOW"; then
65
+ echo " cloned flow exists already"
66
+ else
67
+ "$KCADM" create "authentication/flows/first%20broker%20login/copy" \
68
+ -r "$AKUKO_REALM" -s "newName=$FBL_FLOW"
69
+ echo " cloned First Broker Login → '$FBL_FLOW'"
70
+ fi
71
+
72
+ # ── Step 2: create or update the OIDC Identity Provider ────────────────────
73
+ IDP_PAYLOAD=$(cat <<JSON
74
+ {
75
+ "alias": "$IDP_ALIAS",
76
+ "displayName": "$IDP_ALIAS",
77
+ "providerId": "oidc",
78
+ "enabled": true,
79
+ "trustEmail": true,
80
+ "firstBrokerLoginFlowAlias": "$FBL_FLOW",
81
+ "config": {
82
+ "clientId": "$BROKER_CLIENT_ID",
83
+ "clientSecret": "$BROKER_CLIENT_SECRET",
84
+ "clientAuthMethod": "client_secret_post",
85
+ "issuer": "$UPSTREAM_ISSUER_URL",
86
+ "authorizationUrl": "$UPSTREAM_AUTH_URL",
87
+ "tokenUrl": "$UPSTREAM_TOKEN_URL",
88
+ "userInfoUrl": "$UPSTREAM_USERINFO_URL",
89
+ "jwksUrl": "$UPSTREAM_JWKS_URL",
90
+ "logoutUrl": "$UPSTREAM_LOGOUT_URL",
91
+ "defaultScope": "openid profile email",
92
+ "syncMode": "FORCE",
93
+ "useJwksUrl": "true"
94
+ }
95
+ }
96
+ JSON
97
+ )
98
+ if "$KCADM" get "identity-provider/instances/$IDP_ALIAS" -r "$AKUKO_REALM" >/dev/null 2>&1; then
99
+ echo " IdP '$IDP_ALIAS' exists; updating"
100
+ "$KCADM" update "identity-provider/instances/$IDP_ALIAS" -r "$AKUKO_REALM" -f - <<<"$IDP_PAYLOAD"
101
+ else
102
+ echo " creating IdP '$IDP_ALIAS'"
103
+ "$KCADM" create identity-provider/instances -r "$AKUKO_REALM" -f - <<<"$IDP_PAYLOAD"
104
+ fi
105
+
106
+ # ── Step 3: email Attribute Importer mapper (Force sync) ────────────────────
107
+ # Essential — Akuko matches brokered users to existing accounts by email.
108
+ EMAIL_MAPPER=$(cat <<JSON
109
+ {
110
+ "name": "email",
111
+ "identityProviderAlias": "$IDP_ALIAS",
112
+ "identityProviderMapper": "oidc-user-attribute-idp-mapper",
113
+ "config": { "claim": "email", "user.attribute": "email", "syncMode": "FORCE" }
114
+ }
115
+ JSON
116
+ )
117
+ if ! "$KCADM" create "identity-provider/instances/$IDP_ALIAS/mappers" \
118
+ -r "$AKUKO_REALM" -f - <<<"$EMAIL_MAPPER" 2>/dev/null; then
119
+ echo " (email mapper exists)"
120
+ fi
121
+
122
+ # ── Step 4: disable verification pages in the cloned flow ──────────────────
123
+ # Review Profile + Handle Existing Account render HTML that cannot display in a
124
+ # cross-origin embed iframe; disable them so brokered login proceeds to silent
125
+ # linking. Create User If Unique stays ALTERNATIVE so new users are still made.
126
+ EXEC_PATH="authentication/flows/$(enc_spaces "$FBL_FLOW")/executions"
127
+ ROWS="$("$KCADM" get "$EXEC_PATH" -r "$AKUKO_REALM" \
128
+ --format csv --fields id,displayName --noquotes 2>/dev/null || true)"
129
+ while IFS=',' read -r exec_id display_name; do
130
+ [[ -z "$exec_id" || "$exec_id" == "id" ]] && continue
131
+ lower="$(printf '%s' "$display_name" | tr '[:upper:]' '[:lower:]')"
132
+ requirement=""
133
+ case "$lower" in
134
+ *"review profile"*) requirement="DISABLED" ;;
135
+ *"handle existing account"*) requirement="DISABLED" ;;
136
+ *"create user if unique"*) requirement="ALTERNATIVE" ;;
137
+ esac
138
+ if [[ -n "$requirement" ]]; then
139
+ "$KCADM" update "$EXEC_PATH" -r "$AKUKO_REALM" \
140
+ -b "{\"id\":\"$exec_id\",\"requirement\":\"$requirement\"}" </dev/null 2>/dev/null \
141
+ && echo " flow: '$display_name' → $requirement" \
142
+ || echo " flow: '$display_name' update FAILED (continuing)"
143
+ fi
144
+ done <<<"$ROWS"
145
+
146
+ # ── Step 5: add idp-auto-link so brokered users link by email silently ─────
147
+ LINK_SUBFLOW="authentication/flows/$(enc_spaces "$FBL_FLOW User creation or linking")/executions/execution"
148
+ if "$KCADM" get "$EXEC_PATH" -r "$AKUKO_REALM" --fields providerId 2>/dev/null | grep -q "idp-auto-link"; then
149
+ echo " flow: 'Automatically set existing user' present already"
150
+ else
151
+ "$KCADM" create "$LINK_SUBFLOW" -r "$AKUKO_REALM" \
152
+ -b '{"provider":"idp-auto-link"}' </dev/null 2>/dev/null \
153
+ && echo " flow: added idp-auto-link" \
154
+ || echo " flow: add idp-auto-link FAILED (continuing)"
155
+ fi
156
+ # idp-auto-link lands DISABLED on create — promote it to ALTERNATIVE.
157
+ AUTO_LINK_ID="$("$KCADM" get "$EXEC_PATH" -r "$AKUKO_REALM" \
158
+ --format csv --fields id,providerId --noquotes 2>/dev/null \
159
+ | awk -F',' '$2=="idp-auto-link"{print $1}' | head -1)"
160
+ if [[ -n "$AUTO_LINK_ID" ]]; then
161
+ "$KCADM" update "$EXEC_PATH" -r "$AKUKO_REALM" \
162
+ -b "{\"id\":\"$AUTO_LINK_ID\",\"requirement\":\"ALTERNATIVE\"}" </dev/null 2>/dev/null \
163
+ && echo " flow: idp-auto-link → ALTERNATIVE" \
164
+ || echo " flow: idp-auto-link requirement update FAILED (continuing)"
165
+ fi
166
+
167
+ cat <<EOF
168
+ ✓ Path D broker configured.
169
+ Akuko realm : $AKUKO_REALM
170
+ IdP alias : $IDP_ALIAS (pass to the SDK as idpHint="$IDP_ALIAS")
171
+ redirect URI : $KC_ADMIN_URL/realms/$AKUKO_REALM/broker/$IDP_ALIAS/endpoint
172
+ (must be allow-listed on the upstream IdP's OIDC client)
173
+ EOF
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@onaio/akuko-embed-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic embed SDK for Akuko dashboards",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "author": "Ona (https://ona.io)",
8
+ "homepage": "https://github.com/onaio/akuko-app/tree/develop/packages/embed-sdk#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/onaio/akuko-app.git",
12
+ "directory": "packages/embed-sdk"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "main": "./dist/akuko-embed-sdk.umd.cjs",
18
+ "module": "./dist/akuko-embed-sdk.js",
19
+ "types": "./dist/index.d.ts",
20
+ "files": [
21
+ "dist",
22
+ "docs",
23
+ "examples",
24
+ "README.md"
25
+ ],
26
+ "devDependencies": {
27
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
28
+ "@typescript-eslint/parser": "^8.56.1",
29
+ "@vitest/coverage-v8": "4.0.18",
30
+ "eslint": "^8.57.1",
31
+ "jsdom": "^26.1.0",
32
+ "typescript": "^5.7.3",
33
+ "vite": "^7.3.1",
34
+ "vitest": "^4.0.18"
35
+ },
36
+ "scripts": {
37
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.build.json",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint src --max-warnings 0"
42
+ }
43
+ }