feat: add Okta sync for user and group attribute statements#12
feat: add Okta sync for user and group attribute statements#12abdelrahman-zaki wants to merge 6 commits into
Conversation
WalkthroughAdds a new PostAuthentication workflow that validates an Okta connection, parses Okta SAML attribute statements, maps attributes to Kinde user properties (joining multi-value attributes), and PATCHes the user’s properties via the Kinde API. Exposes Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant K as Kinde Auth
participant W as OktaAttributesSync
participant O as Okta (SAML)
participant A as Kinde API
U->>K: Authenticate via Okta connection
K-->>U: Authentication success
K->>W: PostAuthentication event (userId, context, assertion)
W->>W: Validate context.auth.connectionId == OKTA_CONNECTION_ID
W->>O: Read assertion.attributeStatements
O-->>W: Attributes (name -> values)
W->>W: Build attributes map & propertiesToUpdate (join multi-values)
alt propertiesToUpdate not empty
W->>A: PATCH users/{userId}/properties { propertiesToUpdate }
A-->>W: 200 OK / error
W-->>K: Log result (errors retained)
else no updates
W-->>K: Exit (no-op)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
postUserAuthentication/syncUserPhoneGoogleWorkspace.ts (1)
37-48: ConfirmworkflowSettings.iduniqueness expectations.Same note as the Okta file about
id: "postAuthentication". If this should be unique per workflow instance, rename; if it denotes the stage, you’re fine.See the repository-wide scan script in my earlier comment to review ids.
🧹 Nitpick comments (8)
postUserAuthentication/syncAttributesOkta.ts (4)
95-98: Consider accepting common Okta attribute name variants to reduce setup friction.Many Okta admins instinctively use
mobilePhoneanduserTypeas attribute Names. Supporting these as fallbacks (while keeping the documented mapping) makes the example more forgiving.Apply this diff:
- const phoneAttrNames = ["phone_number"]; - const userTypeAttrNames = ["user_type"]; - const groupsAttrNames = ["groups"]; + // Accept documented names plus common variants admins may configure + const phoneAttrNames = ["phone_number", "phone", "mobilephone"]; + const userTypeAttrNames = ["user_type", "usertype"]; + const groupsAttrNames = ["groups", "group"];
110-112: Stabilize and deduplicate groups to avoid noisy updates.Okta may return groups in varying order. Sorting and deduplicating prevents churn on every login and keeps the property stable.
Apply this diff:
- const groupsArray = getAllStrings(findAttr(groupsAttrNames)); + const groupsArray = Array.from( + new Set(getAllStrings(findAttr(groupsAttrNames))) + ).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
122-129: Best-effort property updates to reduce blast radius of partial failures.A single failed PUT currently rejects the entire Promise.all and stops the workflow. If partial updates are acceptable, switch to
Promise.allSettledso one failing property doesn’t block others.Apply this diff:
- await Promise.all([ + await Promise.allSettled([ setProp(phonePropertyKey, phoneValue), setProp(userTypePropertyKey, userTypeValue), setProp(groupsPropertyKey, groupsValue), ]);Also applies to: 131-135
113-115: Optional: Decide on behavior when attributes are absent (clear vs. retain).Currently, properties are only set when a value exists; missing attributes leave existing values untouched. If you need “true sync” semantics (e.g., clear groups when none are sent), we can add a configurable mode (via env var) to explicitly clear or unset properties when absent.
Would you like me to add a CLEAR_ON_ABSENT flag and implement property clearing safely?
postUserAuthentication/syncUserPhoneGoogleWorkspace.ts (4)
56-58: Harden the connection gating to also require the env var be set.Mirror the Okta check so the workflow does nothing if
GOOGLE_WORKSPACE_CONNECTION_IDis unset.Apply this diff:
- if (connectionId !== googleWorkspaceConnectionId) return; + if (!googleWorkspaceConnectionId || connectionId !== googleWorkspaceConnectionId) return;
71-71: Make phone extraction resilient to non-string values.Align with the Okta workflow by coercing to string before trimming.
Apply this diff:
- const phoneValue = phoneAttr?.values?.[0]?.value?.trim() || null; + const phoneValue = (phoneAttr?.values?.[0]?.value ?? "").toString().trim() || null;
79-81: URL-encode the property key in the endpoint path.Safer and consistent with the Okta workflow.
Apply this diff:
- await kindeAPI.put({ - endpoint: `users/${userId}/properties/${phonePropertyKey}?value=${encodeURIComponent(phoneValue)}` - }); + await kindeAPI.put({ + endpoint: `users/${userId}/properties/${encodeURIComponent( + phonePropertyKey + )}?value=${encodeURIComponent(phoneValue)}`, + });
50-53: Reduce duplication of SAML helper types.These types are identical to those in the Okta workflow. Consider moving them to a small shared module (e.g., postUserAuthentication/_samlTypes.ts) and importing in both files.
I can factor out a minimal shared module and update imports in both files if you’d like.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
postUserAuthentication/syncAttributesOkta.ts(1 hunks)postUserAuthentication/syncUserPhoneGoogleWorkspace.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T06:39:44.342Z
Learnt from: ages96
PR: kinde-starter-kits/workflow-examples#6
File: postUserAuthentication/impossibleTravelWorkflow.ts:0-0
Timestamp: 2025-06-19T06:39:44.342Z
Learning: In Kinde's postUserAuthentication workflows, after the OTP authentication step, event.context.user consistently returns null for fields like name and email, with only the user ID being available. Therefore, kindeAPI.get() is required to retrieve the full user details including name and email fields.
Applied to files:
postUserAuthentication/syncUserPhoneGoogleWorkspace.ts
🧬 Code Graph Analysis (2)
postUserAuthentication/syncUserPhoneGoogleWorkspace.ts (1)
postUserAuthentication/syncNewUserToHubspotWorkflow.ts (2)
handlePostAuth(45-123)extractMatchingProperties(69-79)
postUserAuthentication/syncAttributesOkta.ts (1)
postUserAuthentication/syncNewUserToHubspotWorkflow.ts (2)
handlePostAuth(45-123)extractMatchingProperties(69-79)
🔇 Additional comments (3)
postUserAuthentication/syncAttributesOkta.ts (2)
79-87: Connection gating and provider data access look correct.You correctly gate execution by
OKTA_CONNECTION_IDand early-return when SAML assertion data is missing. This prevents unnecessary API calls and avoids null dereferences.
61-72: Confirm uniqueness ofworkflowSettings.idacross workflows
We’ve identified multiple workflows sharing the sameid: "postAuthentication"which could collide at registration:
- postUserAuthentication/syncUserPhoneGoogleWorkspace.ts
- postUserAuthentication/syncAttributesOkta.ts
- postUserAuthentication/syncNewUserToHubspotWorkflow.ts
Please verify—according to the runtime’s registration logic—whether
workflowSettings.idmust be globally unique. If so, update these IDs (e.g.,"postAuthenticationOktaSync","postAuthenticationGooglePhoneSync","postAuthenticationHubspotSync") to avoid conflicts.postUserAuthentication/syncUserPhoneGoogleWorkspace.ts (1)
54-76: Good: Only uses userId from event context.Per prior learnings, only the user ID is reliable here post-OTP. Using
event.context.user.idexclusively and fetching nothing else is correct.
5c890c1 to
5b62cb6
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (6)
postUserAuthentication/syncAttributesOktaWorkflow.ts (6)
65-76: Confirm failure behavior: do you really want to block login if the sync fails?With
failurePolicy.action: "stop", an exception (e.g., Kinde API outage) will stop the workflow, which can block user sign-in depending on platform behavior. If non-critical, consider a fail-open approach.Apply if you prefer to not block auth on sync failures:
export const workflowSettings: WorkflowSettings = { id: "postAuthentication", name: "OktaAttributesSync", failurePolicy: { - action: "stop", + action: "continue", }, trigger: WorkflowTrigger.PostAuthentication, bindings: { "kinde.env": {}, url: {}, }, };
82-86: Consider adding common Okta attribute aliases as fallbacks.Many Okta setups default to
mobilePhoneanduserType. Supporting these (in addition to the normalized names) reduces setup friction.Minimal change to accept both (case-insensitive) without altering the rest of the logic:
const attributeSyncConfig = [ - { samlName: "phone_number", kindeKey: "phone_number", multiValue: false }, - { samlName: "user_type", kindeKey: "user_type", multiValue: false }, + { samlName: "phone_number", kindeKey: "phone_number", multiValue: false }, + { samlName: "mobilephone", kindeKey: "phone_number", multiValue: false }, + { samlName: "user_type", kindeKey: "user_type", multiValue: false }, + { samlName: "usertype", kindeKey: "user_type", multiValue: false }, { samlName: "groups", kindeKey: "groups", multiValue: true }, ];
88-97: Add guardrails/logging for missing connection or attributes to aid troubleshooting.Silent returns make operational debugging harder. A couple of low-noise logs help confirm why the sync skipped.
export default async function handlePostAuth(event: onPostAuthenticationEvent) { const connectionId = event.context.auth.connectionId; const oktaConnectionId = getEnvironmentVariable("OKTA_CONNECTION_ID")?.value; - if (!oktaConnectionId || connectionId !== oktaConnectionId) return; + if (!oktaConnectionId) { + console.warn("OKTA_CONNECTION_ID not set. Skipping Okta attribute sync."); + return; + } + if (connectionId !== oktaConnectionId) return; const attributeStatements = event.context.auth.provider?.data?.assertion ?.attributeStatements as SamlAttributeStatement[] | undefined; - if (!attributeStatements?.length) return; + if (!attributeStatements?.length) { + console.log("No SAML attribute statements on assertion. Nothing to sync."); + return; + }
98-112: Accumulate duplicate SAML attributes across statements and de-duplicate values.Okta can emit the same attribute in multiple statements. Current code overwrites earlier values. Merge instead to avoid data loss.
-const samlAttributesMap = (attributeStatements ?? []) - .flatMap((statement) => statement.attributes ?? []) - .reduce((acc, attr) => { - const name = attr.name?.toLowerCase().trim(); - if (name) { - const values = (attr.values ?? []) - .map((v) => v.value?.trim()) - .filter((v): v is string => !!v); - if (values.length > 0) { - acc.set(name, values); - } - } - return acc; - }, new Map<string, string[]>()); +const samlAttributesMap = (attributeStatements ?? []) + .flatMap((statement) => statement.attributes ?? []) + .reduce((acc, attr) => { + const name = attr.name?.toLowerCase().trim(); + if (name) { + const values = (attr.values ?? []) + .map((v) => v.value?.trim()) + .filter((v): v is string => !!v); + if (values.length > 0) { + const existing = acc.get(name) ?? []; + const merged = Array.from(new Set([...existing, ...values])); + acc.set(name, merged); + } + } + return acc; + }, new Map<string, string[]>());
115-124: Normalize multi-value properties and avoid duplicate group entries.Join is fine, but duplicates can creep in. Consider de-duping to keep properties tidy.
if (values && values.length > 0) { if (config.multiValue) { - propertiesToUpdate[config.kindeKey] = values.join(","); + propertiesToUpdate[config.kindeKey] = Array.from(new Set(values)).join(","); } else { propertiesToUpdate[config.kindeKey] = values[0]; } }
53-64: Reconsider exposing phone numbers in tokens unless strictly required.The comment advises toggling “Private” off so values appear in tokens. Phone numbers are PII; only expose in tokens if a downstream consumer needs them, otherwise leave as private to reduce token size and leakage risk.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
postUserAuthentication/syncAttributesOktaWorkflow.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: ages96
PR: kinde-starter-kits/workflow-examples#6
File: postUserAuthentication/impossibleTravelWorkflow.ts:0-0
Timestamp: 2025-06-19T06:39:44.342Z
Learning: In Kinde's postUserAuthentication workflows, after the OTP authentication step, event.context.user consistently returns null for fields like name and email, with only the user ID being available. Therefore, kindeAPI.get() is required to retrieve the full user details including name and email fields.
🧬 Code Graph Analysis (1)
postUserAuthentication/syncAttributesOktaWorkflow.ts (2)
postUserAuthentication/syncNewUserToHubspotWorkflow.ts (2)
handlePostAuth(45-123)extractMatchingProperties(69-79)postUserAuthentication/checkIPWithAbuseIPDBWorkflow.ts (1)
handlePostAuth(237-394)
🔇 Additional comments (2)
postUserAuthentication/syncAttributesOktaWorkflow.ts (2)
128-130: LGTM: Using onlyevent.context.user.idis correct for post-auth flows.Per prior learnings, only the user ID is reliably present at this stage; name/email often aren’t. No extra user fetch needed here.
131-134: Verify Kinde API client’s PATCH payload keyIt’s unclear from the codebase whether
kindeAPI.patchexpects its JSON payload under abody(ordata) property or if it treatsparamsas the request body. Most HTTP clients (e.g. Axios, Fetch wrappers) mapparamsto query‐string parameters and usebody(ordata) for the request payload. Ifpatchis implemented this way, your current call may be sending{ properties: … }in the URL rather than the HTTP body, causing updates to silently fail.Please confirm the
createKindeAPIcontract in@kinde/infrastructure—specifically the signature ofpatch—and adjust accordingly. For example, ifbodyis required, update:await kindeAPI.patch({ endpoint: `users/${userId}/properties`, - params: { properties: propertiesToUpdate }, + body: { properties: propertiesToUpdate }, });Key locations to review:
- postUserAuthentication/syncAttributesOktaWorkflow.ts (lines 131–134)
- existingPassword/dripFeedMigrationWorkflow.ts (lines 114–117)
DaveOrDead
left a comment
There was a problem hiding this comment.
This all looks great, can you add an entry to the Readme, and wrap the API call in a try/catch
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
postUserAuthentication/syncAttributesOktaWorkflow.ts (3)
21-24: Doc nit: grammar fix in comments.
Change “setup” → “set up” for clarity.-// * KINDE_WF_M2M_CLIENT_SECRET - Ensure this is setup with sensitive flag -// enabled to prevent accidental sharing +// * KINDE_WF_M2M_CLIENT_SECRET - Ensure this is set up with the sensitive flag +// enabled to prevent accidental sharing
65-76: Unused binding “url”.
If not required by createKindeAPI in this runtime, drop it to reduce surface area. If required, ignore this.export const workflowSettings: WorkflowSettings = { @@ trigger: WorkflowTrigger.PostAuthentication, bindings: { "kinde.env": {}, - url: {}, }, };
128-130: Guard against missing user ID.
Per prior learning, only ID is guaranteed; add a defensive check.- const kindeAPI = await createKindeAPI(event); - const userId = event.context.user.id; + const kindeAPI = await createKindeAPI(event); + const userId = event.context.user?.id; + if (!userId) return;Ensure this guard won’t mask real issues if user.id should always be present in your tenant’s flow.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
postUserAuthentication/syncAttributesOktaWorkflow.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: ages96
PR: kinde-starter-kits/workflow-examples#6
File: postUserAuthentication/impossibleTravelWorkflow.ts:0-0
Timestamp: 2025-06-19T06:39:44.342Z
Learning: In Kinde's postUserAuthentication workflows, after the OTP authentication step, event.context.user consistently returns null for fields like name and email, with only the user ID being available. Therefore, kindeAPI.get() is required to retrieve the full user details including name and email fields.
🧬 Code graph analysis (1)
postUserAuthentication/syncAttributesOktaWorkflow.ts (1)
postUserAuthentication/syncAttributesSamlWorkflow.ts (4)
handlePostAuth(73-127)acc(84-95)statement(83-83)v(88-88)
🔇 Additional comments (4)
postUserAuthentication/syncAttributesOktaWorkflow.ts (4)
1-7: Imports look correct and minimal.
93-97: SAML attribute extraction is defensively coded. LGTM.
98-112: Attribute normalization and filtering look solid.
Lowercasing names and trimming values prevents common mismatches.
126-126: Early exit when nothing to update is good.
| type SamlValue = { value?: string }; | ||
| type SamlAttribute = { name?: string; values?: SamlValue[] }; | ||
| type SamlAttributeStatement = { attributes?: SamlAttribute[] }; | ||
|
|
||
| const attributeSyncConfig = [ | ||
| { samlName: "phone_number", kindeKey: "phone_number", multiValue: false }, | ||
| { samlName: "user_type", kindeKey: "user_type", multiValue: false }, | ||
| { samlName: "groups", kindeKey: "groups", multiValue: true }, | ||
| ]; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Type the mapping and allow multiple SAML names per property.
Prevents typos, enables future aliasing (e.g., “mobilePhone” vs “phone_number”), and centralizes normalization.
type SamlValue = { value?: string };
type SamlAttribute = { name?: string; values?: SamlValue[] };
type SamlAttributeStatement = { attributes?: SamlAttribute[] };
-const attributeSyncConfig = [
- { samlName: "phone_number", kindeKey: "phone_number", multiValue: false },
- { samlName: "user_type", kindeKey: "user_type", multiValue: false },
- { samlName: "groups", kindeKey: "groups", multiValue: true },
-];
+type AttributeSyncMapping = {
+ samlNames: string[]; // all names will be normalized to lowercase
+ kindeKey: string;
+ multiValue: boolean;
+};
+
+const attributeSyncConfig: readonly AttributeSyncMapping[] = [
+ { samlNames: ["phone_number"], kindeKey: "phone_number", multiValue: false },
+ { samlNames: ["user_type"], kindeKey: "user_type", multiValue: false },
+ { samlNames: ["groups"], kindeKey: "groups", multiValue: true },
+] as const;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type SamlValue = { value?: string }; | |
| type SamlAttribute = { name?: string; values?: SamlValue[] }; | |
| type SamlAttributeStatement = { attributes?: SamlAttribute[] }; | |
| const attributeSyncConfig = [ | |
| { samlName: "phone_number", kindeKey: "phone_number", multiValue: false }, | |
| { samlName: "user_type", kindeKey: "user_type", multiValue: false }, | |
| { samlName: "groups", kindeKey: "groups", multiValue: true }, | |
| ]; | |
| type SamlValue = { value?: string }; | |
| type SamlAttribute = { name?: string; values?: SamlValue[] }; | |
| type SamlAttributeStatement = { attributes?: SamlAttribute[] }; | |
| type AttributeSyncMapping = { | |
| samlNames: string[]; // all names will be normalized to lowercase | |
| kindeKey: string; | |
| multiValue: boolean; | |
| }; | |
| const attributeSyncConfig: readonly AttributeSyncMapping[] = [ | |
| { samlNames: ["phone_number"], kindeKey: "phone_number", multiValue: false }, | |
| { samlNames: ["user_type"], kindeKey: "user_type", multiValue: false }, | |
| { samlNames: ["groups"], kindeKey: "groups", multiValue: true }, | |
| ] as const; |
🤖 Prompt for AI Agents
In postUserAuthentication/syncAttributesOktaWorkflow.ts around lines 78 to 86,
the attributeSyncConfig is currently untyped and only supports a single samlName
per entry; change it to a typed mapping that allows samlNames: string[] (e.g.,
interface AttributeMapping { samlNames: string[]; kindeKey: string; multiValue:
boolean }) and update the config entries to use samlNames arrays (allowing
aliases), then update the code that reads attributes to iterate/scan all
samlNames when resolving a Kinde key (pick first present SAML attribute or merge
values for multiValue), and adjust types/usages accordingly so TypeScript
enforces the new shape and prevents typos.
| export default async function handlePostAuth(event: onPostAuthenticationEvent) { | ||
| const connectionId = event.context.auth.connectionId; | ||
| const oktaConnectionId = getEnvironmentVariable("OKTA_CONNECTION_ID")?.value; | ||
| if (!oktaConnectionId || connectionId !== oktaConnectionId) return; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Guard on SAML protocol before doing any work.
Avoids unnecessary processing on non-SAML providers.
export default async function handlePostAuth(event: onPostAuthenticationEvent) {
+ if (event.context?.auth?.provider?.protocol !== "saml") return;
const connectionId = event.context.auth.connectionId;
const oktaConnectionId = getEnvironmentVariable("OKTA_CONNECTION_ID")?.value;
if (!oktaConnectionId || connectionId !== oktaConnectionId) return;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export default async function handlePostAuth(event: onPostAuthenticationEvent) { | |
| const connectionId = event.context.auth.connectionId; | |
| const oktaConnectionId = getEnvironmentVariable("OKTA_CONNECTION_ID")?.value; | |
| if (!oktaConnectionId || connectionId !== oktaConnectionId) return; | |
| export default async function handlePostAuth(event: onPostAuthenticationEvent) { | |
| if (event.context?.auth?.provider?.protocol !== "saml") return; | |
| const connectionId = event.context.auth.connectionId; | |
| const oktaConnectionId = getEnvironmentVariable("OKTA_CONNECTION_ID")?.value; | |
| if (!oktaConnectionId || connectionId !== oktaConnectionId) return; |
🤖 Prompt for AI Agents
In postUserAuthentication/syncAttributesOktaWorkflow.ts around lines 88-92, add
an early return that guards on the authentication protocol being SAML before
proceeding; specifically, inspect the protocol on the incoming event (prefer
event.context.connection?.protocol and fall back to event.context.protocol) and
if the value is not 'saml2' or 'saml' return immediately to avoid running
Okta-specific logic for non-SAML providers.
|
Wrapped the API call in a try catch, updated the id, and switched the failurePolicy from |
Explain your changes
This workflow syncs a user's mobilePhone, userType, and groups from Okta SAML attribute statements into the phone_number, user_type, and groups properties in Kinde after authentication.
Checklist
🛟 If you need help, consider asking for advice over in the Kinde community.
Summary by CodeRabbit