A dead simple SAML 2.0 client library for Server Side Swift applications. This library provides an easy way to integrate SAML authentication into your Swift applications.
- Generate SAML Authentication Requests
- Generate SAML Logout Requests
- Parse and validate SAML Responses
- Extract user attributes from SAML Assertions
- Support for RelayState parameter
- Cryptographic support via Swift Crypto for future signature validation
- HTTP client support via AsyncHTTPClient for metadata fetching
- Simple and intuitive API
- Cross-platform support (macOS, Linux, iOS, tvOS, watchOS, visionOS)
- Fully tested
Add SAMLClient as a dependency in your Package.swift file:
dependencies: [
.package(url: "https://github.com/thoven87/saml-client-swift.git", from: "1.0.0")
]Then add it to your target dependencies:
targets: [
.target(
name: "YourTarget",
dependencies: ["SAMLClient"]),
]First, create a SAML configuration with your Identity Provider (IdP) details:
import SAMLClient
let configuration = SAMLConfiguration(
entityId: "https://yourapp.com/saml/metadata",
idpSSOURL: "https://idp.example.com/sso",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
idpSLOURL: "https://idp.example.com/slo" // Optional: for logout support
)
let client = SAMLClient(configuration: configuration)To start the SAML authentication flow, generate a redirect URL and redirect the user to it:
// Generate the login URL
let loginURL = try client.getLoginURL()
// Optionally, you can include a relay state to maintain application state
let loginURLWithState = try client.getLoginURL(relayState: "/dashboard")
// Redirect the user to this URL
print("Redirect to: \(loginURL)")After the user authenticates with the IdP, they will be redirected back to your Assertion Consumer Service (ACS) URL with a SAML response. Parse and validate this response:
// Get the SAMLResponse parameter from the POST request
let samlResponseBase64 = request.body["SAMLResponse"] // From your web framework
// Parse and validate the response
// Optionally provide clock skew tolerance (in seconds) for time validation
do {
let response = try client.parseResponse(samlResponseBase64, clockSkew: 60)
// Access the assertion data
if let assertion = response.assertion {
print("User ID: \(assertion.nameID ?? "unknown")")
print("Session Index: \(assertion.sessionIndex ?? "none")")
// Access user attributes
if let email = assertion.attributes["email"]?.first {
print("Email: \(email)")
}
if let firstName = assertion.attributes["firstName"]?.first {
print("First Name: \(firstName)")
}
// Create user session, etc.
// IMPORTANT: Store nameID and sessionIndex for logout
// session["nameID"] = assertion.nameID
// session["sessionIndex"] = assertion.sessionIndex
}
} catch {
print("Failed to parse SAML response: \(error)")
}To start the SAML logout flow (Single Logout), generate a redirect URL and redirect the user to it:
// Get user information from session
let nameID = session["nameID"] // The NameID from the authentication assertion
let sessionIndex = session["sessionIndex"] // Optional: from the authentication assertion
// Generate the logout URL
let logoutURL = try client.getLogoutURL(
nameID: nameID,
sessionIndex: sessionIndex,
relayState: "/"
)
// Redirect the user to this URL
print("Redirect to: \(logoutURL)")Note: The IdP Single Logout Service URL (idpSLOURL) must be configured in your SAMLConfiguration for logout to work.
After the IdP processes the logout, they will redirect back to your Single Logout Service (SLO) endpoint. You should clear the user's session:
// In your SLO endpoint handler
session.clear()
// Optionally parse the RelayState to redirect the user
let redirectURL = relayState ?? "/"For frameworks that provide request body as Data or ByteBuffer, you can use the convenience overloads:
// From Data
let bodyData: Data = ...
let response = try client.parseResponse(data: bodyData, clockSkew: 60)
// From ByteBuffer (requires NIOCore)
#if canImport(NIOCore)
import NIOCore
let buffer: ByteBuffer = ...
let response = try client.parseResponse(buffer: buffer, clockSkew: 60)
#endifYou can manually validate assertion time conditions using the built-in utility methods:
if let assertion = response.assertion {
// Validate with default settings (no clock skew)
let validationResult = assertion.validateTimeConditions()
if validationResult.isValid {
print("Assertion is valid")
} else {
print("Validation failed: \(validationResult.description)")
}
// Or use the convenience method with clock skew tolerance
if assertion.isValid(clockSkew: 60) {
print("Assertion is valid (with 60 second clock skew tolerance)")
}
}The validation utility handles:
- NotBefore condition: Ensures the assertion is not used before it becomes valid
- NotOnOrAfter condition: Ensures the assertion has not expired
- Clock skew tolerance: Allows for time differences between servers
You can fetch and parse IdP metadata automatically:
import SAMLClient
// Create configuration from IdP metadata URL
let configuration = try await SAMLConfiguration.fromMetadata(
entityId: "https://yourapp.com/saml/metadata",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
metadataURL: "https://idp.example.com/metadata"
)
let client = SAMLClient(configuration: configuration)
// Or manually fetch and parse
let httpClient = SAMLHTTPClient()
let (ssoURL, certificate) = try await httpClient.fetchAndParseMetadata(
from: "https://idp.example.com/metadata"
)
// No need to shutdown - using shared HTTPClientEnable signature validation and request signing:
let configuration = SAMLConfiguration(
entityId: "https://yourapp.com/saml/metadata",
idpSSOURL: "https://idp.example.com/sso",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
idpCertificate: """
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKL0UG...
-----END CERTIFICATE-----
""",
privateKey: """
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BA...
-----END PRIVATE KEY-----
""",
signRequests: true,
validateSignatures: true
)
// Requests will be automatically signed
let loginURL = try client.getLoginURL()
// Responses will be automatically validated
let response = try client.parseResponse(samlResponseBase64)You can configure additional options:
let configuration = SAMLConfiguration(
entityId: "https://yourapp.com/saml/metadata",
idpSSOURL: "https://idp.example.com/sso",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
nameIDPolicyFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
)You can specify alternative NameID policy formats to match your IdP requirements:
let configuration = SAMLConfiguration(
entityId: "https://yourapp.com/saml/metadata",
idpSSOURL: "https://idp.example.com/sso",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
nameIDPolicyFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
)Common NameID formats include:
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress(default)urn:oasis:names:tc:SAML:2.0:nameid-format:persistenturn:oasis:names:tc:SAML:2.0:nameid-format:transienturn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
To configure ADFS to work with this library, you should go to the MMC snap-in for ADFS and add a Relying Party Trust with the following properties:
- In the Identifiers tab, add a Relying Party Identifier that matches the
entityIdyou'll provide when creating yourSAMLConfiguration. - In the Endpoints tab, add the URL that will process SAML responses (your
assertionConsumerServiceURL) to the list, using POST for the Binding value.
To obtain the metadata provider XML, load this URL in your browser:
https://myserver.domain.com/FederationMetadata/2007-06/FederationMetadata.xml
Then use it to configure your client:
let configuration = try await SAMLConfiguration.fromMetadata(
entityId: "https://yourapp.com/saml/metadata",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
metadataURL: "https://myserver.domain.com/FederationMetadata/2007-06/FederationMetadata.xml"
)
let client = SAMLClient(configuration: configuration)To configure Okta to work with this library, create an SAML 2.0 application with the following settings:
- The Single sign on URL should be the URL that processes SAML responses (your
assertionConsumerServiceURL, e.g.,https://yourapp.com/saml/acs). - The Audience URI should be a value that matches the
entityIdyou'll specify when creating yourSAMLConfiguration.
You can then fetch the Okta metadata URL from your Okta application settings and use it to configure your client:
let configuration = try await SAMLConfiguration.fromMetadata(
entityId: "https://yourapp.com/saml/metadata",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
metadataURL: "https://your-okta-domain.okta.com/app/your-app-id/sso/saml/metadata"
)
let client = SAMLClient(configuration: configuration)To generate the public/private keys for encryption and request signing, use the following OpenSSL commands:
# Generate certificate and private key
openssl req -new -x509 -days 365 -nodes -sha256 -out saml-public-key.crt -keyout saml-private-key.pem
# Convert to PKCS8 format (PEM)
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in saml-private-key.pem -out saml-private-key.key
# Convert to DER format (for some IdPs that require .pk8 format)
openssl pkcs8 -topk8 -nocrypt -inform PEM -in saml-private-key.key -outform DER -out saml-private-key.pk8To add the keys to your SAML client configuration (needed only if you have encrypted assertions or if you want to sign requests):
// Read the certificate and private key from files
let publicKey = try String(contentsOfFile: "saml-public-key.crt")
let privateKey = try String(contentsOfFile: "saml-private-key.key")
let configuration = SAMLConfiguration(
entityId: "https://yourapp.com/saml/metadata",
idpSSOURL: "https://idp.example.com/sso",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs",
privateKey: privateKey,
signRequests: true,
validateSignatures: true
)
let client = SAMLClient(configuration: configuration)Dependencies: This library includes:
- Swift Crypto 4.0+: For cryptographic operations including signature validation
- AsyncHTTPClient 1.29+: For HTTP operations such as fetching IdP metadata
Cryptographic Operations:
The library now includes SAMLCrypto module for:
- XML signature verification using RSA
- Request signing with private keys
- Certificate parsing and validation
HTTP Operations:
The library includes SAMLHTTPClient for:
- Fetching IdP metadata from URLs
- Parsing metadata to extract SSO URLs and certificates
- Creating configurations directly from metadata URLs
If you're using Vapor with a single identity provider, you can easily integrate SAMLClient into your routes:
import Vapor
import SAMLClient
func routes(_ app: Application) throws {
let config = SAMLConfiguration(
entityId: "https://yourapp.com/saml/metadata",
idpSSOURL: "https://idp.example.com/sso",
assertionConsumerServiceURL: "https://yourapp.com/saml/acs"
)
let samlClient = SAMLClient(configuration: config)
// Login route - initiate SAML authentication
app.get("login") { req -> Response in
let loginURL = try samlClient.getLoginURL()
return req.redirect(to: loginURL.absoluteString)
}
// ACS route - handle SAML response
app.post("saml", "acs") { req -> Response in
guard let samlResponse = try? req.content.get(String.self, at: "SAMLResponse") else {
throw Abort(.badRequest, reason: "Missing SAMLResponse")
}
let response = try samlClient.parseResponse(samlResponse)
if let assertion = response.assertion,
let email = assertion.attributes["email"]?.first {
// Create user session
req.session.data["user_email"] = email
return req.redirect(to: "/dashboard")
}
throw Abort(.unauthorized)
}
}Configuration object for the SAML client.
Properties:
entityId: Your application's entity ID (Service Provider ID)idpSSOURL: The Identity Provider's SSO URLidpSLOURL: Optional Identity Provider's Single Logout Service URL (required for logout functionality)assertionConsumerServiceURL: Your application's ACS URL (where IdP sends the response)idpCertificate: Optional certificate for signature validation (reserved for future implementation)privateKey: Optional private key for signing requests (reserved for future implementation)signRequests: Whether to sign authentication requests (reserved for future implementation, default: false)validateSignatures: Whether to validate response signatures (reserved for future implementation, default: true)nameIDPolicyFormat: The NameID policy format to use in authentication requests (default:urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress)
Main client for handling SAML authentication.
Methods:
getLoginURL(relayState:): Generate a redirect URL for authenticationgetLogoutURL(nameID:sessionIndex:relayState:): Generate a redirect URL for logoutparseResponse(_:clockSkew:): Parse and validate a SAML response from base64 string with optional clock skew toleranceparseResponse(data:clockSkew:): Parse and validate a SAML response from Data with optional clock skew toleranceparseResponse(buffer:clockSkew:): Parse and validate a SAML response from ByteBuffer (requires NIOCore) with optional clock skew tolerancegenerateMetadata(singleLogoutServiceURL:validUntil:): Generate Service Provider metadata XML
Represents a parsed SAML response.
Properties:
id: The response IDstatus: The response status (e.g., "Success")issuer: The IdP issuerassertion: The SAML assertion containing user dataxml: The raw XML response
Represents a SAML assertion.
Properties:
id: The assertion IDnameID: The user's NameID (typically email or username)nameIDFormat: The format of the NameIDattributes: Dictionary of user attributessessionIndex: The session index for logoutnotBefore: The assertion validity start timenotOnOrAfter: The assertion validity end time
Methods:
validateTimeConditions(currentDate:clockSkew:): Validate the assertion's time conditions, returnsAssertionValidationResultisValid(currentDate:clockSkew:): Check if the assertion is currently valid, returnsBool
Result of assertion time validation.
Cases:
.valid: The assertion is valid.notYetValid(notBefore:currentDate:): The assertion is not yet valid.expired(notOnOrAfter:currentDate:): The assertion has expired
Properties:
isValid: Boolean indicating if the validation passeddescription: Human-readable description of the validation result
Cryptographic utilities for SAML operations.
Methods:
verifySignature(xml:certificate:): Verify an XML signature using RSAsignXML(xml:privateKey:): Sign XML with RSA private key
HTTP client for SAML operations.
Methods:
fetchMetadata(from:): Fetch IdP metadata from a URLparseIdPMetadata(_:): Parse IdP metadata and extract SSO URL and certificatefetchAndParseMetadata(from:): Fetch and parse IdP metadata in one operation
Note: Uses HTTPClient.shared - no manual shutdown required
Static Methods:
SAMLConfiguration.fromMetadata(entityId:assertionConsumerServiceURL:metadataURL:httpClient:): Create configuration from metadata URL
The library throws SAMLError for various error conditions:
encodingError: Failed to encode datadecodingError: Failed to decode datacompressionError: Failed to compress datainvalidURL: Invalid URL constructioninvalidResponse: Invalid SAML responsesignatureValidationFailed: Signature validation failedconfigurationError: Configuration error
- Swift 6.0+
- macOS 15+, iOS 16+, tvOS 16+, watchOS 9+, visionOS 1+, or Linux
- Swift Crypto 4.0+: Apple's cryptographic library for signature operations
- AsyncHTTPClient 1.29+: Swift Server's async HTTP client for HTTP operations
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
Check out the Examples directory for complete working examples:
- HummingbirdExample.swift: Complete integration with Hummingbird V2 framework
Inspired by justinbleach/saml-client