Mocking a SAML Identity Provider
MockServer can stand up a complete mock SAML 2.0 Identity Provider (IdP) with a single call, so you can test SAML-secured applications without connecting to a real IdP. This is useful for:
- Integration testing — verify your Service Provider (SP) correctly consumes a signed SAML assertion and establishes a session
- Offline development — develop and test locally without network access to a real IdP
- Fast CI pipelines — eliminate the external IdP dependency and its network latency from your test suite
- Rejection-path testing — deliberately produce expired, wrong-audience, or tampered assertions to confirm your SP rejects them
A single PUT /mockserver/saml generates up to three endpoints:
- Metadata (
GET /saml/metadata) — SAML 2.0 IdP metadata XML containing the signing X.509 certificate, aSingleSignOnService, and aSingleLogoutService(both HTTP-POST binding). - SSO (
/saml/sso) — implements the SP-initiated Web-Browser-SSO POST profile. Returns an auto-submitting HTML form that POSTs a freshly XML-DSig-signed SAMLResponseto your SP's Assertion Consumer Service (ACS), echoingRelayStateand theAuthnRequestIDasInResponseTo. - Single Logout (
/saml/logout) — accepts aLogoutRequestand returns a signedLogoutResponseform-POSTed to your SP's Single-Logout service, echoingRelayState.
The signing credential is generated as a self-signed RSA key by default (no real IdP infrastructure required) and its certificate is published in the metadata so your SP can validate the assertion signature. You may supply your own PEM-encoded certificate and key, or choose a different signing algorithm.
Stand Up a Mock IdP With Defaults
new MockServerClient("localhost", 1080)
.mockSamlProvider();
const http = require('http');
const req = http.request({
host: 'localhost',
port: 1080,
path: '/mockserver/saml',
method: 'PUT'
}, function (res) {
console.log("SAML IdP created, status " + res.statusCode);
});
req.end();
import requests
requests.put("http://localhost:1080/mockserver/saml")
require 'net/http'
uri = URI('http://localhost:1080/mockserver/saml')
Net::HTTP.start(uri.host, uri.port) do |http|
http.request(Net::HTTP::Put.new(uri))
end
package main
import "net/http"
func main() {
req, _ := http.NewRequest("PUT",
"http://localhost:1080/mockserver/saml", nil)
http.DefaultClient.Do(req)
}
using System.Net.Http;
using var httpClient = new HttpClient();
await httpClient.PutAsync(
"http://localhost:1080/mockserver/saml",
null
);
use reqwest::blocking::Client;
let client = Client::new();
client.put("http://localhost:1080/mockserver/saml")
.send()
.unwrap();
$ch = curl_init('http://localhost:1080/mockserver/saml');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
curl -v -X PUT "http://localhost:1080/mockserver/saml"
Configure the IdP
new MockServerClient("localhost", 1080)
.mockSamlProvider(
new SamlProviderConfiguration()
.setIdpEntityId("https://idp.example.com/saml/idp")
.setSpEntityId("https://sp.example.com/metadata")
.setAssertionConsumerServiceUrl("https://sp.example.com/acs")
.setSpSingleLogoutServiceUrl("https://sp.example.com/slo")
.setSubjectNameId("alice@example.com")
.setAttributes(Map.of("email", "alice@example.com", "role", "admin"))
.setSigningAlgorithm("ES256")
);
const http = require('http');
const body = JSON.stringify({
"idpEntityId": "https://idp.example.com/saml/idp",
"spEntityId": "https://sp.example.com/metadata",
"assertionConsumerServiceUrl": "https://sp.example.com/acs",
"spSingleLogoutServiceUrl": "https://sp.example.com/slo",
"subjectNameId": "alice@example.com",
"attributes": {
"email": "alice@example.com",
"role": "admin"
},
"signingAlgorithm": "ES256"
});
const req = http.request({
host: 'localhost',
port: 1080,
path: '/mockserver/saml',
method: 'PUT',
headers: {'Content-Type': 'application/json'}
}, function (res) {
console.log("SAML IdP created, status " + res.statusCode);
});
req.write(body);
req.end();
import requests
requests.put("http://localhost:1080/mockserver/saml", json={
"idpEntityId": "https://idp.example.com/saml/idp",
"spEntityId": "https://sp.example.com/metadata",
"assertionConsumerServiceUrl": "https://sp.example.com/acs",
"spSingleLogoutServiceUrl": "https://sp.example.com/slo",
"subjectNameId": "alice@example.com",
"attributes": {
"email": "alice@example.com",
"role": "admin"
},
"signingAlgorithm": "ES256"
})
require 'net/http'
require 'json'
uri = URI('http://localhost:1080/mockserver/saml')
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Put.new(uri, 'Content-Type' => 'application/json')
request.body = {
idpEntityId: 'https://idp.example.com/saml/idp',
spEntityId: 'https://sp.example.com/metadata',
assertionConsumerServiceUrl: 'https://sp.example.com/acs',
spSingleLogoutServiceUrl: 'https://sp.example.com/slo',
subjectNameId: 'alice@example.com',
attributes: { email: 'alice@example.com', role: 'admin' },
signingAlgorithm: 'ES256'
}.to_json
http.request(request)
end
package main
import (
"bytes"
"net/http"
)
func main() {
body := []byte(`{
"idpEntityId": "https://idp.example.com/saml/idp",
"spEntityId": "https://sp.example.com/metadata",
"assertionConsumerServiceUrl": "https://sp.example.com/acs",
"spSingleLogoutServiceUrl": "https://sp.example.com/slo",
"subjectNameId": "alice@example.com",
"attributes": {
"email": "alice@example.com",
"role": "admin"
},
"signingAlgorithm": "ES256"
}`)
req, _ := http.NewRequest("PUT",
"http://localhost:1080/mockserver/saml",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
}
using System.Net.Http;
using System.Text;
using var httpClient = new HttpClient();
var json = @"{
""idpEntityId"": ""https://idp.example.com/saml/idp"",
""spEntityId"": ""https://sp.example.com/metadata"",
""assertionConsumerServiceUrl"": ""https://sp.example.com/acs"",
""spSingleLogoutServiceUrl"": ""https://sp.example.com/slo"",
""subjectNameId"": ""alice@example.com"",
""attributes"": {
""email"": ""alice@example.com"",
""role"": ""admin""
},
""signingAlgorithm"": ""ES256""
}";
await httpClient.PutAsync(
"http://localhost:1080/mockserver/saml",
new StringContent(json, Encoding.UTF8, "application/json")
);
use reqwest::blocking::Client;
let client = Client::new();
let body = r#"{
"idpEntityId": "https://idp.example.com/saml/idp",
"spEntityId": "https://sp.example.com/metadata",
"assertionConsumerServiceUrl": "https://sp.example.com/acs",
"spSingleLogoutServiceUrl": "https://sp.example.com/slo",
"subjectNameId": "alice@example.com",
"attributes": {
"email": "alice@example.com",
"role": "admin"
},
"signingAlgorithm": "ES256"
}"#;
client.put("http://localhost:1080/mockserver/saml")
.header("Content-Type", "application/json")
.body(body.to_string())
.send()
.unwrap();
$json = <<<'JSON'
{
"idpEntityId": "https://idp.example.com/saml/idp",
"spEntityId": "https://sp.example.com/metadata",
"assertionConsumerServiceUrl": "https://sp.example.com/acs",
"spSingleLogoutServiceUrl": "https://sp.example.com/slo",
"subjectNameId": "alice@example.com",
"attributes": {
"email": "alice@example.com",
"role": "admin"
},
"signingAlgorithm": "ES256"
}
JSON;
$ch = curl_init('http://localhost:1080/mockserver/saml');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => $json,
CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
curl -v -X PUT "http://localhost:1080/mockserver/saml" -d '{
"idpEntityId": "https://idp.example.com/saml/idp",
"spEntityId": "https://sp.example.com/metadata",
"assertionConsumerServiceUrl": "https://sp.example.com/acs",
"spSingleLogoutServiceUrl": "https://sp.example.com/slo",
"subjectNameId": "alice@example.com",
"attributes": {
"email": "alice@example.com",
"role": "admin"
},
"signingAlgorithm": "ES256"
}'
Test Your SP's Rejection Paths
Set any of the negative-test flags to make the IdP deliberately mint a defective assertion, so you can confirm your SP rejects it (rather than silently accepting it):
new MockServerClient("localhost", 1080)
.mockSamlProvider(
new SamlProviderConfiguration()
.setExpiredAssertion(true)
.setWrongAudience(true)
.setTamperedSignature(true)
);
const http = require('http');
const body = JSON.stringify({
"expiredAssertion": true,
"wrongAudience": true,
"tamperedSignature": true
});
const req = http.request({
host: 'localhost',
port: 1080,
path: '/mockserver/saml',
method: 'PUT',
headers: {'Content-Type': 'application/json'}
}, function (res) {
console.log("SAML IdP created, status " + res.statusCode);
});
req.write(body);
req.end();
import requests
requests.put("http://localhost:1080/mockserver/saml", json={
"expiredAssertion": True,
"wrongAudience": True,
"tamperedSignature": True
})
require 'net/http'
require 'json'
uri = URI('http://localhost:1080/mockserver/saml')
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Put.new(uri, 'Content-Type' => 'application/json')
request.body = {
expiredAssertion: true,
wrongAudience: true,
tamperedSignature: true
}.to_json
http.request(request)
end
package main
import (
"bytes"
"net/http"
)
func main() {
body := []byte(`{
"expiredAssertion": true,
"wrongAudience": true,
"tamperedSignature": true
}`)
req, _ := http.NewRequest("PUT",
"http://localhost:1080/mockserver/saml",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
}
using System.Net.Http;
using System.Text;
using var httpClient = new HttpClient();
var json = @"{
""expiredAssertion"": true,
""wrongAudience"": true,
""tamperedSignature"": true
}";
await httpClient.PutAsync(
"http://localhost:1080/mockserver/saml",
new StringContent(json, Encoding.UTF8, "application/json")
);
use reqwest::blocking::Client;
let client = Client::new();
let body = r#"{
"expiredAssertion": true,
"wrongAudience": true,
"tamperedSignature": true
}"#;
client.put("http://localhost:1080/mockserver/saml")
.header("Content-Type", "application/json")
.body(body.to_string())
.send()
.unwrap();
$json = <<<'JSON'
{
"expiredAssertion": true,
"wrongAudience": true,
"tamperedSignature": true
}
JSON;
$ch = curl_init('http://localhost:1080/mockserver/saml');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => $json,
CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
curl -v -X PUT "http://localhost:1080/mockserver/saml" -d '{
"expiredAssertion": true,
"wrongAudience": true,
"tamperedSignature": true
}'
Configuration Reference
All fields are optional; the defaults produce a fully functional mock IdP.
About the two ports in the defaults: http://localhost:8080 refers to your Service Provider application (the spEntityId, ACS, and SLO URLs are where MockServer POSTs the signed SAML response, so they point at your app), while http://localhost:1080 is MockServer itself acting as the IdP (metadata, SSO, and logout endpoints). This is not a typo — change the sp* URLs to wherever your application actually listens.
| Field | Default | Description |
|---|---|---|
idpEntityId | http://localhost:1080/saml/idp | The IdP entity id published in metadata and used as the assertion Issuer. |
spEntityId | http://localhost:8080/saml/sp | The SP entity id, emitted as the assertion's AudienceRestriction. |
assertionConsumerServiceUrl | http://localhost:8080/saml/acs | The SP ACS URL the signed Response is form-POSTed to. |
spSingleLogoutServiceUrl | http://localhost:8080/saml/slo | The SP SLO URL the signed LogoutResponse is form-POSTed to. |
metadataUrl | /saml/metadata | The IdP metadata endpoint path. |
ssoServiceUrl | /saml/sso | The IdP SSO endpoint path. |
sloServiceUrl | /saml/logout | The IdP Single-Logout endpoint path. Set to null to omit the SLO endpoint and metadata entry. |
subjectNameId | mock-user@example.com | The authenticated subject's NameID. |
nameIdFormat | ...emailAddress | The NameID Format. |
attributes | {} | Attribute name/value pairs emitted in an AttributeStatement. |
sessionDurationSeconds | 3600 | Validity window for the assertion's Conditions / session. |
signingAlgorithm | RSA / SHA-256 | Signing algorithm: one of RS256, RS384, RS512, ES256, ES384, ES512. The metadata certificate always matches the signing key. |
signingCertificatePem / signingPrivateKeyPem | self-signed | Supply your own PEM-encoded signing credential. The private key is never serialized back out. |
expiredAssertion | false | Negative test: place NotOnOrAfter in the past so a conformant SP rejects the assertion as expired. |
wrongAudience | false | Negative test: emit an Audience that does not match spEntityId. |
tamperedSignature | false | Negative test: corrupt the assertion signature so verification fails. |
Point Your Service Provider at the Mock IdP
Configure your SP to use the mock IdP's metadata URL. For example, with Spring Security SAML:
spring:
security:
saml2:
relyingparty:
registration:
mockserver:
assertingparty:
metadata-uri: http://localhost:1080/saml/metadata
acs:
location: https://sp.example.com/acs
singlelogout:
url: https://sp.example.com/slo
The SP will load the IdP signing certificate from the metadata and validate the assertion signature automatically. Each request to the SSO endpoint mints a fresh, signed Response.