# Single Sign-On

With our Json Web Token ([JWT](https://jwt.io/)) SSO implementation, you will be able to automatically register or log members into your Spot. If you enable SSO, a user will go through the following flow:

{% tabs %}
{% tab title="From your App" %}

1. Your user clicks on a link in your app to enter your Spot.
2. Clicking on this link triggers the creation of a signed token (see below).
3. Your app redirects the user to your Spot and passes the token along.
4. If we can validate the token, we log the user in and identify them using the information passed in the token (like their name and email address).
   {% endtab %}

{% tab title="From your Spot" %}

1. A user who clicks on `Login` or `Join` on your Spot (go.meltingspot.io/spot/...) will be redirected to a page on your website (the `authorisation URL`, see below).
2. On your app, the user will need to login or sign up.
3. Your app will generate a signed token (see how to below).
4. Your app will redirect the user to your Spot and pass the token along.
5. If we can validate the token, we log the user in and identify them using the information passed in the token (like their name and email address).

<figure><img src="https://3055204660-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FrzBxnWaSListMwx3rzKu%2Fuploads%2FltD1DS2waCoJPs7DxYEw%2Fimage.png?alt=media&#x26;token=ae1e779f-c746-4a54-9a50-d32e6d6e6d6e" alt=""><figcaption></figcaption></figure>
{% endtab %}
{% endtabs %}

## Implementation

### 1. Enable JWT SSO in your Spot settings

First, you will need to activate the SSO in your Spot settings :

1. Add an authorisation URL. It is the URL of the page where the user is redirected to be authenticated on your side (2nd bullet point in the "[From your Spot](#from-your-spot)" flow).\
   \&#xNAN;*�� If you want to run local tests, use `127.0.0.1` in the URL, as `localhost` URLs are blocked by our system.*
2. Copy your private key.
3. Turn SSO on once you are ready.

<figure><img src="https://3055204660-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FrzBxnWaSListMwx3rzKu%2Fuploads%2FY5Mv1r33J97tJOC7Y6wp%2Fimage.png?alt=media&#x26;token=06d3b0c7-a7e7-4780-b4db-c60f2c3d6465" alt=""><figcaption></figcaption></figure>

You will need to create a custom signed link for your app that would automatically pass information to your Spot, such as: the email, first and last name of the member you want to log in.

### 2. **Generate an SSO token**

You will need to create a custom signed JWT for your user that would automatically pass information to your Spot.

1. **Install JWT library**

First, you'll have to install a library that allows you to create the token embedding your user's information.

{% tabs %}
{% tab title="Node.js" %}

```javascript
npm install --save jsonwebtoken
```

{% endtab %}

{% tab title="Python" %}
{% code fullWidth="true" %}

```python
pip install PyJWT
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}

```php
composer require firebase/php-jwt
```

{% endtab %}

{% tab title="C#" %}

```csharp
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.IdentityModel.Tokens
dotnet add package Newtonsoft.Json
```

{% endtab %}
{% endtabs %}

2. **Create the token**

Next, you'll have to prepare a signed JWT defining some or all available user information. You'll have to use the private key copied from the Spot settings.&#x20;

{% tabs %}
{% tab title="Node.js" %}
{% code fullWidth="false" %}

```javascript
const jwt = require('jsonwebtoken');
const privateKey = 'YOUR_PRIVATE_KEY';

function createToken(user) {
  const userData = {
    sub: user.id, // Your own user ID
    firstName: user.firstName,
    lastName: user.lastName,
    email: user.email,
    title: user.title,       // optional
    avatarUrl: user.avatarUrl, // optional
    lang: user.lang,         // optional
    timezone: user.timezone, // optional
    groups: {
      join: ["groupId1", "groupId2"], // optional list of group IDs to join
      leave: ["groupId3"]             // optional list of group IDs to leave
    },
    domains: {
      set: {
        default: "https://some-default-url.com/for/this/member",
        customContext: "https://some-custom-context-url.com/for/this/member"
        // add other domains if necessary
      },
      unset: ['default', 'customContext']
    },
    customPropertiesValues: {
      "customPropertiesSlug1": ["valeurSlug1", "valeurSlug2"],
      "customPropertiesSlug2": "valeur"
    } // optional
  };

  return jwt.sign(userData, privateKey, { algorithm: "HS256" });
}
```

{% endcode %}
{% endtab %}

{% tab title="Python" %}

```python
import jwt

private_key = "YOUR_PRIVATE_KEY"

def create_token(user):
    user_data = {
        'sub': user.id,  # Your own user ID
        'firstName': user.firstName,
        'lastName': user.lastName,
        'email': user.email,
        'title': user.title,         # optional
        'avatarUrl': user.avatarUrl, # optional
        'lang': user.lang,           # optional
        'timezone': user.timezone,   # optional
        'groups': {
            'join': ["groupId1", "groupId2"],  # optional list of groups to join
            'leave': ["groupId3"]              # optional list of groups to leave
        },
        'domains': {
            'set': {
                'default': "https://some-default-url.com/for/this/member",
                'customContext': "https://some-custom-context-url.com/for/this/member"
                # add other domains if necessary
            },
            'unset': ['default', 'customContext']
        },
        'customPropertiesValues': {
            "customPropertiesSlug1": ["valeurSlug1", "valeurSlug2"],
            "customPropertiesSlug2": "valeur"
        } # optional
    }
    token = jwt.encode(user_data, private_key, algorithm='HS256')
    return token
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php
use Firebase\JWT\JWT;

$privateKey = 'YOUR_PRIVATE_KEY';

function createToken($user) {
    global $privateKey;

    $userData = [
        'sub'                   => $user['id'],          // Your own user ID
        'firstName'             => $user['firstName'],
        'lastName'              => $user['lastName'],
        'email'                 => $user['email'],
        'title'                 => $user['title'] ?? null,      // optional
        'avatarUrl'             => $user['avatarUrl'] ?? null,  // optional
        'lang'                  => $user['lang'] ?? null,       // optional
        'timezone'              => $user['timezone'] ?? null,   // optional
        'groups'                => [
            'join'  => $user['groups']['join'] ?? [],
            'leave' => $user['groups']['leave'] ?? []
        ], // optional
        'domains'               => [
            'set'   => [
                'default'      => "https://some-default-url.com/for/this/member",
                'customContext'=> "https://some-custom-context-url.com/for/this/member",
                // add other domains if necessary
            ],
            'unset' => ['default', 'customContext']
        ], // optional
        'customPropertiesValues' => [
            "customPropertiesSlug1" => ["valeurSlug"],
            "customPropertiesSlug2" => "valeur"
        ] // optional
    ];

    return JWT::encode($userData, $privateKey, 'HS256');
}
?>
```

{% endtab %}

{% tab title="C#" %}

```csharp
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;

public class JwtTokenService
{
    private readonly string _privateKey;

    public JwtTokenService(string privateKey)
    {
        _privateKey = privateKey;
    }

    public string CreateToken(User user)
    {
        var claims = new List<Claim>
        {
            new Claim("sub", user.Id),
            new Claim("firstName", user.FirstName),
            new Claim("lastName", user.LastName),
            new Claim("email", user.Email)
        };

        if (!string.IsNullOrEmpty(user.Title))
            claims.Add(new Claim("title", user.Title));

        if (!string.IsNullOrEmpty(user.AvatarUrl))
            claims.Add(new Claim("avatarUrl", user.AvatarUrl));

        if (!string.IsNullOrEmpty(user.Lang))
            claims.Add(new Claim("lang", user.Lang));

        if (!string.IsNullOrEmpty(user.Timezone))
            claims.Add(new Claim("timezone", user.Timezone));

        if (user.Groups != null)
        {
            if (user.Groups.Join != null && user.Groups.Join.Length > 0)
                claims.Add(new Claim("groups.join", JsonConvert.SerializeObject(user.Groups.Join)));

            if (user.Groups.Leave != null && user.Groups.Leave.Length > 0)
                claims.Add(new Claim("groups.leave", JsonConvert.SerializeObject(user.Groups.Leave)));
        }

        if (user.Domains != null)
        {
            if (user.Domains.Set != null && user.Domains.Set.Count > 0)
                claims.Add(new Claim("domains.set", JsonConvert.SerializeObject(user.Domains.Set)));

            if (user.Domains.Unset != null && user.Domains.Unset.Length > 0)
                claims.Add(new Claim("domains.unset", JsonConvert.SerializeObject(user.Domains.Unset)));
        }
 
        if (user.CustomPropertiesValues != null)
        {
            foreach (var kvp in user.CustomPropertiesValues)
            {
                claims.Add(new Claim($"customPropertiesValues.{kvp.Key}", JsonConvert.SerializeObject(kvp.Value)));
            }
        }

        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.UTF8.GetBytes(_privateKey);
        
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(key), 
                SecurityAlgorithms.HmacSha256)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}

public class User
{
    public string Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Title { get; set; }
    public string AvatarUrl { get; set; }
    public string Lang { get; set; }
    public string Timezone { get; set; }
    public Groups Groups { get; set; }
    public Domains Domains { get; set; }
    public Dictionary<string, object> CustomPropertiesValues { get; set; }
}

public class Groups
{
    public string[] Join { get; set; }
    public string[] Leave { get; set; }
}

public class Domains
{
    public Dictionary<string, string> Set { get; set; }
    public string[] Unset { get; set; }
}
```

{% endtab %}
{% endtabs %}

You can use all the following fields in the JWT:

<table><thead><tr><th width="152">Parameter</th><th width="98">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>sub</code></td><td>string</td><td><p><strong>(required)</strong></p><p>The ID of the user in your product or platform. This value will be stored as an external ID in MeltingSpot. It should be 255 characters or less.</p></td></tr><tr><td><code>firstName</code></td><td>string</td><td><p><strong>(required)</strong></p><p>The first name of the user. It should be 255 characters or less.</p></td></tr><tr><td><code>lastName</code></td><td>string</td><td><p><strong>(required)</strong></p><p>The last name of the user. It should be 255 characters or less.</p></td></tr><tr><td><code>email</code></td><td>string</td><td><p><strong>(required)</strong></p><p>The email of the user. This email address is considered as a verified address. You should make sure you've verified it on your side. It should be 255 characters or less and follow a valid email address format.</p></td></tr><tr><td><code>title</code></td><td>string</td><td>The job title of the user in plain text format.</td></tr><tr><td><code>avatarUrl</code></td><td>string</td><td>A full URL to the user's profile picture. It should include https:// or http://. You should make sure it's a valid URL on your side.</td></tr><tr><td><code>lang</code></td><td>string</td><td><p>The default locale to apply in the application. Currently, MeltingSpot supports:</p><ul><li><code>en</code> for English</li><li><code>fr</code> for French</li><li><code>de</code> for Deutsch</li><li>If not specified or invalid, it will default to <code>en</code>.</li></ul></td></tr><tr><td><code>timezone</code></td><td>string</td><td>The default timezone to apply in the application. If not specified or invalid, it will default to <code>Europe/Paris</code>.</td></tr><tr><td><code>iat</code></td><td>number</td><td>The issue time of the JWT.</td></tr><tr><td><code>exp</code></td><td>number</td><td>The expiration time of the JWT. Although this value is not required, it's highly recommended to set it to 60 seconds from now. If not set, the token will be valid forever and can introduce security issues.</td></tr><tr><td><code>groups:join</code></td><td>string[]</td><td>The groups to which you want to add the member. Simply retrieve the id of the groups in question from the group selection menu of the audience table.</td></tr><tr><td><code>groups:leave</code></td><td>string[]</td><td>The groups whose members you wish to remove. Simply retrieve the id of the groups in question from the group selection menu in the audience table.</td></tr><tr><td><code>customPropertiesValues</code></td><td><em>property type</em></td><td>If you have custom properties, you can specify the value these properties will take on for the member.</td></tr><tr><td><code>domains: set</code></td><td>string[]</td><td>If you have embedded the Spot into several domains, specify which domains the member can have access to. They will be redirected to the <code>default</code> domain key when clicking on a notification email.</td></tr><tr><td><code>domains: unset</code></td><td>string[]</td><td>If you have embedded the Spot into several domains and specified member redirection domains, you can remove them via this parameter (URL of a software for which the member no longer has a license, website whose URL has changed...). You'll need to reuse the keys defined in the <code>set</code> object.</td></tr></tbody></table>

### **3. Redirection to MeltingSpot**

Once the user token has been created, you need to redirect the user to a URL by passing the token as a parameter. This URL is supplied to you as a parameter (`redirectUrl`) when the user lands on your authorization URL. Basically, it looks like this:

```url
https://go.meltingspot.io/spot/<Your Spot ID>/sso/jwt
```

You need to add the token as a parameter as follows:

```url
https://go.meltingspot.io/spot/<Your Spot ID>/sso/jwt?ms_token=<Your generated SSO Token>
```

In most cases, the redirect URL we provide (`redirectUrl`) also contains a referrerUrl parameter that returns the user to the page they were on when they logged in in SSO mode.

You can force the value of this parameter, but to be valid, the value must represent a path relative to `https://go.meltingspot.io` and your Spot (e.g. `?referrerUrl=/spot/129487c9-6acc-43d9-ab96-182ded763538/lives`).

{% hint style="info" %}
Your Spot ID is the string following spot/ in your Spot's url (eg: `go.meltingspot.io/spot/129487c9-6acc-43d9-ab96-182ded763538` -> Spot ID is `129487c9-6acc-43d9-ab96-182ded763538`).
{% endhint %}

### Embed / widgets + SSO = ❤️ <a href="#embed-widgets--sso" id="embed-widgets--sso"></a>

Once SSO authentication has been set up for your Spot, you can pass on the JWT token authenticating the user in the Spot embed or widget installation scripts. This will enable every user in your space to access the embed or widget with a connected status. When displaying your Spot in embed, your members will no longer have to click on 'Continue with SSO' to sign up or log in 😉

To do this, you need to add the `authToken` parameter to the embed or widget installation script parameters.

<figure><img src="https://3055204660-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FrzBxnWaSListMwx3rzKu%2Fuploads%2F9IFNwY0NNcvsu3KRe1ix%2Fimage.png?alt=media&#x26;token=b9d25b99-c335-4de8-9d3c-3934f576ee75" alt="SSO authentication in embed and widgets script" width="563"><figcaption></figcaption></figure>

## Good to know

* **What happens when the user logs in?**\
  If the user does not exist, we will create the user using the provided information in the JWT and log him in. If the user exists, it will only log him in, without updating his information.
* **What happens to existing users when I activate the SSO on my Spot?**

  If you activate the SSO, your existing members can still connect using a password. They will be able to use both authentication methods (SSO or email + password) as long as they use the same email. Both authentication methods will be attached to the same user.
* **What if members join my private Spot through SSO?**\
  Members who register to your Spot through SSO are automatically `accepted`, even if your Spot is private. You can always deactivate them later.
* **What happens if my user updates his email address on my app?**\
  The next time on of your users logs in to your Spot, we will consider the user to be a new member. If the user wants to connect to their account attached to their former email, the user will have to authenticate through login + password.
* **Can I decide on which page of my Spot a member lands after logging in with SSO?**

  Yes, when your users access your Spot from your application, you can use the "referrerUrl" parameter to send them to any page in your Spot.
* **How are handled members' status (Invited / Pending / Declined / Rejected / Deactivated / Left) when they join / reconnect to a Spot with SSO?**\
  [-> Check it here!](https://help.meltingspot.io/english/audience/members/members-status#member-status-update-rules-on-registration-or-connection)
