mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-08-06 10:50:19 +03:00
Initial commit - 611 cybersecurity skills across all subdomains
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: None
|
||||
description: Implement SAML 2.0 Single Sign-On (SSO) using Okta as the Identity Provider (IdP). This skill covers end-to-end configuration of SAML authentication flows, attribute mapping, certificate management, a
|
||||
domain: cybersecurity
|
||||
subdomain: identity-access-management
|
||||
tags: [iam, identity, access-control, authentication, saml, sso, okta]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: MIT
|
||||
---
|
||||
# Implementing SAML SSO with Okta
|
||||
|
||||
## Overview
|
||||
Implement SAML 2.0 Single Sign-On (SSO) using Okta as the Identity Provider (IdP). This skill covers end-to-end configuration of SAML authentication flows, attribute mapping, certificate management, and security hardening for enterprise SSO deployments.
|
||||
|
||||
## Objectives
|
||||
- Configure Okta as a SAML 2.0 Identity Provider
|
||||
- Implement SP-initiated and IdP-initiated SSO flows
|
||||
- Map SAML attributes and configure assertion encryption
|
||||
- Enforce SHA-256 signatures and secure certificate rotation
|
||||
- Test SSO flows with SAML tracer tools
|
||||
- Implement Single Logout (SLO) handling
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### SAML 2.0 Authentication Flow
|
||||
1. **SP-Initiated Flow**: User accesses Service Provider -> SP generates AuthnRequest -> Redirect to Okta IdP -> User authenticates -> Okta sends SAML Response -> SP validates assertion -> Access granted
|
||||
2. **IdP-Initiated Flow**: User authenticates at Okta -> Selects application -> Okta sends unsolicited SAML Response -> SP validates -> Access granted
|
||||
|
||||
### Critical Security Requirements
|
||||
- **SHA-256 Signatures**: All SAML assertions must use SHA-256 (not SHA-1) for digital signatures
|
||||
- **Assertion Encryption**: Encrypt SAML assertions using AES-256 to protect attribute values in transit
|
||||
- **Audience Restriction**: Configure audience URI to prevent assertion replay across different SPs
|
||||
- **NotBefore/NotOnOrAfter**: Enforce time validity windows to prevent stale assertion usage
|
||||
- **InResponseTo Validation**: Verify assertion corresponds to the original AuthnRequest
|
||||
|
||||
### Okta Application Configuration
|
||||
- **Single Sign-On URL**: The ACS (Assertion Consumer Service) endpoint on the SP
|
||||
- **Audience URI (SP Entity ID)**: Unique identifier for the SP
|
||||
- **Name ID Format**: EmailAddress, Persistent, or Transient
|
||||
- **Attribute Statements**: Map Okta user profile attributes to SAML assertion attributes
|
||||
- **Group Attribute Statements**: Include group membership for RBAC
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create SAML Application in Okta
|
||||
1. Navigate to Applications > Create App Integration
|
||||
2. Select SAML 2.0 as the sign-on method
|
||||
3. Configure General Settings (App Name, Logo)
|
||||
4. Set Single Sign-On URL (ACS URL)
|
||||
5. Set Audience URI (SP Entity ID)
|
||||
6. Configure Name ID Format and Application Username
|
||||
|
||||
### Step 2: Configure Attribute Mapping
|
||||
- Map `user.email` to `email` attribute
|
||||
- Map `user.firstName` and `user.lastName` to name attributes
|
||||
- Add group attribute statements for role-based access
|
||||
- Configure attribute value formats (Basic, URI Reference, Unspecified)
|
||||
|
||||
### Step 3: Download and Install IdP Metadata
|
||||
- Download Okta IdP metadata XML
|
||||
- Extract IdP SSO URL, IdP Entity ID, and X.509 certificate
|
||||
- Install certificate on SP side for signature validation
|
||||
- Configure SP metadata with ACS URL and Entity ID
|
||||
|
||||
### Step 4: Implement SP-Side SAML Processing
|
||||
- Parse and validate SAML Response XML
|
||||
- Verify digital signature using IdP certificate
|
||||
- Check audience restriction, time conditions, and InResponseTo
|
||||
- Extract authenticated user identity and attributes
|
||||
- Create application session based on assertion data
|
||||
|
||||
### Step 5: Security Hardening
|
||||
- Enforce SHA-256 for all signature operations
|
||||
- Enable assertion encryption with AES-256-CBC
|
||||
- Configure session timeout and re-authentication policies
|
||||
- Implement SAML artifact binding for sensitive deployments
|
||||
- Set up certificate rotation procedure before expiry
|
||||
|
||||
### Step 6: Testing and Validation
|
||||
- Use SAML Tracer browser extension for debugging
|
||||
- Validate SP-initiated and IdP-initiated flows
|
||||
- Test with multiple user accounts and group memberships
|
||||
- Verify SLO functionality
|
||||
- Test certificate rotation without downtime
|
||||
|
||||
## Security Controls
|
||||
| Control | NIST 800-53 | Description |
|
||||
|---------|-------------|-------------|
|
||||
| Authentication | IA-2 | Multi-factor authentication through Okta |
|
||||
| Session Management | SC-23 | SAML session lifetime controls |
|
||||
| Audit Logging | AU-3 | Log all SSO authentication events |
|
||||
| Certificate Management | SC-17 | PKI certificate lifecycle management |
|
||||
| Access Enforcement | AC-3 | SAML attribute-based access control |
|
||||
|
||||
## Common Pitfalls
|
||||
- Using SHA-1 instead of SHA-256 for SAML signatures
|
||||
- Not validating InResponseTo in SAML responses (replay attacks)
|
||||
- Clock skew between IdP and SP causing assertion rejection
|
||||
- Failing to restrict audience URI allowing assertion forwarding
|
||||
- Not implementing certificate rotation before expiry causes outage
|
||||
|
||||
## Verification
|
||||
- [ ] SAML SSO login completes successfully via SP-initiated flow
|
||||
- [ ] IdP-initiated flow correctly authenticates users
|
||||
- [ ] SAML assertions use SHA-256 signatures
|
||||
- [ ] Attribute mapping correctly populates user profile
|
||||
- [ ] Session timeout forces re-authentication
|
||||
- [ ] SLO properly terminates sessions on both IdP and SP
|
||||
- [ ] Certificate rotation tested without service interruption
|
||||
@@ -0,0 +1,128 @@
|
||||
# SAML SSO Implementation Checklist Template
|
||||
|
||||
## Project Information
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Service Provider | [Application Name] |
|
||||
| Identity Provider | Okta |
|
||||
| Implementation Lead | [Name] |
|
||||
| Target Go-Live Date | [Date] |
|
||||
| Environment | [ ] Development [ ] Staging [ ] Production |
|
||||
|
||||
## Pre-Implementation Checklist
|
||||
|
||||
### IdP (Okta) Configuration
|
||||
- [ ] Okta organization provisioned and accessible
|
||||
- [ ] Administrative access to Okta confirmed
|
||||
- [ ] Okta MFA enabled for admin accounts
|
||||
- [ ] Application created in Okta (SAML 2.0)
|
||||
- [ ] SSO URL configured: `____________________________`
|
||||
- [ ] Audience URI (SP Entity ID) set: `____________________________`
|
||||
- [ ] Name ID Format selected: [ ] Email [ ] Persistent [ ] Transient
|
||||
- [ ] Attribute statements configured:
|
||||
- [ ] email -> `user.email`
|
||||
- [ ] firstName -> `user.firstName`
|
||||
- [ ] lastName -> `user.lastName`
|
||||
- [ ] groups -> (group attribute filter)
|
||||
- [ ] Signature algorithm set to SHA-256
|
||||
- [ ] Response signed: [ ] Yes [ ] No
|
||||
- [ ] Assertion signed: [ ] Yes [ ] No
|
||||
- [ ] Assertion encryption enabled: [ ] Yes [ ] No
|
||||
- [ ] Users/Groups assigned to application
|
||||
|
||||
### SP Configuration
|
||||
- [ ] SP SAML library installed and configured
|
||||
- [ ] ACS URL endpoint implemented: `____________________________`
|
||||
- [ ] SP Entity ID configured: `____________________________`
|
||||
- [ ] IdP metadata imported (certificate, SSO URL, Entity ID)
|
||||
- [ ] Signature validation implemented (SHA-256)
|
||||
- [ ] InResponseTo validation enabled
|
||||
- [ ] Audience restriction validation enabled
|
||||
- [ ] Time condition validation with clock skew tolerance
|
||||
- [ ] Session creation after successful assertion
|
||||
- [ ] Error handling for failed assertions
|
||||
|
||||
### Security Configuration
|
||||
- [ ] All endpoints use HTTPS
|
||||
- [ ] SHA-256 enforced (SHA-1 rejected)
|
||||
- [ ] Assertion encryption with AES-256-CBC
|
||||
- [ ] Certificate pinning or strong validation
|
||||
- [ ] CSRF protection on ACS endpoint
|
||||
- [ ] Rate limiting on authentication endpoints
|
||||
- [ ] Session timeout configured: ____ minutes
|
||||
- [ ] Idle timeout configured: ____ minutes
|
||||
- [ ] Single Logout (SLO) endpoint configured
|
||||
|
||||
## Certificate Management
|
||||
|
||||
### Current Certificate
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Issuer | |
|
||||
| Subject | |
|
||||
| Serial Number | |
|
||||
| Valid From | |
|
||||
| Valid To | |
|
||||
| SHA-256 Fingerprint | |
|
||||
| Algorithm | |
|
||||
|
||||
### Certificate Rotation Schedule
|
||||
| Date | Action | Status |
|
||||
|------|--------|--------|
|
||||
| | Generate new certificate in Okta | [ ] Done |
|
||||
| | Distribute new certificate to SP team | [ ] Done |
|
||||
| | Install new certificate on SP (dual-cert) | [ ] Done |
|
||||
| | Activate new certificate on Okta | [ ] Done |
|
||||
| | Monitor for authentication failures | [ ] Done |
|
||||
| | Remove old certificate from SP | [ ] Done |
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### SP-Initiated SSO
|
||||
- [ ] User redirected to Okta login page
|
||||
- [ ] Successful authentication creates SP session
|
||||
- [ ] User attributes correctly mapped
|
||||
- [ ] Group memberships correctly populated
|
||||
- [ ] Invalid credentials properly rejected
|
||||
- [ ] MFA prompt appears when configured
|
||||
|
||||
### IdP-Initiated SSO
|
||||
- [ ] Login from Okta dashboard tile works
|
||||
- [ ] Session created correctly on SP
|
||||
- [ ] Deep links preserved after authentication
|
||||
|
||||
### Single Logout
|
||||
- [ ] SP-initiated logout terminates IdP session
|
||||
- [ ] IdP-initiated logout terminates SP session
|
||||
- [ ] All session cookies properly cleared
|
||||
|
||||
### Error Scenarios
|
||||
- [ ] Expired assertion rejected
|
||||
- [ ] Invalid signature rejected
|
||||
- [ ] Wrong audience URI rejected
|
||||
- [ ] Replay attack prevented (InResponseTo)
|
||||
- [ ] Clock skew beyond tolerance rejected
|
||||
|
||||
## Post-Implementation
|
||||
|
||||
### Monitoring
|
||||
- [ ] Authentication success/failure alerts configured
|
||||
- [ ] Certificate expiration monitoring (30-day warning)
|
||||
- [ ] Okta System Log integration with SIEM
|
||||
- [ ] SP authentication logs forwarded to SIEM
|
||||
- [ ] Dashboard for SSO health metrics
|
||||
|
||||
### Documentation
|
||||
- [ ] Architecture diagram updated
|
||||
- [ ] Runbook for SSO troubleshooting created
|
||||
- [ ] Certificate rotation procedure documented
|
||||
- [ ] Break-glass access procedure documented
|
||||
- [ ] User communication sent
|
||||
|
||||
## Sign-Off
|
||||
| Role | Name | Date | Signature |
|
||||
|------|------|------|-----------|
|
||||
| Security Lead | | | |
|
||||
| Application Owner | | | |
|
||||
| IAM Team Lead | | | |
|
||||
| Change Manager | | | |
|
||||
@@ -0,0 +1,41 @@
|
||||
# Standards and References - SAML SSO with Okta
|
||||
|
||||
## SAML 2.0 Standards
|
||||
- **OASIS SAML 2.0 Core**: Defines assertions, protocols, bindings, and profiles
|
||||
- http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
||||
- **SAML 2.0 Bindings**: HTTP Redirect, HTTP POST, HTTP Artifact, SOAP
|
||||
- http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
|
||||
- **SAML 2.0 Profiles**: Web Browser SSO Profile, Single Logout Profile
|
||||
- http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf
|
||||
|
||||
## NIST Standards
|
||||
- **NIST SP 800-63B**: Digital Identity Guidelines - Authentication and Lifecycle Management
|
||||
- https://pages.nist.gov/800-63-3/sp800-63b.html
|
||||
- **NIST SP 800-53 Rev 5**: Security and Privacy Controls
|
||||
- IA-2: Identification and Authentication (Organizational Users)
|
||||
- IA-8: Identification and Authentication (Non-Organizational Users)
|
||||
- AC-3: Access Enforcement
|
||||
- AU-3: Content of Audit Records
|
||||
- SC-23: Session Authenticity
|
||||
- **NIST SP 1800-35**: Implementing a Zero Trust Architecture
|
||||
- https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.1800-35.pdf
|
||||
|
||||
## Okta Documentation
|
||||
- **Okta SAML Integration Guide**: https://developer.okta.com/docs/guides/build-sso-integration/saml2/main/
|
||||
- **Understanding SAML with Okta**: https://developer.okta.com/docs/concepts/saml/
|
||||
- **Okta SAML App Configuration**: https://help.okta.com/en-us/content/topics/apps/apps-about-saml.htm
|
||||
|
||||
## Security Best Practices
|
||||
- **OWASP SAML Security Cheat Sheet**: Covers SAML signature wrapping attacks, XML injection
|
||||
- **SSO Best Practices 2025**: SHA-256 enforcement, assertion encryption, certificate rotation
|
||||
- https://clerk.com/articles/sso-best-practices-for-secure-scalable-logins
|
||||
|
||||
## XML Security Standards
|
||||
- **XML Signature (XMLDSig)**: W3C standard for XML digital signatures
|
||||
- **XML Encryption**: W3C standard for encrypting XML content
|
||||
- **XML Canonicalization (C14N)**: Required for consistent signature verification
|
||||
|
||||
## Compliance Frameworks
|
||||
- **SOC 2 Type II**: Logical access controls through SSO
|
||||
- **ISO 27001**: A.9.4.2 Secure log-on procedures
|
||||
- **PCI DSS 4.0**: Requirement 8 - Identify Users and Authenticate Access
|
||||
@@ -0,0 +1,92 @@
|
||||
# SAML SSO Implementation Workflows
|
||||
|
||||
## Workflow 1: SP-Initiated SSO Flow
|
||||
|
||||
```
|
||||
User -> Service Provider -> Okta IdP -> User Authenticates -> Okta -> Service Provider -> User
|
||||
```
|
||||
|
||||
### Detailed Steps:
|
||||
1. User accesses protected resource on Service Provider
|
||||
2. SP checks for existing session - none found
|
||||
3. SP generates SAML AuthnRequest with:
|
||||
- Issuer (SP Entity ID)
|
||||
- AssertionConsumerServiceURL
|
||||
- NameIDPolicy (email format)
|
||||
- RequestID (for InResponseTo validation)
|
||||
4. SP redirects user to Okta SSO URL with base64-encoded AuthnRequest
|
||||
5. Okta authenticates user (credentials, MFA if configured)
|
||||
6. Okta generates SAML Response containing:
|
||||
- Signed assertion with SHA-256
|
||||
- Subject NameID (user identifier)
|
||||
- Conditions (NotBefore, NotOnOrAfter, AudienceRestriction)
|
||||
- AuthnStatement (authentication context)
|
||||
- AttributeStatement (mapped user attributes)
|
||||
7. Okta POSTs SAML Response to SP ACS URL
|
||||
8. SP validates SAML Response:
|
||||
- Verify XML signature against Okta certificate
|
||||
- Check InResponseTo matches original request ID
|
||||
- Validate time conditions (with clock skew tolerance)
|
||||
- Verify audience restriction matches SP Entity ID
|
||||
- Check authentication context class
|
||||
9. SP extracts user identity and attributes
|
||||
10. SP creates local session and grants access
|
||||
|
||||
## Workflow 2: IdP-Initiated SSO Flow
|
||||
|
||||
### Steps:
|
||||
1. User logs into Okta dashboard
|
||||
2. User clicks on application tile
|
||||
3. Okta generates unsolicited SAML Response (no InResponseTo)
|
||||
4. Okta POSTs to SP ACS URL
|
||||
5. SP validates assertion (no InResponseTo check)
|
||||
6. SP creates session
|
||||
|
||||
### Security Note:
|
||||
IdP-initiated SSO is less secure because it cannot validate InResponseTo, making it more susceptible to replay attacks. Use SP-initiated flow when possible.
|
||||
|
||||
## Workflow 3: Certificate Rotation
|
||||
|
||||
### Steps:
|
||||
1. Generate new X.509 certificate in Okta (Admin > Settings > Security)
|
||||
2. Download new certificate (do not yet set as active)
|
||||
3. Install new certificate on SP alongside existing certificate
|
||||
4. Configure SP to accept assertions signed with either certificate
|
||||
5. Activate new certificate in Okta
|
||||
6. Monitor for authentication failures
|
||||
7. After validation period, remove old certificate from SP
|
||||
8. Update SAML metadata on both sides
|
||||
|
||||
### Timeline:
|
||||
- Day 0: Generate new certificate and distribute to SP team
|
||||
- Day 1-7: SP installs new certificate (dual-cert mode)
|
||||
- Day 8: Activate new certificate in Okta
|
||||
- Day 8-14: Monitor authentication logs for failures
|
||||
- Day 15: Remove old certificate from SP
|
||||
|
||||
## Workflow 4: Single Logout (SLO)
|
||||
|
||||
### Steps:
|
||||
1. User initiates logout at SP
|
||||
2. SP generates SAML LogoutRequest
|
||||
3. SP sends LogoutRequest to Okta SLO endpoint
|
||||
4. Okta terminates IdP session
|
||||
5. Okta sends LogoutRequest to all other SPs in session
|
||||
6. Each SP terminates local session
|
||||
7. Okta sends LogoutResponse to initiating SP
|
||||
8. SP confirms logout to user
|
||||
|
||||
## Workflow 5: Troubleshooting Authentication Failures
|
||||
|
||||
### Diagnostic Steps:
|
||||
1. Install SAML Tracer browser extension
|
||||
2. Reproduce the failed SSO attempt
|
||||
3. Capture the SAML AuthnRequest and Response
|
||||
4. Check for common issues:
|
||||
- **Signature Invalid**: Certificate mismatch or SHA-1 vs SHA-256
|
||||
- **Audience Mismatch**: SP Entity ID doesn't match Okta config
|
||||
- **Time Condition Failed**: Clock skew > configured tolerance
|
||||
- **NameID Format Mismatch**: SP expects different format
|
||||
- **Missing Attributes**: Attribute mapping not configured
|
||||
5. Review Okta System Log for error details
|
||||
6. Verify SP metadata matches Okta configuration
|
||||
@@ -0,0 +1,499 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SAML SSO Configuration Validator and Health Checker for Okta
|
||||
|
||||
This script validates SAML SSO configurations, checks certificate
|
||||
expiration, tests metadata endpoints, and monitors authentication
|
||||
health for Okta-based SAML integrations.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import base64
|
||||
import hashlib
|
||||
import datetime
|
||||
import json
|
||||
import ssl
|
||||
import socket
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class SAMLConfig:
|
||||
"""SAML configuration parameters."""
|
||||
idp_sso_url: str
|
||||
idp_entity_id: str
|
||||
sp_entity_id: str
|
||||
sp_acs_url: str
|
||||
sp_slo_url: str = ""
|
||||
name_id_format: str = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
||||
signature_algorithm: str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
digest_algorithm: str = "http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
assertion_encrypted: bool = False
|
||||
certificate_path: str = ""
|
||||
metadata_url: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Result of a validation check."""
|
||||
check_name: str
|
||||
passed: bool
|
||||
severity: str # critical, high, medium, low
|
||||
message: str
|
||||
remediation: str = ""
|
||||
|
||||
|
||||
class SAMLSSOValidator:
|
||||
"""Validates SAML SSO configurations and health."""
|
||||
|
||||
SAML_NS = {
|
||||
'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
|
||||
'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
||||
'md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||
'ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'xenc': 'http://www.w3.org/2001/04/xmlenc#'
|
||||
}
|
||||
|
||||
WEAK_ALGORITHMS = [
|
||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
||||
"http://www.w3.org/2000/09/xmldsig#sha1",
|
||||
"http://www.w3.org/2000/09/xmldsig#dsa-sha1",
|
||||
]
|
||||
|
||||
def __init__(self, config: SAMLConfig):
|
||||
self.config = config
|
||||
self.results: List[ValidationResult] = []
|
||||
|
||||
def validate_all(self) -> List[ValidationResult]:
|
||||
"""Run all SAML SSO validation checks."""
|
||||
self.results = []
|
||||
self._check_signature_algorithm()
|
||||
self._check_digest_algorithm()
|
||||
self._check_name_id_format()
|
||||
self._check_urls()
|
||||
self._check_entity_ids()
|
||||
self._check_assertion_encryption()
|
||||
self._check_certificate_expiration()
|
||||
self._check_metadata_endpoint()
|
||||
self._check_slo_configuration()
|
||||
return self.results
|
||||
|
||||
def _check_signature_algorithm(self):
|
||||
"""Verify SHA-256 or stronger signature algorithm is used."""
|
||||
if self.config.signature_algorithm in self.WEAK_ALGORITHMS:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Signature Algorithm Strength",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"Weak signature algorithm detected: {self.config.signature_algorithm}",
|
||||
remediation="Upgrade to SHA-256: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
))
|
||||
elif "sha256" in self.config.signature_algorithm.lower() or \
|
||||
"sha384" in self.config.signature_algorithm.lower() or \
|
||||
"sha512" in self.config.signature_algorithm.lower():
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Signature Algorithm Strength",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message=f"Strong signature algorithm in use: {self.config.signature_algorithm}"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Signature Algorithm Strength",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message=f"Unknown signature algorithm: {self.config.signature_algorithm}",
|
||||
remediation="Use a known SHA-256+ algorithm for SAML signatures"
|
||||
))
|
||||
|
||||
def _check_digest_algorithm(self):
|
||||
"""Verify SHA-256 or stronger digest algorithm."""
|
||||
if "sha1" in self.config.digest_algorithm.lower() and "sha1" not in "sha128":
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Digest Algorithm Strength",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"Weak digest algorithm: {self.config.digest_algorithm}",
|
||||
remediation="Upgrade to SHA-256: http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Digest Algorithm Strength",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message=f"Digest algorithm acceptable: {self.config.digest_algorithm}"
|
||||
))
|
||||
|
||||
def _check_name_id_format(self):
|
||||
"""Validate NameID format configuration."""
|
||||
valid_formats = [
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
|
||||
]
|
||||
if self.config.name_id_format in valid_formats:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="NameID Format",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message=f"Valid NameID format: {self.config.name_id_format}"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="NameID Format",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message=f"Non-standard NameID format: {self.config.name_id_format}",
|
||||
remediation="Use a standard SAML NameID format (emailAddress, persistent, or transient)"
|
||||
))
|
||||
|
||||
def _check_urls(self):
|
||||
"""Validate that all URLs use HTTPS."""
|
||||
urls_to_check = {
|
||||
"IdP SSO URL": self.config.idp_sso_url,
|
||||
"SP ACS URL": self.config.sp_acs_url,
|
||||
}
|
||||
if self.config.sp_slo_url:
|
||||
urls_to_check["SP SLO URL"] = self.config.sp_slo_url
|
||||
if self.config.metadata_url:
|
||||
urls_to_check["Metadata URL"] = self.config.metadata_url
|
||||
|
||||
for name, url in urls_to_check.items():
|
||||
if not url:
|
||||
continue
|
||||
if url.startswith("https://"):
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} HTTPS Check",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message=f"{name} uses HTTPS: {url}"
|
||||
))
|
||||
elif url.startswith("http://"):
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} HTTPS Check",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"{name} uses insecure HTTP: {url}",
|
||||
remediation=f"Change {name} to use HTTPS"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} URL Validation",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message=f"{name} has invalid URL format: {url}",
|
||||
remediation="Ensure URL starts with https://"
|
||||
))
|
||||
|
||||
def _check_entity_ids(self):
|
||||
"""Validate Entity IDs are properly configured."""
|
||||
if self.config.idp_entity_id == self.config.sp_entity_id:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Entity ID Uniqueness",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message="IdP Entity ID and SP Entity ID are identical",
|
||||
remediation="Ensure IdP and SP have different Entity IDs"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Entity ID Uniqueness",
|
||||
passed=True,
|
||||
severity="critical",
|
||||
message="IdP and SP Entity IDs are unique"
|
||||
))
|
||||
|
||||
for name, entity_id in [("IdP", self.config.idp_entity_id), ("SP", self.config.sp_entity_id)]:
|
||||
if not entity_id:
|
||||
self.results.append(ValidationResult(
|
||||
check_name=f"{name} Entity ID Configured",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"{name} Entity ID is empty",
|
||||
remediation=f"Configure {name} Entity ID in SAML settings"
|
||||
))
|
||||
|
||||
def _check_assertion_encryption(self):
|
||||
"""Check if assertion encryption is enabled."""
|
||||
if self.config.assertion_encrypted:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Assertion Encryption",
|
||||
passed=True,
|
||||
severity="high",
|
||||
message="SAML assertion encryption is enabled"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Assertion Encryption",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message="SAML assertion encryption is not enabled",
|
||||
remediation="Enable assertion encryption with AES-256-CBC to protect attribute values in transit"
|
||||
))
|
||||
|
||||
def _check_certificate_expiration(self):
|
||||
"""Check if the IdP signing certificate is approaching expiration."""
|
||||
if not self.config.certificate_path:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Expiration",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message="No certificate path configured for expiration check",
|
||||
remediation="Provide certificate_path to enable expiration monitoring"
|
||||
))
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.config.certificate_path, 'r') as f:
|
||||
cert_pem = f.read()
|
||||
|
||||
# Extract base64-encoded certificate data
|
||||
cert_lines = []
|
||||
in_cert = False
|
||||
for line in cert_pem.strip().split('\n'):
|
||||
if 'BEGIN CERTIFICATE' in line:
|
||||
in_cert = True
|
||||
continue
|
||||
if 'END CERTIFICATE' in line:
|
||||
break
|
||||
if in_cert:
|
||||
cert_lines.append(line.strip())
|
||||
|
||||
cert_der = base64.b64decode(''.join(cert_lines))
|
||||
cert_hash = hashlib.sha256(cert_der).hexdigest()
|
||||
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Loaded",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message=f"Certificate loaded successfully. SHA-256 fingerprint: {cert_hash[:16]}..."
|
||||
))
|
||||
|
||||
except FileNotFoundError:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Expiration",
|
||||
passed=False,
|
||||
severity="critical",
|
||||
message=f"Certificate file not found: {self.config.certificate_path}",
|
||||
remediation="Download the IdP signing certificate and save to the configured path"
|
||||
))
|
||||
except Exception as e:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Certificate Expiration",
|
||||
passed=False,
|
||||
severity="high",
|
||||
message=f"Error reading certificate: {str(e)}",
|
||||
remediation="Ensure certificate file is valid PEM format"
|
||||
))
|
||||
|
||||
def _check_metadata_endpoint(self):
|
||||
"""Verify the SAML metadata endpoint is accessible."""
|
||||
if not self.config.metadata_url:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="low",
|
||||
message="No metadata URL configured",
|
||||
remediation="Configure metadata URL for automated configuration updates"
|
||||
))
|
||||
return
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
self.config.metadata_url,
|
||||
headers={'User-Agent': 'SAML-SSO-Validator/1.0'}
|
||||
)
|
||||
ctx = ssl.create_default_context()
|
||||
response = urllib.request.urlopen(req, context=ctx, timeout=10)
|
||||
|
||||
if response.status == 200:
|
||||
content = response.read().decode('utf-8')
|
||||
if 'EntityDescriptor' in content:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message="Metadata endpoint accessible and contains valid SAML metadata"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message="Metadata endpoint accessible but does not contain SAML metadata",
|
||||
remediation="Verify the metadata URL returns valid SAML EntityDescriptor XML"
|
||||
))
|
||||
except urllib.error.URLError as e:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message=f"Cannot reach metadata endpoint: {str(e)}",
|
||||
remediation="Verify metadata URL is correct and accessible from this network"
|
||||
))
|
||||
except Exception as e:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Metadata Endpoint",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message=f"Error checking metadata: {str(e)}"
|
||||
))
|
||||
|
||||
def _check_slo_configuration(self):
|
||||
"""Check if Single Logout is configured."""
|
||||
if self.config.sp_slo_url:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Single Logout (SLO)",
|
||||
passed=True,
|
||||
severity="medium",
|
||||
message=f"SLO endpoint configured: {self.config.sp_slo_url}"
|
||||
))
|
||||
else:
|
||||
self.results.append(ValidationResult(
|
||||
check_name="Single Logout (SLO)",
|
||||
passed=False,
|
||||
severity="medium",
|
||||
message="Single Logout (SLO) is not configured",
|
||||
remediation="Configure SLO endpoint to ensure proper session termination across all SPs"
|
||||
))
|
||||
|
||||
def parse_saml_metadata(self, metadata_xml: str) -> Dict:
|
||||
"""Parse SAML metadata XML and extract configuration parameters."""
|
||||
result = {
|
||||
"entity_id": "",
|
||||
"sso_urls": [],
|
||||
"slo_urls": [],
|
||||
"certificates": [],
|
||||
"name_id_formats": [],
|
||||
"attributes": []
|
||||
}
|
||||
|
||||
try:
|
||||
root = ET.fromstring(metadata_xml)
|
||||
|
||||
# Extract Entity ID
|
||||
result["entity_id"] = root.get("entityID", "")
|
||||
|
||||
# Extract SSO endpoints
|
||||
for sso in root.findall('.//md:SingleSignOnService', self.SAML_NS):
|
||||
result["sso_urls"].append({
|
||||
"binding": sso.get("Binding", ""),
|
||||
"location": sso.get("Location", "")
|
||||
})
|
||||
|
||||
# Extract SLO endpoints
|
||||
for slo in root.findall('.//md:SingleLogoutService', self.SAML_NS):
|
||||
result["slo_urls"].append({
|
||||
"binding": slo.get("Binding", ""),
|
||||
"location": slo.get("Location", "")
|
||||
})
|
||||
|
||||
# Extract certificates
|
||||
for cert in root.findall('.//ds:X509Certificate', self.SAML_NS):
|
||||
if cert.text:
|
||||
result["certificates"].append(cert.text.strip())
|
||||
|
||||
# Extract NameID formats
|
||||
for nid in root.findall('.//md:NameIDFormat', self.SAML_NS):
|
||||
if nid.text:
|
||||
result["name_id_formats"].append(nid.text.strip())
|
||||
|
||||
except ET.ParseError as e:
|
||||
result["error"] = f"Failed to parse metadata XML: {str(e)}"
|
||||
|
||||
return result
|
||||
|
||||
def generate_report(self) -> str:
|
||||
"""Generate a validation report."""
|
||||
if not self.results:
|
||||
self.validate_all()
|
||||
|
||||
report_lines = [
|
||||
"=" * 70,
|
||||
"SAML SSO CONFIGURATION VALIDATION REPORT",
|
||||
"=" * 70,
|
||||
f"Report Date: {datetime.datetime.now().isoformat()}",
|
||||
f"IdP Entity ID: {self.config.idp_entity_id}",
|
||||
f"SP Entity ID: {self.config.sp_entity_id}",
|
||||
f"IdP SSO URL: {self.config.idp_sso_url}",
|
||||
f"SP ACS URL: {self.config.sp_acs_url}",
|
||||
"-" * 70,
|
||||
""
|
||||
]
|
||||
|
||||
passed = [r for r in self.results if r.passed]
|
||||
failed = [r for r in self.results if not r.passed]
|
||||
|
||||
critical_failures = [r for r in failed if r.severity == "critical"]
|
||||
high_failures = [r for r in failed if r.severity == "high"]
|
||||
|
||||
report_lines.append(f"SUMMARY: {len(passed)} passed, {len(failed)} failed")
|
||||
report_lines.append(f" Critical failures: {len(critical_failures)}")
|
||||
report_lines.append(f" High failures: {len(high_failures)}")
|
||||
report_lines.append("")
|
||||
|
||||
if failed:
|
||||
report_lines.append("FAILURES:")
|
||||
report_lines.append("-" * 40)
|
||||
for r in sorted(failed, key=lambda x: {"critical": 0, "high": 1, "medium": 2, "low": 3}[x.severity]):
|
||||
report_lines.append(f" [{r.severity.upper()}] {r.check_name}")
|
||||
report_lines.append(f" Issue: {r.message}")
|
||||
if r.remediation:
|
||||
report_lines.append(f" Fix: {r.remediation}")
|
||||
report_lines.append("")
|
||||
|
||||
if passed:
|
||||
report_lines.append("PASSED CHECKS:")
|
||||
report_lines.append("-" * 40)
|
||||
for r in passed:
|
||||
report_lines.append(f" [PASS] {r.check_name}: {r.message}")
|
||||
report_lines.append("")
|
||||
|
||||
report_lines.append("=" * 70)
|
||||
overall = "PASS" if not critical_failures else "FAIL"
|
||||
report_lines.append(f"OVERALL RESULT: {overall}")
|
||||
report_lines.append("=" * 70)
|
||||
|
||||
return "\n".join(report_lines)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run SAML SSO validation with example configuration."""
|
||||
config = SAMLConfig(
|
||||
idp_sso_url="https://your-org.okta.com/app/your-app/sso/saml",
|
||||
idp_entity_id="http://www.okta.com/exk1234567890",
|
||||
sp_entity_id="https://your-app.example.com/saml/metadata",
|
||||
sp_acs_url="https://your-app.example.com/saml/acs",
|
||||
sp_slo_url="https://your-app.example.com/saml/slo",
|
||||
signature_algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
digest_algorithm="http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
assertion_encrypted=True,
|
||||
metadata_url="https://your-org.okta.com/app/exk1234567890/sso/saml/metadata"
|
||||
)
|
||||
|
||||
validator = SAMLSSOValidator(config)
|
||||
report = validator.generate_report()
|
||||
print(report)
|
||||
|
||||
# Export results as JSON
|
||||
results_json = []
|
||||
for r in validator.results:
|
||||
results_json.append({
|
||||
"check": r.check_name,
|
||||
"passed": r.passed,
|
||||
"severity": r.severity,
|
||||
"message": r.message,
|
||||
"remediation": r.remediation
|
||||
})
|
||||
|
||||
with open("saml_validation_results.json", "w") as f:
|
||||
json.dump(results_json, f, indent=2)
|
||||
print("\nResults exported to saml_validation_results.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user