This article will explain how to use Thinkific's SSO (Single Sign On) feature to sign into your Thinkific school from your website.
ATTENTION: This article is no longer up to date, and should be automatically redirecting you to a more current article in our Developer-specific documentation. If for some reason you are not redirected, please use this link to see the most recent version of this article.
In this article:
About This Feature
SSO, or Single Sign On, is a feature that allows a user to sign into an external website and a Thinkific school in one action. A demonstration of this feature is available here.
This is a Plus plan feature that should only be implemented by those with access to software development resources. If you are not familiar with SSO, it is likely not required for your situation
How It Works
First a user will sign into your external site. Once they have been authenticated, your application will construct a token (JWT) and redirect to Thinkific’s SSO URL with this token as a query string parameter. Thinkific deconstructs this payload and either finds the user and signs them in, or if they don’t already have an account on your Thinkific site, creates an account and signs them in.
The Thinkific SSO URL
The Thinkific SSO URL is the URL on your Thinkific school that you redirect to after a user has successfully authenticated in your system. It has the following structure:
https://{your-school}.thinkific.com/api/sso/v2/sso/jwt?jwt={payload}&return_to={url to return to}&error_url={url to redirect to in the case of an error}
- The jwt parameter is the JWT payload that you construct and is REQUIRED.
- The return_to is the url that you want the user to be redirected to after signing in to your Thinkific school. It is OPTIONAL.
- The error_url is the url that you want the user to be redirected to in the case of an error. This is OPTIONAL.
- If the return_to url is not supplied, the user will be redirected to their default page within your Thinkific school.
- If the error_url is not supplied, the user will be redirected to the return_to url that you supplied OR, if no return_url is supplied, to a generic Thinkific error page.
The JWT Payload
Your application must construct the JWT payload and sign it using your Thinkific API key. The key can be found under the API section of your Thinkific admin dashboard, under Settings > Code & analytics. The best practice for security is to generate the JWT token on a server and then retrieve it from the client side, rather than exposing the JWT shared secret in the client-side JavaScript.
Note: Do not base 64 encode your API key when signing your JWT token. Our application expects the API key as-is when verifying your JWT token.
The JWT payload is typically constructed as a hash. The following attributes are supported:
[{ "first_name": "Thinkific", "last_name": "Admin", "email": "thinkific@thinkific.com", "iat": 1520875725 "external_id": "thinkific@thinkific.com",
"bio": "Mostly harmless", "company": "Thinkific", "timezone": "America/Los_Angeles", }]
- email (required) - the email of the authenticated user. If external_id is not supplied, email will be used as the unique identifier.
- first_name (required)- the first name of the authenticated user.
- last_name (required)- the last name of the authenticated user.
- iat (required) - must be the number of seconds since UNIX epoch. This is essentially the time that the JWT payload was issued.
- external_id - an identifier for the authenticated user. This is typically the id of the user in your system. This is OPTIONAL, but if supplied will be used as the unique identifier of the user. What this means is that when your student is trying to log in and you have included external_id in the payload, the Thinkific database will look for a user with that external_id. If that user is not found, the system will attempt to create them.
- bio - a textual bio of the user. This is OPTIONAL.
- company - the user's company. This is OPTIONAL.
- timezone - the user's timezone abbreviation (as defined here: https://www.iana.org/time-zones). This is OPTIONAL.
There are various libraries for working with JWT. A list can be found here. We recommend using the HS256 algorithm.
One thing to be aware of is that the JWT payload is merely encoded and signed, not encrypted, so don't put any sensitive data in the hash table. JWT works by serializing the JSON that is being transmitted to a string. It then base 64 encodes that string and then makes an HMAC of the base 64 string which depends on the shared secret. This produces a signature that the recipient side can use to validate the user.
The return_to parameter
After a user is authenticated in your system, it should redirect to your Thinkific school's SSO URL. As part of the redirect URL, you can append a return_to query string parameter that is an encoded URL that the user will be returned to after the sign in to your Thinkific school is complete. For example:
https://{your-school}.thinkific.com/api/sso/v2/sso/jwt?jwt={payload}&return_to=https%3A%2F%2Fyoursite.com
The error_url parameter
When attempting to use the SSO, errors can occur. For example, the user's email address may be improperly formatted. In these cases, you may want the user to be redirected to an error page of your choosing. For example:
https://{your-school}.thinkific.com/api/sso/v2/sso/jwt?jwt={payload}&return_to=https%3A%2F%2Fyoursite.com&error_url=https%3A%2F%2Fyoursite.com/sso_error
Error Handling
If an error occurs, the user will not be signed into your Thinkific school. There are several cases when this might occur:
- Your code does not provide the JWT payload
- Your JWT payload is not correctly signed
- Your JWT payload is expired - this is based on the age of the IAT parameter. Thinkific allows a 120 second leeway of accuracy to account for things like clock skew.
- A validation error occurs when creating a new user in your Thinkific school (if one of the required parameters is missing, for example).
- Some other unforeseen exception occurs.
If an error does occur, and you have provided a return_to parameter, Thinkific's SSO will not sign the user in and will redirect to the supplied return_to URL with 2 parameters:
- kind - the type of error (currently one of 6 possibilities: jwt, validation, expired_token, invalid_iat and unspecified)
- message - the error message
If an error does occur, and you have provided an error_url parameter, Thinkific's SSO will not sign the user in and will redirect to the supplied error_url with the same parameters as above.
- The error_url is used preferentially to the return_to url if both are provided
If an error occurs and you have not provided a return_to parameter, Thinkific's SSO will not sign the user in and will display the error message on the default page of your Thinkific school.
Error Messages
MESSAGE: kind=jwt&message=Signature+verification+raised
If you see this error message, make sure to use the unique API key for the Thinkific site instead of "API_KEY".
Customizing Your Site Theme
Often when using SSO, you'll want to keep your student users from signing up in the traditional manner.
There are three steps we recommend to customize your Thinkific site theme. You can read more about customizing here.
- Catch-all redirect in your theme's meta_tags to redirect your students from the Thinkific Sign In or Sign Up pages to your external site.
Example:<script>window.location.href = 'https://www.yoursite.com';</script>
- Redirect the "Sign In" link in your header to your external site.
- Redirect the "Buy" button for users who are not signed in to your external site.
Examples
Ruby Example
# Assuming that you've set your API key and Thinkific subdomain in the environment, you
# can use the Thinkific SSO from your controller like this example.
require 'securerandom' unless defined?(SecureRandom)
class SessionController < ApplicationController
# Configuration
THINKIFIC_API_KEY = ENV["THINKIFIC_API_KEY"]
THINKIFIC_SUBDOMAIN = ENV["THINKIFIC_SUBDOMAIN"]
def create
if user = User.authenticate(params[:login], params[:password])
# If the submitted credentials pass, then log user into Thinkific
sign_into_thinkific(user)
else
render :new, :notice => "Invalid credentials"
end
end
private
def sign_into_thinkific(user)
# This is the meat of the business, set up the parameters you wish
# to forward to Thinkific. All parameters are documented in this page.
iat = Time.now.to_i
jti = "#{iat}/#{SecureRandom.hex(18)}"
payload = JWT.encode({
:iat => iat,
:jti => jti,
:first_name => user.first_name,
:last_name => user.last_name,
:email => user.email,
}, THINKIFIC_API_KEY)
redirect_to thinkific_sso_url(payload)
end
def thinkific_sso_url(payload)
url = "http://#{THINKIFIC_SUBDOMAIN}.thinkific.com/api/sso/v2/sso/jwt?jwt=#{payload}"
url += "&return_to=#{URI.escape(params["return_to"])}" if params["return_to"].present?
url += "&error_url=#{URI.escape(params["error_url"])}" if params["error_url"].present?
url
end
end
PHP Example
// Create token header as a JSON string $header = json_encode(['typ' = 'JWT', 'alg' = 'HS256']); // Create token payload as a JSON string $payload = json_encode([ 'email'='test@thinkific.com', 'first_name'='Test', 'last_name' = 'User', 'iat' = time(), ]); // Encode Header to Base64Url String $base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header)); // Encode Payload to Base64Url String $base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload)); // Create Signature Hash $key = 'API_KEY'; $signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $key, true); // Encode Signature to Base64Url String $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature)); // Create JWT $jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; $baseUrl = "https://yoursite.thinkific.com/api/sso/v2/sso/jwt?jwt="; $returnTo = urlencode("https://yoursite.thinkific.com"); $errorUrl = urlencode("https://yoursite.thinkific.com"); $url = $baseUrl . $jwt . "&return_to=" . $returnTo . "&error_url=" . $errorUrl; echo $url;
C# Example
C# .Net Core 2.2 using Newtonsoft.Json; using System; using System.Security.Cryptography; public JsonResult ThinkificLogin() { //get API key from vault string PlainTextSecurityKey = _configuration["ThinkificAPIKey"]; //create header object then convert to a JSON string and encoding character,s finally converting to base 64. string HeaderObj = JsonConvert.SerializeObject(new { typ = "JWT", alg = "HS256" }); var HeaderBase64 = FixChars(Base64Encode(HeaderObj)); //create payload object then convert to a JSON string and encoding characters, finally converting to base 64. var Payload = new { first_name = "Name 1", last_name = "Name 2", email = "email@email.com", iat = GetUnixEpoch() }; var PayloadBase64 = FixChars(Base64Encode(JsonConvert.SerializeObject(Payload))); //Base 64 header and base 64 Payload need to be concatinated seperated by . for use creating the signature. var headerAndPayload = HeaderBase64 + "." + PayloadBase64; //HMAC the crap out of the Plain Text key, after converting it to a byte array HMACSHA256 hmac = new HMACSHA256(System.Text.Encoding.ASCII.GetBytes(PlainTextSecurityKey)); //Hash the combined header and payload, converting the result to a base 64 then encoading the resulting characters. var SignatureBase64 = FixChars(Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.ASCII.GetBytes(headerAndPayload)))); //build the final token. var JWT = HeaderBase64 + "." + PayloadBase64 + "." + SignatureBase64; //breath a sigh of releaf that this portion of your project is completed! return new JsonResult(JWT); } public string FixChars(string Input) { Input = Input.Replace('+', '-'); Input = Input.Replace('/', '_'); if (Input.Contains('=')) { Input = Input.Replace("=", string.Empty); } return Input; } public string Base64Encode(string plainText) { var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); return Convert.ToBase64String(plainTextBytes); } public long GetUnixEpoch() { TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1); long secondsSinceEpoch = (int)t.TotalSeconds; return secondsSinceEpoch; }
As this code will vary widely across themes and external sites, we can't give specific instructions. If you have any questions, please reach out!