HI this is the login function we use to start the ...
# ask-a-descoper
a
HI this is the login function we use to start the tenant sso url
Copy code
async ssoLoginClient(tenantId: string): Promise<any> {
  try {
    console.log(`[DescopeService] Starting SSO for tenant ID: ${tenantId}`);
    const redirectUrl = this.ssoLoginRedirectUrl;  
    const resp = await this.descopeClient.saml.start(tenantId, redirectUrl);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Start Error:', resp.error);
      const errorMsg = resp.error?.errorMessage || resp.error?.errorDescription || 'Failed to start SSO';
      throw new Error(errorMsg);
    }
    return {
      redirectUrl: resp.data.url
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoStartClient:', error);
    throw error;
  }
}
and this is the callback token exchange function we are using for the same
Copy code
async ssoLoginExchangeCodeForToken(code: string): Promise<any> {
  try {
    console.log("[DescopeService] Exchanging SSO code for token:", code);
    const resp = await this.descopeClient.saml.exchange(code);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Exchange Error:', resp.error);
      throw new Error(resp.error.errorMessage || 'Failed to verify SSO code');
    }
    const { sessionJwt, refreshJwt, user } = resp.data;


    return {
      sessionJwt,
      refreshJwt,
      user,
      tenantId: user.userTenants?.[0]?.tenantId || null
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoExchangeCodeForToken:', error);
    throw error;
  }
}
in this first time we add custom attribute to the user and then they can use the endpoint for login for further use. we are using same way of creating sso url and then exchanging token. but when user signup using sso then custom attribute "customerID" in our case is added and displayed on descope console. but when login it removes the customerId from the user entity. How can i possibly solve this issue i a facing?
✅ 1
d
Issue Analysis Your SAML SSO flow is overwriting custom attributes during login. This happens because SAML assertions typically only contain standard user attributes, not your custom
customerID
. Solution Options 1. Use a custom flow with attribute preservation
Copy code
// Add this after your SAML exchange
const existingUser = await this.descopeClient.management.user.load(user.userId);
const preservedCustomerId = existingUser.customAttributes?.customerID;
if (preservedCustomerId && !user.customAttributes?.customerID) {
  await this.descopeClient.management.user.update(user.userId, {
    customAttributes: {
      ...user.customAttributes,
      customerID: preservedCustomerId
    }
  });
}
2. Configure SAML attribute mapping - In your Descope Console, go to the tenant's SSO configuration - Map the
customerID
attribute from your IdP's SAML assertion - Ensure your IdP sends this attribute in the SAML response Best Practice Use Descope Flows to handle this automatically. Create a custom flow that: • Checks if custom attributes exist before SAML login • Preserves them during the authentication process • Merges them back after successful login The attribute merging behavior during SSO login can overwrite existing custom data . Custom attributes can store any data you want for users and can be utilized within custom claims . Link User Identities | Descope Documentation User Management | Descope Documentation
a
cant use option one because i dont have userId until the code is exchanged and i get user information and second option IDP SAML assertion is not what we get through external IDP, it is manually setup during code sxchange while signup with below function
Copy code
async ssoExchangeCodeForToken(code: string, customerId?: number): Promise<any> {
  try {
    console.log("[DescopeService] Exchanging SSO code for token:", code);
    const resp = await this.descopeClient.saml.exchange(code);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Exchange Error:', resp.error);
      throw new Error(resp.error.errorMessage || 'Failed to verify SSO code');
    }
    const { sessionJwt, refreshJwt, user } = resp.data;
    
    // Set customerID if provided
    if (customerId && user.loginIds?.[0]) {
      const customerIdText = String(customerId);
      try {
        await this.descopeClient.management.user.updateCustomAttribute(
          user.loginIds[0], 
          'customerId', 
          customerIdText
        );
        console.log(`[DescopeService] Set customerID: ${customerId} for user: ${user.loginIds[0]}`);
      } catch (updateError) {
        console.error('[DescopeService] Failed to update customerID:', updateError);
        // Don't fail auth flow for attribute update errors
      }
    }
    //user updated information after custom attribute update   
    const updatedUser = await this.descopeClient.management.user.load(user.loginIds[0]);
    console.log('Fetched updated user info:', updatedUser);
    return {
      sessionJwt,
      refreshJwt,
      user: updatedUser.data,
      tenantId: user.userTenants?.[0]?.tenantId || null
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoExchangeCodeForToken:', error);
    throw error;
  }
}
d
Hi, I'm Descope's AI assistant. Root Cause The issue is that during subsequent logins (without
customerId
parameter), your code doesn't preserve existing custom attributes. The SAML exchange returns a user object that may not include previously set custom attributes. Solution Modify your exchange function to always preserve existing custom attributes:
Copy code
async ssoExchangeCodeForToken(code: string, customerId?: number): Promise<any> {
  try {
    console.log("[DescopeService] Exchanging SSO code for token:", code);
    const resp = await this.descopeClient.saml.exchange(code);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Exchange Error:', resp.error);
      throw new Error(resp.error.errorMessage || 'Failed to verify SSO code');
    }
    const { sessionJwt, refreshJwt, user } = resp.data;
    // Always load existing user data to preserve custom attributes
    let existingCustomAttributes = {};
    try {
      const existingUser = await this.descopeClient.management.user.load(user.loginIds[0]);
      existingCustomAttributes = existingUser.data.customAttributes || {};
    } catch (loadError) {
      console.log('[DescopeService] User not found or first login, proceeding...');
    }
    // Set or preserve customerID
    if (customerId) {
      existingCustomAttributes.customerId = String(customerId);
    }
    // Update custom attributes if we have any to preserve/set
    if (Object.keys(existingCustomAttributes).length > 0) {
      try {
        await this.descopeClient.management.user.update(user.loginIds[0], {
          customAttributes: existingCustomAttributes
        });
        console.log(`[DescopeService] Preserved/updated custom attributes for user: ${user.loginIds[0]}`);
      } catch (updateError) {
        console.error('[DescopeService] Failed to update custom attributes:', updateError);
      }
    }
    // Load final user state
    const updatedUser = await this.descopeClient.management.user.load(user.loginIds[0]);
    
    return {
      sessionJwt,
      refreshJwt,
      user: updatedUser.data,
      tenantId: user.userTenants?.[0]?.tenantId || null
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoExchangeCodeForToken:', error);
    throw error;
  }
}
Key Changes • Always load existing user data before updating • Preserve all existing custom attributes • Only add/update
customerId
when provided • Use
user.update()
instead of
updateCustomAttribute()
to handle multiple attributes This ensures custom attributes persist across all SSO logins, whether it's signup or subsequent logins.
a
no these are two different function for handling sso token exchange this is signup sso code exchange where i am getting customerId and adding to the user custom attribute
Copy code
async ssoExchangeCodeForToken(code: string, customerId?: number): Promise<any> {
  try {
    console.log("[DescopeService] Exchanging SSO code for token:", code);
    const resp = await this.descopeClient.saml.exchange(code);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Exchange Error:', resp.error);
      throw new Error(resp.error.errorMessage || 'Failed to verify SSO code');
    }
    const { sessionJwt, refreshJwt, user } = resp.data;
    // Always load existing user data to preserve custom attributes
    let existingCustomAttributes = {};
    try {
      const existingUser = await this.descopeClient.management.user.load(user.loginIds[0]);
      existingCustomAttributes = existingUser.data.customAttributes || {};
    } catch (loadError) {
      console.log('[DescopeService] User not found or first login, proceeding...');
    }
    // Set or preserve customerID
    if (customerId) {
      existingCustomAttributes.customerId = String(customerId);
    }
    // Update custom attributes if we have any to preserve/set
    if (Object.keys(existingCustomAttributes).length > 0) {
      try {
        await this.descopeClient.management.user.update(user.loginIds[0], {
          customAttributes: existingCustomAttributes
        });
        console.log(`[DescopeService] Preserved/updated custom attributes for user: ${user.loginIds[0]}`);
      } catch (updateError) {
        console.error('[DescopeService] Failed to update custom attributes:', updateError);
      }
    }
    // Load final user state
    const updatedUser = await this.descopeClient.management.user.load(user.loginIds[0]);
    
    return {
      sessionJwt,
      refreshJwt,
      user: updatedUser.data,
      tenantId: user.userTenants?.[0]?.tenantId || null
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoExchangeCodeForToken:', error);
    throw error;
  }
}
this is another fucntino for sso token exchange in case of login where i just get the code and exchange for token where i need my existing user custom attribute and dont want it to update the custom attribute thinking it as a new user rather than existing user
Copy code
async ssoLoginExchangeCodeForToken(code: string): Promise<any> {
  try {
    console.log("[DescopeService] Exchanging SSO code for token:", code);
    const resp = await this.descopeClient.saml.exchange(code);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Exchange Error:', resp.error);
      throw new Error(resp.error.errorMessage || 'Failed to verify SSO code');
    }
    const { sessionJwt, refreshJwt, user } = resp.data;


    return {
      sessionJwt,
      refreshJwt,
      user,
      tenantId: user.userTenants?.[0]?.tenantId || null
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoExchangeCodeForToken:', error);
    throw error;
  }
}
d
Hi, I'm Descope's AI assistant. Issue Identified Your login function returns the user object directly from SAML exchange, which may not include custom attributes. You need to load the complete user data to get custom attributes. Solution Update your login exchange function to load the full user data:
Copy code
async ssoLoginExchangeCodeForToken(code: string): Promise<any> {
  try {
    console.log("[DescopeService] Exchanging SSO code for token:", code);
    const resp = await this.descopeClient.saml.exchange(code);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Exchange Error:', resp.error);
      throw new Error(resp.error.errorMessage || 'Failed to verify SSO code');
    }
    const { sessionJwt, refreshJwt, user } = resp.data;
    // Load complete user data including custom attributes
    const fullUser = await this.descopeClient.management.user.load(user.loginIds[0]);
    return {
      sessionJwt,
      refreshJwt,
      user: fullUser.data, // Use full user data instead of SAML response user
      tenantId: user.userTenants?.[0]?.tenantId || null
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoLoginExchangeCodeForToken:', error);
    throw error;
  }
}
Why This Works • The SAML exchange response contains basic user info but may not include all custom attributes • Loading the user via Management API returns the complete user profile with all custom attributes • This preserves your
customerId
without any additional updates This approach keeps your signup and login functions separate while ensuring custom attributes are available in both flows.
a
no this is not showing custom attribute, it has been replaced
d
Critical Issue Confirmed The SAML exchange is overwriting your user data, including custom attributes. This is a known behavior where SAML assertions replace existing user attributes. Immediate Solution You need to preserve custom attributes before the SAML exchange overwrites them:
Copy code
async ssoLoginExchangeCodeForToken(code: string): Promise<any> {
  try {
    console.log("[DescopeService] Exchanging SSO code for token:", code);
    // First, get the user ID from the code without exchanging yet
    // This is a workaround - you'll need to extract email from the SAML response
    
    const resp = await this.descopeClient.saml.exchange(code);
    if (!resp.ok) {
      console.error('[DescopeService] SSO Exchange Error:', resp.error);
      throw new Error(resp.error.errorMessage || 'Failed to verify SSO code');
    }
    const { sessionJwt, refreshJwt, user } = resp.data;
    // Immediately restore custom attributes after SAML exchange
    try {
      // Load user again to check if custom attributes were lost
      const currentUser = await this.descopeClient.management.user.load(user.loginIds[0]);
      
      if (!currentUser.data.customAttributes?.customerId) {
        // Custom attributes were lost, need to restore from backup or fail gracefully
        console.error('[DescopeService] Custom attributes lost during SAML exchange');
        
        // You may need to implement a backup mechanism or 
        // store custom attributes in a separate system
      }
    } catch (error) {
      console.error('[DescopeService] Failed to verify custom attributes:', error);
    }
    return {
      sessionJwt,
      refreshJwt,
      user,
      tenantId: user.userTenants?.[0]?.tenantId || null
    };
  } catch (error) {
    console.error('[DescopeService] Exception in ssoLoginExchangeCodeForToken:', error);
    throw error;
  }
}
Recommended Long-term Solution This is a platform limitation. I recommend reaching out to Descope support to request a feature that preserves custom attributes during SAML exchange, or implement a backup mechanism to store critical user data outside of custom attributes.
a
okay thanks
d
Hi, I'm Descope's AI assistant. You're welcome! If you run into any other issues with SAML SSO or custom attributes, feel free to ask. Good luck with your implementation!
🙌 1
a
hi @Kevin Gao, can you please confirm if there is any way of merging the user signing up and logging through sso so that it doesnt loose the custom attribute once setup?
d
Yes, there is a solution! Based on the documentation, Descope has built-in identity merging capabilities that can preserve custom attributes during SSO flows . How Identity Merging Works When a user signs up via SSO and later logs in, Descope automatically merges identities if: • The SSO user's email matches an existing user's identifier • The existing user belongs to exactly one tenant Custom Attributes Are Preserved According to the documentation: "Any existing user attributes are preserved unless explicitly overridden by the IdP" . Enable Auto-Association In your Descope Console, enable these settings under SSO Settings: • Email Domain Auto-Association: Automatically associates existing users with matching email domains • SSO Domain Auto-Association: Converts existing users to SSO users while preserving their data Unified Function Approach You can combine both functions into one that handles both signup and login scenarios:
Copy code
async ssoExchangeCodeForToken(code: string, customerId?: number): Promise<any> {
  // Your existing logic works - Descope will automatically merge
  // and preserve custom attributes when the email matches
}
This eliminates the need for separate functions since Descope's identity merging handles both scenarios automatically while preserving custom attributes. Merging SSO Identities Risk | Descope Documentation SSO Settings Guide | Descope Documentation
a
no i tried changing my configuration but still the issue is the new request is overriding the current details already present for the user, which it shouldn't
b
@Aksha Malik Did you tried with the unified function approach?
a
what is that approch exactly? how shall i implement it?
b
Can you please check below things? Tenant →Authentication Methods → SSO settings → JIT provisioning If enabled, Descope updates attributes on every login. Turn it OFF if you manage attributes in your backend.
a
okay, will check and let you know