I have two very simple node.js applications: idp
(Identity Provider) and sp
(Service Provider). These apps don't have any specific business logic, I just want to create a very simple single sign-on (SSO) example in node.js. More specifically, I want to implement an Identity Provider (IdP) initiated single sign-on (SSO), but I don't understand how to generate a SAML IdP response, and moreover, I don't understand what IdP/SP metadata files should have (I am new to SSO and SAML 2.0 protocol).
I have the following node.js servers:
Identity Provider (IdP) Server:
/*
* This is the code in the idp.js file
*/
const express = require('express');
const saml = require('samlify');
const fs = require('fs');
const app = express();
const port = 3000;
const idp = saml.IdentityProvider({
metadata: fs.readFileSync(__dirname + '/idp-metadata.xml')
});
const sp = saml.ServiceProvider({
metadata: fs.readFileSync(__dirname + '/sp-metadata.xml')
});
app.get('/metadata', (req, res) => {
res.type('application/xml');
res.send(idp.getMetadata());
});
/*
* The endpoint that is used to initiate IdP SSO.
*/
app.get('/idpinitsso', async (req, res) => {
try {
// As far as I understand when this endpoint is called I need to generate SAML Response
// and then send an HTTP POST request with SAMLResponse url-encoded body.
// But the question is how to generate SAML Response?
} catch (e) {
console.log(e)
}
res.status(204).send()
})
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
Service Provider (SP) Server:
/*
* This is the code in the sp.js file
*/
const express = require('express');
const saml = require('samlify');
const fs = require('fs');
const app = express();
const port = 3001;
const sp = saml.ServiceProvider({
metadata: fs.readFileSync(__dirname + '/sp-metadata.xml')
});
const idp = saml.IdentityProvider({
metadata: fs.readFileSync(__dirname + '/idp-metadata.xml')
});
app.get('/metadata', (req, res) => {
res.type('application/xml');
res.send(sp.getMetadata());
});
app.post('/acs', async (req, res) => {
const parseResult = sp.parseLoginResponse(idp, 'post', req)
console.log(parseResult)
res.status(204).send()
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
idp-metadata.xml (this XML is taken from https://samlify.js.org/#/idp + https://samlify.js.org/#/key-generation that was used to generate X509Certificate)
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3000/metadata">
<IDPSSODescriptor xmlns:ds="http://www.w3.org/2000/09/xmldsig#" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIFeDCCA2ACCQDM8Gu+flnutzANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMQ0wCwYDVQQKDARBeG9uMQ0wCwYDVQQLDARBeG9uMQ0wCwYDVQQDDARBeG9uMRwwGgYJKoZIhvcNAQkBFg1uaWtAZ21haWwuY29tMB4XDTIzMTExMzE5MTkwMVoXDTMzMTExMDE5MTkwMVowfjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9yazENMAsGA1UECgwEQXhvbjENMAsGA1UECwwEQXhvbjENMAsGA1UEAwwEQXhvbjEcMBoGCSqGSIb3DQEJARYNbmlrQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANK68twNmdNIYN5+Au/fNOM5f7JTlet/Cyxb4znmK51YNDYioROdvk1Z1Zvn7eZZrwA34ID/sMnHxKHMksdTSbgAmXox50LLJpx4/kTWwzw/NH1IvXD137nBfkcdr41pe56+i6uc5O2yWbzaVMzZKbEC448lkL6bFAoc3s5aRL1YVlPZsHolItINZReBCW80jdEwT1lI0jQTuD3qRaU3QPbNaT/RY39NGdVkuOoBWICyyvO6N7HHwv+UmIlLzvQH1gLYI2+pDbTyH33lUDnslN5tch5/x/m/TZFek2KpZQ2gIoihYtrZscvHzYVsaeW5A/PEqvsOfQMHbpzFktlfufZ/dcgV8lBey36itxp82/DW5SEQmZBUqnoISVTNiq1j2goALvoF5l+lnQtkyJCRACayln/U7z4ktaTJGxs/O9eXkXsi+FTOmWVWn0NRCmHQTERX+3zCreLExMGTCLSNPKBRbyo0ydYsHR55GkkxCQbwRy671hkm0W4yF/YkDcW2WIFF2bvSy0/wCHFTU0PxzIl07vwlMejIaYibW8cv+hxa9nLvhilvpZ9wPFaLaMzWKsPcYDgnic/W/3niy2uSGrH5uLBPax3jb3cyLiFNEdUAEdYLOhGco0WWDbUEUMZOhGBlF7M4wtnti+94F4zqW76QRT6WBk8S9Au8Fk/B6fQTAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAAu1lt88IOsQXHnpFSP7ewK1GOjxiNI/k6mYiGT4OowCjBDmeX06/OnVSP58JkdnJUwSRC9f3iblvAD02NyY9IRjGvPPEUgA6G2zmcrTt72XyZIMYh1yDyLdMuWQAtRQvs75x9MeQWHe7wN5XXkoazSoLxCmyZs8LzYoGwnMxdjO6gq4A/DwXklplMUXSoj3rTbKDXi65CxFzDyEkYPlqJrRE3N7DKCBtuhp5m+EQJZeeCEKBxahhoww1QV5K+qHbMo8Hjg89b+8o82YRYXLcaCYQ9tJayXadx2qk9RghpAhG2TNVZpegPzM9UAJ0bFgh1O4v/oc5QiywRuEEhzO8Ml4fCJ3y3MQBJ/7ESnkJtQZkaErT4TYT8i3hkZL5HPeIZ2/NQbc+DYDyZQQVWy8M26rBQYTEqNuWQCaXKJr03vc2MyXKgZ8Hr/JywzRJOnCvBdSwvu8PffgJsYgexwmU1dwMQQAdA1utJkayOQTsc24YFOIDV4a+p3cvD1GeUDON7swJKnyKX4XBQFejMp5kG7V1p2KB5s0aDOJ+twDnpPCALX3Zs07PScd0H/wiDQkjI/ZQpEMO/2BTH5D9/x40vhnA4olVHRwuDV+xLvi0fwtQfA6T10f7cl9GWfjX0lsf95Qa1u0tj9BjQ2XWcncY5po31Q6aEOLK6biDxlE5kd1</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/trust/saml2/http-post/sso/486670"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3000/trust/saml2/http-post/sso/486670"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://localhost:3000/trust/saml2/soap/sso/486670"/>
</IDPSSODescriptor>
<ContactPerson contactType="technical">
<SurName>Support</SurName>
<EmailAddress>[email protected]</EmailAddress>
</ContactPerson>
</EntityDescriptor>
sp-metadata.xml (this XML is taken from https://samlify.js.org/#/sp + https://samlify.js.org/#/key-generation that was used to generate X509Certificate)
<EntityDescriptor
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
entityID="https://sp.example.org/metadata">
<SPSSODescriptor WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIFeDCCA2ACCQDM8Gu+flnutzANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMQ0wCwYDVQQKDARBeG9uMQ0wCwYDVQQLDARBeG9uMQ0wCwYDVQQDDARBeG9uMRwwGgYJKoZIhvcNAQkBFg1uaWtAZ21haWwuY29tMB4XDTIzMTExMzE5MTkwMVoXDTMzMTExMDE5MTkwMVowfjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9yazENMAsGA1UECgwEQXhvbjENMAsGA1UECwwEQXhvbjENMAsGA1UEAwwEQXhvbjEcMBoGCSqGSIb3DQEJARYNbmlrQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANK68twNmdNIYN5+Au/fNOM5f7JTlet/Cyxb4znmK51YNDYioROdvk1Z1Zvn7eZZrwA34ID/sMnHxKHMksdTSbgAmXox50LLJpx4/kTWwzw/NH1IvXD137nBfkcdr41pe56+i6uc5O2yWbzaVMzZKbEC448lkL6bFAoc3s5aRL1YVlPZsHolItINZReBCW80jdEwT1lI0jQTuD3qRaU3QPbNaT/RY39NGdVkuOoBWICyyvO6N7HHwv+UmIlLzvQH1gLYI2+pDbTyH33lUDnslN5tch5/x/m/TZFek2KpZQ2gIoihYtrZscvHzYVsaeW5A/PEqvsOfQMHbpzFktlfufZ/dcgV8lBey36itxp82/DW5SEQmZBUqnoISVTNiq1j2goALvoF5l+lnQtkyJCRACayln/U7z4ktaTJGxs/O9eXkXsi+FTOmWVWn0NRCmHQTERX+3zCreLExMGTCLSNPKBRbyo0ydYsHR55GkkxCQbwRy671hkm0W4yF/YkDcW2WIFF2bvSy0/wCHFTU0PxzIl07vwlMejIaYibW8cv+hxa9nLvhilvpZ9wPFaLaMzWKsPcYDgnic/W/3niy2uSGrH5uLBPax3jb3cyLiFNEdUAEdYLOhGco0WWDbUEUMZOhGBlF7M4wtnti+94F4zqW76QRT6WBk8S9Au8Fk/B6fQTAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAAu1lt88IOsQXHnpFSP7ewK1GOjxiNI/k6mYiGT4OowCjBDmeX06/OnVSP58JkdnJUwSRC9f3iblvAD02NyY9IRjGvPPEUgA6G2zmcrTt72XyZIMYh1yDyLdMuWQAtRQvs75x9MeQWHe7wN5XXkoazSoLxCmyZs8LzYoGwnMxdjO6gq4A/DwXklplMUXSoj3rTbKDXi65CxFzDyEkYPlqJrRE3N7DKCBtuhp5m+EQJZeeCEKBxahhoww1QV5K+qHbMo8Hjg89b+8o82YRYXLcaCYQ9tJayXadx2qk9RghpAhG2TNVZpegPzM9UAJ0bFgh1O4v/oc5QiywRuEEhzO8Ml4fCJ3y3MQBJ/7ESnkJtQZkaErT4TYT8i3hkZL5HPeIZ2/NQbc+DYDyZQQVWy8M26rBQYTEqNuWQCaXKJr03vc2MyXKgZ8Hr/JywzRJOnCvBdSwvu8PffgJsYgexwmU1dwMQQAdA1utJkayOQTsc24YFOIDV4a+p3cvD1GeUDON7swJKnyKX4XBQFejMp5kG7V1p2KB5s0aDOJ+twDnpPCALX3Zs07PScd0H/wiDQkjI/ZQpEMO/2BTH5D9/x40vhnA4olVHRwuDV+xLvi0fwtQfA6T10f7cl9GWfjX0lsf95Qa1u0tj9BjQ2XWcncY5po31Q6aEOLK6biDxlE5kd1</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<AssertionConsumerService isDefault="true" index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3001/acs"/>
</SPSSODescriptor>
</EntityDescriptor>
I tried to use idp.createLoginResponse(sp)
but it didn't work for me because I got the ERR_CREATE_RESPONSE_UNDEFINED_BINDING
error.
I also tried to generate it like this:
const user = { email: '[email protected]' };
const sampleRequestInfo = { extract: { request: { id: 'request_id' } } };
const samlResponse = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user);
But I get the following error: "Error [ERR_CRYPTO_SIGN_KEY_REQUIRED]: No key provided for signing."
.
I couldn't find a working example in https://samlify.js.org/ with IdP-initiated SSO, so I would really appreciate it if you could provide me with a minimal working example or suggest any good node.js library that can handle this (or just have an IdP-Initiated SSO example) or any other example in any other language.
Well, after several days of struggle, I finally understood how SSO works and implemented it (IdP-initiated SSO, where our service acts as an Identity Provider for a third-party service). I'll leave my answer here in case someone has to do the same and has problems.
First of all, I recommend that you read how SSO flow works. I'll leave a couple of good resources here for you to read:
So, in a nutshell, an IdP-initiated SSO flow (where your service acts as the IdP) works like this:
SAMLResponse
.SAMLResponse
to the Service Provider's ACS (Assertion Consumer Service) URL.Before you start writing code, you need to do the following:
You can create your own metadata file (IdP metadata) here: https://www.samltool.com/idp_metadata.php.
It is highly recommended to sign the XML metadata file, so create the files
private_key.pem
andpublic_cert.cer
by running the following commands:You should then add the body from
public_cert.cer
to the "SP X.509 cert (same cert for sign/encrypt)" field when creating the IdP metadata file here: https://www.samltool.com/idp_metadata.phpFinally, when everything is ready (you have both SP and IdP metadata files), we can move on to creating our IdP.
Our IdP was written in Node.js, so we used samlify for creating SAMLResponse:
Start the server and send an HTTP POST request to
http://localhost:3000/api/sso/saml2/idp/login
, you will receive the following JSON body in response:After that, send an HTTP POST request with Content-Type
application/x-www-form-urlencoded
toentityEndpoint
with the bodySAMLResponse
.For your convenience, I will also post a minimal working example here: https://github.com/MykytaManuilenko/sso-saml-example