Mario
Mario2mo ago

External User Not Found when trying to auto link SAML Users by Email

Hi, I have set up Okta as an Identity Provider on Zitadel, and I am trying to get the auto linking part of it working. I have a created a user in Zitadel with the same email as my Okta account. Now when I click Sign In with Okta, on the login screen, I get External User Not Found. I've played around with different settings on both Zitadel and Okta side and currently, I think I have all this set up correctly. I've been digging in the source code and the database a little bit, and I think the issue might be on this file here: https://github.com/zitadel/zitadel/blob/e57a9b57c8e770383316599a338ceef023d96de6/internal/idp/providers/saml/mapper.go#L57 All other providers have this interface implemented, however it seems like saml doesn't really link any attributes besides the ID, so when checkAutoLink function is called, all the properties here are returned empty (including email and username). Is this intentional or is there something I might be doing wrong? My IDP template configuration as a reference:
zitadel-authentication=> SELECT * FROM projections.idp_templates6;
-[ RECORD 1 ]-------+------------------------------
id | xxxxxx
creation_date | 2025-07-01 16:26:40.591936+00
change_date | 2025-07-17 07:21:49.810715+00
sequence | 134
resource_owner | xxxxx
instance_id | xxxxx
state | 1
name | Okta SAML IDP
owner_type | 1
type | 12
owner_removed | f
is_creation_allowed | t
is_linking_allowed | t
is_auto_creation | f
is_auto_update | t
auto_linking | 2
zitadel-authentication=> SELECT * FROM projections.idp_templates6;
-[ RECORD 1 ]-------+------------------------------
id | xxxxxx
creation_date | 2025-07-01 16:26:40.591936+00
change_date | 2025-07-17 07:21:49.810715+00
sequence | 134
resource_owner | xxxxx
instance_id | xxxxx
state | 1
name | Okta SAML IDP
owner_type | 1
type | 12
owner_removed | f
is_creation_allowed | t
is_linking_allowed | t
is_auto_creation | f
is_auto_update | t
auto_linking | 2
Thank you 🙏
GitHub
zitadel/internal/idp/providers/saml/mapper.go at e57a9b57c8e7703833...
ZITADEL - Identity infrastructure, simplified for you. - zitadel/zitadel
4 Replies
Mario
MarioOP2mo ago
For anyone that runs into this same issue, there's a workaround with using actions:
let logger = require("zitadel/log");

function setExternalUserInfo(ctx, api) {
try {
const providerInfo = ctx.v1?.providerInfo;
const attributes = providerInfo?.attributes || {};
const nameID = providerInfo?.iD;

const email =
attributes.email?.[0] ||
attributes.mail?.[0] ||
attributes.Email?.[0] ||
nameID;

if (email) {
api.setEmail(email);
api.setEmailVerified(true);
}

const firstName = attributes.first_name?.[0] || attributes.given_name?.[0] || attributes.FirstName?.[0];
if (firstName) {
api.setFirstName(firstName);
}

const lastName = attributes.last_name?.[0] || attributes.family_name?.[0] || attributes.surname?.[0] || attributes.LastName?.[0];
if (lastName) {
api.setLastName(lastName);
}

const displayName = attributes.displayName?.[0] || `${firstName || ""} ${lastName || ""}`.trim() || email;
if (displayName) {
api.setDisplayName(displayName);
}

const preferredUsername = attributes.preferred_username?.[0] || attributes.username?.[0] || email;
if (preferredUsername) {
api.setPreferredUsername(preferredUsername);
}

const language = attributes.locale?.[0] || attributes.language?.[0];
if (language) {
api.setPreferredLanguage(language);
}

const phone = attributes.phone_number?.[0] || attributes.telephone?.[0];
if (phone) {
api.setPhone(phone);
api.setPhoneVerified(true);
}

} catch (err) {
logger.error("Script error:", err.message);
}
}
let logger = require("zitadel/log");

function setExternalUserInfo(ctx, api) {
try {
const providerInfo = ctx.v1?.providerInfo;
const attributes = providerInfo?.attributes || {};
const nameID = providerInfo?.iD;

const email =
attributes.email?.[0] ||
attributes.mail?.[0] ||
attributes.Email?.[0] ||
nameID;

if (email) {
api.setEmail(email);
api.setEmailVerified(true);
}

const firstName = attributes.first_name?.[0] || attributes.given_name?.[0] || attributes.FirstName?.[0];
if (firstName) {
api.setFirstName(firstName);
}

const lastName = attributes.last_name?.[0] || attributes.family_name?.[0] || attributes.surname?.[0] || attributes.LastName?.[0];
if (lastName) {
api.setLastName(lastName);
}

const displayName = attributes.displayName?.[0] || `${firstName || ""} ${lastName || ""}`.trim() || email;
if (displayName) {
api.setDisplayName(displayName);
}

const preferredUsername = attributes.preferred_username?.[0] || attributes.username?.[0] || email;
if (preferredUsername) {
api.setPreferredUsername(preferredUsername);
}

const language = attributes.locale?.[0] || attributes.language?.[0];
if (language) {
api.setPreferredLanguage(language);
}

const phone = attributes.phone_number?.[0] || attributes.telephone?.[0];
if (phone) {
api.setPhone(phone);
api.setPhoneVerified(true);
}

} catch (err) {
logger.error("Script error:", err.message);
}
}
You set this as the action to trigger on : External Authentication - Post Authentication to add the details to your external user using the details received from your provider. Then after this step, zitadel is going to check and auto-link your external user.
巾水
巾水2mo ago
I got a linked user, but zitadel still let me input code and password, is there anyway to auto link.
No description
No description
Rajat
Rajat2mo ago
hey @巾水 you can create a new human user and link you IDP on the fly https://zitadel.com/docs/apis/resources/user_service_v2/user-service-add-human-user then pre-fill the idpLinks with your IDP details.
巾水
巾水2mo ago
Thanks for your reply. I was thinking if this will create a new account or use the linked account. Actually I have a freeipa idp already, and I add a dingtalk idp , I want the dingtalk idp link the freeipa idp automaticly. As you see zitadel has link the two idps, but I don't want user input anyting , I want user use dingtalk login with the freeipa account. Thanks again.

Did you find this page helpful?