Sso Identity Provider Config
blocksecurityEnterprise Single Sign-On (SAML 2.0 / OIDC) identity provider configuration wizard with IdP preset picker, SP credentials, XML metadata dropzone, X.509 cert PEM validator, domain enforcement switch, and interactive SAML test runner.
Installation
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/sso-identity-provider-config.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/sso-identity-provider-config.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/sso-identity-provider-config.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/sso-identity-provider-config.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/sso-identity-provider-configInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initialSsoEnabled | boolean | true | optional |
initialPreset | 'okta''entra''google''custom' | 'okta' | optional |
initialEnforceDomain | boolean | true | optional |
class | HTMLAttributes['class'] | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
IdpPresetinterface IdpPreset {
id: IdpPresetId
name: string
subtitle: string
badge: string
defaultSsoUrl: string
defaultEntityId: string
domain: string
defaultCert: string
}ClaimAttributeinterface ClaimAttribute {
samlAttribute: string
mappedField: string
sampleValue: string
}Files installed (1)
app/components/blocks/SsoIdentityProviderConfig.vue32.5 kB
<script setup lang="ts"> import { computed, ref } from 'vue' import type { HTMLAttributes } from 'vue' import { Check, CheckCircle2, Copy, Download, ExternalLink, FileCode, Globe, KeyRound, Lock, Play, RotateCw, Save, ShieldAlert, ShieldCheck, UploadCloud, UserCheck, X, } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' export type IdpPresetId = 'okta' | 'entra' | 'google' | 'custom' interface IdpPreset { id: IdpPresetId name: string subtitle: string badge: string defaultSsoUrl: string defaultEntityId: string domain: string defaultCert: string } interface ClaimAttribute { samlAttribute: string mappedField: string sampleValue: string } interface Props { initialSsoEnabled?: boolean initialPreset?: IdpPresetId initialEnforceDomain?: boolean class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { initialSsoEnabled: true, initialPreset: 'okta', initialEnforceDomain: true, }) const idpPresets: IdpPreset[] = [ { id: 'okta', name: 'Okta', subtitle: 'SAML 2.0 & OIDC Enterprise', badge: 'Popular', defaultSsoUrl: 'https://company.okta.com/app/sso/saml', defaultEntityId: 'http://www.okta.com/exk849201', domain: 'company.okta.com', defaultCert: `-----BEGIN CERTIFICATE----- MIIDpDCCAoygAwIBAgIGAX2v5L3oMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJV UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG A1UECgwET2t0YTEUMBIGA1UECwwLU1NPIFNlcnZpY2UxGDAWBgNVBAMMD2NvbXBhbnkub2t0 YS5jb20xIDAeBgkqhkiG9w0BCQEWEWFkbWluQG9rdGEuY29tMB4XDTI2MDgyMTE0MzAwMFoX DTI4MTAyMTE0MzAwMFowgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh -----END CERTIFICATE-----`, }, { id: 'entra', name: 'Microsoft Entra ID', subtitle: 'Azure AD & Microsoft 365', badge: 'Enterprise', defaultSsoUrl: 'https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/saml2', defaultEntityId: 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/', domain: 'login.microsoftonline.com', defaultCert: `-----BEGIN CERTIFICATE----- MIIDBTCCAe2gAwIBAgIQJn0zJk1vVbtJpZ0R4k0D1DANBgkqhkiG9w0BAQsFADAtMSsw KQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTI2MDgy MTE0MzAwMFoXDTI4MTAyMTE0MzAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNz Y29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -----END CERTIFICATE-----`, }, { id: 'google', name: 'Google Workspace', subtitle: 'Google Cloud Identity SAML', badge: 'SAML 2.0', defaultSsoUrl: 'https://accounts.google.com/o/saml2/idp?idpid=C0123abcd', defaultEntityId: 'https://accounts.google.com/o/saml2?idpid=C0123abcd', domain: 'accounts.google.com', defaultCert: `-----BEGIN CERTIFICATE----- MIIDdDCCAlygAwIBAgIGAY7GZpBqMA0GCSqGSIb3DQEBCwUAMHQxCzAJBgNVBAYTAlVT MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MR8wHQYD VQQKExZHb29nbGUgV29ya3NwYWNlIElBTSExMB8GA1UEAxMYR29vZ2xlIElkUCBTYW1s IFNpZ25lcjAeFw0yNjA4MjExNDMwMDBaFw0yODEwMjExNDMwMDBaMHQxCzAJBgNVBAYT -----END CERTIFICATE-----`, }, { id: 'custom', name: 'Custom SAML 2.0', subtitle: 'PingFederate, OneLogin, Keycloak', badge: 'Universal', defaultSsoUrl: 'https://sso.company.com/idp/profile/SAML2/POST/SSO', defaultEntityId: 'https://sso.company.com/idp/shibboleth', domain: 'sso.company.com', defaultCert: `-----BEGIN CERTIFICATE----- MIIDcDCCAligAwIBAgIJAN8FpW1mG+hBMA0GCSqGSIb3DQEBCwUAMFgxCzAJBgNVBAYT AlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMQ8wDQYDVQQKDAZDdXN0b20xIDAeBgNVBAMM F2N1c3RvbS5pZHAucHJvdmlkZXIubGFiMB4XDTI2MDgyMTE0MzAwMFoXDTI4MTAyMTE0 MzAwMFowWDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExDzANBgNVBAoM -----END CERTIFICATE-----`, }, ] const spAcsUrl = 'https://auth.uipkge.dev/sso/saml/callback' const spEntityId = 'https://auth.uipkge.dev/sso/saml/metadata' const spNameIdFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' const spMetadataXmlUrl = 'https://auth.uipkge.dev/sso/saml/metadata.xml' const spCertificatePem = `-----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIUW0Qz0K7VzN8XlP4wQ6k9J1vYgLAwDQYJKoZIhvcNAQEL BQAwMzEXMBUGA1UEAwwNYXV0aC51aXBrZ2UuZGV2MRgwFgYDVQQKDA9VSVBLR0Ug QXV0aCBTU08wHhcNMjYwODIxMTQzMDAwWhcNMzEwODIxMTQzMDAwWjAzMRcwFQYD VQQDDA5hdXRoLnVpcGtnZS5kZXYxGDAWBgNVBAoMD1VJUEtHRSBBdXRoIFNTTzCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL1K9sU3FvX+7nQ6m2W4j1k9 -----END CERTIFICATE-----` const ssoEnabled = ref(props.initialSsoEnabled) const selectedPresetId = ref<IdpPresetId>(props.initialPreset) const enforceDomain = ref(props.initialEnforceDomain) const currentPreset = computed(() => { return idpPresets.find((p) => p.id === selectedPresetId.value) ?? idpPresets[0] }) const idpSsoUrl = ref(currentPreset.value.defaultSsoUrl) const idpEntityId = ref(currentPreset.value.defaultEntityId) const idpCertPem = ref(currentPreset.value.defaultCert) const copiedKey = ref<string | null>(null) let copyTimer: ReturnType<typeof setTimeout> | undefined async function copyText(text: string, key: string) { try { await navigator.clipboard.writeText(text) copiedKey.value = key if (copyTimer) clearTimeout(copyTimer) copyTimer = setTimeout(() => { if (copiedKey.value === key) copiedKey.value = null }, 2000) } catch { // Clipboard unavailable } } function handleSelectPreset(presetId: IdpPresetId) { selectedPresetId.value = presetId const preset = idpPresets.find((p) => p.id === presetId) if (preset) { idpSsoUrl.value = preset.defaultSsoUrl idpEntityId.value = preset.defaultEntityId idpCertPem.value = preset.defaultCert xmlUploadedNotice.value = false } } function downloadSpCertificate() { if (typeof window === 'undefined') return const blob = new Blob([spCertificatePem], { type: 'application/x-pem-file' }) const url = URL.createObjectURL(blob) const anchor = document.createElement('a') anchor.href = url anchor.download = 'uipkge-sp-certificate.crt' document.body.appendChild(anchor) anchor.click() document.body.removeChild(anchor) URL.revokeObjectURL(url) } const isDraggingXml = ref(false) const xmlUploadedNotice = ref(false) function handleXmlDrop(e: DragEvent) { e.preventDefault() isDraggingXml.value = false simulateXmlUpload() } function handleXmlDragOver(e: DragEvent) { e.preventDefault() isDraggingXml.value = true } function handleXmlDragLeave() { isDraggingXml.value = false } function simulateXmlUpload() { const preset = currentPreset.value idpSsoUrl.value = preset.defaultSsoUrl idpEntityId.value = preset.defaultEntityId idpCertPem.value = preset.defaultCert xmlUploadedNotice.value = true setTimeout(() => { xmlUploadedNotice.value = false }, 4000) } const isSaving = ref(false) const saveSuccessNotice = ref(false) function handleSaveConfig() { if (isSaving.value) return isSaving.value = true setTimeout(() => { isSaving.value = false saveSuccessNotice.value = true setTimeout(() => { saveSuccessNotice.value = false }, 3500) }, 600) } const testModalOpen = ref(false) const isTesting = ref(false) function openTestModal() { testModalOpen.value = true isTesting.value = true setTimeout(() => { isTesting.value = false }, 500) } const simulatedClaims: ClaimAttribute[] = [ { samlAttribute: 'urn:oid:0.9.2342.19200300.100.1.1', mappedField: 'nameID', sampleValue: 'alex.morgan@company.com' }, { samlAttribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', mappedField: 'email', sampleValue: 'alex.morgan@company.com', }, { samlAttribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname', mappedField: 'firstName', sampleValue: 'Alex', }, { samlAttribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname', mappedField: 'lastName', sampleValue: 'Morgan', }, { samlAttribute: 'http://schemas.xmlsoap.org/claims/Group', mappedField: 'groups', sampleValue: '["Engineering", "IAM-Admins", "SSO-Users"]', }, { samlAttribute: 'urn:oasis:names:tc:SAML:2.0:assertion:SessionIndex', mappedField: 'sessionIndex', sampleValue: '_sess_8f29ab0192e47', }, ] </script> <template> <div data-slot="sso-identity-provider-config" :class="cn('w-full space-y-6', props.class)"> <!-- Header Banner & Top Controls --> <Card class="border-border bg-card/80 shadow-xs backdrop-blur-xs"> <CardHeader class="pb-4"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <KeyRound class="size-4" /> </div> <CardTitle class="text-base font-semibold">Single Sign-On (SSO) & SAML 2.0 Configuration</CardTitle> <Badge v-if="ssoEnabled" variant="success" class="font-mono text-xs"> <span class="bg-success mr-1.5 inline-block size-2 animate-pulse rounded-full" /> SSO Active </Badge> <Badge v-else variant="outline" class="text-muted-foreground border-border font-mono text-xs"> Disabled </Badge> </div> <CardDescription class="text-xs"> Connect your enterprise IdP (Okta, Azure AD / Entra, Google Workspace, OneLogin) for federated SAML authentication. </CardDescription> </div> <div class="flex flex-wrap items-center gap-3"> <div class="border-border bg-muted/30 flex items-center gap-3 rounded-lg border px-3 py-1.5"> <div class="text-right"> <p class="text-foreground text-xs font-medium">SSO Status</p> <p class="text-muted-foreground text-xs"> {{ ssoEnabled ? 'Enabled for @company.com' : 'Disabled' }} </p> </div> <Switch v-model="ssoEnabled" aria-label="Toggle Single Sign-On" /> </div> <Button variant="outline" size="sm" class="gap-1.5 text-xs font-medium" :disabled="!ssoEnabled" @click="openTestModal" > <Play class="size-3.5" /> <span>Test SAML Login</span> </Button> </div> </div> </CardHeader> </Card> <!-- Save Success Banner --> <div v-if="saveSuccessNotice" class="border-success/30 bg-success/10 text-success flex items-center justify-between rounded-lg border px-4 py-3 text-xs" role="status" > <div class="flex items-center gap-2"> <CheckCircle2 class="size-4 shrink-0" /> <span >SSO configuration saved successfully. All assertions will validate against {{ currentPreset.name }}.</span > </div> <button type="button" class="text-success hover:opacity-75" aria-label="Dismiss alert" @click="saveSuccessNotice = false" > <X class="size-3.5" /> </button> </div> <!-- IdP Preset Picker Grid --> <div class="space-y-3"> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <h3 class="text-foreground text-xs font-semibold tracking-tight uppercase">Identity Provider Presets</h3> <p class="text-muted-foreground text-xs"> Choose your identity platform to load SAML endpoints and formatting defaults </p> </div> <Badge variant="outline" class="border-border font-mono text-xs"> SAML 2.0 / Web SSO </Badge> </div> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4"> <button v-for="preset in idpPresets" :key="preset.id" type="button" :class=" cn( 'group relative flex flex-col justify-between rounded-xl border p-4 text-left transition-colors duration-150', 'focus-visible:ring-ring cursor-pointer focus-visible:ring-2 focus-visible:outline-none', selectedPresetId === preset.id ? 'border-primary bg-primary/5 ring-primary shadow-xs ring-1' : 'border-border bg-card hover:border-primary/40 hover:bg-accent/40', ) " @click="handleSelectPreset(preset.id)" > <div class="space-y-2"> <div class="flex items-center justify-between"> <div :class=" cn( 'flex size-8 items-center justify-center rounded-lg text-xs font-bold transition-colors', selectedPresetId === preset.id ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground group-hover:bg-primary/10 group-hover:text-primary', ) " > <span v-if="preset.id === 'okta'">O</span> <span v-else-if="preset.id === 'entra'">⊞</span> <span v-else-if="preset.id === 'google'">G</span> <Globe v-else class="size-4" /> </div> <Badge :variant="selectedPresetId === preset.id ? 'default' : 'secondary'" class="font-mono text-xs"> {{ preset.badge }} </Badge> </div> <div> <p class="text-foreground text-sm font-semibold">{{ preset.name }}</p> <p class="text-muted-foreground mt-0.5 text-xs">{{ preset.subtitle }}</p> </div> </div> <div class="border-border/60 mt-4 flex items-center justify-between border-t pt-2 text-xs"> <span class="text-muted-foreground max-w-[140px] truncate font-mono text-xs">{{ preset.domain }}</span> <span v-if="selectedPresetId === preset.id" class="text-primary flex items-center gap-1 text-xs font-medium" > <Check class="size-3" /> <span>Active</span> </span> <span v-else class="text-muted-foreground group-hover:text-foreground text-xs">Select →</span> </div> </button> </div> </div> <!-- 2-Column Configuration Form --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- Left Column: Service Provider (SP) Credentials --> <div class="space-y-6 lg:col-span-6"> <Card class="border-border bg-card shadow-xs"> <CardHeader class="pb-4"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <ShieldCheck class="size-4" /> </div> <div> <CardTitle class="text-sm font-semibold">Service Provider (SP) Credentials</CardTitle> <CardDescription class="text-xs"> Copy these values into your Identity Provider's SAML setup </CardDescription> </div> </div> <Badge variant="outline" class="font-mono text-xs">Our Endpoints</Badge> </div> </CardHeader> <CardContent class="space-y-4 pt-0"> <!-- ACS URL --> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="sp-acs-url" class="text-foreground text-xs font-medium"> Assertion Consumer Service (ACS) URL </label> <Button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-6 px-2 text-xs" @click="copyText(spAcsUrl, 'acs')" > <Check v-if="copiedKey === 'acs'" class="text-success mr-1 size-3" /> <Copy v-else class="mr-1 size-3" /> <span>{{ copiedKey === 'acs' ? 'Copied' : 'Copy' }}</span> </Button> </div> <Input id="sp-acs-url" :model-value="spAcsUrl" readonly class="bg-muted/30 font-mono text-xs select-all" /> <p class="text-muted-foreground text-xs"> The callback URL where your IdP transmits HTTP-POST SAML assertion responses. </p> </div> <!-- Entity ID / Audience --> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="sp-entity-id" class="text-foreground text-xs font-medium"> SP Entity ID / Audience URI </label> <Button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-6 px-2 text-xs" @click="copyText(spEntityId, 'entity-id')" > <Check v-if="copiedKey === 'entity-id'" class="text-success mr-1 size-3" /> <Copy v-else class="mr-1 size-3" /> <span>{{ copiedKey === 'entity-id' ? 'Copied' : 'Copy' }}</span> </Button> </div> <Input id="sp-entity-id" :model-value="spEntityId" readonly class="bg-muted/30 font-mono text-xs select-all" /> <p class="text-muted-foreground text-xs"> The unique audience restriction URI identifying our service to your IdP. </p> </div> <!-- Protocol Specifications Summary --> <div class="border-border bg-muted/20 space-y-2 rounded-lg border p-3"> <p class="text-foreground text-xs font-medium">Protocol & Binding Requirements</p> <div class="grid grid-cols-2 gap-2 text-xs"> <div> <span class="text-muted-foreground block text-xs">NameID Format</span> <span class="text-foreground font-mono text-xs font-medium">EmailAddress (urn:oasis:...)</span> </div> <div> <span class="text-muted-foreground block text-xs">SAML Binding</span> <span class="text-foreground font-mono text-xs font-medium">HTTP-POST</span> </div> <div> <span class="text-muted-foreground block text-xs">Signing Algorithm</span> <span class="text-foreground font-mono text-xs font-medium">RSA-SHA256</span> </div> <div> <span class="text-muted-foreground block text-xs">Assertion Encryption</span> <span class="text-foreground font-mono text-xs font-medium">Optional (AES-256-GCM)</span> </div> </div> </div> <Separator /> <!-- SP Public Certificate --> <div class="space-y-2"> <div class="flex items-center justify-between"> <label class="text-foreground text-xs font-medium"> SP X.509 Public Certificate </label> <div class="flex items-center gap-1.5"> <Button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-6 px-2 text-xs" @click="copyText(spCertificatePem, 'sp-cert')" > <Check v-if="copiedKey === 'sp-cert'" class="text-success mr-1 size-3" /> <Copy v-else class="mr-1 size-3" /> <span>{{ copiedKey === 'sp-cert' ? 'Copied' : 'Copy PEM' }}</span> </Button> <Button aria-label="Download attachment" variant="outline" size="sm" class="h-6 gap-1 px-2 text-xs" @click="downloadSpCertificate" > <Download class="size-3" /> <span>Download .crt</span> </Button> </div> </div> <pre class="border-border bg-muted/40 text-foreground max-h-[140px] overflow-x-auto rounded-md border p-3 font-mono text-xs leading-relaxed select-all" ><code>{{ spCertificatePem }}</code></pre> <p class="text-muted-foreground text-xs"> Import this certificate into your IdP if assertion signing or encryption is required. </p> </div> </CardContent> <CardFooter class="border-border text-muted-foreground flex items-center justify-between border-t pt-3 text-xs" > <span class="flex items-center gap-1.5"> <FileCode class="size-3.5" /> <span>SP Metadata XML Endpoint</span> </span> <Button variant="ghost" size="sm" class="text-primary hover:text-primary/80 h-6 gap-1 px-2 text-xs" @click="copyText(spMetadataXmlUrl, 'meta-url')" > <span>{{ copiedKey === 'meta-url' ? 'Copied URL' : 'Copy metadata.xml URL' }}</span> <ExternalLink class="size-3" /> </Button> </CardFooter> </Card> </div> <!-- Right Column: Identity Provider (IdP) Metadata Form --> <div class="space-y-6 lg:col-span-6"> <Card class="border-border bg-card shadow-xs"> <CardHeader class="pb-4"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <Lock class="size-4" /> </div> <div> <CardTitle class="text-sm font-semibold">Identity Provider (IdP) Metadata</CardTitle> <CardDescription class="text-xs"> Configure SAML endpoints and credentials provided by {{ currentPreset.name }} </CardDescription> </div> </div> <Badge variant="secondary" class="font-mono text-xs">{{ currentPreset.name }}</Badge> </div> </CardHeader> <CardContent class="space-y-4 pt-0"> <!-- XML Upload Dropzone --> <div :class=" cn( 'border-border relative flex cursor-pointer flex-col items-center justify-center rounded-xl border border-dashed p-4 text-center transition-colors', isDraggingXml ? 'border-primary bg-primary/10' : 'bg-muted/20 hover:border-primary/50 hover:bg-muted/40', ) " @dragover="handleXmlDragOver" @dragleave="handleXmlDragLeave" @drop="handleXmlDrop" @click="simulateXmlUpload" > <div class="bg-primary/10 text-primary mb-2 grid size-9 place-items-center rounded-full"> <UploadCloud class="size-4" /> </div> <p class="text-foreground text-xs font-semibold"> Drag & drop IdP <span class="font-mono">metadata.xml</span> to auto-fill </p> <p class="text-muted-foreground mt-0.5 text-xs"> Or click here to simulate parsing your Identity Provider metadata file </p> </div> <!-- XML Notice --> <div v-if="xmlUploadedNotice" class="border-success/30 bg-success/10 text-success flex items-center gap-2 rounded-lg border p-2.5 text-xs" > <CheckCircle2 class="size-4 shrink-0" /> <span>Parsed SSO URL, Entity ID, and X.509 Certificate from metadata XML.</span> </div> <!-- IdP SSO URL --> <div class="space-y-1.5"> <label for="idp-sso-url" class="text-foreground text-xs font-medium"> IdP Single Sign-On (SSO) URL </label> <Input id="idp-sso-url" v-model="idpSsoUrl" placeholder="https://company.okta.com/app/sso/saml" class="font-mono text-xs" /> <p class="text-muted-foreground text-xs"> The HTTP-POST endpoint at your IdP where users are redirected for authentication. </p> </div> <!-- IdP Entity ID / Issuer --> <div class="space-y-1.5"> <label for="idp-entity-id" class="text-foreground text-xs font-medium"> IdP Entity ID / Issuer URI </label> <Input id="idp-entity-id" v-model="idpEntityId" placeholder="http://www.okta.com/exk849201" class="font-mono text-xs" /> <p class="text-muted-foreground text-xs"> The unique issuer string provided in your IdP metadata document. </p> </div> <!-- X.509 Certificate PEM Textarea --> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="idp-cert-pem" class="text-foreground text-xs font-medium"> IdP X.509 Signing Certificate (PEM) </label> <Badge variant="outline" class="border-success/40 text-success font-mono text-xs"> Valid (Expires Oct 2028) </Badge> </div> <Textarea id="idp-cert-pem" v-model="idpCertPem" :rows="5" no-resize class="border-border/70 font-mono text-xs leading-relaxed" placeholder="-----BEGIN CERTIFICATE-----..." /> <p class="text-muted-foreground text-xs"> Public X.509 certificate used by our SP to verify the cryptographic signature on SAML assertions. </p> </div> <Separator /> <!-- Domain Enforcement Switch --> <div class="border-border bg-muted/20 flex items-start justify-between gap-3 rounded-lg border p-3"> <div class="space-y-0.5"> <div class="flex items-center gap-1.5"> <ShieldAlert class="text-primary size-4" /> <p class="text-foreground text-xs font-semibold">Enforce SSO for all domain emails</p> </div> <p class="text-muted-foreground text-xs"> Require all users with <span class="text-foreground font-mono font-medium">@company.com</span> email addresses to log in via SSO. Password logins will be blocked. </p> </div> <Switch v-model="enforceDomain" class="mt-1" aria-label="Enforce SSO for domain" /> </div> </CardContent> <CardFooter class="border-border flex flex-wrap items-center justify-between gap-2 border-t pt-4"> <Button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground text-xs" @click="handleSelectPreset(selectedPresetId)" > Reset to Defaults </Button> <Button size="sm" class="gap-1.5 text-xs font-medium shadow-xs" :disabled="isSaving || !idpSsoUrl.trim() || !idpEntityId.trim()" @click="handleSaveConfig" > <RotateCw v-if="isSaving" class="size-3.5 animate-spin" /> <Save v-else class="size-3.5" /> <span>{{ isSaving ? 'Saving...' : 'Save SSO Configuration' }}</span> </Button> </CardFooter> </Card> </div> </div> <!-- Test SAML Login Diagnostic Dialog --> <Dialog :open="testModalOpen" @update:open="testModalOpen = $event"> <DialogContent class="sm:max-w-xl"> <DialogHeader> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <UserCheck class="size-4" /> </div> <div> <DialogTitle>SAML 2.0 Authentication Diagnostic</DialogTitle> <DialogDescription> Live assertion handshake simulation with {{ currentPreset.name }} </DialogDescription> </div> </div> </DialogHeader> <div class="space-y-4 py-2"> <!-- Handshake Status Banner --> <div class="border-success/30 bg-success/10 text-success flex items-center justify-between rounded-lg border p-3 text-xs" > <div class="flex items-center gap-2"> <CheckCircle2 class="size-4 shrink-0" /> <div> <p class="font-semibold">HTTP 200 OK — SAML Handshake Validated</p> <p class="text-xs opacity-90">All signature envelopes matched X.509 certificate</p> </div> </div> <Badge variant="success" class="font-mono text-xs">Success</Badge> </div> <!-- Step Verification Checklist --> <div class="border-border bg-muted/20 space-y-2 rounded-lg border p-3 text-xs"> <h4 class="text-foreground font-semibold">Verification Pipeline</h4> <div class="space-y-1.5"> <div class="text-foreground flex items-center gap-2"> <Check class="text-success size-3.5 shrink-0" /> <span >1. AuthnRequest dispatched with SP signature to <code class="font-mono text-xs">{{ idpSsoUrl }}</code></span > </div> <div class="text-foreground flex items-center gap-2"> <Check class="text-success size-3.5 shrink-0" /> <span>2. SAMLResponse assertion received and verified with RSA-SHA256 signature</span> </div> <div class="text-foreground flex items-center gap-2"> <Check class="text-success size-3.5 shrink-0" /> <span >3. AudienceRestriction matched <code class="font-mono text-xs">{{ spEntityId }}</code></span > </div> <div class="text-foreground flex items-center gap-2"> <Check class="text-success size-3.5 shrink-0" /> <span>4. Assertion timestamp valid within 300s clock-skew tolerance window</span> </div> </div> </div> <!-- Asserted Claims Table --> <div class="space-y-1.5"> <h4 class="text-foreground text-xs font-semibold">Asserted Identity Claims & Attributes</h4> <div class="border-border bg-muted/10 overflow-hidden rounded-md border text-xs"> <table class="w-full"> <thead> <tr class="border-border bg-muted/40 text-muted-foreground border-b text-left font-medium"> <th class="p-2 text-xs">Field</th> <th class="p-2 text-xs">Sample Claim Value</th> </tr> </thead> <tbody class="divide-border divide-y font-mono text-xs"> <tr v-for="claim in simulatedClaims" :key="claim.mappedField" class="hover:bg-muted/20"> <td class="text-primary p-2 font-semibold">{{ claim.mappedField }}</td> <td class="text-foreground max-w-[240px] truncate p-2">{{ claim.sampleValue }}</td> </tr> </tbody> </table> </div> </div> </div> <DialogFooter> <Button variant="outline" size="sm" @click="testModalOpen = false">Close</Button> <Button size="sm" class="gap-1.5" @click="openTestModal"> <RotateCw class="size-3.5" /> <span>Re-test Handshake</span> </Button> </DialogFooter> </DialogContent> </Dialog> </div> </template>
Raw manifest:https://uipkge.dev/r/vue/sso-identity-provider-config.json