This tutorial is intended as a guide for developers implementing Unblu’s visitor SSO using JSON Web Tokens (JWT) in an application.

Adding visitor SSO to your application has a number of benefits:

  • Agents know who they are speaking to.

  • Visitors can view their old conversations from different devices.

  • Different people using the same device each has their own conversation history.

Before you continue, you may want to review the general introduction to Unblu Cloud SSO in our official documentation.

Note
Logging in to Unblu using a JWT was introduced in Unblu 6.13.0.

Data flow

The diagram below illustrates the order in which data flows between a visitor, your application, and the Unblu server during visitor SSO.

Your web
application
Your web...
Visitor
Visitor
Enter
credentials
Enter...
Return
JWT
Return...
Unblu
Unblu
Send JWT
Send JWT
Set session ID in cookie
Set sess...
Load public key
Load pub...
Create and sign JWT
using private key
Create a...
Validate JWT
using public key
Validate...
1
1
3
3
2
2
4
4
7
7
6
6
5
5
Viewer does not support full SVG 1.1
  1. The visitor is authenticated using your application’s standard authentication mechanism.

  2. The application creates a JWT and signs it with a private key.

  3. The application sends the JWT to the visitor’s browser.

  4. The visitor’s browser sends the JWT to Unblu (POST /unblu/rest/v3/authenticator/loginWithSecureToken)

  5. Unblu loads the public key used to sign the JWT from the application. Unblu will cache the keys and therefore not request it for every validation.

  6. Unblu validates the JWT signature and attributes (iss, aud, exp).

  7. Unblu sets a session cookie in the visitor’s browser.

Important
The Unblu backend and the host application must be running in the same second-level domain (e.g. example.com). If they don’t, some browsers — Safari, for example — will treat the Unblu authentication cookie as a third-party cookie and block it.

Configuring Unblu for visitor SS0

By default, visitor SSO is disabled. Use the configuration below to activate it.

Visitor SSO Configuration in Unblu
# Ensure that /unblu (visitor) and /co-unblu (agent) are separated
# and authentication cookies are set on the correct path
com.unblu.identifier.publicPathPrefix=${systemIdentifier}
com.unblu.identifier.restrictedPathPrefix=co-${systemIdentifier}

# Visitor SSO is typically used in a cross-site setup, so we disable
# site-embedded mode
com.unblu.identifier.siteEmbeddedSetup=false

com.unblu.authentication.untrusted.sources=LOCAL

com.unblu.authentication.jwt.jwkUrl=https://application.example.com/api/jwk
com.unblu.authentication.jwt.expectedIssuer=https://application.example.com
com.unblu.authentication.jwt.expectedAudience=https://example.unblu.cloud
com.unblu.authentication.jwt.useEncryption=false
#com.unblu.authentication.jwt.encryptionKey=<private key, if encryption is enabled>

# Enable authenticator/loginWithSecureToken
com.unblu.authentication.tokenSignup.enabled=true
com.unblu.authentication.tokenSignup.claimMapping.username=username
com.unblu.authentication.tokenSignup.claimMapping.email=email
com.unblu.authentication.tokenSignup.claimMapping.firstName=firstName
com.unblu.authentication.tokenSignup.claimMapping.lastName=lastName

# To use cross-site cookies, most browsers require that you use HTTPS.
# Enable the configuration properties below if you run the application without
# a reverse proxy providing HTTPS connections.
#com.unblu.identifier.cookieSecureFlag=true
#com.unblu.runtime.jetty.securehttpenabled=true
#com.unblu.runtime.jetty.keystorepath=tls.p12
#com.unblu.runtime.jetty.keystorepassword=password

The URL configured in the jwkUrl property must be accessible from the Unblu server. expectedIssuer and expectedAudience must match the iss and aud claims as set by the application.

Generating an RSA key pair to encrypt the JWT

You can encrypt the JWT. This will hide the content of the JWT payload from the visitor if they intercept the request in their browser.

To use JWT encryption, generate an RSA key pair. Encryption uses a separate key pair, and the application uses Unblu’s public key to encrypt the JWT after signing it with its own private key.

The snippet belows shows how to generate an RSA key pair using openssl.

Generate RSA key pair
openssl genpkey -algorithm RSA -out unblu.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in unblu.pem -out unblu_public.pem

Generating a JWT

The code samples that follow are part of a sample implementation of visitor SSO and may be freely copied.

The application needs to create a JWT signed with an RSA key pair and make it accessible from the browser JavaScript after the user authenticated with his application specific credentials.

The JWT must be signed with a key pair referenced by its Key ID (kid). It must include the user attributes as claims. The claims must be named as configured in the claimMapping in the Unblu configuration.

Decoded sample JWT
Token header
------------
{
"typ": "JWT",
"alg": "RS256",
"kid": "5d2acf7b-b5e3-4a08-8e71-0953f8cdf1f6"
}

Token claims
------------
{
"aud": "https://example.unblu.cloud",
"email": "peter.muster@example.com",
"exp": 1622640746,
"firstName": "Peter",
"iat": 1622637146,
"iss": "https://application.example.com",
"lastName": "Muster",
"username": "pmuster"
}
Note
You can check a JWT with a service such as jwt.io.
Create a JWT in Kotlin using the Nimbus library
val header = JWSHeader.Builder(JWSAlgorithm.RS256)
    .type(JOSEObjectType.JWT)
    .keyID(key.keyID)
    .build()
val expiration = Date(System.currentTimeMillis() + configuration.validFor.toMillis())
val payload = JWTClaimsSet.Builder()
    .issuer(configuration.issuer)
    .audience(configuration.audience)
    .issueTime(Date())
    .expirationTime(expiration) // (1)
    .claim("email", userInfo.email)
    .claim("username", userInfo.username)
    .claim("firstName", userInfo.firstname)
    .claim("lastName", userInfo.lastname)
    .claim("logoutToken", session.id)
    .build()
val signedJWT = SignedJWT(header, payload)

signedJWT.sign(signer)

val jwt: String = if (configuration.encryption) {
    // Create JWE object with signed JWT as payload
    val jweHeader = JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
        .contentType("JWT")
        .build()
    val jweObject = JWEObject(jweHeader, Payload(signedJWT))
    // Encrypt with the recipient's public key
    jweObject.encrypt(encrypter)
    jweObject.serialize()
} else {
    // Create a signed JWT
    signedJWT.serialize()
}
  1. Unblu will accept the JWT to start a session until the expiration time is in the past. The session can go on much longer than the JWT expiration. In production environments, we recommend a JWT expiration of 60 seconds.

    The RSA key pair used to sign the JWT can be static, or an ephemeral key rotated on a regular basis. Unblu just needs to be able to load a key using JSON Web Key (JWK) at all times. If you use rotating keys, you should therefore include some grace period during which you serve both the old and the new key in the same JWK key set.

Generate RSA key pair to sign JWTs
private val key: RSAKey = RSAKeyGenerator(2048)
    .keyUse(KeyUse.SIGNATURE)
    .keyID(UUID.randomUUID().toString())
    .generate()
private val signer = RSASSASigner(key.toRSAPrivateKey())
Expose the public key in a JWK set in a Spring Boot application
@GetMapping("jwk")
fun keys(): Map<String, Any> {
    return JWKSet(key.toPublicJWK()).toJSONObject()
}
Example response of the JWK endpoint
{
  "keys": [
    {
      "kty": "RSA",
      "e": "AQAB",
      "use": "sig",
      "kid": "5d2acf7b-b5e3-4a08-8e71-0953f8cdf1f6",
      "n": "42"
    }
  ]
}

Starting an Unblu authentication session using a JWT

Unblu exposes an endpoint to check whether a user is currently authenticated. We recommend calling this endpoint before starting a new authentication.

Check whether a user has a valid session in Unblu
/**
  * Calls the authentication verification endpoint of Unblu.
  *
  * @returns {Promise<boolean>} Whether the user is authenticated
  */
async checkAuthentication () {
  const options = { credentials: 'include' }; // (1)
  return await fetch(this.unbluBaseUrl + '/rest/v3/authenticator/isAuthenticated', options)
    .then(response => {
      if (!response.ok) {
        const message = `An error has occurred: ${response.status}`;
        throw new Error(message);
      }
      return response;
    })
    .then(response => response.json());
}
  1. All browsers block third-party cookies unless you set credentials: 'include' (fetch) or withCredentials = true (XMLHttpRequest)

If checkAuthentication() returns false, you can start the authentication process. After obtaining the signed JWT from the application, the JWT must be sent to Unblu in a POST request to the endpoint unblu/rest/authenticator/loginWithSecureToken.

Call loginWithSecureToken from the visitor’s browser
/**
  * Starts an Unblu authentication session using a JWT.
  * @returns {Promise}, fulfilled when login succeeded, rejected when login failed.
  */
activateUnbluJwt (jwt) {
  const request = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    },
    body: JSON.stringify({ token: jwt, type: 'JWT' }),
    credentials: 'include'
  };
  const loginUrl = `${this.unbluBaseUrl}/rest/v3/authenticator/loginWithSecureToken?x-unblu-apikey=${this.unbluApiKey}`;
  return fetch(loginUrl, request)
    .then((response) => {
      if (response.ok) {
        console.log('Unblu session activated');
      } else {
        throw new Error('Failed to activate token!');
      }
    });
}

The response to loginWithSecureToken includes a Set-Cookie header.

Example authentication cookie
Set-Cookie: x-unblu-authsession="4c2sGUuN-6GM8pL9-szYsb8_AQlEM49nJDY~";Path=/unblu;Expires=Wed, 09 Jun 2021 14:41:12 GMT;SameSite=None;Secure;HttpOnly

The Unblu backend and the host application must run on the same second-level domain (e.g. company.com). Otherwise, some browsers (i.e. Safari and future version of Chrome) will treat the Unblu authentication cookie as a third-party cookie and therefore block it. To circumvent this issue, create a subdomain such as chat.example.com that points to our cloud IP address. Your website should then use this subdomain to call Unblu.

Ending an Unblu session

Depending on your risk assessment, you may want to end the Unblu session when the application performs a logout.

Call clientLogout from the visitor’s browser
/**
  * Calls the Unblu logout endpoint.
  * @returns {Promise}, fulfilled when logout succeeded, rejected when logout failed.
  */
clientLogout () {
  const request = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    },
    body: JSON.stringify({ redirectOnSuccess: null }),
    credentials: 'include'
  };
  const logoutUrl = this.unbluBaseUrl + '/rest/v3/authenticator/logout';
  return fetch(logoutUrl, request)
    .then((response) => {
      if (response.ok) {
        console.log('Unblu logout successful');
        location.reload();
      } else {
        console.log('Logout failed', response);
      }
    });
}
Execute logout from the application backend (server-to-server API call)
@GetMapping("logout")
fun logout(session: WebSession) : String {
    val header = JWSHeader.Builder(JWSAlgorithm.RS256)
        .type(JOSEObjectType.JWT)
        .keyID(key.keyID)
        .build()
    val expiration = Date(System.currentTimeMillis() + configuration.validFor.toMillis())
    val payload = JWTClaimsSet.Builder()
        .issuer(configuration.issuer)
        .audience(configuration.audience)
        .issueTime(Date())
        .expirationTime(expiration) // (1)
        .claim("logoutToken", session.id)
        .build()
    val signedJWT = SignedJWT(header, payload)

    signedJWT.sign(signer)

    val jwt: String = if (configuration.encryption) {
        // Create JWE object with signed JWT as payload
        val jweHeader = JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
            .contentType("JWT")
            .build()
        val jweObject = JWEObject(jweHeader, Payload(signedJWT))
        // Encrypt with the recipient's public key
        jweObject.encrypt(encrypter)
        jweObject.serialize()
    } else {
        // Create a signed JWT
        signedJWT.serialize()
    }

    val targetURI = URI.create(unbluConfiguration.serverUrl + unbluConfiguration.entryPath + "/rest/v3/authenticator/logoutWithSecureToken?x-unblu-apikey=" + unbluConfiguration.apiKey)
    val client = HttpClient.newBuilder().build()
    val request = HttpRequest.newBuilder()
        .uri(targetURI)
        .POST(HttpRequest.BodyPublishers.ofString("{\n\"token\":\"$jwt\",\n\"type\":\"JWT\"\n}"))
        .header("Content-Type", "application/json;charset=UTF-8")
        .build()
    val response = client.send(request, HttpResponse.BodyHandlers.ofString())

    session.invalidate()

    return response.body()
}