feat: handle idp and sp entity ids separately

This commit is contained in:
Shivam Mishra
2025-09-02 15:46:13 +05:30
parent 8859017642
commit 0d54ebf85b
3 changed files with 102 additions and 50 deletions
@@ -391,7 +391,8 @@
"TITLE": "SAML SSO",
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
"ACS_URL": {
"LABEL": "ACS URL"
"LABEL": "ACS URL",
"TOOLTIP": "Assertion Consumer Service URL - Configure this URL in your IdP as the destination for SAML responses"
},
"SSO_URL": {
"LABEL": "SSO URL",
@@ -404,13 +405,19 @@
"PLACEHOLDER": "-----BEGIN CERTIFICATE-----\nMIIC..."
},
"FINGERPRINT": {
"LABEL": "Fingerprint"
"LABEL": "Fingerprint",
"TOOLTIP": "SHA-1 fingerprint of the certificate - Use this to verify the certificate in your IdP configuration"
},
"COPY_SUCCESS": "Copied to clipboard",
"SP_ENTITY_ID": {
"LABEL": "Service Provider Entity ID",
"HELP": "Unique identifier for this application as a service provider. Leave blank to use default.",
"PLACEHOLDER": "my-unique-sp"
"LABEL": "SP Entity ID",
"HELP": "Unique identifier for this application as a service provider (auto-generated).",
"TOOLTIP": "Unique identifier for Chatwoot as the Service Provider - Configure this in your IdP settings"
},
"IDP_ENTITY_ID": {
"LABEL": "Identity Provider Entity ID",
"HELP": "Unique identifier for your identity provider (usually found in IdP configuration)",
"PLACEHOLDER": "https://your-idp.com/saml"
},
"UPDATE_BUTTON": "Update SAML Settings",
"API": {
@@ -420,7 +427,7 @@
"DISABLED": "SAML settings disabled successfully"
},
"VALIDATION": {
"REQUIRED_FIELDS": "SSO URL and Certificate are required fields"
"REQUIRED_FIELDS": "SSO URL, Identity Provider Entity ID, and Certificate are required fields"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The SAML SSO feature is only available in the Enterprise plans.",
@@ -437,6 +444,10 @@
"ATTRIBUTE_MAPPING": {
"TITLE": "SAML Attribute Setup",
"DESCRIPTION": "The following attribute mappings must be configured in your identity provider"
},
"INFO_SECTION": {
"TITLE": "Service Provider Information",
"TOOLTIP": "Copy these values and configure them in your Identity Provider to establish the SAML connection"
}
}
},
@@ -6,11 +6,15 @@ import { useAlert } from 'dashboard/composables';
import { useAccount } from 'dashboard/composables/useAccount';
import NextButton from 'next/button/Button.vue';
defineProps({
const props = defineProps({
fingerprint: {
type: String,
default: '',
},
spEntityId: {
type: String,
default: '',
},
});
const { t } = useI18n();
@@ -21,6 +25,34 @@ const acsUrl = computed(() => {
return `${currentHost}/omniauth/saml/callback?account_id=${accountId.value}`;
});
const allInfoItems = computed(() => [
{
key: 'ACS_URL',
label: t('SECURITY_SETTINGS.SAML.ACS_URL.LABEL'),
value: acsUrl.value,
tooltip: t('SECURITY_SETTINGS.SAML.ACS_URL.TOOLTIP'),
show: true,
},
{
key: 'SP_ENTITY_ID',
label: t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.LABEL'),
value: props.spEntityId,
tooltip: t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.TOOLTIP'),
show: !!props.spEntityId,
},
{
key: 'FINGERPRINT',
label: t('SECURITY_SETTINGS.SAML.FINGERPRINT.LABEL'),
value: props.fingerprint,
tooltip: t('SECURITY_SETTINGS.SAML.FINGERPRINT.TOOLTIP'),
show: !!props.fingerprint,
},
]);
const visibleInfoItems = computed(() =>
allInfoItems.value.filter(item => item.show)
);
const handleCopy = async text => {
await copyTextToClipboard(text);
useAlert(t('SECURITY_SETTINGS.SAML.COPY_SUCCESS'));
@@ -28,43 +60,43 @@ const handleCopy = async text => {
</script>
<template>
<section
class="rounded-xl border border-n-weak bg-n-solid-1 w-full text-sm text-n-slate-12 divide-y divide-n-weak"
>
<div class="pl-4 pr-1 py-1 flex justify-between items-center">
<div class="flex">
<span class="text-n-slate-11 w-24">
{{ t('SECURITY_SETTINGS.SAML.ACS_URL.LABEL') }}
</span>
{{ acsUrl }}
</div>
<NextButton
type="button"
ghost
sm
slate
icon="i-lucide-copy"
@click="handleCopy(acsUrl)"
<div class="space-y-4">
<div class="flex items-center gap-2">
<h3 class="text-sm font-medium text-n-slate-12">
{{ t('SECURITY_SETTINGS.SAML.INFO_SECTION.TITLE') }}
</h3>
<i
v-tooltip.top="t('SECURITY_SETTINGS.SAML.INFO_SECTION.TOOLTIP')"
class="i-lucide-info text-n-slate-10 w-4 h-4 cursor-help"
/>
</div>
<div
v-if="fingerprint"
class="pl-4 pr-1 py-1 flex justify-between items-center"
<section
class="rounded-xl border border-n-weak bg-n-solid-1 w-full text-sm text-n-slate-12 divide-y divide-n-weak"
>
<div class="flex">
<span class="text-n-slate-11 w-24">
{{ t('SECURITY_SETTINGS.SAML.FINGERPRINT.LABEL') }}
</span>
{{ fingerprint }}
<div
v-for="item in visibleInfoItems"
:key="item.key"
class="pl-4 pr-1 py-1 flex justify-between items-center"
>
<div class="flex items-center gap-2">
<span class="text-n-slate-11 w-32 flex items-center gap-1">
{{ item.label }}
<i
v-tooltip.top="item.tooltip"
class="i-lucide-info text-n-slate-9 w-3 h-3 cursor-help"
/>
</span>
<span class="flex-1">{{ item.value }}</span>
</div>
<NextButton
type="button"
ghost
sm
slate
icon="i-lucide-copy"
@click="handleCopy(item.value)"
/>
</div>
<NextButton
type="button"
ghost
sm
slate
icon="i-lucide-copy"
@click="handleCopy(fingerprint)"
/>
</div>
</section>
</section>
</div>
</template>
@@ -22,6 +22,7 @@ const ssoUrl = ref('');
const certificate = ref('');
const fingerprint = ref('');
const spEntityId = ref('');
const idpEntityId = ref('');
const roleMappings = ref({});
const isEnabled = ref(false);
const isSubmitting = ref(false);
@@ -42,6 +43,7 @@ const loadSamlSettings = async () => {
ssoUrl.value = settings.sso_url;
certificate.value = settings.certificate || '';
spEntityId.value = settings.sp_entity_id || '';
idpEntityId.value = settings.idp_entity_id || '';
roleMappings.value = settings.role_mappings || {};
fingerprint.value = settings.fingerprint || '';
isEnabled.value = ssoUrl.value !== '';
@@ -72,7 +74,7 @@ const saveSamlSettings = async settings => {
// Update local state with response data including fingerprint and id
if (response?.data) {
id.value = response.data.id;
fingerprint.value = response.data.certificate_fingerprint || '';
fingerprint.value = response.data.fingerprint || '';
}
useAlert(t('SECURITY_SETTINGS.SAML.API.SUCCESS'));
@@ -90,7 +92,7 @@ const saveSamlSettings = async settings => {
};
const handleSubmit = async () => {
if (!ssoUrl.value || !certificate.value) {
if (!ssoUrl.value || !certificate.value || !idpEntityId.value) {
useAlert(t('SECURITY_SETTINGS.SAML.VALIDATION.REQUIRED_FIELDS'));
return;
}
@@ -98,7 +100,7 @@ const handleSubmit = async () => {
const settings = {
sso_url: ssoUrl.value,
certificate: certificate.value,
sp_entity_id: spEntityId.value,
idp_entity_id: idpEntityId.value,
role_mappings: roleMappings.value,
};
@@ -110,6 +112,7 @@ const handleDisable = async () => {
ssoUrl.value = '';
certificate.value = '';
spEntityId.value = '';
idpEntityId.value = '';
fingerprint.value = '';
roleMappings.value = {};
@@ -144,7 +147,11 @@ onMounted(() => {
</div>
</template>
<SamlInfoSection class="mb-5" :fingerprint="fingerprint" />
<SamlInfoSection
class="mb-5"
:fingerprint="fingerprint"
:sp-entity-id="spEntityId"
/>
<SamlAttributeMap class="mb-5" />
<form class="grid gap-5" @submit.prevent="handleSubmit">
@@ -163,13 +170,15 @@ onMounted(() => {
</WithLabel>
<WithLabel
:label="t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.LABEL')"
:help-message="t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.HELP')"
:label="t('SECURITY_SETTINGS.SAML.IDP_ENTITY_ID.LABEL')"
:help-message="t('SECURITY_SETTINGS.SAML.IDP_ENTITY_ID.HELP')"
required
>
<TextInput
v-model="spEntityId"
v-model="idpEntityId"
class="w-full"
:placeholder="t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.PLACEHOLDER')"
:placeholder="t('SECURITY_SETTINGS.SAML.IDP_ENTITY_ID.PLACEHOLDER')"
required
/>
</WithLabel>