The SNS launch protocol enables portal applications to integrate external applications like tools, games and treatments seamlessly into their platform. The SNS launch protocol connects applications like tools, games and treatment to a portal like environment. The portal, or consumer in this context, is the system that has an active session with an authenticated user in the system. The consumer prepares a launch by creating a JWT token that contains all the launch details needed for the producer to function properly. The producer in this context is the application that delivers functionality to the user in the portal, either in the context of the portal as an iframe, or in its own context. The producer of the launch receives the JWT token and unpacks the information in the token to identify the user and the target treatment and launches a new session for the user.
As an extension to the basic version of the protocol as described above, the producer and consumer are able to communicate directly within the session of the user in order to exchange additional information or register progress and outcomes. The concept of these services are service profiles, each consumer and producer can implement and agree on the usage of various profiles that extend the basic usage.
The SNS Launch protocol is highly inspired by the Learner Tool Interoperability (LTI) which has had a tremendous impact on the relation between learner management systems and tool providers. LTI has simplified the integration of external tools into learner management systems, the whole landscape of tool providers has emerged. The key concepts the LTI being successful has been:
The SNS launch standard applies these concepts when it comes to defining a successful launch protocol. The key differences are:
The SNS Launch protocol has the following goals.
The protocol should be easy to implement, hours instead of days, and days instead of weeks. It does so by standing on the back of giants; that is make use of existing technologies and standards.
The protocol should be easy to configure from both the producer and consumers' side. In the essence, an exchange of endpoint URL and public key pair should be sufficient.
The architecture should not rely on external or central services and should be point-to-point in the sense that parties should be able to connect without relying on other parties and scale infinitely.
The protocol should mitigate against most common attacks by aligning to pre-existing proven technologies like JWT.
The protocol should support anonymous identities and be reluctant to disseminate personal information.
This guide describes how to implement the SNS Launch protocol. The protocol consists of:
The core procedure of the launch looks as follows:
request
.The form-post-redirect message In step 5) in the communication protocol, the user is redirected to the producer with the JWT message using a post form. The form needs to:
Appendix D contains a reference implementation of such a page.
The message makes use of the JSON Web Token (JWT) standard. Implementations in various languages are widely available. The concept of a JWT token is it consists of a header containing metadata of the token, a body or payload that consists of a set of required fields, and a signature that should be validated.
The JWT message consists out of the following fields, the fields with an asterix (*) are required.
Description | Field | Value |
---|---|---|
User identity* | sub | User unique identification, see format for details. |
User email | User email | |
First name | given_name | User first name |
Middle name | middle_name | User middle name |
Last name | family_name | User last name |
Subject* | resource_id | Identification of the target treatment |
Issuer* | iss | URL base of the the consumer (aka the sending party) |
Audience* | aud | URL base of the producer (aka the receiving party). |
Unique message id* | jti | UUID or anything else that makes the message unique |
Issue time | iat | Timestamp from the time of creation |
Expiration time* | exp | Timestamp for the expiration time |
Public / Private key* | - | Signing private key, public key for validation. |
The format for the user identity is an urn. This identifier is prefixed urn:sns:user, subsequently the reverse domain of the identity platform and finally the user identity. The format is as follows:
urn:sns:user:<domain>:<user>
For example:
urn:sns:user:nl.issuer:123456
{
"alg": "RS256",
"typ": "JWT"
}
{
"sub": "urn:sns:user:nl.issuer:123456",
"aud": "audience.nl",
"iss": "issuer.nl",
"resource_id": "paniek",
"last_name": "Vries",
"middle_name": "de",
"exp": 1550663222,
"iat": 1550662922,
"first_name": "Klaas",
"jti": "a5d155b2-d8b4-43bb-8730-1646ae35357c",
"email": "klaas@devries.nl"
}
Field | Remark | Scope |
---|---|---|
Application URL | The endpoint of the producer application. | Per application |
Public / Private key | The public / private keys | Preferably per application |
Note that SamenBeter expects links to open in a new tab.
Field | Remark | Scope |
---|---|---|
Consumer public key | The key to validate the consumer JWT message with. | Per consumer, based on the iss field value. |
Test key and secret, please never use outside a test context.
Type: RSA Length: 2024
MIIBHjANBgkqhkiG9w0BAQEFAAOCAQsAMIIBBgKB/gC+0zqjfI2zKvvjwUwE4JiLYyUqazpxWD+hmyLCEXgzfbHIWvwRD54M8PJqCt+9Iq3PBIvpZoJezQ5rztEWN6OI7qoXq4ygZ4YTXGU+ErfqLlvyMv/PfbuHU7oRS+4W0iq2mPwQQXSKMDJz4qSORa75p6xMMHd38xJgHQ6tBwPFMbwhpGsGpCFpxRqlMR735D8gRbhFbSexxMhbyqpQTro0u6xPFoAecldiCJ8KNlp2/NNcRgMZKVIU3rwhp52JcnI90by8UZoD0ItlRoXdaBmmQORWRrm2SC1rRu+KFidzjxe2cRiFVXqthqe1Ttm29atUeVftJhEgb7UpxKJPAgMBAAE=
Type: RSA Length: 2024
MIIEpwIBADANBgkqhkiG9w0BAQEFAASCBJEwggSNAgEAAoH+AL7TOqN8jbMq++PBTATgmItjJSprOnFYP6GbIsIReDN9scha/BEPngzw8moK370irc8Ei+lmgl7NDmvO0RY3o4juqherjKBnhhNcZT4St+ouW/Iy/899u4dTuhFL7hbSKraY/BBBdIowMnPipI5FrvmnrEwwd3fzEmAdDq0HA8UxvCGkawakIWnFGqUxHvfkPyBFuEVtJ7HEyFvKqlBOujS7rE8WgB5yV2IInwo2Wnb801xGAxkpUhTevCGnnYlycj3RvLxRmgPQi2VGhd1oGaZA5FZGubZILWtG74oWJ3OPF7ZxGIVVeq2Gp7VO2bb1q1R5V+0mESBvtSnEok8CAwEAAQKB/VO7cg6Mt8y3fsHIbqfxOV5oScWcOY/Erl8mKJFJgxns/JayvcpqtOpuy6AWV2ixj9y33QC0V15r0fkiTgLWtS5/sykhwFoeMunJ8C7VndfnMbdMA42zWRcfeRTf4YAoBlALPwePASklzu2ktJotH4MyvNrNpY5/nT+JYIgx/LxhIwk/HxJ6uVYiFpAINfAGfBphcgxzKWnV23WvRYtrIJc/XXLvSxK08tvoZfm4c4quf1i3LpTc+1mZmT+jefZoXQcWUnEbCk5Q/8gvDigHMbdOlTqT4/iNj/03PmueWsljiyhbXDYOVGJCaGQpeNaFnhilXPrYEBkAvXIOg6ECfw7l7td0wyPP0vCYFcbQEr3qng9vg2ISVas8gIOU/OeKNSJ9+wbKWcd0DAztxGShuqDZjBXj+RSEL1XrABjDpk9RqpgkBx3NNXEbCBnYg3+LU8HCtUBWi5amaJi8JH2839cVXjdZbPXBPmp5S93SKjmuoiBas8oKITh0yEwwdb8CfwzPAeg765BhD4AmwSzoQRy6Sfxf6R0Z8Uo9a2mxBiGSKPvX7zQMG384208FvTlaW3UoOAhSN6HsfBwWT9pzRIaWAkFP8CWxRiRqzg20FYzTweQZOnqje6YRYSocX64l22zhqV3Y3DdqevIiGpxDFqFM8QXeaAcchCvg6LpTl3ECfwqlC1RynwM1eLhjUhvti5aazjilKrCl/QQOhJx/lXwyaeitLvEZH7C9H+cU8+AbFmfbSJZTfyLDl7bB5B3NnUTLSyLNizAl8WtRLyaYZsx41m15G1xO+gm3+MA4nbIhg6YAJINTp+CoJFqbNDPX+EeimUCYziErv7TA7GRTs60Cfws28F+KnzzBjtXQmNCd5eymOwNKYovFXBt5XWOjyE96boHa1ahHdYfVm0c8KipeL7eLaEv42JbgvOXGr1IAHJ6OFxliSUxnQ5e9H/6ljzzHZ3s0j5wzKZ8EloNNZoTOxqk1h5oQtveaNl1seMoaf2TpPhq6WXDoidz1Ri9l4zECfmzg4k6Jo2YpZVAm1xQU5SPYDawH4DNlWeTMnqBEwfZap7wu79zJkZYdCaegzabb/FxFSu0+21djZbq4+PdtsxIqmg8pObu2s7z+BqC0iM5z01deygAfgP4NRzmQqvECiDmjKWxXZlzQNPxnlu3MJZMrfDXTSzDeIBph1YOIag==
Near future developments will consists of the following
The SNS launch producers can test with the following tool: sns-consumer.edia-tst.eu The tools allows to send a SNS Launch request to a given endpoint, and makes use of the test key and secret as provided in Appendix A.
This tool has been build by EDIA to easily test and mimic a launch. Want to test it? From the left side (User details), for example:
The tool consumer can use as endpoint the following: sns-producer.edia-tst.eu/validate.html
<!doctype html>
<html lang="nl">
<head>
<title>SNS - Bevestiging delen persoonsgegevens</title>
<!-- Compiled and minified CSS, Replace with self hosted file in production. -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Compiled and minified JavaScript, Replace with self hosted file in production. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
</head>
<body>
<form action="ACTION" id="form" method="post">
<input name="request" type="hidden" value="JWT_TOKEN"/>
</form>
<div class="container" id="confirm" style="display: none">
<h2>De volgende informatie wordt gedeeld met {aud}</h2>
<table class="striped">
<thead>
<tr>
<th style="width: 25%">Veld</th>
<th style="width: 75%">Waarde</th>
</tr>
</thead>
<tbody>
<tr id="first_name">
<td>Voornaam</td>
<td>{first_name}</td>
</tr>
<tr id="last_name">
<td>Achternaam</td>
<td>{last_name}</td>
</tr>
<tr id="email">
<td>E-mail</td>
<td>{email}</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td style="width: 25%">Ik wil deze melding niet meer zien:</td>
<td style="width: 75%">
<!-- Switch -->
<div class="switch" id="futureSuppress">
<label>
Nee <input type="checkbox"> <span class="lever"></span> Ja
</label>
</div>
</td>
</tr>
</tbody>
</table>
<div class="section">
<button id="cancel" class="waves-effect waves-teal btn-flat">Annuleren<i class="material-icons right">cancel</i>
</button>
<button id="proceed" class="waves-effect waves-light btn">Akkoord <i class="material-icons right">send</i>
</button>
</div>
</div>
</body>
<script>
var aud;
/**
* The parts of the JWT body that is considered to be personal info.
*/
var userParts = ['first_name', 'last_name', 'email'];
/**
* Utility function that replaces {keys} in text nodes of a
* element and its children.
*/
var replaceInTextNodes = function (element, seach_and_replace) {
var nodeValue = element.nodeValue;
if (nodeValue) {
for (var key in seach_and_replace) {
if (seach_and_replace.hasOwnProperty(key)) {
nodeValue = nodeValue.replace('{' + key + '}', seach_and_replace[key]);
}
}
element.nodeValue = nodeValue;
}
for (var i in element.childNodes) {
var child = element.childNodes[i];
if (child) {
replaceInTextNodes(child, seach_and_replace);
}
}
};
/**
* Reads the jwt and returns the body part as object.
*/
var readJwtToken = function (jwt) {
var parts = jwt.split('.');
return JSON.parse(atob(parts[1]))
};
/**
* This function parses the JWT and looks for personal info.
* If found, it will populate the form with the information and
* the form will display.
*
* @returns {boolean} true if the JWT has personal info.
*/
var parseAndReadJwt = function () {
var request_el = document.getElementsByName('request');
if (request_el.length > 0) {
var value = request_el[0].getAttribute('value');
var jwt = readJwtToken(value);
var elements = {};
for (var i in userParts) {
if (jwt[userParts[i]]) {
elements[userParts[i]] = jwt[userParts[i]];
} else {
document.getElementById(userParts[i]).style.display = 'none'
}
}
// Set the top variable aud to the JWT audience.
aud = jwt.aud;
if (getSuppressCookie()) {
return false;
}
if (Object.keys(elements)) {
elements.aud = jwt.aud;
var form = document.getElementById('confirm');
replaceInTextNodes(form, elements);
form.style.display = ''
}
}
// Check if the JWT actually had personal info.
return Object.keys(elements).length !== 0
};
/**
* The cancel button function.
*/
var cancel = function () {
window.history.back();
};
/**
* The proceed button function.
*/
var proceed = function () {
document.getElementById('form').submit();
};
var getCookieName = function () {
var base64 = btoa(aud || "unk");
base64 = base64.replace(/=+/g, '');
return "sc_" + base64;
};
/**
* Gets a suppress consent cookie
*/
var getSuppressCookie = function () {
// Generate the cookie name based on the base64 string of the domain.
var name = getCookieName();
// Read the cookie
var decodedCookies = decodeURIComponent(document.cookie).split('; ');
for (var i in decodedCookies) {
var cookieValue = decodedCookies[i].split('=');
var cookie = cookieValue[0];
var value = cookieValue[1];
if (cookie === name) {
return value === 'true';
}
}
return false;
}
;
/**
* Add a suppress consent cookie, valid for one year and specific to the domain.
*/
var addSuppressCookie = function () {
// Generate the cookie name based on the base64 string of the domain.
var name = getCookieName();
var expiry = new Date();
// Set expiry to a year
expiry.setTime(expiry.getTime() + (356 * 24 * 60 * 60 * 1000));
document.cookie = name + '=true;expires=' + expiry.toUTCString();
};
/**
* Remove the suppress consent cookie.
*/
var removeSuppressCookie = function () {
// Generate the cookie name based on the base64 string of the domain.
var name = getCookieName();
var expiry = new Date();
expiry.setTime(0);
document.cookie = name + '=false;expires=' + expiry.toUTCString();
};
/**
* Initializes the event listeners for this page.
*/
var initListeners = function () {
document.getElementById('futureSuppress').addEventListener('change', function (event) {
if (event.target) {
if (event.target.checked) {
addSuppressCookie();
} else {
removeSuppressCookie();
}
}
});
document.getElementById('cancel').addEventListener('click', function () {
cancel();
});
document.getElementById('proceed').addEventListener('click', function () {
proceed();
});
};
/**
* The onload handler.
*/
window.onload = function () {
// Check if there is a need to render the form. There are two conditions
// on which the form should not be rendered:
// 1) There is no personal data in the cookie.
// 2) The user has opted in for consent.
if (parseAndReadJwt()) {
initListeners();
} else {
document.forms[0].submit();
}
};
</script>
</html>
There are several code examples in different programming language demonstrating how to implement SNS Launch protocol:
language | GitHub repository |
---|---|
Java | OpenSNS-BeterMetElkaar-SSOLaunchSamenBeter-Java |
Python | OpenSNS-BeterMetElkaar-SSOLaunchSamenBeter-Python |
PHP | OpenSNS-BeterMetElkaar-SSOLaunchSamenBeter-PHP |