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 uipkge-ng@latest add sso-identity-provider-config$npx uipkge-ng@latest add sso-identity-provider-config$yarn dlx uipkge-ng@latest add sso-identity-provider-config$bunx uipkge-ng@latest add sso-identity-provider-configManifest:
https://uipkge.dev/r/angular/sso-identity-provider-config.jsonInstalls to:src/app/components/blocks/sso-identity-provider-config/Variants
Loading interactive previews…
Loading interactive previews…
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 (2)
src/app/components/blocks/sso-identity-provider-config/sso-identity-provider-config.component.ts37.6 kB
// Enterprise 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. // Port of the Vue block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Check, CheckCircle2, Copy, Download, ExternalLink, FileCode, Globe, KeyRound, Lock, LucideAngularModule, Play, RotateCw, Save, ShieldAlert, ShieldCheck, UploadCloud, UserCheck, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' import { UiTextareaComponent } from '@/ui/textarea/textarea.component' 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 } 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 spMetadataXmlUrl = 'https://auth.uipkge.dev/sso/saml/metadata.xml' const spCertificatePem = `-----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIUW0Qz0K7VzN8XlP4wQ6k9J1vYgLAwDQYJKoZIhvcNAQEL BQAwMzEXMBUGA1UEAwwNYXV0aC51aXBrZ2UuZGV2MRgwFgYDVQQKDA9VSVBLR0Ug QXV0aCBTU08wHhcNMjYwODIxMTQzMDAwWhcNMzEwODIxMTQzMDAwWjAzMRcwFQYD VQQDDA5hdXRoLnVpcGtnZS5kZXYxGDAWBgNVBAoMD1VJUEtHRSBBdXRoIFNTTzCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL1K9sU3FvX+7nQ6m2W4j1k9 -----END CERTIFICATE-----` const simulatedClaims: ClaimAttribute[] = [ { samlAttribute: 'urn:oid:0.9.2342.19200300.100.1.1', mappedField: 'nameID', sampleValue: '[email protected]' }, { samlAttribute: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', mappedField: 'email', sampleValue: '[email protected]', }, { 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', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-sso-identity-provider-config, [ui-sso-identity-provider-config]', standalone: true, // Vue renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiInputComponent, UiSeparatorComponent, UiSwitchComponent, UiTextareaComponent, ], template: ` <div data-slot="sso-identity-provider-config" [class]="rootClass"> <!-- Header Banner & Top Controls --> <div ui-card class="border-border bg-card/80 shadow-xs backdrop-blur-xs"> <div ui-card-header 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"> <lucide-icon [img]="KeyRound" class="size-4" /> </div> <h3 ui-card-title class="text-base font-semibold">Single Sign-On (SSO) & SAML 2.0 Configuration</h3> @if (ssoEnabled()) { <span ui-badge variant="success" class="font-mono text-xs"> <span class="bg-success mr-1.5 inline-block size-2 animate-pulse rounded-full"></span> SSO Active </span> } @else { <span ui-badge variant="outline" class="text-muted-foreground border-border font-mono text-xs"> Disabled </span> } </div> <p ui-card-description class="text-xs"> Connect your enterprise IdP (Okta, Azure AD / Entra, Google Workspace, OneLogin) for federated SAML authentication. </p> </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> <button ui-switch [checked]="ssoEnabled()" (checkedChange)="ssoEnabled.set($event)" aria-label="Toggle Single Sign-On" ></button> </div> <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs font-medium" [disabled]="!ssoEnabled()" (click)="openTestModal()" > <lucide-icon [img]="Play" class="size-3.5" /> <span>Test SAML Login</span> </button> </div> </div> </div> </div> <!-- Save Success Banner --> @if (saveSuccessNotice()) { <div 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"> <lucide-icon [img]="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.set(false)" > <lucide-icon [img]="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> <span ui-badge variant="outline" class="border-border font-mono text-xs">SAML 2.0 / Web SSO</span> </div> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4"> @for (preset of presets; track preset.id) { <button type="button" [class]="presetCardClass(preset.id)" (click)="handleSelectPreset(preset.id)"> <div class="space-y-2"> <div class="flex items-center justify-between"> <div [class]="presetIconClass(preset.id)"> @if (preset.id === 'okta') { <span>O</span> } @else if (preset.id === 'entra') { <span>⊞</span> } @else if (preset.id === 'google') { <span>G</span> } @else { <lucide-icon [img]="Globe" class="size-4" /> } </div> <span ui-badge [variant]="selectedPresetId() === preset.id ? 'default' : 'secondary'" class="font-mono text-xs" > {{ preset.badge }} </span> </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> @if (selectedPresetId() === preset.id) { <span class="text-primary flex items-center gap-1 text-xs font-medium"> <lucide-icon [img]="Check" class="size-3" /> <span>Active</span> </span> } @else { <span 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"> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header 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"> <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> <div> <h3 ui-card-title class="text-sm font-semibold">Service Provider (SP) Credentials</h3> <p ui-card-description class="text-xs"> Copy these values into your Identity Provider's SAML setup </p> </div> </div> <span ui-badge variant="outline" class="font-mono text-xs">Our Endpoints</span> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="angular-sp-acs-url" class="text-foreground text-xs font-medium"> Assertion Consumer Service (ACS) URL </label> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-6 px-2 text-xs" (click)="copyText(spAcsUrlValue, 'acs')" > @if (copiedKey() === 'acs') { <lucide-icon [img]="Check" class="text-success mr-1 size-3" /> } @else { <lucide-icon [img]="Copy" class="mr-1 size-3" /> } <span>{{ copiedKey() === 'acs' ? 'Copied' : 'Copy' }}</span> </button> </div> <ui-input id="angular-sp-acs-url" [value]="spAcsUrlValue" [readonly]="true" 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> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="angular-sp-entity-id" class="text-foreground text-xs font-medium"> SP Entity ID / Audience URI </label> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-6 px-2 text-xs" (click)="copyText(spEntityIdValue, 'entity-id')" > @if (copiedKey() === 'entity-id') { <lucide-icon [img]="Check" class="text-success mr-1 size-3" /> } @else { <lucide-icon [img]="Copy" class="mr-1 size-3" /> } <span>{{ copiedKey() === 'entity-id' ? 'Copied' : 'Copy' }}</span> </button> </div> <ui-input id="angular-sp-entity-id" [value]="spEntityIdValue" [readonly]="true" 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> <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> <ui-separator /> <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 ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-6 px-2 text-xs" (click)="copyText(spCertificatePemValue, 'sp-cert')" > @if (copiedKey() === 'sp-cert') { <lucide-icon [img]="Check" class="text-success mr-1 size-3" /> } @else { <lucide-icon [img]="Copy" class="mr-1 size-3" /> } <span>{{ copiedKey() === 'sp-cert' ? 'Copied' : 'Copy PEM' }}</span> </button> <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="h-6 gap-1 px-2 text-xs" (click)="downloadSpCertificate()" > <lucide-icon [img]="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>{{ spCertificatePemValue }}</code></pre> <p class="text-muted-foreground text-xs"> Import this certificate into your IdP if assertion signing or encryption is required. </p> </div> </div> <div ui-card-footer 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"> <lucide-icon [img]="FileCode" class="size-3.5" /> <span>SP Metadata XML Endpoint</span> </span> <button ui-button variant="ghost" size="sm" class="text-primary hover:text-primary/80 h-6 gap-1 px-2 text-xs" (click)="copyText(spMetadataXmlUrlValue, 'meta-url')" > <span>{{ copiedKey() === 'meta-url' ? 'Copied URL' : 'Copy metadata.xml URL' }}</span> <lucide-icon [img]="ExternalLink" class="size-3" /> </button> </div> </div> </div> <!-- Right Column: Identity Provider (IdP) Metadata Form --> <div class="space-y-6 lg:col-span-6"> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header 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"> <lucide-icon [img]="Lock" class="size-4" /> </div> <div> <h3 ui-card-title class="text-sm font-semibold">Identity Provider (IdP) Metadata</h3> <p ui-card-description class="text-xs"> Configure SAML endpoints and credentials provided by {{ currentPreset().name }} </p> </div> </div> <span ui-badge variant="secondary" class="font-mono text-xs">{{ currentPreset().name }}</span> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <div [class]="dropzoneClass" (dragover)="handleXmlDragOver($event)" (dragleave)="handleXmlDragLeave()" (drop)="handleXmlDrop($event)" (click)="simulateXmlUpload()" > <div class="bg-primary/10 text-primary mb-2 grid size-9 place-items-center rounded-full"> <lucide-icon [img]="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> @if (xmlUploadedNotice()) { <div class="border-success/30 bg-success/10 text-success flex items-center gap-2 rounded-lg border p-2.5 text-xs" > <lucide-icon [img]="CheckCircle2" class="size-4 shrink-0" /> <span>Parsed SSO URL, Entity ID, and X.509 Certificate from metadata XML.</span> </div> } <div class="space-y-1.5"> <label for="angular-idp-sso-url" class="text-foreground text-xs font-medium"> IdP Single Sign-On (SSO) URL </label> <ui-input id="angular-idp-sso-url" [value]="idpSsoUrl()" (valueChange)="idpSsoUrl.set($event)" 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> <div class="space-y-1.5"> <label for="angular-idp-entity-id" class="text-foreground text-xs font-medium"> IdP Entity ID / Issuer URI </label> <ui-input id="angular-idp-entity-id" [value]="idpEntityId()" (valueChange)="idpEntityId.set($event)" 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> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="angular-idp-cert-pem" class="text-foreground text-xs font-medium"> IdP X.509 Signing Certificate (PEM) </label> <span ui-badge variant="outline" class="border-success/40 text-success font-mono text-xs"> Valid (Expires Oct 2028) </span> </div> <ui-textarea id="angular-idp-cert-pem" [value]="idpCertPem()" (valueChange)="idpCertPem.set($event)" [rows]="5" 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> <ui-separator /> <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"> <lucide-icon [img]="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> <button ui-switch class="mt-1" [checked]="enforceDomain()" (checkedChange)="enforceDomain.set($event)" aria-label="Enforce SSO for domain" ></button> </div> </div> <div ui-card-footer class="border-border flex flex-wrap items-center justify-between gap-2 border-t pt-4"> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground text-xs" (click)="handleSelectPreset(selectedPresetId())" > Reset to Defaults </button> <button ui-button size="sm" class="gap-1.5 text-xs font-medium shadow-xs" [disabled]="isSaving() || !idpSsoUrl().trim() || !idpEntityId().trim()" (click)="handleSaveConfig()" > @if (isSaving()) { <lucide-icon [img]="RotateCw" class="size-3.5 animate-spin" /> } @else { <lucide-icon [img]="Save" class="size-3.5" /> } <span>{{ isSaving() ? 'Saving...' : 'Save SSO Configuration' }}</span> </button> </div> </div> </div> </div> <!-- Test SAML Login Diagnostic Dialog --> <ui-dialog [open]="testModalOpen()" (openChange)="testModalOpen.set($event)"> <ui-dialog-content class="sm:max-w-xl"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="UserCheck" class="size-4" /> </div> <div> <h2 ui-dialog-title>SAML 2.0 Authentication Diagnostic</h2> <p ui-dialog-description>Live assertion handshake simulation with {{ currentPreset().name }}</p> </div> </div> </div> <div class="space-y-4 py-2"> <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"> <lucide-icon [img]="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> <span ui-badge variant="success" class="font-mono text-xs">Success</span> </div> <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"> <lucide-icon [img]="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"> <lucide-icon [img]="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"> <lucide-icon [img]="Check" class="text-success size-3.5 shrink-0" /> <span> 3. AudienceRestriction matched <code class="font-mono text-xs">{{ spEntityIdValue }}</code> </span> </div> <div class="text-foreground flex items-center gap-2"> <lucide-icon [img]="Check" class="text-success size-3.5 shrink-0" /> <span>4. Assertion timestamp valid within 300s clock-skew tolerance window</span> </div> </div> </div> <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"> @for (claim of claims; track claim.mappedField) { <tr 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> <div ui-dialog-footer> <button ui-button variant="outline" size="sm" (click)="testModalOpen.set(false)">Close</button> <button ui-button size="sm" class="gap-1.5" (click)="openTestModal()"> <lucide-icon [img]="RotateCw" class="size-3.5" /> <span>Re-test Handshake</span> </button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiSsoIdentityProviderConfigComponent { protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Copy = Copy protected readonly Download = Download protected readonly ExternalLink = ExternalLink protected readonly FileCode = FileCode protected readonly Globe = Globe protected readonly KeyRound = KeyRound protected readonly Lock = Lock protected readonly Play = Play protected readonly RotateCw = RotateCw protected readonly Save = Save protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly UploadCloud = UploadCloud protected readonly UserCheck = UserCheck protected readonly X = X protected readonly presets = idpPresets protected readonly claims = simulatedClaims protected readonly spAcsUrlValue = spAcsUrl protected readonly spEntityIdValue = spEntityId protected readonly spMetadataXmlUrlValue = spMetadataXmlUrl protected readonly spCertificatePemValue = spCertificatePem @Input() set initialSsoEnabled(v: boolean) { this.ssoEnabled.set(v ?? true) } @Input() set initialPreset(v: IdpPresetId) { if (v) this.handleSelectPreset(v) } @Input() set initialEnforceDomain(v: boolean) { this.enforceDomain.set(v ?? true) } @Input('class') className?: string readonly ssoEnabled = signal(true) readonly selectedPresetId = signal<IdpPresetId>('okta') readonly enforceDomain = signal(true) readonly currentPreset = computed(() => idpPresets.find((p) => p.id === this.selectedPresetId()) ?? idpPresets[0]!) readonly idpSsoUrl = signal(idpPresets[0]!.defaultSsoUrl) readonly idpEntityId = signal(idpPresets[0]!.defaultEntityId) readonly idpCertPem = signal(idpPresets[0]!.defaultCert) readonly copiedKey = signal<string | null>(null) private copyTimer: ReturnType<typeof setTimeout> | undefined readonly isDraggingXml = signal(false) readonly xmlUploadedNotice = signal(false) readonly isSaving = signal(false) readonly saveSuccessNotice = signal(false) readonly testModalOpen = signal(false) readonly isTesting = signal(false) get rootClass(): string { return cn('w-full space-y-6', this.className) } get dropzoneClass(): string { return cn( 'border-border relative flex cursor-pointer flex-col items-center justify-center rounded-xl border border-dashed p-4 text-center transition-colors', this.isDraggingXml() ? 'border-primary bg-primary/10' : 'bg-muted/20 hover:border-primary/50 hover:bg-muted/40', ) } presetCardClass(id: IdpPresetId): string { return 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', this.selectedPresetId() === id ? 'border-primary bg-primary/5 ring-primary shadow-xs ring-1' : 'border-border bg-card hover:border-primary/40 hover:bg-accent/40', ) } presetIconClass(id: IdpPresetId): string { return cn( 'flex size-8 items-center justify-center rounded-lg text-xs font-bold transition-colors', this.selectedPresetId() === id ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground group-hover:bg-primary/10 group-hover:text-primary', ) } async copyText(text: string, key: string): Promise<void> { try { await navigator.clipboard.writeText(text) this.copiedKey.set(key) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => { if (this.copiedKey() === key) this.copiedKey.set(null) }, 2000) } catch { // Clipboard unavailable } } handleSelectPreset(presetId: IdpPresetId): void { this.selectedPresetId.set(presetId) const preset = idpPresets.find((p) => p.id === presetId) if (preset) { this.idpSsoUrl.set(preset.defaultSsoUrl) this.idpEntityId.set(preset.defaultEntityId) this.idpCertPem.set(preset.defaultCert) this.xmlUploadedNotice.set(false) } } downloadSpCertificate(): void { 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) } handleXmlDrop(e: DragEvent): void { e.preventDefault() this.isDraggingXml.set(false) this.simulateXmlUpload() } handleXmlDragOver(e: DragEvent): void { e.preventDefault() this.isDraggingXml.set(true) } handleXmlDragLeave(): void { this.isDraggingXml.set(false) } simulateXmlUpload(): void { const preset = this.currentPreset() this.idpSsoUrl.set(preset.defaultSsoUrl) this.idpEntityId.set(preset.defaultEntityId) this.idpCertPem.set(preset.defaultCert) this.xmlUploadedNotice.set(true) setTimeout(() => this.xmlUploadedNotice.set(false), 4000) } handleSaveConfig(): void { if (this.isSaving()) return this.isSaving.set(true) setTimeout(() => { this.isSaving.set(false) this.saveSuccessNotice.set(true) setTimeout(() => this.saveSuccessNotice.set(false), 3500) }, 600) } openTestModal(): void { this.testModalOpen.set(true) this.isTesting.set(true) setTimeout(() => this.isTesting.set(false), 500) } }src/app/components/blocks/sso-identity-provider-config/index.ts0.1 kB
export { UiSsoIdentityProviderConfigComponent, type IdpPresetId } from './sso-identity-provider-config.component'
Raw manifest:https://uipkge.dev/r/angular/sso-identity-provider-config.json