Single Sign-On (SSO) experience

This example demonstrates a flow where your visitors will have a single sign-on experience when visiting your restricted Docs site. You will exchange their existing authentication in your system with a signed JavaScript Web Token (JWT) used to give access to the site. In this way, your visitors will not need to worry about another sign-in screen when accessing your Docs site, while you still have confidence that visitors who are not authenticated with your systems will not have access.

Overview

We will implement a simple endpoint on http://localhost:3000/signin. This URL is what we will use when configuring site restrictions later on. When the visitor tries to access the restricted site, then they will be redirected to this URL.

The code for this example can be found on GitHub.

Project setup

To begin with, we will create a new project using npm:

mkdir custom-callback-sso-example
cd custom-callback-sso-example
npm init -y

This will create a new NPM project in a folder named custom-callback-sso-example.

Open package.json in a code editor and add the following to the scripts section:

...
"scripts": {
  "start": "nodemon index.js"
}
...

Install dependencies

We will start by adding a few dependencies that will make building the endpoint much easier.

  • ExpressJS - Web application framework to ease the implementation of an HTTP endpoint.
  • jsonwebtoken - One of the implementations of JWT for NodeJS. See jwt.io for libraries for most languages.
  • cookie-parser - ExpressJS middleware that makes working with browser cookies easier.
  • nodemon - Makes development easier as it automatically restarts your web server whenever you make updates to the code.
npm install --save express cookie-parser jsonwebtoken
npm install --save-dev nodemon

Create sign-in page

We will start by implementing the sign-in page that will handle the exchange of authentication and redirect when the visitor is sent here from the restricted Docs site.

Before we do that, we will create some helper functions first.

Create a new file called index.js and add the following to it:

const express = require('express')
const cookieParser = require('cookie-parser');

const app = express()
const port = 3000

app.use(cookieParser())

const resolveExistingAuthentication = req => {
    // Here you can resolve your existing authentication, whether you take that
    // from a session, cookie, or other storage forms
    // In this example we have a very simple cookie, if set means the visitor is
    // already authenticated
    const email = req.cookies?.loggedInEmail

    return {
        email,
        valid: !!email,
    }
}

app.listen(port, () => {
    console.log(`Server listening on port ${port}`)
})

The resolveExistingAuthentication function is where you will identify if the visitor is already signing into your app. Feel free to replace the implementation of the function with something that suits you better. In this example, we use a very simple and not very secure cookie that we don’t verify for validity. In a real application, we expect that you will ensure the validity of the user authentication in a more robust way.

Next, we will add the code for creating the JSON Web Token. This code will use the shared secret that you get when enabling the site restriction through the Docs API.

const jwt = require('jsonwebtoken')

const sharedSecret = 'ENTER SHARED SECRET HERE'

const createToken = existingAuthentication => {
    const tokenPayload = {
        // Expires in 1 minute, resulting in a new call to /signin
        // In your own implementation you will likely want a longer expiration
        exp: Math.floor(Date.now() / 1000) + (60),
        sub: existingAuthentication.email,
    }

    // Create signed JSON Web Token
    return jwt.sign(tokenPayload,
                    sharedSecret,
                    {algorithm: 'HS512'})
}

This function uses the output of the resolveExistingAuthentication so make sure to keep those two functions in sync if you change them.

Now we can implement the endpoint that handles authentication when redirected from the restricted Docs site:

const docsSiteUrl = 'ENTER BASE URL FOR YOUR RESTRICTED DOCS SITE'

app.get('/signin', (req, res) => {
    // Help Scout will always include the path that the visitor was requesting
    // as a query parameter
    const returnTo = req.query.returnTo

    const existingAuthentication = resolveExistingAuthentication(req)

    if (existingAuthentication.valid) {
        const token = createToken(existingAuthentication)

        const redirectUrl = `${docsSiteUrl.replace(/\/+$/, '')}/authcallback?token=${token}&returnTo=${returnTo}`
        res.redirect(redirectUrl)
    } else {

        res.send(`
            <html>
                <body>
                    <h1>You are not authenticated</h1>
                    <p>
                        Go back to the <a href="/">front page</a> and click "sign in"
                        before going to <a href="${docsSiteUrl}">the site</a>
                    </p>
                </body>
            </html>
        `)
    }
})

The code will automatically create a signed token and redirect the visitor back to your Docs site as long as there is an existing authentication.

You can see this in action by starting the server using this command:

npm start

and opening http://localhost:3000/signin in a browser.

Simulate that the visitor is already signed in

As this is not a real app with real user authentication, we will simulate that the visitor is authenticated in the app.

Let’s start by creating a new page in our app on http://localhost:3000/signin

const defaultEmail = "john@example.com"

app.get("/", (req, res) => {
    const email = req.cookies?.loggedInEmail

    const signedInTemplate = `
        <p>
            You are signed in as <code>${email}</code> and can continue to the restricted Docs site directly:
        </p>
        <p>
            <a href="${docsSiteUrl}">${docsSiteUrl}</a>
        </p>
        <p>
            You can also sign out again by clicking <a href="/setcookie?clear=true">here</a>
        </p>
    `

    const signedOutTemplate = `
        <p>
            To simulate that you are already logged in this app, click the link below
        </p>
        <p>
            <a href="/setcookie">Sign in as ${defaultEmail}</a>
        </p>
    `

    res.send(`
        <html>
            <body>
                ${email ? signedInTemplate : signedOutTemplate}
            </body>
        </html>
    `)
})

The above renders a very simple page that changes its content based on the presence of a browser cookie called loggedInEmail. It also provides an easy way to “authenticate” by just clicking a link.

Lastly, we will implement a simple endpoint that will handle the authentication by simply setting or clearing a browser cookie:

app.get("/setcookie", (req, res) => {
    const clearCookie = req.query.clear
    
    if (clearCookie) {
        res.clearCookie("loggedInEmail")
    } else {
        res.cookie("loggedInEmail", defaultEmail)
    }

    res.redirect("/")
})

The authentication is intentionally kept simple as the focus of this example is the exchange of authentication tokens and the single sign-on experience for the visitors.

Testing the flow

To test the flow you can use one of your own sites that you have set up restrictions on. If you want to just try this sample code, we also host an example site that is restricted and we have made the shared secret public for this specific site.

Once you have the Docs site URL and the shared secret, you only need to change these two constants:

const docsSiteUrl = 'ENTER BASE URL FOR YOUR RESTRICTED DOCS SITE'
const sharedSecret = 'ENTER SHARED SECRET HERE'

Test against our sample site

We have created an example site that you can find on:

https://restricted-docs-example-site.helpscoutdocs.com/

It is restricted using the following Docs API call:

PUT /v1/sites/[SITE_ID]/restricted
Authorization: Basic [AUTHENTICATION]
Content-Type: application/json

{
    "enabled": true,
    "authentication": "CALLBACK",
    "callbackConfiguration": {
        "signInUrl": "http://localhost:3000/signin"
    }
}

You can configure your local instance by setting the two constants to these values:

const docsSiteUrl = 'https://restricted-docs-example-site.helpscoutdocs.com'
const sharedSecret = 'pQKpsnVo3TS7efPYC9FcSRoCCyB3aI+MYpI+omktNzE='