Aksha Malik
03/05/2026, 2:48 PMasync 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
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?DevRev
03/05/2026, 2:49 PMcustomerID.
Solution Options
1. Use a custom flow with attribute preservation
// 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 DocumentationAksha Malik
03/05/2026, 3:29 PMasync 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;
}
}DevRev
03/05/2026, 3:29 PMcustomerId 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:
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.Aksha Malik
03/05/2026, 3:34 PMasync 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
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;
}
}DevRev
03/05/2026, 3:34 PMasync 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.Aksha Malik
03/05/2026, 3:39 PMDevRev
03/05/2026, 3:40 PMasync 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.Aksha Malik
03/05/2026, 4:27 PMDevRev
03/05/2026, 4:28 PMAksha Malik
03/05/2026, 4:33 PMDevRev
03/05/2026, 4:33 PMasync 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 DocumentationAksha Malik
03/05/2026, 4:51 PMBhagwanta Yeole
03/05/2026, 10:33 PMAksha Malik
03/06/2026, 4:37 AMBhagwanta Yeole
03/06/2026, 11:18 AMAksha Malik
03/06/2026, 11:52 AM