Cookie Authentication should be used when you have an existing login system which you want to use for the community, and you want your users to be able to sign in once and be logged in to all your websites, rather than having to re-authenticate which each website individually. For example, your main website may already have a login system, and you want users to be logged in to the community automatically when they log in to your main website (and vice versa).
When Cookie SSO is enabled, the following things happen:
- The Login & Registration urls for the community are redirected to point to the external authentication system
- When the external login system logs a user in, a cookie is set that allows the community to log the user in, and create their account if it doesn't exist in the community.
[toc]
How Cookie SSO Works
Cookie SSO passes a secure message about the logged in user from your external system to Telligent Community through an HTTP Cookie. This message contains encrypted information about the user's session, as well as other data that Telligent Community can use to validate the authenticity and integrity of the message.
When a user logs in to your main single sign on system, it will create a cookie containing the data about a user's session. When the user visits your Telligent Community, Telligent Community will read the session data from the main single sign on system. If a user with the given username exists, then that user is logged in. If the user does not exist, then a new user is created using the specified session data, then that user is logged in.
Limitations of Cookie SSO
For Cookie SSO to work, the cookie needs to be shared between your external authentication system, and the community. Due to the how browsers allow cookies to be shared, this means that your community and your login page must have a common ancestor that isn't a top level domain. For example login.mycompany.com
& community.mycompany.com
have a common ancestor of mycompany.com
and so can be used with Cookie SSO. mycompany.com
and mycommunity.com
only have a common ancestor of .com
, which is a top level domain.
Initial Setup
Before you can use Cookie SSO, you need to decide on the cookie encryption mode, and configure Telligent Community to accept the cookie for SSO.
There are two modes that can be used to generate the cookie
- AES-HMAC - This mode encrypts the session data with AES in CBC mode, then computes an HMAC-SHA256 hash of the IV and encrypted session data to validate the integrity of the message
- AES-GCM - This mode uses Authenticated AES in GCM mode, which both encrypts the session data, and generates an authentication tag in a single operation.
Both modes provide confidentiality, authentication & integrity assurances on the cookie.
Once you have chosen the authentication approach, you need to generate the encryption keys to use:
- AES-HMAC - the key MUST be 128, 192 or 256 bits, and the HMAC key MUST be 256 bits.
- AES-GCM - a 256-bit encryption key MUST be used. An HMAC Key is not needed.
To enable the use of the cookie for SSO, go to Administration > Authentication > Authentication Methods > Cookie Authentication Single-Sign-On Client, enable this functionality by checking the Enabled box, and configure the cookie Options and Encryption Options to meet the configuration of your SSO cookie. Once configured, click Save to enable the reading and processing of SSO cookies within Telligent Community.
Generating the SSO Cookie
1. Create Session Data
The session data in a single sign on cookie consists of a set of key value pairs describing the session of the user to log in.
Name | Key | Type | Description | Required |
---|---|---|---|---|
Username | username |
string |
The username of the user to login or create. This username MUST be unique, and SHOULD NOT be an email address or contain any sensitive information (see Privacy section for more details) | Yes |
emailAddress |
string |
The email address for the user. This email address MUST be unique. | Yes | |
Expiry Date | expiryDate |
DateTime |
The date at which the authentication cookie becomes invalid. (see Expiry Date vs Expires below for more details) | Yes |
Roles | roles |
string[] |
A comma separated list of roles to add the user to. A user will always be additionally added to "Everyone" and "Registered Users" roles. | No |
Display Name | commonname |
string |
A friendly name that may be displayed instead of a username. Unlike username, Display Names may be duplicated. | No |
2. Serialize the Session Data
The session data need to be serialized as a multi-valued cookie (according to RFC 2109) to produce the plaintext session data.
username=JSmith&emailAddress=john.smith@example.org
3. Encrypt the session data
For each cookie generated, generate a unique Initialisation Vector (IV). For AES-GCM, this SHOULD be a 96-bit value, for AES-HMAC, it MUST be the same length as the encryption key you used. Use this along with your chosen encryption algorithm to encrypt the encoded session data producing the ciphertext.
private byte[] EncryptSessionData(byte[] encryptionKey, byte[] iv, string serializedSessionData) { using (var aes = new AesCryptoServiceProvider()) { aes.Mode = CipherMode.CBS; using (var encryptor = aes.CreateEncryptor(encryptionKey, iv)) { byte[] plainTextBytes = Encoding.UTF8.GetBytes(serializedSessionData); using (var memoryStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); cryptoStream.FlushFinalBlock(); } return memoryStream.ToArray(); } } } }
4. Generate Authentication Data
When using AES in GCM Mode, this is the Authentication Tag produced by the AES GCM algorithm. When using AES in CBC mode, then the MAC is the SHA256 encrypted hash of the concatenation of the IV byte array, followed by the cipher text byte array. This value is used by Telligent Community when decoding the cookie to authenticate the cookie.
5. Create the cookie
Encode the IV, MAC and CipherText into a single cookie by base64 encoidng each value on their own, then concatenating these values, delimited by a '$'
{Base64Encode(IV)}${Base64Encode(MAC)}${Base64Encode(CipherText)}
6. Send the cookie to the client
Set-Cookie: AuthenticatedUser=6oX6iPtc7K0t6rxqj/smOQ==$caVgfxncWPSWynh/+ODlLlkBLGR7neFs5zJT3VMfxYk=$RosxFm0ZaVG3tMoV2zDfjEoxnjuOIyVc+ymrennvfJxUbJ7PwVwvMjOOV4JR96Y70HEZPSs+nboOOBEzVNWF/g==; HttpOnly
Sample Data
The following values are an example of how an example cookie is built up. These values can be used to test any cookie generation code you write. (Any binary data is shown as a Base64 encoded string)
AES CBC + HMAC:
- Encryption Key (base64):
FFhrYY4xw9Y/xRKE7eS4jV/2YaPbpt7ryvjJ1E8SwV0=
- HMAC Key (base64):
NNeWjU+i4/V9lkVhIRoWY3CfxBy7nmU3okSD/9fBqnScP8DbdY7elgow0xi3LDyQWMd795gnL+2v+ZHpYUJlMg==
- Plaintext (string):
username=example&emailAddress=example@example.org
- Initialisation Vector (base64):
6oX6iPtc7K0t6rxqj/smOQ==
- CipherText:
RosxFm0ZaVG3tMoV2zDfjEoxnjuOIyVc+ymrennvfJxUbJ7PwVwvMjOOV4JR96Y70HEZPSs+nboOOBEzVNWF/g==
- HMAC (Base64):
caVgfxncWPSWynh/+ODlLlkBLGR7neFs5zJT3VMfxYk=
- Cookie Content (string):
6oX6iPtc7K0t6rxqj/smOQ==$caVgfxncWPSWynh/+ODlLlkBLGR7neFs5zJT3VMfxYk=$RosxFm0ZaVG3tMoV2zDfjEoxnjuOIyVc+ymrennvfJxUbJ7PwVwvMjOOV4JR96Y70HEZPSs+nboOOBEzVNWF/g==
AES GCM:
- Encryption Key (base64):
FFhrYY4xw9Y/xRKE7eS4jV/2YaPbpt7ryvjJ1E8SwV0=
- HMAC Key: (Not Used)
- Plaintext (string):
username=example&emailAddress=example@example.org
- Initialisation Vector (base64):
yEKcjquPkAF+7GeQ
- CipherText (base64):
+BW+eTnnzezORFMZAwPVdmzDlWl1A8i1Ak+tfv3iMM+NCyPTZViowjF17DaBdcCdVQ==
- MAC (base64):
aahmltkpzeIQRytPxDO7ZA==
- Cookie Content (string):
EKcjquPkAF+7GeQ$aahmltkpzeIQRytPxDO7ZA==$+BW+eTnnzezORFMZAwPVdmzDlWl1A8i1Ak+tfv3iMM+NCyPTZViowjF17DaBdcCdVQ==
Sample C# Code
AES HMAC
using System; using System.Collections.Specialized; using System.IO; using System.Security.Cryptography; using System.Text; public sealed class AesHmacCookieGenerator : IDisposable { private AesCryptoServiceProvider _aes; private readonly byte[] _hmacKey; public AesHmacCookieGenerator(byte[] encryptionKey, byte[] hmacKey) { _hmacKey = hmacKey; _aes = new AesCryptoServiceProvider { Mode = CipherMode.CBC, Key = encryptionKey }; } public string GenerateCookieContent(NameValueCollection sessionData, byte[] iv) { var encodedSessionData = EncodeSessionData(sessionData); byte[] cipherText = EncryptSessionData(encodedSessionData, iv); byte[] mac = ComputeHMAC(cipherText, iv); return EncodeCookie(iv, mac, cipherText); } private static string EncodeSessionData(NameValueCollection sessionData) { var builder = new StringBuilder(); for (int i = 0; i < sessionData.Count; i++) { if (i > 0) builder.Append('&'); builder.Append(sessionData.Keys[i]); builder.Append('='); builder.Append(sessionData[i]); } return builder.ToString(); } private static string EncodeCookie(byte[] iv, byte[] mac, byte[] cipherText) { return Convert.ToBase64String(iv) + "$" + Convert.ToBase64String(mac) + "$" + Convert.ToBase64String(cipherText); } private byte[] EncryptSessionData(string encodedSessionData, byte[] iv) { byte[] plainTextBytes = Encoding.UTF8.GetBytes(encodedSessionData); using (MemoryStream memoryStream = new MemoryStream()) { using (ICryptoTransform encryptor = _aes.CreateEncryptor(_aes.Key, iv)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); } return memoryStream.ToArray(); } } } private byte[] ComputeHMAC(byte[] ciphertext, byte[] iv) { byte[] messageToMac = new byte[iv.Length + ciphertext.Length]; Array.Copy(iv, 0, messageToMac, 0, iv.Length); Array.Copy(ciphertext, 0, messageToMac, iv.Length, ciphertext.Length); using (HMACSHA256 hmacsha256 = new HMACSHA256(_hmacKey)) { return hmacsha256.ComputeHash(messageToMac); } } public void Dispose() { if (_aes != null) { _aes.Dispose(); _aes = null; } } }
AES GCM
using System; using System.Collections.Specialized; using System.IO; using System.Security.Cryptography; using System.Text; using Security.Cryptography; public sealed class AesGcmCookieGenerator : IDisposable { private AuthenticatedAesCng _aes; public AesGcmCookieGenerator(byte[] encryptionKey) { _aes = new AuthenticatedAesCng(); _aes.Key = encryptionKey; } public string GenerateCookieContent(NameValueCollection sessionData, byte[] iv) { var encodedSessionData = EncodeSessionData(sessionData); byte[] cipherText; byte[] mac; EncryptAndGenerateMac(encodedSessionData, iv, out cipherText, out mac); return EncodeCookie(iv, mac, cipherText); } private static string EncodeSessionData(NameValueCollection sessionData) { var builder = new StringBuilder(); for (int i = 0; i < sessionData.Count; i++) { if (i > 0) builder.Append('&'); builder.Append(sessionData.Keys[i]); builder.Append('='); builder.Append((sessionData[i])); } return builder.ToString(); } private static string EncodeCookie(byte[] iv, byte[] mac, byte[] cipherText) { return Convert.ToBase64String(iv) + "$" + Convert.ToBase64String(mac) + "$" + Convert.ToBase64String(cipherText); } private void EncryptAndGenerateMac(string encodedSessionData, byte[] iv, out byte[] cipherText, out byte[] mac) { byte[] plainTextBytes = Encoding.UTF8.GetBytes(encodedSessionData); using (MemoryStream memoryStream = new MemoryStream()) { using (IAuthenticatedCryptoTransform encryptor = _aes.CreateAuthenticatedEncryptor(_aes.Key, iv)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); } mac = encryptor.GetTag(); cipherText = memoryStream.ToArray(); } } } public void Dispose() { if (_aes != null) { _aes.Dispose(); _aes = null; } } }
Troubleshooting
If Telligent Community fails to process the SSO cookie, it will log an error and delete the cookie to prevent further validation failures. To view these errors, go to Administration > Monitoring > Exceptions. Filter the Exception Type to "Validation Error".
If you lose access to the Control Panel (e.g. because you can no longer log in due to Cookie SSO configuration issues), you can use an emergency access page to get back in to your community. You MUST access this page from the server itself. This page can be found at ~/controlpanel/localaccess.aspx
, and will allow you to log in with the credentials you used prior to enabling the Cookie SSO plugin.
Security & Privacy Considerations of using Cookie SSO
Using Email Addresses as Usernames
You SHOULD NOT use email addresses as usernames. Usernames are used as the visible unique identifier of a user in a community, so if using an email address in stead of the username will cause the user's email address to be publicly exposed. If your existing system uses an email address as a username, then you should look at using something else for the username - for example an integer, a UUID identifier, or have users explicitly create their usernames upon first sign-in to the community.
Expiry Date vs Expires
To ensure security of your cookie, you MUST always set the Expiry Date session data value. If you want your authentication cookie to last beyond the user closing their browser then you MAY additionally set the Expires cookie attribute to the same value as the Expiry Date. You MUST NOT set the Expires attribute in place of the Expiry Date session data value. The Expires cookie attribute simply instructs the browser to delete the cookie when the expiration time is hit. If for any reason the browser ignores the expiration time, or an attacker manages to steal the cookie, then they can continue to use the cookie beyond the expiration time. The Expiry Date session data value combats this - if a cookie is recieved that has passed it's expiration date, it is considered invalid.
Generating an Initalisation Vector in GCM Mode
When using AES in GCM mode, you MUST pay very careful attention to how you generate the IV - in particular ensuring that every single IV used is unique. Due to the birthday paradox, randomly generating an IV does not provide a sufficiently strong guarantee of uniqueness. For more details on the problem, and recommended approaches for generating the IV, refer to sections 8, 8.2 and Appendix A of NIST Special Publication 800-38D.
Cookie attributes
When creating the cookie, you SHOULD consider set the following attributes on the cookie to maximise the security of the cookie
- HttpOnly - You should mark the cookie as HttpOnly to improve the security of the authentication cookie. This attribute instructs the browser to prevent hte cookie from being read from JavaScript. See https://www.owasp.org/index.php/HttpOnly for more details.
- Secure - if both your login page and community are only accessible over SSL, or you only want the login cookie to be sent over SSL, mark the cookie as Secure so it's never sent over non-SSL connections. See https://www.owasp.org/index.php/SecureFlag for more details.
- Domain - When the single sign-on page and Telligent Community are on different domains, the cookie domain MUST be set to ensure the cookie can be sent to Telligent Community. This domain SHOULD be set to the closest common ancestor shared by the two domains. (For example if your single sign-on page is on
example.com
, and the community is atcommunity.example.com
, then the cookie domain should be set to "example.com
"). When sharing a cookie across multiple subdomains, make sure you do not put a leading dot in front of the domain name (e.g., ".example.com
") - having a leading dot in front of the domain will cause the cookie to be visible only on that exact domain, and not its children. - Expires - Specifies the time at which the browser should consider the cookie invalid and delete it. If expires isn't set, then the browser will keep the cookie until the browser session is closed.
- SameSite - This is an experimental attribute, currently implemented in Chrome, only that prevents the cookie from being sent in cross domain requests, which can protect against some forms of CSRF attacks - https://tools.ietf.org/html/draft-west-first-party-cookies-07 .