-
Notifications
You must be signed in to change notification settings - Fork 10
Add workflows to dynamically capture IdP claims and inject into tokens #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Koosha-Owji
wants to merge
3
commits into
kinde-starter-kits:main
Choose a base branch
from
Koosha-Owji:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
captureIdpClaimsToTokens/AddIdpClaimsToTokensWorkflow.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { | ||
| onTokensGenerationEvent, | ||
| WorkflowSettings, | ||
| WorkflowTrigger, | ||
| accessTokenCustomClaims, | ||
| idTokenCustomClaims, | ||
| createKindeAPI, | ||
| } from "@kinde/infrastructure"; | ||
|
|
||
| /** | ||
| * Add IdP Claims to Tokens Workflow | ||
| * | ||
| * This workflow reads IdP claims that were captured during authentication | ||
| * (stored in the `idp_claims` user property) and adds them to tokens. | ||
| * | ||
| * This workflow works in conjunction with CaptureIdpClaimsWorkflow: | ||
| * 1. CaptureIdpClaimsWorkflow (PostAuthentication) - captures IdP claims and stores them | ||
| * 2. This workflow (TokensGeneration) - reads stored claims and adds them to tokens | ||
| * | ||
| * Benefits: | ||
| * - Automatically includes ALL IdP claims in tokens (no hardcoding needed) | ||
| * - Claims persist across token refreshes | ||
| * - Easy to customize which claims go into which tokens | ||
| * - Works with any OAuth2/OIDC provider | ||
| * | ||
| * Setup: | ||
| * 1. Create a Machine-to-Machine (M2M) application in Kinde with this scope: | ||
| * - read:user_properties | ||
| * | ||
| * 2. Add these environment variables to your workflow: | ||
| * - KINDE_WF_M2M_CLIENT_ID (from your M2M app) | ||
| * - KINDE_WF_M2M_CLIENT_SECRET (from your M2M app, mark as sensitive) | ||
| * | ||
| * 3. Deploy the CaptureIdpClaimsWorkflow first (PostAuthentication trigger) | ||
| * 4. Deploy this workflow (TokensGeneration trigger) | ||
| * 5. Authenticate via a social connection to test | ||
| * 6. Your tokens will include all captured IdP claims with "idp_" prefix | ||
| * | ||
| * Example token claims after deployment: | ||
| * - idp_email: user's email from IdP | ||
| * - idp_name: user's full name from IdP | ||
| * - idp_sub: user's unique ID at the IdP | ||
| * - idp_picture: user's profile picture URL | ||
| * - Plus any other claims provided by the IdP | ||
| * | ||
| * Note: This workflow runs on EVERY token generation, not just initial authentication. | ||
| * The claims are fetched from stored user properties via Management API. | ||
| * | ||
| * Trigger: user:tokens_generation | ||
| */ | ||
|
|
||
| export const workflowSettings: WorkflowSettings = { | ||
| id: "addIdpClaimsToTokens", | ||
| name: "Add IdP Claims to Tokens", | ||
| trigger: WorkflowTrigger.UserTokenGeneration, | ||
| failurePolicy: { | ||
| action: "stop", | ||
| }, | ||
| bindings: { | ||
| "kinde.accessToken": { | ||
| audience: [], | ||
| }, | ||
| "kinde.idToken": {}, | ||
| "kinde.env": {}, | ||
| "url": {}, | ||
| }, | ||
| }; | ||
|
|
||
| export default async function handleTokensGeneration(event: onTokensGenerationEvent) { | ||
| // Get user ID from the event | ||
| const userId = event.user?.id || event.context?.user?.id; | ||
|
|
||
| if (!userId) { | ||
| console.error("User ID is missing from event"); | ||
| return; | ||
| } | ||
|
|
||
| // Create the Kinde API client to fetch user properties | ||
| const kindeAPI = await createKindeAPI(event); | ||
|
|
||
| // Fetch user properties via Management API | ||
| let userProperties: Record<string, any> = {}; | ||
|
|
||
| try { | ||
| const propertiesResponse = await kindeAPI.get({ | ||
| endpoint: `users/${userId}/properties`, | ||
| }); | ||
|
|
||
| // The response structure is: { data: { properties: [...] } } | ||
| const properties = propertiesResponse?.data?.properties || propertiesResponse?.properties; | ||
|
|
||
| // Convert properties array to a key-value object | ||
| if (properties && Array.isArray(properties)) { | ||
| for (const prop of properties) { | ||
| userProperties[prop.key] = prop.value; | ||
| } | ||
| } | ||
| } catch (error: any) { | ||
| console.error("Failed to fetch user properties:", error?.message || error); | ||
| return; | ||
| } | ||
|
|
||
| // Check if the idp_claims property exists | ||
| if (!userProperties.idp_claims) { | ||
| // User hasn't authenticated via IdP or CaptureIdpClaimsWorkflow isn't deployed | ||
| return; | ||
| } | ||
|
|
||
| // Parse the JSON from the idp_claims property | ||
| let idpClaims: Record<string, any>; | ||
| try { | ||
| idpClaims = JSON.parse(userProperties.idp_claims as string); | ||
| } catch (error) { | ||
| console.error("Failed to parse idp_claims property:", error); | ||
| return; | ||
| } | ||
|
|
||
| // Initialize token custom claims with dynamic types | ||
| const accessToken = accessTokenCustomClaims<Record<string, any>>(); | ||
| const idToken = idTokenCustomClaims<Record<string, any>>(); | ||
|
|
||
| // Add all IdP claims to tokens with "idp_" prefix | ||
| // Skip metadata fields (those starting with "_") | ||
| let claimsAdded = 0; | ||
|
|
||
| for (const [claimName, claimValue] of Object.entries(idpClaims)) { | ||
| // Skip metadata fields like _provider and _last_updated | ||
| if (claimName.startsWith('_')) { | ||
| continue; | ||
| } | ||
|
|
||
| // Add to both access and ID tokens with "idp_" prefix | ||
| const tokenClaimName = `idp_${claimName}`; | ||
| accessToken[tokenClaimName] = claimValue; | ||
| idToken[tokenClaimName] = claimValue; | ||
|
|
||
| claimsAdded++; | ||
| } | ||
|
|
||
| if (claimsAdded > 0) { | ||
| console.log(`Added ${claimsAdded} IdP claims to tokens (provider: ${idpClaims._provider || 'unknown'})`); | ||
| } | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| /** | ||
| * Capture IdP Claims Workflow | ||
| * | ||
| * This workflow captures ALL claims from social identity providers (Google, Microsoft, etc.) | ||
| * and stores them in a single user property as JSON. | ||
| * | ||
| * This workflow works in conjunction with AddIdpClaimsToTokensWorkflow: | ||
| * 1. This workflow (PostAuthentication) - captures and stores IdP claims | ||
| * 2. AddIdpClaimsToTokensWorkflow (TokensGeneration) - reads stored claims and adds them to tokens | ||
| * | ||
| * Benefits: | ||
| * - Automatically captures all IdP claims without hardcoding specific claim names | ||
| * - Only creates ONE property instead of many individual properties | ||
| * - Preserves original IdP token structure and values | ||
| * - New claims from IdP are automatically captured without code changes | ||
| * | ||
| * Setup: | ||
| * 1. Create a Machine-to-Machine (M2M) application in Kinde with these scopes: | ||
| * - read:properties | ||
| * - create:properties | ||
| * - update:user_properties | ||
| * | ||
| * 2. Add these environment variables to your workflow: | ||
| * - KINDE_WF_M2M_CLIENT_ID (from your M2M app) | ||
| * - KINDE_WF_M2M_CLIENT_SECRET (from your M2M app, mark as sensitive) | ||
| * | ||
| * 3. Get your property category ID: | ||
| * - Go to Settings → Properties → Categories in Kinde dashboard | ||
| * - Create a category (e.g., "Identity Provider") or use an existing one | ||
| * - Update the PROPERTY_CATEGORY_ID constant below with your category ID | ||
| * | ||
| * 4. Deploy this workflow | ||
| * 5. Deploy the AddIdpClaimsToTokensWorkflow (companion workflow) | ||
| * 6. Authenticate via a social connection to test | ||
| * | ||
| * Supported Providers: | ||
| * - Any OAuth2/OIDC provider (Google, Microsoft, GitHub, etc.) | ||
| * | ||
| * Trigger: user:post_authentication | ||
| */ | ||
|
|
||
| import { | ||
| WorkflowSettings, | ||
| WorkflowTrigger, | ||
| createKindeAPI, | ||
| } from "@kinde/infrastructure"; | ||
|
|
||
| export const workflowSettings: WorkflowSettings = { | ||
| id: "captureIdpClaimsJson", | ||
| name: "Capture IdP Claims as JSON", | ||
| failurePolicy: { | ||
| action: "stop", | ||
| }, | ||
| trigger: WorkflowTrigger.PostAuthentication, | ||
| bindings: { | ||
| "kinde.env": {}, | ||
| "url": {} | ||
| }, | ||
| }; | ||
|
|
||
| export default async function captureIdpClaimsWorkflow(event: any) { | ||
| const provider = event.context?.auth?.provider; | ||
|
|
||
| // Only process OAuth2/OIDC social connections | ||
| if (!provider || provider.protocol !== "oauth2") { | ||
| console.log("Not an OAuth2 authentication, skipping"); | ||
| return; | ||
| } | ||
|
|
||
| const idTokenClaims = provider.data?.idToken?.claims; | ||
|
|
||
| if (!idTokenClaims) { | ||
| console.log("No ID token claims available"); | ||
| return; | ||
| } | ||
|
|
||
| const userId = event.context?.user?.id; | ||
|
|
||
| if (!userId) { | ||
| console.error("User ID is missing from event context"); | ||
| throw new Error("User ID is required"); | ||
| } | ||
|
|
||
| // Create the Kinde API client | ||
| const kindeAPI = await createKindeAPI(event); | ||
|
|
||
| // CONFIGURATION: Update this with your property category ID | ||
| // Find your category ID in: Settings → Properties → Categories | ||
| const PROPERTY_CATEGORY_ID = "Add the category ID here"; // TODO: Replace with your category ID | ||
| const IDP_CLAIMS_PROPERTY_KEY = "idp_claims"; | ||
|
|
||
| // Check if the idp_claims property exists, create it if not | ||
| let propertyExists = false; | ||
|
|
||
| try { | ||
| const propertiesResponse = await kindeAPI.get({ | ||
| endpoint: 'properties', | ||
| params: { | ||
| context: 'usr', // Filter for user properties only | ||
| }, | ||
| }); | ||
|
|
||
| propertyExists = (propertiesResponse.properties || []).some( | ||
| (prop: any) => prop.key === IDP_CLAIMS_PROPERTY_KEY | ||
| ); | ||
| } catch (error) { | ||
| console.error("Error fetching existing properties:", error); | ||
| throw error; | ||
| } | ||
|
|
||
| // Create the property if it doesn't exist | ||
| if (!propertyExists) { | ||
| try { | ||
| await kindeAPI.post({ | ||
| endpoint: 'properties', | ||
| params: { | ||
| key: IDP_CLAIMS_PROPERTY_KEY, | ||
| name: "IdP Claims", | ||
| description: "All claims from the identity provider stored as JSON", | ||
| type: 'multi_line_text', | ||
| context: 'usr', | ||
| is_private: false, // Can be included in tokens if needed | ||
| category_id: PROPERTY_CATEGORY_ID, | ||
| }, | ||
| }); | ||
|
|
||
| console.log(`Created property: ${IDP_CLAIMS_PROPERTY_KEY}`); | ||
| } catch (error: any) { | ||
| console.error(`Failed to create property '${IDP_CLAIMS_PROPERTY_KEY}':`, error?.message || error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| // Step 3: Filter out claims we don't want to store | ||
| const ignoreClaims = new Set([ | ||
| // Standard JWT claims (not useful to store) | ||
| 'iss', // Issuer | ||
| 'aud', // Audience | ||
| 'exp', // Expiration time | ||
| 'iat', // Issued at | ||
| 'nbf', // Not before | ||
| 'jti', // JWT ID | ||
| 'azp', // Authorized party | ||
| 'nonce', // Nonce for replay protection | ||
| 'auth_time', // Authentication time | ||
| 'at_hash', // Access token hash | ||
| 'c_hash', // Code hash | ||
|
|
||
| // Microsoft-specific noise claims | ||
| 'aio', // Microsoft internal state token (very long, not useful) | ||
| 'ver', // Token version | ||
| 'rh', // Microsoft refresh token hint | ||
| 'uti', // Microsoft unique token identifier (internal use) | ||
| 'ipaddr', // IP address (privacy concern, changes frequently) | ||
|
|
||
| // Google-specific noise claims | ||
| 'nonce', // Already in standard claims | ||
|
|
||
| // Other common noise claims | ||
| 'sid', // Session ID (changes per session) | ||
| 's_hash', // State hash | ||
| ]); | ||
|
|
||
| const claimsToStore: Record<string, any> = {}; | ||
|
|
||
| for (const [claimName, claimValue] of Object.entries(idTokenClaims)) { | ||
| if (!ignoreClaims.has(claimName) && claimValue !== null && claimValue !== undefined) { | ||
| claimsToStore[claimName] = claimValue; | ||
| } | ||
| } | ||
|
|
||
| // Add metadata | ||
| claimsToStore._provider = provider.provider; | ||
| claimsToStore._last_updated = new Date().toISOString(); | ||
|
|
||
| // Convert to JSON and update the user property | ||
| const claimsJson = JSON.stringify(claimsToStore, null, 2); | ||
|
|
||
| try { | ||
| await kindeAPI.patch({ | ||
| endpoint: `users/${userId}/properties`, | ||
| params: { | ||
| properties: { | ||
| [IDP_CLAIMS_PROPERTY_KEY]: claimsJson | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| console.log(`Successfully captured ${Object.keys(claimsToStore).filter(k => !k.startsWith('_')).length} IdP claims from ${provider.provider}`); | ||
| } catch (error: any) { | ||
| console.error("Error updating user properties:", error?.message || error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.