Using SAML SSO with Velo

Single Sign On (SSO) is a great way to make life easier for your site visitors and encourage them to register as a user on your site. Visitors can log in to your site using their Google, Facebook, or other accounts, or using their work account.

This article describes how to implement SAML, Security Assertion Markup Language, an open standard that allows identity providers (IDP) to pass authorization credentials to service providers (SP). To learn about implementing SSO with oAuth, see Using OAuth SSO with Velo.

In our example, the service provider is our Wix site. We will also need an Identity Provider. The IDP authenticates our visitor and passes the visitor's credentials along to our site.

For this tutorial we are going to use Auth0 as our IDP. Like many other services, Auth0 offers a free trial to help you get going. Note that while the SAML protocols are standard, each IDP has their subtle differences, so if you use a different IDP, you may need to tweak some of the settings in the functions used here.

The SAML Authentication Sequence

The sequence diagram below shows the basic flow between the browser, our Wix site (the SP), and the IDP.

  1. The user clicks a sign-in button.
  2. The SP generates a SAML request using the IDP and SP metadata, and passes the request and URL to the browser.
  3. The browser sends the SAML request to the IDP.
  4. The IDP presents a login page, based on the SAML request.
  5. The user provides login credentials.
  6. The IDP posts the assertion to the SP. A SAML assertion is an XML document that contains the user authorization and user credentials.
  7. The SP decrypts, validates, and extracts the credentials from the assertion.
  8. The SP (Wix backend) generates a session token, and passes a redirect back to the browser with the session token in the query parameters.
  9. The browser redirects to the front-end page.
  10. The session token is used to sign in automatically.

Setting Up

There are a number of tasks to accomplish before starting to code:

  1. Set the name of the callback function that handles the assertion
  2. Create an SSL certificate and private key pair.
  3. Set up the IDP.
  4. Download the IDP metadata.
  5. Set up the SP metadata.

Naming the Assertion Callback Function

This function handles the POST request from the IDP containing the encrypted user credentials. The function is defined in the http-functions.js back end file. Our function needs to handle the post of the Assertion Consumer Service (ACS) URL so let's call it post_assertion. The ACS is an XML document that contains the user authorization and user credentials.

If our site's URL is https://mysite.wixsite.com/saml-demo, then the URL of our assertion handler is https://mysite.wixsite.com/saml-demo/\_functions/assertion

Creating Certificates and Keys

We'll be encrypting and decrypting requests and responses between the SP and IDP. To do this we'll need an X509 private key and certificate.

To generate a private key and certificate, go to your system prompt and enter the following:

Copy
1
openssl genrsa -out private.pem
2
openssl req -new -x509 -key private.pem -out public.crt

You will be asked a number of questions. It is enough to answer the first one with a two-letter country code.

This will producte two files, private.pem containing the private key and public.crt containing the public certificate. These files will be created in the directory where you ran the commands.

Remember where you put public.crt as we'll need it later.

Note If you are using Windows, you may need to install OpenSSL before running these commands

Using the Secrets Manager

We'll use the Wix Secrets Manager to securely store our private key secret. This is more secure than pasting the key into our backend code. Make sure never to expose your private key.

In the Secrets Manager, set the Name of the secret to spPrivateKey.

Use a text editor to copy and paste the entire contents of private.pem including the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- lines into the Value field.

Enter a description so that you remember what this secret is used for.

If you haven't used the Secrets Manager before, here's how to use it.
  1. Go to your Wix Dashboard.

  2. Select Developer Tools and then Secrets Manager.

  3. Click the Store Secret button and enter the following details:

  4. Name: The name of the secret to be used in the code.

  5. Value: Paste the text value of the secret.

  6. Enter a brief description to remind you what this secret is used for, and click Save.

Setting Up the IDP

We need to configure IDP and export some metadata. This metadata will be used in our code when creating the SAML request, and when we want to decrypt the response and extract the credentials.

Set up a trial account. For this tutorial, we used Auth0. Sign up here for their free trial.

Once you have set up your trial account, follow these steps to create an application with SAML2 capabilities.

  1. Click Applications in the Auth0 dashboard.

  2. Click Create Application, give your application a name, select Regular Web Applications and click Create.

  3. Click Settings in the top bar.

  4. Scroll down to Allowed Callback URLs and enter the URL of your assertion function. This is the URL that the IDP will call with a POST when the user signs in. In our case that's going to be https://mysite.wixsite.com/saml-demo/\_functions/assertion which corresponds to the function post_assertion in the http-functions.js file. More on that when we get to the code.

  5. Scroll to the bottom and click Save Changes.

  6. Scroll back up to the top bar and click Addons.

  7. Click SAML2 WEB APP.

  8. The IDP needs our public certificate and Auth0 requires that it fits on a single line.

    The public key was that file we stored as public.crt earlier on.

    In the settings box, add "signingCert": inside the curly braces.The format for the public key is:

    -----BEGIN PUBLIC KEY-----nMIGf...bpP/t3\n+JGNGIRMj1hF1rnb6QIDAQAB\n-----END PUBLIC KEY-----\nso you will need to reformat your certificate to change all newline characters (\n) to actual "\"s followed by "n"s.

    Use the following if you are on mac or linux:

    $ awk '{printf "%s\\n", $0}' public.crt

    Use the following command in PowerShell if you are on Windows:

    PS C:\Users\Wix> (Get-Content .\public.crt) -join "\n"

It should look like this:

  1. Scroll all the way to the bottom and hit Enable, then Save.

  2. Scroll up to the top of the window and click Usage in the top bar**.**

  3. In the Identity Provider Metadata section, click the Download link and save the file.

  4. Ccreate a backend file called metadata.js.

  5. Copy the XML from the downloaded file into metadata.js and export it as the constant idpMetadata. Use backticks ( ` ) to enclose the XML so that the end of line is ignored.

  6. Edit the IDPSSODescriptor and add WantAuthnRequestsSigned="true" as in Line 2 below.

Your code should look like this:

Copy
1
export const idpMetadata =`<EntityDescriptor entityID="urn:my-saml-test.us.auth0.com" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
2
<IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
3
<KeyDescriptor use="signing">
4
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
5
<X509Data>
6
<X509Certificate>MIIDDTCCAfWgAwIBAgIJGBvK29ZE/GY1MA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNVBAMTGW15LXNhbWwtdGVzdC51cy5hdXRoMC5jb20wHhcNMjAxMDAxMDk0MjIyWhcNMzQwNjEwMDk0MjIyWjAkMSIwIAYDVQQDExlteS1zYW1sLXRlc3QudXMuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4yxmeg9zZn/Q1TzI27hQIknZaSklARUAlEtayykvrC9glB7SBuo1b0rz0w+M1GAKzdgcM3Ca0a0iC/wfH9a0YbXXE1vwj5tlMqISEKEPv/SOep1glPrK7m3s8ndPsfVN/4cxQbncVkLSgwORylWaU1UMU8Im3yvL7WcL0uSKCUQWr/XEbQsNqn+dDIjwXCAC2smXho7nm5Alz2xwR1bORLCLPTcJuy2pr0LxuzZiw366gADy/mG21OBOnPYqTbPZJuRE1I46av/atJVEPFrn3hklsyVaS4M3YEl7x+nW0rzenvqQvwh56ZDvF2cNUQBMqjtblclJd4ipp3x0bLuziQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQzlOf36gQB747obsUfV8wb6jLVAjAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAMs2wZW/CQtZFHS7tltolBNMDTuPokrRUlZYT6KoKdTh0Uk4Okq0luCEwAoxbyidpBMEWUIKP74iEttK72QiUF8Wf93DZtd0pC/IgMlFR+ef5z7IEunFTUJ0DsutdQh6BDPZKqoo/gG3eefBeCt0lYav/aF0v2+vzHZUc9wWGYuuKs8wjp4a+hxll3/lgzX/jGvvl69ckJ1TW71LtUF42RRtZcvbj5eC1xxC9jrF6hsLrdwQpaiVB0obyp260iXetc1cSuyVVThGQL3XIq4H7ngLGikJk53lESOJknn0DoTzg1mVnKZ6t47WcWU3eOQomgUVyxszkrwEpEmEXKEc+r8=</X509Certificate>
7
</X509Data>
8
</KeyInfo>
9
</KeyDescriptor>
10
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://my-saml-test.us.auth0.com/samlp//1234567890yNX1W1MZQgfzwSmhG1YoN0/logout"/>
11
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://my-saml-test.us.auth0.com/samlp/1234567890yNX1W1MZQgfzwSmhG1YoN0/logout"/>
12
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
13
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
14
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
15
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://my-saml-test.us.auth0.com/samlp/1234567890yNX1W1MZQgfzwSmhG1YoN0"/>
16
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://my-saml-test.us.auth0.com/samlp//1234567890yNX1W1MZQgfzwSmhG1YoN0"/>
17
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="E-Mail Address" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
18
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Given Name" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
19
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Name" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
20
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Surname" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
21
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Name ID" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
22
</IDPSSODescriptor>
23
</EntityDescriptor>`

Setting up the SP Metadata

Now that we have our private key and certificate we can set up our service provider metadata. This metadata will be used to create and encrypt the SAML requests.

To create the metadata we'll use a handy util at https://www.samltool.com/sp_metadata.php.

You should be on the Build SP Metadata page.

  1. Chose an EntitiyId, any unique name is OK. Let's use saml-demo.
  2. Attribute Consume Service Endpoint (HTTP-POST): This is where you enter the Assertion Consumer Service endpoint that will handle the assertion. In our case, as we defined above it's https://mysite.wixsite.com/saml-demo/\_functions/assertion.
  3. SP X.509 cert: Open the public.crt file with a text editor. This is the file we created in the Certificates and Keys step. Copy and paste the file's content into the SP X.509 cert field.
  4. Set AuthnRequestsSigned and WantAssertionsSigned to True.
  5. Scroll down and hit BUILD SP METADATA.
  6. Copy the XML from the green SP Metatada box.
  7. Open the backend file called metadata.js that you created for the IPD metadata and paste the copied XML into the file. Export it as spMetadata. Use back-ticks ( ` ) to enclose the XML so that the end of line is ignored.Your code should look like this :
Copy
1
export const spMetadata = `<?xml version="1.0"?>
2
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2020-10-09T10:14:25Z" cacheDuration="PT604800S" entityID="saml-demo">
3
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
4
<md:KeyDescriptor use="signing">
5
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6
<ds:X509Data>
7
<ds:X509Certificate>MIIE6DCCAtACCQCgAlaiRpc7ITANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJJTDELMAkGA1UECAwCaWwxDTALBgNVBAcMBEhlcmUxCzAJBgNVBAoMAk1lMB4XDTIwMTAwNzA4NDc1N1oXDTIwMTEwNjA4NDc1N1owNjELMAkGA1UEBhMCSUwxCzAJBgNVBAgMAmlsMQ0wCwYDVQQHDARIZXJlMQswCQYDVQQKDAJNZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM1JcEsUkUWsQhqn5E61MEjzkyOJQoF91WrQTsKhNsA0KiRoVcZ4OkGUUFYWrSCZXxab3VrghBURhW9UhJE5zho9rXX1PHvqnzEtS+/hzoQgBFZG3Halo/U06HeVXD4n2uJTlb/K/1KHXbUsJ3IniIA+IRhmlP69Oi/ZVQvjkfLDBbillp4vMPcf/ReT0/IGG3zRPX61byLmVoWLxzuQlqhojilRgfdn+gvLpDgleMaw9FR0EjkolTj6VW6A7nlw5MTKaMDATUGRnfUP+pbnq4fluXdfVzcNHF3JFQ8ECvePm1XqAr9hwIk6UX50gFjUM1A+hSDt298V55iG6RugwLaoO83p5tVd3g0xkxamPRAKxxSZfNia3y4HJB8cGTVor4SnwvkL8mweIr/xmDayyFzLYl5Tux7uk/nwzFN80EG9MieEjFgTMKgXzi3TGnyJQspypM8dqyN6eUpn0N/rKB5jaeMgXnUqRhrKc0aQtMUkhqAzTMMWFnbDySAKHJhq+HI4SXFEXhf51GM2tkoEh8OBhwbcoB8tco60I7cBxT0kakqoFWHG5jCTd4ksGzUkXoTeYrQSyxE/DyCOfZBzkvljJLcqoOQwoukYV13WLOfsNgETIiAhe0PXhqi0B8kxn6SXVqhVaVKhsXiy4FbClLOefPFHH+wtViNYjWQUIVctAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAM0XAs4qZGwYN34YxqYThVHGDcWljO9DiKlCt3xbBBRX9ZI8VKOWHNtswdBbdraPtAMT0D052xSWMbXOGlnGWPHxjvLCsWfGDnxJ4KCyItkzKuoIm7LRmSpQWHDwZNTKlQVm9ap192vvOgLa53mHavOZJ7NPoVkO29MC7+NyUa8exn9Zj8XbRGr2pioYK4LMub4oFthuAHVg5RIzHmIMzJpZYew8OrSYXLHLDKLWNLQHDOwDl+Bic02IQIF/hCfflviGGLCk3yjiAVP4HnpkJgtdDkgMi+YihG8znlyHpSmB60WrlSjbbxgxkdmPePEhZP5BrYeSyV6ZM2I5dpj3GpG94JN5RC44BpL23IXlS89dzRfo/cKKIZ63uA45rH6vVNEqAnKfSFGRI0OuafEdx8Cirgkw7WXMHkzqEm5wYlt4XV5akavi0PErS9rrYGlMpfUxT2csyjCww6k/UaxBDaS9GAySAMwRPsuA853XY0/eSESWNj72i0bGCjT1Dx6MWaE/UVERTSL04qxK/ACJT9aMnOAjs2r1gFYZq57l1AC5eadXtcJt/s6C/uEXgvXhyPvvHnYisu7S2lu5rOg1N9cvVftWbeRgTgAEsr3cz04CZ4ZZRADhpVpE38Shq2vPNrf4qqkjbM1BgOGcXtx/lDVUUlM4l7FEjWNfrUWbueqh</ds:X509Certificate>
8
</ds:X509Data>
9
</ds:KeyInfo>
10
</md:KeyDescriptor>
11
<md:KeyDescriptor use="encryption">
12
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
13
<ds:X509Data>
14
<ds:X509Certificate>MIIE6DCCAtACCQCgAlaiRpc7ITANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJJTDELMAkGA1UECAwCaWwxDTALBgNVBAcMBEhlcmUxCzAJBgNVBAoMAk1lMB4XDTIwMTAwNzA4NDc1N1oXDTIwMTEwNjA4NDc1N1owNjELMAkGA1UEBhMCSUwxCzAJBgNVBAgMAmlsMQ0wCwYDVQQHDARIZXJlMQswCQYDVQQKDAJNZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM1JcEsUkUWsQhqn5E61MEjzkyOJQoF91WrQTsKhNsA0KiRoVcZ4OkGUUFYWrSCZXxab3VrghBURhW9UhJE5zho9rXX1PHvqnzEtS+/hzoQgBFZG3Halo/U06HeVXD4n2uJTlb/K/1KHXbUsJ3IniIA+IRhmlP69Oi/ZVQvjkfLDBbillp4vMPcf/ReT0/IGG3zRPX61byLmVoWLxzuQlqhojilRgfdn+gvLpDgleMaw9FR0EjkolTj6VW6A7nlw5MTKaMDATUGRnfUP+pbnq4fluXdfVzcNHF3JFQ8ECvePm1XqAr9hwIk6UX50gFjUM1A+hSDt298V55iG6RugwLaoO83p5tVd3g0xkxamPRAKxxSZfNia3y4HJB8cGTVor4SnwvkL8mweIr/xmDayyFzLYl5Tux7uk/nwzFN80EG9MieEjFgTMKgXzi3TGnyJQspypM8dqyN6eUpn0N/rKB5jaeMgXnUqRhrKc0aQtMUkhqAzTMMWFnbDySAKHJhq+HI4SXFEXhf51GM2tkoEh8OBhwbcoB8tco60I7cBxT0kakqoFWHG5jCTd4ksGzUkXoTeYrQSyxE/DyCOfZBzkvljJLcqoOQwoukYV13WLOfsNgETIiAhe0PXhqi0B8kxn6SXVqhVaVKhsXiy4FbClLOefPFHH+wtViNYjWQUIVctAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAM0XAs4qZGwYN34YxqYThVHGDcWljO9DiKlCt3xbBBRX9ZI8VKOWHNtswdBbdraPtAMT0D052xSWMbXOGlnGWPHxjvLCsWfGDnxJ4KCyItkzKuoIm7LRmSpQWHDwZNTKlQVm9ap192vvOgLa53mHavOZJ7NPoVkO29MC7+NyUa8exn9Zj8XbRGr2pioYK4LMub4oFthuAHVg5RIzHmIMzJpZYew8OrSYXLHLDKLWNLQHDOwDl+Bic02IQIF/hCfflviGGLCk3yjiAVP4HnpkJgtdDkgMi+YihG8znlyHpSmB60WrlSjbbxgxkdmPePEhZP5BrYeSyV6ZM2I5dpj3GpG94JN5RC44BpL23IXlS89dzRfo/cKKIZ63uA45rH6vVNEqAnKfSFGRI0OuafEdx8Cirgkw7WXMHkzqEm5wYlt4XV5akavi0PErS9rrYGlMpfUxT2csyjCww6k/UaxBDaS9GAySAMwRPsuA853XY0/eSESWNj72i0bGCjT1Dx6MWaE/UVERTSL04qxK/ACJT9aMnOAjs2r1gFYZq57l1AC5eadXtcJt/s6C/uEXgvXhyPvvHnYisu7S2lu5rOg1N9cvVftWbeRgTgAEsr3cz04CZ4ZZRADhpVpE38Shq2vPNrf4qqkjbM1BgOGcXtx/lDVUUlM4l7FEjWNfrUWbueqh</ds:X509Certificate>
15
</ds:X509Data>
16
</ds:KeyInfo>
17
</md:KeyDescriptor>
18
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
19
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://mysite.wixsite.com/saml-demo/_functions/assertion" index="1"/>
20
</md:SPSSODescriptor>
21
</md:EntityDescriptor>`

In the metadata, check the following items:

  • SPSSODescriptor Line 3. Make sure that AuthnRequestsSigned="true" WantAssertionsSigned="true" are both there. If not, or if they are false, it may cause trouble later on when creating the ServiceProvider and IdentityProvider objects.
  • AssertionConsumerService Binding, Line 19. This holds the URL that will handle the callback once our visitor has signed in. i.e. "https://mysite.wixsite.com/saml-demo/\_functions/assertion"

Installing the Samlify Package

Before we start coding we need to install the samlify package so we can call its functions from our code.

To install a package:

  1. In the left sidebar of the editor, navigate to Packages & Apps. In Wix Studio, you can find this in the Code tab.
  2. Under npm click Install packages from npm.
  3. Search for "samlify" and click Install next to the samlify package.

Adding the Code

Add the following code files to your backend files:

  • saml.web.js: Accessible from the front end, to generate SAML requests.
  • create-providers.js: To work with the samlify package to create the SP and IDP objects. This is a .js file and therefore not accessible from the front end. We do not want these functions accessible from the front end as they contain metadata and secrets that we do not want to expose.
  • http-functions.js: To handle the assertion callback from the IDP, and retrieve the user's details.

Note You must use the file name "http-functions.js" to handle the web callbacks but you can call the other files any name you like.

Adding Landing Page Code

Add a login button to your landing page. Then add the following code:

Copy
1
import wixLocationFrontend from 'wix-location-frontend';
2
import {session} from 'wix-storage-frontend';
3
import { generateSAMLRequest } from 'backend/saml.web';
4
5
$w.onReady(function () {
6
7
$w('#loginButton').onClick(() => {
8
generateSAMLRequest()
9
.then((request) => {
10
session.setItem("requestId", request.id);
11
wixLocationFrontend.to(request.context);
12
});
13
});
14
});

Understanding the Code

Lines 1-2: Import the modules that we will work with, incuding our backend function, generateSAMLRequest, which we will create below.
Lines 7-8: When a site visitor clicks login the button, we call the backend generateSAMLRequest function, which will return the SAML request Id, and the URL with the SAML request.
Line 10: Store the requestId in the session variables. The requestId will be compared to the inResponseTo Id that is returned in the SAML response. This will help us protect against someone copying the final log-in URL and the session token.
Line 11: Direct the browser to the request URL with wixLocationFrontend.to, so the visitor can log in using their credentials on the IDP.

Generating the SAML Request

Create the sp and idp objects, using their respective metadata, and use these objects to create SAML request. Return the SAML request URL to front end.

The generateSAMLRequest function in saml.web.js, returns a SAML request URL to the front end, which calls wixLocationFrontend.to() to direct the browser to that URL. At this point the visitor sees the IDP's sign-in window and enters their user and password. Once the IDP authenticates the visitor, it redirects back to our site, to the URL defined in the AssertionConsumerService Binding element in the SP metadata. This is the same URL that we set up inthe IDP's console. In our example, this will redirect to the post_assertion function, which is defined in http-functions.js.

Add the following code to saml.web.js.

Copy
1
import { Permissions, webMethod } from 'wix-web-module';
2
import { notFound } from 'wix-http-functions';
3
import { createIdp, createSp } from 'backend/create-providers';
4
5
export const generateSAMLRequest = webMethod(Permissions.Anyone, async () => {
6
try {
7
//create the idp and sp objects using samlify
8
const idp = await createIdp();
9
const sp = await createSp();
10
11
//create the request url and return it and the request id to the fronted page.
12
const { id, context } = sp.createLoginRequest(idp, 'redirect')
13
return { id, context };
14
15
} catch (error) {
16
console.error('generateSAMLRequest error:', error.message);
17
return notFound({ status: 404 });
18
}
19
});

Understanding the Code

Lines 1-3: Import the modules that we will work with.
Lines 8-9: When the front end calls generateSAMLRequest, we create the identity provider and service provider objects using createIdp and createSp.
Lines 12-13: Use the samlify function, createLoginRequest to generate the SAML request id and URL and return them to the front end. The request id will be matched with the inResponseTo id in the SAML response.

Create the idp object using the metadata in idpMetadata.

Add the following code to create-providers.js.

Copy
1
import { getSecret } from 'wix-secrets-backend';
2
import { IdentityProvider, ServiceProvider } from 'samlify';
3
import { spMetadata, idpMetadata } from 'backend/metadata.js';
4
5
export function createIdp() {
6
//create the IdentityProvider object using the metadata in idpMetadata
7
try {
8
const idp = IdentityProvider({
9
metadata: idpMetadata
10
});
11
return idp;
12
} catch (error) {
13
console.error('Error in createIdp detected', error.message);
14
}
15
}

Understanding the Code

Lines 1-3: Import the modules that we will work with. Note the import of spMetadata and idpMetadata from backend/metadata.js. We created the metadata.js file when setting up the IDP and setting up the SP metadata.
Line 8: Use the samlify IdentityProvider object and the metadata from the identity provider to create the idp object.
This object is used when creating the login request and again when we parse the login response in the http-functions.
Line 11: Return the idp object to the generateSAMLRequest function

The createSp function retrieves the private key from from the Secrets Manager, and uses it with our service provider metadata to create the sp object.

Add the following code to create-providers.js.

Copy
1
export async function createSp() {
2
try {
3
4
// retrieve the private key from the Secrets Manager
5
const spPrivateKey = await getSecret('spPrivateKey');
6
7
//create the ServiceProvider object usning the metadata and the private key
8
const sp = ServiceProvider({
9
metadata: spMetadata,
10
privateKey: spPrivateKey
11
});
12
return sp;
13
14
} catch (error) {
15
console.error('Error in createSp detected', error.message);
16
}
17
}

Understanding the Code

Line 5: Retrieve the private key that was stored in the Secrets Manager in the Using the Secrets Manager section.
Lines 8-10: Use the samlify ServiceProider object, metadata for the service provider, and the private key, to create the sp object.
Line 12: Return the sp object to the generateSAMLRequest function.

Note If there are any mismatches between the IDP and SP metadata, it may fail at this point. The most common error is **ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG,** and iscaused by not setting WantAuthnRequestsSigned="true" in the IDP metadata when the SP metadata hasAuthnRequestsSigned="true" in the SPSSODescriptor, or having AuthnRequestsSigned="false".

Handling the AssertionConsumerService Request

In the post_assertion function, we create the sp and idp objects to decrypt and extract the SAML response. Samlify requires that we validate the response so we call setSchemaValidator but we are not going deal with validation in this tutorial. For more details see how to set up a validator.

Add the following code to http-functions.js.

Copy
1
import { response, ok, notFound } from 'wix-http-functions';
2
import { createIdp, createSp } from 'backend/create-providers.js';
3
import * as samlify from 'samlify';
4
import { authentication } from 'wix-members-backend';
5
6
const baseUrl = 'https://mysite.wixsite.com/saml-demo';
7
8
export async function post_assertion(request) {
9
try {
10
const [idp, sp] = await Promise.all([createIdp(), createSp()]);
11
12
// we are not using the validation in our example, but if we dont call it, we'll get an error.
13
samlify.setSchemaValidator({
14
validate() {
15
return Promise.resolve('skipped');
16
}
17
});
18
19
const requestBody = await request.body.text();
20
const samlResponse = {
21
body: {
22
SAMLResponse: decodeURIComponent(requestBody.split('SAMLResponse=')[1])
23
}
24
}
25
26
// parse and decrypt the response
27
const parseResult = await sp.parseLoginResponse(idp, 'post', samlResponse);
28
29
//extract the inResponseTo Id
30
const inResponseTo=parseResult.extract.response.inResponseTo
31
32
//extract the email address
33
const email = parseResult.extract.attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'];
34
35
//create the session token to use when logging the visitor in
36
const sessionToken = await authentication.generateSessionToken(email);
37
38
return response({
39
status: 302,
40
headers: {
41
'Location': `${baseUrl}/signed-in?session=${sessionToken}&response=${inResponseTo}`
42
}
43
});
44
45
} catch (error) {
46
const body = await request.body.text();
47
console.error('Error in post_asc', error.message);
48
return notFound({ status: 404 });
49
}
50
}

Understanding the Code

Line 6: Set the base URL for the site. Change this to suite your site.
Line 8: Create the assertion function to handle the post request from the IDP.
Line10: Create the idp and sp objects using the createIdp and createSp imported from saml.web.js.
Lines13-15: Samlify requires that the response is validated, but we are not going to deal with that in this tutorial. For more details see how to set up a validator. Call setSchemaValidator and return a resolved promise.
Line 19: Get the http request body in text format.
Line20: The response body may differ between IDPs. Take the data that comes after SAMLResponse= using the split function and decode it. Reconstruct the response object, adding the body so it can be parsed.
Line 27: Use the samlify function parseLoginResponse to decrypt and extract the XML that is our readable SAML response.
Line 30: Extract the inResponseTo Id which will be matched to the request Id when the user is signed in.
Line 33: Extract the email address from the SAML response XML. Note that the attribute name for the email address will differ for each IDP.
Line 36: Use the email address to create a Wix session token using the wix-members-backend function, generateSessionToken. This token will allow us to log the visitor in to our Wix site. If the email address corresponds to an existing member, a session token for that member is generated. If no member exists with that email address, a new member is created along with a session token for logging that member in.
Lines 38-41: Return a redirect URL, directing the browser to the signed-in landing page, adding the session token and inResponseTo Id as query fields.

Signed-in Landing Page

The visitor lands on the signed-in landing page where the session token is used to log our visitor in.

Create a page called signed-in. Make sure that the signed-in page has the correct URL. The URL should be https://mysite.wixsite.com/saml-demo/signed-in. Replace https://mysite.wixsite.com to suite your site. Follow this guide to set the URL.

Add a members area to your site, then add a Member Profile Card element to your signed-in page to display who the current member is.

Add the following code to the signed-in page.

Copy
1
import wixLocationFrontend from 'wix-location-frontend';
2
import { session } from 'wix-storage-frontend';
3
import { authentication } from 'wix-members-frontend';
4
5
$w.onReady(() => {
6
7
const query = wixLocationFrontend.query;
8
const sessionToken = query.session;
9
const responseID = query.response;
10
const requestID = session.getItem("requestId")
11
12
if (sessionToken && (responseID === requestID)) {
13
authentication.applySessionToken(sessionToken)
14
} else {
15
console.error("Signin Failed", sessionToken)
16
wixLocationFrontend.to('https://mysite.wixsite.com/saml-demo')
17
}
18
});

Understanding the Code

Lines 7-9: Retrieve the session token and inResponseTo id from the query parameters.
Line 10: Retrieve the requestId that was stored in the session variables before signing in.
Lines 12-13: If the requestId matches the inResponseTo Id, then we know that this broswe session is the one that made the login request. If a session token exists, apply the session token to log the visitor in.
Lines 15-16: If there is no session token, or the Ids do not match, return the visitor to the log-in page. Change the URL on line 16 to suite your site.

Was this helpful?
Yes
No