Multi-Language SDK & Client Implementation Examples
This guide provides end-to-end integration examples for interacting with Ternis Auth across major programming languages and frameworks.
1. TypeScript / Node.js (Express)
Complete implementation of the Authorization Code flow with PKCE using native Node.js crypto and fetch:
import express, { Request, Response } from 'express';
import crypto from 'crypto';
const app = express();
const PORT = 3000;
// Ternis Auth Configuration
const AUTH_ISSUER = 'https://auth.ternis.net';
const CLIENT_ID = '9dc6e84a-714c-4e89-9a29-bc828cf99874';
const CLIENT_SECRET = process.env.TERNIS_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/callback';
// Temporary memory store for state & verifier (use Redis or session in production)
const pkceStore = new Map<string, string>();
function base64UrlEncode(buffer: Buffer): string {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// 1. Redirect user to Ternis Auth
app.get('/login', (req: Request, res: Response) => {
const state = base64UrlEncode(crypto.randomBytes(32));
const verifier = base64UrlEncode(crypto.randomBytes(32));
const challenge = base64UrlEncode(crypto.createHash('sha256').update(verifier).digest());
pkceStore.set(state, verifier);
const authUrl = new URL(`${AUTH_ISSUER}/oauth/authorize`);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'openid profile email ternis:sso');
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
res.redirect(authUrl.toString());
});
// 2. OAuth Callback
app.get('/auth/callback', async (req: Request, res: Response) => {
const { code, state } = req.query as { code?: string; state?: string };
if (!code || !state || !pkceStore.has(state)) {
return res.status(400).send('Invalid state or authorization code');
}
const verifier = pkceStore.get(state)!;
pkceStore.delete(state);
// 3. Exchange code for access token
const tokenParams = new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET || '',
redirect_uri: REDIRECT_URI,
code,
code_verifier: verifier,
});
const tokenRes = await fetch(`${AUTH_ISSUER}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenParams.toString(),
});
const tokens = await tokenRes.json();
// 4. Fetch User Profile
const userRes = await fetch(`${AUTH_ISSUER}/oauth/userinfo`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const user = await userRes.json();
res.send(`
<h1>Welcome, ${user.name}!</h1>
<p>Email: ${user.email}</p>
<img src="https://user.t-cdn.de/${user.sub}.png?size=128" alt="Avatar" width="64" height="64" />
`);
});
app.listen(PORT, () => console.log(`Server listening on http://localhost:${PORT}`));
2. Python (FastAPI / Requests)
import os
import hashlib
import base64
import secrets
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse, HTMLResponse
import requests
app = FastAPI()
AUTH_ISSUER = "https://auth.ternis.net"
CLIENT_ID = "9dc6e84a-714c-4e89-9a29-bc828cf99874"
CLIENT_SECRET = os.getenv("TERNIS_CLIENT_SECRET", "")
REDIRECT_URI = "http://localhost:8000/callback"
pkce_cache = {}
def generate_pkce():
verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip("=")
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).decode().rstrip("=")
return verifier, challenge
@app.get("/login")
def login():
state = secrets.token_hex(16)
verifier, challenge = generate_pkce()
pkce_cache[state] = verifier
url = (
f"{AUTH_ISSUER}/oauth/authorize?"
f"response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}"
f"&scope=openid+profile+email+ternis:sso&state={state}"
f"&code_challenge={challenge}&code_challenge_method=S256"
)
return RedirectResponse(url)
@app.get("/callback")
def callback(code: str, state: str):
verifier = pkce_cache.pop(state, None)
if not verifier:
raise HTTPException(status_code=400, detail="Invalid state token")
token_res = requests.post(
f"{AUTH_ISSUER}/oauth/token",
data={
"grant_type": "authorization_code",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI,
"code": code,
"code_verifier": verifier,
},
)
token_res.raise_for_status()
tokens = token_res.json()
user_res = requests.get(
f"{AUTH_ISSUER}/oauth/userinfo",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
user_res.raise_for_status()
user = user_res.json()
return HTMLResponse(f"""
<h2>Welcome, {user['name']}</h2>
<p>UUID: <code>{user['sub']}</code></p>
<img src="https://avatar.t-cdn.de/{user['preferred_username']}.svg" width="64" height="64" />
""")
3. PHP (Standard PHP / cURL)
<?php
$issuer = 'https://auth.ternis.net';
$clientId = '9dc6e84a-714c-4e89-9a29-bc828cf99874';
$clientSecret = getenv('TERNIS_CLIENT_SECRET');
$redirectUri = 'http://localhost:8080/callback.php';
session_start();
// 1. Initiate Login
if (isset($_GET['login'])) {
$verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$state = bin2hex(random_bytes(16));
$_SESSION['pkce_verifier'] = $verifier;
$_SESSION['oauth_state'] = $state;
$url = $issuer . '/oauth/authorize?' . http_build_query([
'response_type' => 'code',
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'scope' => 'openid profile email ternis:sso',
'state' => $state,
'code_challenge' => $challenge,
'code_challenge_method' => 'S256',
]);
header('Location: ' . $url);
exit;
}
// 2. Handle Callback
if (isset($_GET['code']) && isset($_GET['state'])) {
if ($_GET['state'] !== $_SESSION['oauth_state']) {
die('State mismatch CSRF detected');
}
$ch = curl_init($issuer . '/oauth/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'grant_type' => 'authorization_code',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'redirect_uri' => $redirectUri,
'code' => $_GET['code'],
'code_verifier' => $_SESSION['pkce_verifier'],
]));
$tokenResponse = json_decode(curl_exec($ch), true);
curl_close($ch);
// 3. Fetch UserInfo
$ch = curl_init($issuer . '/oauth/userinfo');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $tokenResponse['access_token']
]);
$user = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "<h1>Welcome, " . htmlspecialchars($user['name']) . "!</h1>";
echo "<img src='https://user.t-cdn.de/" . urlencode($user['sub']) . ".png?size=128' width='64' height='64' />";
}
4. Go (golang.org/x/oauth2)
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"golang.org/x/oauth2"
)
var oauthConfig = &oauth2.Config{
ClientID: "9dc6e84a-714c-4e89-9a29-bc828cf99874",
ClientSecret: os.Getenv("TERNIS_CLIENT_SECRET"),
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"openid", "profile", "email", "ternis:sso"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://auth.ternis.net/oauth/authorize",
TokenURL: "https://auth.ternis.net/oauth/token",
},
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
url := oauthConfig.AuthCodeURL("random-state-string", oauth2.AccessTypeOffline)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func handleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := oauthConfig.Exchange(context.Background(), code)
if err != nil {
http.Error(w, "Failed to exchange token", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Access Token: %s\n", token.AccessToken)
}
func main() {
http.HandleFunc("/login", handleLogin)
http.HandleFunc("/callback", handleCallback)
log.Fatal(http.ListenAndServe(":8080", nil))
}