Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.terang.ai/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Terang AI supports Single Sign-On (SSO) via signed JWT tokens. Your organization signs a JWT with your private key, and Terang AI verifies it using your public key. This allows your members to access Terang AI directly from your platform without creating a separate account.
Before you can make any SSO or API requests, the following must be registered and whitelisted by the Terang AI team:
  1. Domain (iss) — your organization’s domain used as the JWT issuer (e.g. iai.or.id)
  2. Server IP address(es) — the outbound IP(s) that will send requests to api.terang.ai
Please contact us at founders@terang.ai with your domain and server IPs to get started. CIDR notation (e.g. 103.x.x.0/24) is supported.

Finding Your Server IP

The IP address you need to whitelist is the outbound IP of your server — the IP that api.terang.ai sees when your server makes a request.
The IP whitelist is enforced on the direct connection to api.terang.ai. If your integration redirects the user’s browser with the JWT in the URL (e.g. window.location = "https://api.terang.ai/sso/verify?token=..."), the gateway will see the user’s browser IP, not your server’s IP, and the request will be rejected.To use IP whitelisting you must deliver the JWT server-to-server — for example by proxying the redirect through your backend so that the HTTP request to api.terang.ai originates from your whitelisted server. If this is not possible for your platform, contact us to discuss alternatives.
Use the public IP address of your server. You can find it by running:
curl ifconfig.me
GKE pods use ephemeral IPs by default, which change on restart. Set up Cloud NAT to get a static outbound IP:
  1. Go to Network Services → Cloud NAT in the GCP Console
  2. Create a NAT gateway for your VPC and region
  3. Choose Manual IP and assign a static IP
  4. Share that static IP with us
This ensures all outbound traffic from your cluster uses a predictable IP.
  • EC2: Use the instance’s Elastic IP (public static IP)
  • EKS: Set up a NAT Gateway in your VPC with an Elastic IP. All pod traffic will route through it.
    1. Go to VPC → NAT Gateways in the AWS Console
    2. Create a NAT Gateway with an Elastic IP
    3. Update your private subnet route table to route 0.0.0.0/0 through the NAT Gateway
    4. Share the Elastic IP with us
Most serverless platforms use shared, rotating IPs which cannot be whitelisted. Options:
  • Use a proxy service with a static IP (e.g. QuotaGuard, Fixie)
  • Move SSO/API calls to a dedicated server with a static IP
  • Contact us to discuss alternative authentication methods

How it works

Base URLs

EnvironmentURL
Productionhttps://api.terang.ai
Developmenthttps://api.dev.terang.ai

JWT Specification

Use the RS256 algorithm. The kid field must match the key ID registered with Terang AI (or the kid in your JWKS):
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-1"
}
FieldTypeMax LengthRequiredDescription
algstring5 charsYesMust be RS256
typstring3 charsYesMust be JWT
kidstring128 charsYesKey ID — used to look up the correct public key from your JWKS. Must match a kid in your registered JWKS, or the key shared directly with Terang AI

Payload

{
  "iss": "your-domain.com",
  "aud": "terang.ai",
  "sub": "member",
  "email": "andi@your-domain.com",
  "name": "Andi Wijaya",
  "membershipId": "0001234",
  "iat": 1710000000,
  "exp": 1710000300,
  "jti": "TOKEN_MD5_HASH"
}
FieldTypeMax LengthRequiredDescription
issstring253 charsYesYour domain (e.g. iai.or.id). Max 253 per RFC 1035 domain name limit
audstring9 charsYesMust be exactly terang.ai
substring100 charsYesSubject type, e.g. member
emailstring254 charsYesMember’s email address. Max 254 per RFC 5321
namestring255 charsNoMember’s display name. Used as the user’s name on first login. Falls back to the local part of email if omitted
membershipIdstring255 charsNoUnique member ID in your system. Stored in the LMS user record for cross-system reference
iatnumber10 digitsYesIssued at (Unix timestamp)
expnumber10 digitsYesExpiration (Unix timestamp, max 5 minutes after iat)
jtistring64 charsYesJWT ID. Must be a unique token/session ID generated by your system (e.g., your internal signon MD5 hash = 32 chars, or a UUID = 36 chars). This maps the Terang LMS session directly to your authentication event and prevents replay attacks.
The exp must be at most 5 minutes after iat. Tokens with a longer expiry will be rejected.

Public Key Exchange

The Terang AI LMS needs your public key to verify JWT signatures. The gateway itself does not perform cryptographic verification — it only routes requests based on the iss claim. You have two options for sharing your public key: Expose your public key at a standard JWKS endpoint. This supports key rotation automatically — when you rotate keys, Terang AI picks up the new key without any manual update.
GET https://your-domain.com/.well-known/jwks.json
Response format:
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-id-1",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGcQ...",
      "e": "AQAB"
    }
  ]
}
FieldTypeMax LengthDescription
ktystring3 charsKey type, must be RSA
kidstring128 charsUnique key ID — used to match against the JWT header’s kid
usestring3 charsMust be sig (signature)
algstring5 charsMust be RS256
nstring~342 charsRSA modulus, Base64url-encoded. For a 2048-bit key: 342 chars
estring4 charsRSA exponent, Base64url-encoded. Typically AQAB (4 chars)
Share your JWKS URL with the Terang AI team, and we will configure it on our end.
Need help generating the JWKS response? See our Implementation Examples for copy-paste code in PHP and Node.js.

Option 2: Share Public Key Directly

If you cannot host a JWKS endpoint, you can share the PEM-formatted public key directly with the Terang AI team.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
With this option, key rotation requires manual coordination. You must notify the Terang AI team before rotating your key, otherwise JWT verification will fail. We strongly recommend Option 1 for production use.

SSO Redirect

Once the JWT is ready, your backend (not the user’s browser) must call the gateway:
GET https://api.terang.ai/sso/verify?token=<JWT_TOKEN>
Do not redirect the user’s browser directly to api.terang.ai/sso/verify (e.g. header('Location: https://api.terang.ai/sso/verify?token=...') or window.location = ...).The gateway’s IP whitelist checks the IP of whoever hits it. If your browser forwards the user there, the gateway sees the user’s IP and will reject the request with 403 IP x.x.x.x is not whitelisted.Always call the gateway from your backend and forward the Location header to the user’s browser. See the implementation examples below.

Gateway behavior

The gateway (api.terang.ai) does not verify the JWT itself. It:
  1. Peeks at the iss claim to identify your organization
  2. Checks that the request comes from a whitelisted IP (your backend’s outbound IP)
  3. Returns a HTTP 302 response with a Location header pointing to the Terang AI LMS
Your backend must then forward that Location header to the user’s browser (via its own 302 response) so the browser can follow the rest of the redirect chain — that is where the LMS session cookie is set.

LMS behavior

The LMS then:
  1. Verifies the JWT signature using your registered public key
  2. Validates iss, aud, exp, and jti claims
  3. Finds or creates the user account based on email and membershipId
  4. Creates a session (sets a cookie on the user’s browser) and redirects the user to the dashboard

Success response

On success, the redirect chain the user’s browser follows looks like this:
<your-backend> → 302 → lms.terang.ai/sso/verify?token=...   (forwarded from gateway)
              → 302 → lms.terang.ai/api/auth/sso/callback?token=... (session cookie set here)
              → 302 → /dashboard                             (or /sso/complete for new users)
New users (first SSO login) are redirected to a registration completion page (/sso/complete) where they can set a permanent password. Subsequent logins go directly to /courses (student’s dashboard).
Your HTTP client must not auto-follow redirects when calling api.terang.ai/sso/verify. If it does (e.g. PHP curl’s default CURLOPT_FOLLOWLOCATION=true, file_get_contents, fetch() default in Node.js), the session cookie will land on your backend instead of the user’s browser, and the user will not be logged in.Disable redirect following (CURLOPT_FOLLOWLOCATION=false, redirect: 'manual') and forward the Location header explicitly.

Error responses

Gateway errors — returned as JSON before the request reaches the LMS:
ScenarioHTTP StatusBody
Missing ?token400{"error": "token is required"}
Malformed JWT400{"error": "invalid token format"}
Missing iss claim400{"error": "missing issuer (iss) claim"}
Unknown issuer401{"error": "unknown issuer: <iss>"}
IP not whitelisted403{"error": "IP x.x.x.x is not whitelisted for issuer <iss>"}
LMS errors — the LMS redirects the browser to the sign-in page with an error query parameter:
ScenarioRedirect URL
Invalid JWT signature/auth/sign-in?error=sso_failed&reason=invalid_token
Expired token (exp)/auth/sign-in?error=sso_failed&reason=invalid_token
Account creation failed/auth/sign-in?error=sso_failed&reason=account_creation_failed
Session creation failed/auth/sign-in?error=sso_failed&reason=session_creation_failed

Implementation Examples

PHP

<?php
// Generate a new RSA key pair (run once)
$config = [
    'private_key_bits' => 2048,
    'private_key_type' => OPENSSL_KEYTYPE_RSA,
];

$keyPair = openssl_pkey_new($config);

// Export private key (keep this secret!)
openssl_pkey_export($keyPair, $privateKey);
file_put_contents('private_key.pem', $privateKey);

// Export public key (share with Terang AI)
$publicKey = openssl_pkey_get_details($keyPair)['key'];
file_put_contents('public_key.pem', $publicKey);

echo "Keys generated successfully.\n";
echo "Share public_key.pem with the Terang AI team.\n";

Node.js

import { generateKeyPairSync } from 'crypto';
import fs from 'fs';

const { publicKey, privateKey } = generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});

fs.writeFileSync('private_key.pem', privateKey);
fs.writeFileSync('public_key.pem', publicKey);

console.log('Keys generated. Share public_key.pem with the Terang AI team.');

API Requests

All API calls go through api.terang.ai with a Bearer JWT token in the Authorization header.

Testing

You can decode and inspect your JWT at jwt.io before sending it to Terang AI. Checklist:
  • Your domain (iss) is registered with Terang AI (required for gateway routing)
  • Your server IP address(es) are whitelisted (required for gateway IP check)
  • Public key is shared with Terang AI (via JWKS URL or PEM file, used by the LMS for verification)
  • JWT header uses RS256 algorithm
  • JWT kid header is set (and matches the kid in your JWKS if using Option 1)
  • All required payload fields are present
  • aud is set to terang.ai
  • exp is within 5 minutes of iat
  • jti is unique per request
  • The call to api.terang.ai/sso/verify is made from your backend, not the user’s browser
  • Redirect following is disabled (CURLOPT_FOLLOWLOCATION=false / redirect: 'manual')
  • The Location header returned by the gateway is forwarded to the user’s browser as a 302

Key Rotation Guide

Rotating your keys regularly is a security best practice. The process differs depending on which option you chose for public key exchange.

Rotating with JWKS (Option 1)

With JWKS, you can rotate keys with zero downtime — no coordination with Terang AI needed.
1

Generate a new RSA key pair

Create a new key pair with a new kid (e.g. key-2).
openssl genpkey -algorithm RSA -out new_private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in new_private_key.pem -pubout -out new_public_key.pem
2

Add the new key to your JWKS endpoint

Your JWKS should now return both the old and new keys:
{
  "keys": [
    { "kty": "RSA", "kid": "key-1", "use": "sig", "alg": "RS256", "n": "old-key-n...", "e": "AQAB" },
    { "kty": "RSA", "kid": "key-2", "use": "sig", "alg": "RS256", "n": "new-key-n...", "e": "AQAB" }
  ]
}
3

Start signing JWTs with the new key

Update your JWT signing code to use the new private key and set kid to key-2.
4

Wait, then remove the old key

Wait at least 5 minutes (max JWT lifetime) for all old tokens to expire. Then remove key-1 from your JWKS endpoint.
Terang AI caches JWKS responses for up to 1 hour. If you need the new key to be picked up immediately, contact us.

Rotating with Static PEM (Option 2)

With a static key, rotation requires coordination with the Terang AI team.
1

Generate a new key pair

Same as above — create a new RSA key pair.
2

Send the new public key to Terang AI

Email the new public_key.pem to founders@terang.ai. Do not start using the new key yet.
3

Wait for confirmation

We will update our configuration and confirm when the new key is active.
4

Switch to the new key

Once confirmed, update your signing code to use the new private key.
If you switch to the new private key before we update the public key, all JWT verification will fail and your members will not be able to log in.