Complete server examples
These are complete, working servers. Neither uses a library or SDK: copy the one for your language and adapt it. Save each one as the file named above its code. Both pass every check in the Playground when pointed at the sandbox, and they were also tested against the real identity provider and inside the phone app.
The code on this page is included straight from those tested files, so it cannot differ from what was tested.
Node.js (18 or newer, no packages)#
partner-examples/node/server.js: sign-in, one protected route (/api/me), and the page.
'use strict';
/* A complete partner server in Node.js 18+ with no packages and no SDK.
*
* IDP_URL=https://id.superapp.example CLIENT_ID=app_xxx CLIENT_SECRET=xxx \
* SHELL_ORIGIN=https://shell.superapp.example node server.js
*
* To try it from the docs Playground, also set CORS_ORIGIN to the Playground's origin (for example
* https://superapp.ideapreneurnepal.com).
*/
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const PORT = Number(process.env.PORT || 9300);
const IDP_URL = (process.env.IDP_URL || 'http://localhost:8000').replace(/\/+$/, '');
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const SHELL_ORIGIN = process.env.SHELL_ORIGIN || 'http://localhost:8000';
// Only for trying the server from the docs Playground: the playground page's origin. Leave it unset in production.
const CORS_ORIGIN = process.env.CORS_ORIGIN || '';
if (!CLIENT_ID || !CLIENT_SECRET) throw new Error('Set CLIENT_ID and CLIENT_SECRET');
const NONCE_TTL_MS = 2 * 60 * 1000;
const SESSION_TTL_MS = 60 * 60 * 1000;
const NONCE_RE = /^[A-Za-z0-9_-]{16,128}$/;
const nonces = new Map(); // nonce -> expiry time (use Redis if you run several instances)
const sessions = new Map(); // session -> { sub, name, expires }
// 1. A new one-time nonce for a page load.
function newNonce() {
const nonce = crypto.randomBytes(24).toString('base64url'); // 32 URL-safe characters
const now = Date.now();
for (const [n, expires] of nonces) if (expires < now) nonces.delete(n);
nonces.set(nonce, now + NONCE_TTL_MS);
return nonce;
}
// 2. Ask the identity provider who the token belongs to.
async function verifyWithSuperapp(token, nonce) {
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
try {
const res = await fetch(`${IDP_URL}/api/launch/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Basic ${basic}` },
body: JSON.stringify({ token, nonce }),
signal: AbortSignal.timeout(5000),
});
return { status: res.status, body: await res.json().catch(() => ({ error: 'idp_error' })) };
} catch (_) {
return { status: 503, body: { error: 'idp_unreachable' } };
}
}
// 3. Exchange a launch token for your own session.
async function login(token, nonce) {
if (typeof token !== 'string' || !token || typeof nonce !== 'string' || !NONCE_RE.test(nonce)) {
return { status: 400, body: { error: 'bad_request' } };
}
const expires = nonces.get(nonce) || 0;
nonces.delete(nonce); // burn it first: it can never be used twice
if (expires < Date.now()) return { status: 400, body: { error: 'unknown_or_expired_nonce' } };
const { status, body } = await verifyWithSuperapp(token, nonce);
if (status !== 200) return { status: 401, body: { error: body.error || 'verify_failed' } };
const session = crypto.randomBytes(32).toString('base64url');
sessions.set(session, { sub: body.sub, name: body.name || '', expires: Date.now() + SESSION_TTL_MS });
return { status: 200, body: { session, sub: body.sub, name: body.name || '' } };
}
// 4. Who is calling? Returns { sub, name } or null (answer 401).
function currentUser(authorization) {
const header = authorization || '';
const session = header.startsWith('Bearer ') ? header.slice(7) : '';
const user = sessions.get(session);
if (user && user.expires > Date.now()) return user;
sessions.delete(session);
return null;
}
const json = (res, status, body) => {
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify(body));
};
const readJson = (req) => new Promise((resolve) => {
let raw = '';
req.on('data', (c) => { raw += c; if (raw.length > 16384) req.destroy(); });
req.on('end', () => { try { resolve(JSON.parse(raw || '{}')); } catch (_) { resolve({}); } });
});
http.createServer(async (req, res) => {
const { pathname } = new URL(req.url, 'http://x');
if (CORS_ORIGIN) { // lets the Playground page call this server from the browser
res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Private-Network', 'true');
if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); }
}
// the sign-in routes
if (req.method === 'GET' && pathname === '/api/config') return json(res, 200, { shell_origin: SHELL_ORIGIN });
if (req.method === 'POST' && pathname === '/api/nonce') return json(res, 200, { nonce: newNonce() });
if (req.method === 'POST' && pathname === '/api/session') {
const { token, nonce } = await readJson(req);
const { status, body } = await login(token, nonce);
return json(res, status, body);
}
// everything else needs a session
if (pathname.startsWith('/api/')) {
const user = currentUser(req.headers.authorization);
if (!user) return json(res, 401, { error: 'no_session' });
if (req.method === 'GET' && pathname === '/api/me') return json(res, 200, { id: user.sub, name: user.name });
return json(res, 404, { error: 'not_found' });
}
// the page, with the header that lets only the SuperPreneur web shell frame it
if (req.method === 'GET' && pathname === '/') {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Security-Policy': `frame-ancestors 'self' ${SHELL_ORIGIN}`,
});
return res.end(fs.readFileSync(path.join(__dirname, '../public/index.html')));
}
json(res, 404, { error: 'not_found' });
}).listen(PORT, () => console.log(`Hello partner (Node) on http://localhost:${PORT}`));
Python (3.8 or newer, no packages)#
partner-examples/python/server.py: the same four routes with the same behaviour.
"""A complete partner server in Python 3.8+ with no packages and no SDK.
IDP_URL=https://id.superapp.example CLIENT_ID=app_xxx CLIENT_SECRET=xxx \
SHELL_ORIGIN=https://shell.superapp.example python3 server.py
To try it from the docs Playground, also set CORS_ORIGIN to the Playground's origin (for example
https://superapp.ideapreneurnepal.com).
"""
import base64
import json
import os
import re
import secrets
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
PORT = int(os.environ.get("PORT", "9301"))
IDP_URL = os.environ.get("IDP_URL", "http://localhost:8000").rstrip("/")
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
SHELL_ORIGIN = os.environ.get("SHELL_ORIGIN", "http://localhost:8000")
# Only for trying the server from the docs Playground: the playground page's origin. Leave it unset in production.
CORS_ORIGIN = os.environ.get("CORS_ORIGIN", "")
NONCE_TTL = 120 # seconds
SESSION_TTL = 3600
NONCE_RE = re.compile(r"^[A-Za-z0-9_-]{16,128}$")
lock = threading.Lock()
nonces = {} # nonce -> expiry time (use Redis if you run several instances)
sessions = {} # session -> {"sub", "name", "expires"}
def new_nonce():
"""1. A new one-time nonce for a page load."""
nonce = secrets.token_urlsafe(24) # 32 URL-safe characters
with lock:
now = time.time()
for n in [n for n, exp in nonces.items() if exp < now]:
del nonces[n]
nonces[nonce] = now + NONCE_TTL
return nonce
def verify_with_superapp(token, nonce):
"""2. Ask the identity provider who the token belongs to."""
basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
request = urllib.request.Request(
f"{IDP_URL}/api/launch/verify",
data=json.dumps({"token": token, "nonce": nonce}).encode(),
headers={
"Content-Type": "application/json",
"Authorization": f"Basic {basic}",
"User-Agent": "my-partner-app/1.0", # the hosted service refuses the default Python agent
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=5) as res:
return res.status, json.loads(res.read())
except urllib.error.HTTPError as err:
try:
return err.code, json.loads(err.read())
except ValueError:
return err.code, {"error": "idp_error"}
except (urllib.error.URLError, TimeoutError):
return 503, {"error": "idp_unreachable"}
def login(token, nonce):
"""3. Exchange a launch token for your own session."""
if not isinstance(token, str) or not token or not isinstance(nonce, str) or not NONCE_RE.match(nonce):
return 400, {"error": "bad_request"}
with lock: # burn it first: it can never be used twice
expires = nonces.pop(nonce, 0)
if expires < time.time():
return 400, {"error": "unknown_or_expired_nonce"}
status, body = verify_with_superapp(token, nonce)
if status != 200:
return 401, {"error": body.get("error", "verify_failed")}
session = secrets.token_urlsafe(32)
with lock:
sessions[session] = {"sub": body["sub"], "name": body.get("name", ""), "expires": time.time() + SESSION_TTL}
return 200, {"session": session, "sub": body["sub"], "name": body.get("name", "")}
def current_user(authorization):
"""4. Who is calling? Returns {"sub", "name"} or None (answer 401)."""
header = authorization or ""
session = header[7:] if header.startswith("Bearer ") else ""
with lock:
user = sessions.get(session)
if user and user["expires"] > time.time():
return user
sessions.pop(session, None)
return None
class Handler(BaseHTTPRequestHandler):
def end_headers(self):
if CORS_ORIGIN: # lets the Playground page call this server from the browser
self.send_header("Access-Control-Allow-Origin", CORS_ORIGIN)
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
self.send_header("Access-Control-Allow-Private-Network", "true")
super().end_headers()
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Content-Length", "0")
self.end_headers()
def send_json(self, status, body):
data = json.dumps(body).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
def read_json(self):
try:
length = min(int(self.headers.get("Content-Length", "0")), 16384)
body = json.loads(self.rfile.read(length) or b"{}")
return body if isinstance(body, dict) else {}
except ValueError:
return {}
def do_GET(self):
if self.path == "/api/config":
return self.send_json(200, {"shell_origin": SHELL_ORIGIN})
if self.path.startswith("/api/"):
user = current_user(self.headers.get("Authorization"))
if not user:
return self.send_json(401, {"error": "no_session"})
if self.path == "/api/me":
return self.send_json(200, {"id": user["sub"], "name": user["name"]})
return self.send_json(404, {"error": "not_found"})
if self.path == "/":
data = (Path(__file__).parent.parent / "public" / "index.html").read_bytes()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
# only the SuperPreneur web shell may frame this page
self.send_header("Content-Security-Policy", f"frame-ancestors 'self' {SHELL_ORIGIN}")
self.end_headers()
return self.wfile.write(data)
self.send_json(404, {"error": "not_found"})
def do_POST(self):
if self.path == "/api/nonce":
return self.send_json(200, {"nonce": new_nonce()})
if self.path == "/api/session":
body = self.read_json()
status, result = login(body.get("token"), body.get("nonce"))
return self.send_json(status, result)
self.send_json(404, {"error": "not_found"})
if __name__ == "__main__":
print(f"Hello partner (Python) on http://localhost:{PORT}")
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
The page#
Both servers serve partner-examples/public/index.html. Its script is the browser code from the Browser-side contract, followed by a few lines that call /api/me and show "Namaste, " and the user's name. Set your own background and text colour in the page's stylesheet, as the example does.
Running them#
| Variable | Value |
|---|---|
IDP_URL |
The identity provider's address: https://superapp.ideapreneurnepal.com/sandbox while testing, https://superapp.ideapreneurnepal.com for real users |
CLIENT_ID, CLIENT_SECRET |
sandbox_app and sandbox_secret while testing; the credentials issued when your app was registered for real users |
SHELL_ORIGIN |
Only if you serve the browser stand-in for the app (the web shell): its origin. The mobile app does not need it |
CORS_ORIGIN |
Only to try the server from the Playground: the address of the page, https://superapp.ideapreneurnepal.com. Leave it unset otherwise |
PORT |
Optional. Defaults to 9300 (Node) and 9301 (Python) |
IDP_URL=https://superapp.ideapreneurnepal.com/sandbox CLIENT_ID=sandbox_app CLIENT_SECRET=sandbox_secret node partner-examples/node/server.js
IDP_URL=https://superapp.ideapreneurnepal.com/sandbox CLIENT_ID=sandbox_app CLIENT_SECRET=sandbox_secret python3 partner-examples/python/server.py
Open the app from SuperPreneur. You should see "Namaste" and your name. Opening the address directly in a browser shows "Open this app from SuperPreneur".
Porting to another language or framework#
For Flask, Django, Laravel, Spring, Go or anything else, port four pieces. Everything else is your own app.
| Piece | What it must do |
|---|---|
| Nonce store | Create a random nonce for POST /api/nonce, keep it 2 minutes, remove it atomically when /api/session uses it |
| Verify call | An HTTPS POST to /api/launch/verify with Basic auth, a 5 second timeout and the JSON body {token, nonce} |
| Session store | Map a random session value to {sub, name, expires}; shared across instances if you run several |
| Request check | Return 401 with {"error": "no_session"} when there is no valid Authorization: Bearer session |