tngan / samlify

Node.js library for SAML SSO
https://samlify.js.org
MIT License
609 stars 217 forks source link

Error Creating Response Cannot read property 'replace' of null #460

Open joshuablanco opened 2 years ago

joshuablanco commented 2 years ago

Greetings, right now I'm developing an IDP but I'm having the error shown in the title, probably is something obvious but I already struggled with it a long way. First hand I'm sorry if I'm not writing this wit the guidelines of github forums I'm almost new in this plattforms.

In my IDP

I have the next metadata

<?xml version="1.0"?>
<EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2021-12-06T15:38:00Z" cacheDuration="PT1639064280S" entityID="http://localhost:3000" ID="asd-564y-sfdd">
    <IDPSSODescriptor WantAuthnRequestsSigned="false" 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>MIIFnTCCA4WgAwIBAgIUJdgAcZ2bBd5pr</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </KeyDescriptor>
        <KeyDescriptor use="encryption">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>MIIFnTCCA4WgAwIBAgIUJdgAcZ2bBd5pr</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </KeyDescriptor>
        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/saml/logout"/>
        <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:persistent</NameIDFormat>
        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/saml/login"/>
    </IDPSSODescriptor>
</EntityDescriptor>

and this configuration

const defaultIdpConfig ={
    metadata: fs.readFileSync(path.join("./", "oktaIDPEnc.xml")),
    privateKey: fs.readFileSync(path.join("./", "encryptKey.pem")),
    privateKeyPass: 'foobar',
    encPrivateKey: fs.readFileSync(path.join("./", "encryptKey.pem")),
    encPrivateKeyPass: 'foobar',
    isAssertionEncrypted: true,
    messageSigningOrder: 'encrypt-then-sign'
}

As you can see I'm using the same certificate for sign and encrypt and the same private key such as (privateKey and encPrivateKey). In the already mentioned IDP, I have the next SP metadata.

<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
    xmlns:assertion="urn:oasis:names:tc:SAML:2.0:assertion"
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="http://localhost:8080/metadata?encrypted=true">
    <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
        <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
        <AssertionConsumerService index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8080/sp/acs?encrypted=true"/>
    </SPSSODescriptor>
</EntityDescriptor>

With this configuration.

const sp = saml.ServiceProvider({
    metadata: fs.readFileSync(path.join("./", "SPMetadata.xml")), // OBLIGATORIO PARA TRAER VISTA DE LOGIN DESDE SP
});

I generate the key and certificates with the commands proposed in the documentation.

openssl genrsa -passout pass:foobar -out encryptKey.pem 4096 openssl req -new -x509 -key encryptKey.pem -out encryptionCert.cer -days 3650

In my SP

I Use the same IDP metadata that I already showed before and the same SP metadata. with the next configuration for IDP:

 const oktaIdpEnc = samlify.IdentityProvider({
        metadata: fs.readFileSync(__dirname + '/../metadata/testIDP.xml'),
        isAssertionEncrypted: true,
        messageSigningOrder: 'encrypt-then-sign',
        wantLogoutRequestSigned: true,
    });

and the SP configuration as follows:

const spEnc = samlify.ServiceProvider({
    entityID: 'http://localhost:8080/metadata?encrypted=true',
    authnRequestsSigned: false,
    wantAssertionsSigned: true,
    wantMessageSigned: true,
    wantLogoutResponseSigned: true,
    wantLogoutRequestSigned: true,
    privateKey: fs.readFileSync(__dirname + '/../key/sign/privkey.pem'),
    privateKeyPass: 'VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px',
    encPrivateKey: fs.readFileSync(__dirname + '/../key/encrypt/privkey.pem'),
    assertionConsumerService: [{
        Binding: binding.post,
        Location: 'http://localhost:8080/sp/acs?encrypted=true',
    }]
    });

Leading to the error

In the SP apparently, I don't have issues, the SP sends the SAML Request correctly, but the problem is in the IDP when I'm creating the Response. I'm using this template provided by the test/flow.ts in your repo, I made some changes.

<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}">
    <saml:Issuer>{Issuer}</saml:Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="{StatusCode}"/>
    </samlp:Status>
    <saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
        <saml:Issuer>{Issuer}</saml:Issuer>
        <saml:Subject>
            <saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID>
            <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/>
            </saml:SubjectConfirmation>
        </saml:Subject>
        <saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}">
            <saml:AudienceRestriction>
                <saml:Audience>{Audience}</saml:Audience>
            </saml:AudienceRestriction>
        </saml:Conditions>{AttributeStatement}</saml:Assertion>
</samlp:Response>

The way I'm creating the response is as follows:

const response =  await idp.createLoginResponse(
            sp,
            requestInfo,
            'post',
            user,
            createTemplateCallback(idp,sp,'post',user,requestInfo.extract,samllib,clientTemp)            
        ).catch(err => {
            console.error("Error Creating Response",err.message);
            return err;
        });

So the error when it tries to create the Response is Cannot read property 'replace' of null. Where:

  1. sp is the agent based on the sp configuration already shown
  2. requestiInfo is the extract after parseLoginRequest()
  3. 'post' binding
  4. user ={ email: 'idp@domain.com' };
  5. createTemplateCallback(idp,sp,'post',user,requestInfo.extract,samllib,clientTemp), is the function for creating the response where clientTemp is an object to provide the response attributes it works well.

This flow works without the encryption, setting the SP configuration in the SP just as the react-samlify no encryption.

Mr @tngan I really appreciate your help, I checked the documentation and try to solve this based on your excellent web site https://samlify.js.org/ but I'm stuck.

Thanks.

khaight commented 2 years ago

Curious if you ever had any luck with this? I am running into a similar issue.

joshuablanco commented 2 years ago

Hi khaight, yes, basically the problem was the SP metadata. I was declaring the SP without a certificate and I was trying to encrypt the SAML Response, that logic lead to a problem because the IDP uses the SP certificate to encrypt the SAML Response. Please make me know any more details about your error.