Follow along and copy-paste. No OAuth knowledge and no Azure setup needed.
Send this to the SSO administrator. You get two values back: the Client ID and the Client Secret.
Please add a service to Advantech SSO Service name: (your site's name) Redirect URI: https://myapp.advantech.world/auth/callback
The PHP version uses a different Redirect URI — see the PHP tab in step 2. The Client Secret is a password: keep it in backend environment variables only, never in web pages or Git.
The starter is a small project that runs as is: sign-in page, home page and backend are done. Download and unzip → put the two values from step 1 into the commands → run.
Download starter .zip (Python / Flask)
myapp/ flask_app.py requirements.txt public/login.html public/index.html README.txt
pip install -r requirements.txt export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world export FLASK_SECRET_KEY=change-me-to-a-long-random-string python flask_app.py
Windows PowerShell: write $env:X="Y" instead of export X=Y.
For a local test, use this as your site's address: http://localhost:5000
# Advantech SSO + Flask — pip install flask requests
#
# export SSO_CLIENT_ID=svc_xxxxxxxx
# export SSO_CLIENT_SECRET=xxxxxxxx
# export APP_URL=https://myapp.advantech.world # this site; register APP_URL/auth/callback as Redirect URI
# export FLASK_SECRET_KEY=$(python -c "import secrets;print(secrets.token_hex(32))")
# python flask_app.py (put login.html / index.html in ./public)
import os, secrets
from functools import wraps
from urllib.parse import urlencode
import requests
from flask import Flask, abort, jsonify, redirect, request, send_from_directory, session
SSO = "https://ae-sso.advantech.com"
CLIENT_ID = os.environ["SSO_CLIENT_ID"]
CLIENT_SECRET = os.environ["SSO_CLIENT_SECRET"]
APP_URL = os.environ["APP_URL"].rstrip("/")
REDIRECT_URI = APP_URL + "/auth/callback"
# the session is a signed cookie (max ~4 KB), so keep only the fields you use
KEEP = ("sub", "email", "name", "given_name", "family_name", "job_title", "department",
"company", "office_location", "employee_id", "manager", "picture")
app = Flask(__name__, static_folder="public", static_url_path="")
app.secret_key = os.environ["FLASK_SECRET_KEY"]
app.config.update(SESSION_COOKIE_SECURE=APP_URL.startswith("https://"),
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax")
def safe_next(v):
return v if v and v.startswith("/") and not v.startswith("//") else "/"
# ---------- 1. sign in ----------
# /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY
# service, the SSO returns straight away (no page, no click); otherwise it comes
# back with error=login_required and we show login.html.
@app.get("/auth/login")
def login():
state = secrets.token_urlsafe(16)
# remember several pending sign-ins (two tabs, a double click ...), newest 5
pending = dict(list(session.get("pending", {}).items())[-4:])
pending[state] = safe_next(request.args.get("next"))
session["pending"] = pending
q = {"client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI,
"response_type": "code", "state": state}
if request.args.get("silent") == "1":
q["prompt"] = "none"
return redirect(f"{SSO}/authorize?{urlencode(q)}")
@app.get("/auth/callback")
def callback():
pending = session.get("pending", {})
next_url = pending.pop(request.args.get("state", ""), None)
session["pending"] = pending
if next_url is None: # unknown / reused state
return redirect("/login.html?error=state")
if request.args.get("error"): # login_required (silent try) / access_denied
q = {"next": next_url}
if request.args["error"] != "login_required":
q["error"] = request.args["error"]
return redirect("/login.html?" + urlencode(q))
r = requests.post(f"{SSO}/token", data={
"grant_type": "authorization_code",
"code": request.args.get("code", ""),
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}, timeout=10)
if r.status_code != 200:
return redirect("/login.html?error=token")
user = r.json()["user"]
session["user"] = {k: user[k] for k in KEEP if k in user}
return redirect(next_url)
@app.get("/auth/logout")
def logout():
session.clear()
return redirect(f"{SSO}/logout?" + urlencode({"redirect_uri": APP_URL + "/"}))
# ---------- 2. is the user signed in? ----------
def current_user():
return session.get("user") # None = not signed in
def login_required(view):
@wraps(view)
def wrapper(*args, **kwargs):
if current_user() is None:
if request.path.startswith("/api/"):
abort(401)
# try single sign-on first; login.html is shown only if that fails
return redirect("/auth/login?" + urlencode({"silent": 1, "next": request.full_path.rstrip("?")}))
return view(*args, **kwargs)
return wrapper
# ---------- 3. user data (used by advantech-sso.js) ----------
@app.get("/auth/me")
def me():
u = current_user()
return (jsonify(u), 200) if u else (jsonify(error="not signed in"), 401)
# ---------- your app ----------
@app.get("/")
@login_required
def index():
return send_from_directory("public", "index.html")
@app.get("/api/hello")
@login_required
def hello():
u = current_user()
return {"message": f"Hello {u['name']}", "department": u.get("department")}
if __name__ == "__main__":
app.run(port=5000)
flask>=3.0 requests>=2.31
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
Download starter .zip (Node.js / Express)
myapp/ express_app.mjs package.json public/login.html public/index.html README.txt
npm install export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world export SESSION_SECRET=change-me-to-a-long-random-string npm start
Windows PowerShell: write $env:X="Y" instead of export X=Y.
For a local test, use this as your site's address: http://localhost:3000
// Advantech SSO + Node.js / Express — npm i express express-session (Node 18+)
//
// SSO_CLIENT_ID=svc_xxxxxxxx SSO_CLIENT_SECRET=xxxxxxxx \
// APP_URL=https://myapp.advantech.world SESSION_SECRET=long-random-string \
// node express_app.mjs (put login.html / index.html in ./public)
//
// Register APP_URL/auth/callback as the Redirect URI of the service.
// express-session keeps sessions in memory: use a store (Redis, DB ...) in production.
import crypto from "node:crypto";
import express from "express";
import session from "express-session";
const SSO = "https://ae-sso.advantech.com";
const { SSO_CLIENT_ID: CLIENT_ID, SSO_CLIENT_SECRET: CLIENT_SECRET } = process.env;
const APP_URL = process.env.APP_URL.replace(/\/$/, "");
const REDIRECT_URI = `${APP_URL}/auth/callback`;
const app = express();
app.set("trust proxy", 1);
app.use(session({
secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false,
cookie: { httpOnly: true, secure: APP_URL.startsWith("https://"), sameSite: "lax", maxAge: 8 * 3600e3 },
}));
const safeNext = (v) => (typeof v === "string" && v.startsWith("/") && !v.startsWith("//") ? v : "/");
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY
// service, the SSO returns straight away (no page, no click); otherwise it comes
// back with error=login_required and we show login.html.
app.get("/auth/login", (req, res) => {
const state = crypto.randomBytes(16).toString("hex");
// remember several pending sign-ins (two tabs, a double click ...), newest 5
const pending = Object.entries(req.session.pending || {}).slice(-4);
req.session.pending = Object.fromEntries([...pending, [state, safeNext(req.query.next)]]);
const q = new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI,
response_type: "code", state });
if (req.query.silent === "1") q.set("prompt", "none");
res.redirect(`${SSO}/authorize?${q}`);
});
app.get("/auth/callback", async (req, res) => {
const pending = req.session.pending || {};
const next = Object.hasOwn(pending, String(req.query.state)) ? pending[String(req.query.state)] : null;
delete pending[String(req.query.state)];
if (next === null) return res.redirect("/login.html?error=state"); // unknown / reused state
if (req.query.error) { // login_required (silent try) / access_denied
const q = new URLSearchParams({ next });
if (req.query.error !== "login_required") q.set("error", String(req.query.error));
return res.redirect(`/login.html?${q}`);
}
const r = await fetch(`${SSO}/token`, {
method: "POST",
body: new URLSearchParams({ grant_type: "authorization_code", code: String(req.query.code || ""),
redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
});
if (!r.ok) return res.redirect("/login.html?error=token");
const { user } = await r.json();
req.session.regenerate(() => { // new session id after sign-in
req.session.user = user; // sub, email, name, department, picture ...
res.redirect(next);
});
});
app.get("/auth/logout", (req, res) => {
req.session.destroy(() => {
res.redirect(`${SSO}/logout?` + new URLSearchParams({ redirect_uri: `${APP_URL}/` }));
});
});
// ---------- 2. is the user signed in? ----------
function requireLogin(req, res, next) {
if (req.session.user) return next();
if (req.path.startsWith("/api/")) return res.status(401).json({ error: "not signed in" });
// try single sign-on first; login.html is shown only if that fails
res.redirect("/auth/login?" + new URLSearchParams({ silent: "1", next: req.originalUrl }));
}
// ---------- 3. user data (used by advantech-sso.js) ----------
app.get("/auth/me", (req, res) => {
if (!req.session.user) return res.status(401).json({ error: "not signed in" });
res.json(req.session.user);
});
// ---------- your app ----------
app.get("/", requireLogin, (req, res) => res.sendFile("index.html", { root: "public" }));
app.get("/api/hello", requireLogin, (req, res) => {
res.json({ message: `Hello ${req.session.user.name}`, department: req.session.user.department });
});
app.use(express.static("public")); // login.html etc.
app.listen(3000, () => console.log("http://localhost:3000"));
{
"name": "myapp",
"private": true,
"type": "module",
"scripts": { "start": "node express_app.mjs" },
"dependencies": {
"express": "^4.21.2",
"express-session": "^1.18.1"
}
}
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
myapp/ sso.php login.html index.html README.txt
# PHP 8+ with the curl extension export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world php -S localhost:8000 # on Apache/nginx: copy the files to the web root and set the 3 variables # (Apache: SetEnv SSO_CLIENT_ID ... nginx: fastcgi_param SSO_CLIENT_ID ...;)
Windows PowerShell: write $env:X="Y" instead of export X=Y.
For a local test, use this as your site's address: http://localhost:8000
Redirect URI to register for PHP: https://myapp.advantech.world/sso.php?a=callback
<?php
// Advantech SSO + PHP (8.0+, no framework) — save as sso.php next to login.html / index.html
//
// Set SSO_CLIENT_ID, SSO_CLIENT_SECRET, APP_URL (e.g. https://myapp.advantech.world)
// in the web server environment (or edit the constants below).
// Register APP_URL/sso.php?a=callback as the Redirect URI of the service.
// In the HTML, point advantech-sso.js at this file:
// <script src=".../advantech-sso.js" data-me="/sso.php?a=me"
// data-login="/sso.php?a=login" data-logout="/sso.php?a=logout"></script>
//
// Protect any other PHP page with:
// require __DIR__ . '/sso.php';
// $user = sso_require_login(); // signs in silently, or shows login.html
// echo htmlspecialchars($user['name']);
const SSO = 'https://ae-sso.advantech.com';
define('CLIENT_ID', getenv('SSO_CLIENT_ID'));
define('CLIENT_SECRET', getenv('SSO_CLIENT_SECRET'));
define('APP_URL', rtrim(getenv('APP_URL'), '/'));
define('REDIRECT_URI', APP_URL . '/sso.php?a=callback');
session_set_cookie_params(['httponly' => true, 'secure' => str_starts_with(APP_URL, 'https://'), 'samesite' => 'Lax']);
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
function sso_safe_next($v) {
return is_string($v) && str_starts_with($v, '/') && !str_starts_with($v, '//') ? $v : '/';
}
// ---------- 2. is the user signed in? ----------
function sso_user() {
return $_SESSION['user'] ?? null; // null = not signed in
}
function sso_require_login() {
$u = sso_user();
if ($u === null) {
// try single sign-on first; login.html is shown only if that fails
header('Location: /sso.php?' . http_build_query(['a' => 'login', 'silent' => 1, 'next' => $_SERVER['REQUEST_URI']]));
exit;
}
return $u;
}
// only route when sso.php itself is requested (not when it is require'd)
if (realpath($_SERVER['SCRIPT_FILENAME']) !== __FILE__) return;
switch ($_GET['a'] ?? '') {
// ---------- 1. sign in ----------
// ?silent=1 adds prompt=none: if the user already signed in to ANY service,
// the SSO returns straight away (no page, no click); otherwise it comes back
// with error=login_required and we show login.html.
case 'login':
$state = bin2hex(random_bytes(16));
// remember several pending sign-ins (two tabs, a double click ...), newest 5
$pending = array_slice($_SESSION['pending'] ?? [], -4, null, true);
$pending[$state] = sso_safe_next($_GET['next'] ?? '/');
$_SESSION['pending'] = $pending;
$q = ['client_id' => CLIENT_ID, 'redirect_uri' => REDIRECT_URI,
'response_type' => 'code', 'state' => $state];
if (($_GET['silent'] ?? '') === '1') $q['prompt'] = 'none';
header('Location: ' . SSO . '/authorize?' . http_build_query($q));
exit;
case 'callback':
$state = (string)($_GET['state'] ?? '');
$next = $_SESSION['pending'][$state] ?? null;
unset($_SESSION['pending'][$state]);
if ($state === '' || $next === null) { // unknown / reused state
header('Location: /login.html?error=state'); exit;
}
if (!empty($_GET['error'])) { // login_required (silent try) / access_denied
$q = ['next' => $next];
if ($_GET['error'] !== 'login_required') $q['error'] = $_GET['error'];
header('Location: /login.html?' . http_build_query($q)); exit;
}
$ch = curl_init(SSO . '/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code', 'code' => $_GET['code'] ?? '',
'redirect_uri' => REDIRECT_URI, 'client_id' => CLIENT_ID, 'client_secret' => CLIENT_SECRET,
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) { header('Location: /login.html?error=token'); exit; }
session_regenerate_id(true);
$_SESSION['user'] = json_decode($body, true)['user']; // sub, email, name, department, picture ...
header('Location: ' . $next);
exit;
case 'logout':
$_SESSION = [];
session_destroy();
header('Location: ' . SSO . '/logout?' . http_build_query(['redirect_uri' => APP_URL . '/']));
exit;
// ---------- 3. user data (used by advantech-sso.js) ----------
case 'me':
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$u = sso_user();
if ($u === null) { http_response_code(401); echo '{"error":"not signed in"}'; exit; }
echo json_encode($u, JSON_UNESCAPED_UNICODE);
exit;
default:
http_response_code(404);
}
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-me="/sso.php?a=me" data-login="/sso.php?a=login" data-logout="/sso.php?a=logout" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-me="/sso.php?a=me" data-login="/sso.php?a=login" data-logout="/sso.php?a=logout" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
Download starter .zip (C# / ASP.NET Core)
myapp/ MyApp.csproj Program.cs wwwroot/login.html wwwroot/index.html README.txt
# .NET 8 SDK export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world dotnet run --urls http://localhost:5000
Windows PowerShell: write $env:X="Y" instead of export X=Y.
For a local test, use this as your site's address: http://localhost:5000
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
// Advantech SSO + ASP.NET Core (.NET 8, minimal API)
//
// Environment variables: SSO_CLIENT_ID, SSO_CLIENT_SECRET, APP_URL (e.g. https://myapp.advantech.world)
// dotnet run (login.html / index.html in wwwroot/)
// Register APP_URL/auth/callback as the Redirect URI of the service.
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
const string SSO = "https://ae-sso.advantech.com";
var builder = WebApplication.CreateBuilder(args);
var clientId = builder.Configuration["SSO_CLIENT_ID"]!;
var clientSecret = builder.Configuration["SSO_CLIENT_SECRET"]!;
var appUrl = builder.Configuration["APP_URL"]!.TrimEnd('/');
var redirectUri = appUrl + "/auth/callback";
builder.Services.AddHttpClient();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.LoginPath = "/login.html"; // ?ReturnUrl=... is appended
o.ExpireTimeSpan = TimeSpan.FromHours(8);
o.Cookie.SameSite = SameSiteMode.Lax;
o.Events.OnRedirectToLogin = ctx => // APIs get 401 instead of a redirect
{
if (ctx.Request.Path.StartsWithSegments("/api") || ctx.Request.Path.StartsWithSegments("/auth/me"))
ctx.Response.StatusCode = 401;
else // try single sign-on first; login.html is shown only if that fails
ctx.Response.Redirect("/auth/login?silent=1&next=" + Uri.EscapeDataString(ctx.Request.Path + ctx.Request.QueryString));
return Task.CompletedTask;
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
static string SafeNext(string? v) => v is { Length: > 0 } && v.StartsWith('/') && !v.StartsWith("//") ? v : "/";
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY service,
// the SSO returns straight away (no page, no click); otherwise it comes back with
// error=login_required and we show login.html.
app.MapGet("/auth/login", (HttpContext ctx, string? next, string? silent) =>
{
var state = Convert.ToHexString(RandomNumberGenerator.GetBytes(16));
// one short-lived cookie per pending sign-in, so two tabs don't overwrite each other
var opts = new CookieOptions { HttpOnly = true, Secure = ctx.Request.IsHttps, SameSite = SameSiteMode.Lax, MaxAge = TimeSpan.FromMinutes(10) };
ctx.Response.Cookies.Append("sso_st_" + state, SafeNext(next), opts);
var p = new Dictionary<string, string?> {
["client_id"] = clientId, ["redirect_uri"] = redirectUri, ["response_type"] = "code", ["state"] = state };
if (silent == "1") p["prompt"] = "none";
var q = QueryString.Create(p);
return Results.Redirect($"{SSO}/authorize{q}");
});
app.MapGet("/auth/callback", async (HttpContext ctx, IHttpClientFactory http, string? code, string? state, string? error) =>
{
var key = "sso_st_" + (state ?? "");
var saved = string.IsNullOrEmpty(state) || state.Length != 32 ? null : ctx.Request.Cookies[key];
if (saved is null) return Results.Redirect("/login.html?error=state"); // unknown / reused state
ctx.Response.Cookies.Delete(key);
var next = SafeNext(saved);
if (!string.IsNullOrEmpty(error)) // login_required (silent try) / access_denied
return Results.Redirect("/login.html?next=" + Uri.EscapeDataString(next)
+ (error == "login_required" ? "" : "&error=" + Uri.EscapeDataString(error)));
var res = await http.CreateClient().PostAsync($"{SSO}/token", new FormUrlEncodedContent(new Dictionary<string, string> {
["grant_type"] = "authorization_code", ["code"] = code ?? "", ["redirect_uri"] = redirectUri,
["client_id"] = clientId, ["client_secret"] = clientSecret }));
if (!res.IsSuccessStatusCode) return Results.Redirect("/login.html?error=token");
var user = JsonNode.Parse(await res.Content.ReadAsStringAsync())!["user"]!;
var claims = new List<Claim> {
new("sub", (string)user["sub"]!),
new(ClaimTypes.Name, (string?)user["name"] ?? ""),
new(ClaimTypes.Email, (string?)user["email"] ?? ""),
new("sso_user", user.ToJsonString()), // the whole user JSON (department, picture ...)
};
await ctx.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)));
return Results.Redirect(next);
});
app.MapGet("/auth/logout", async (HttpContext ctx) =>
{
await ctx.SignOutAsync();
return Results.Redirect($"{SSO}/logout?redirect_uri={Uri.EscapeDataString(appUrl + "/")}");
});
// ---------- 2. is the user signed in? ctx.User.Identity.IsAuthenticated / .RequireAuthorization() ----------
static JsonNode? SsoUser(ClaimsPrincipal p) =>
p.Identity?.IsAuthenticated == true ? JsonNode.Parse(p.FindFirst("sso_user")?.Value ?? "{}") : null;
// ---------- 3. user data (used by advantech-sso.js) ----------
app.MapGet("/auth/me", (ClaimsPrincipal p) => Results.Content(SsoUser(p)!.ToJsonString(), "application/json"))
.RequireAuthorization();
// ---------- your app ----------
app.MapGet("/", () => Results.File(Path.Combine(app.Environment.WebRootPath, "index.html"), "text/html"))
.RequireAuthorization();
app.MapGet("/api/hello", (ClaimsPrincipal p) =>
{
var u = SsoUser(p)!;
return Results.Json(new { message = $"Hello {u["name"]}", department = (string?)u["department"] });
}).RequireAuthorization();
app.Run();
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
Download starter .zip (Java / Spring Boot)
myapp/ pom.xml src/main/java/com/example/myapp/MyAppApplication.java src/main/java/com/example/myapp/SsoController.java src/main/resources/application.properties src/main/resources/static/login.html src/main/resources/static/index.html README.txt
# Java 17+ and Maven export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world mvn spring-boot:run
Windows PowerShell: write $env:X="Y" instead of export X=Y.
For a local test, use this as your site's address: http://localhost:8080
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.example.myapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
// Advantech SSO + Java / Spring Boot 3 (spring-boot-starter-web)
//
// application.properties:
// sso.client-id=${SSO_CLIENT_ID}
// sso.client-secret=${SSO_CLIENT_SECRET}
// sso.app-url=${APP_URL}
// server.servlet.session.cookie.same-site=lax
// Register APP_URL/auth/callback as the Redirect URI of the service.
// Put login.html / index.html in src/main/resources/static/.
package com.example.myapp;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClient;
import org.springframework.web.filter.OncePerRequestFilter;
@RestController
public class SsoController {
static final String SSO = "https://ae-sso.advantech.com";
@Value("${sso.client-id}") String clientId;
@Value("${sso.client-secret}") String clientSecret;
@Value("${sso.app-url}") String appUrl;
private final RestClient http = RestClient.create();
String redirectUri() { return appUrl.replaceAll("/$", "") + "/auth/callback"; }
static String enc(String s) { return URLEncoder.encode(s, StandardCharsets.UTF_8); }
static String safeNext(String v) { return v != null && v.startsWith("/") && !v.startsWith("//") ? v : "/"; }
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY service,
// the SSO returns straight away (no page, no click); otherwise it comes back with
// error=login_required and we show login.html.
@GetMapping("/auth/login")
public void login(@RequestParam(required = false) String next, @RequestParam(required = false) String silent,
HttpSession session, HttpServletResponse res) throws IOException {
byte[] b = new byte[16];
new SecureRandom().nextBytes(b);
String state = HexFormat.of().formatHex(b);
// remember several pending sign-ins (two tabs, a double click ...), newest 5
@SuppressWarnings("unchecked")
var pending = (java.util.LinkedHashMap<String, String>) session.getAttribute("pending");
if (pending == null) pending = new java.util.LinkedHashMap<>();
while (pending.size() >= 5) pending.remove(pending.keySet().iterator().next());
pending.put(state, safeNext(next));
session.setAttribute("pending", pending);
res.sendRedirect(SSO + "/authorize?client_id=" + enc(clientId) + "&redirect_uri=" + enc(redirectUri())
+ "&response_type=code&state=" + state + ("1".equals(silent) ? "&prompt=none" : ""));
}
@GetMapping("/auth/callback")
@SuppressWarnings("unchecked")
public void callback(@RequestParam(defaultValue = "") String code, @RequestParam(defaultValue = "") String state,
@RequestParam(required = false) String error,
HttpServletRequest req, HttpServletResponse res) throws IOException {
HttpSession session = req.getSession();
var pending = (Map<String, String>) session.getAttribute("pending");
String next = pending == null ? null : pending.remove(state);
if (next == null) { res.sendRedirect("/login.html?error=state"); return; } // unknown / reused state
if (error != null) { // login_required (silent try) / access_denied
res.sendRedirect("/login.html?next=" + enc(next) + ("login_required".equals(error) ? "" : "&error=" + enc(error)));
return;
}
var form = new LinkedMultiValueMap<String, String>();
form.add("grant_type", "authorization_code");
form.add("code", code);
form.add("redirect_uri", redirectUri());
form.add("client_id", clientId);
form.add("client_secret", clientSecret);
Map<String, Object> body;
try {
body = http.post().uri(SSO + "/token").contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(form).retrieve().body(Map.class);
} catch (Exception e) {
res.sendRedirect("/login.html?error=token");
return;
}
req.changeSessionId(); // new session id after sign-in
req.getSession().setAttribute("user", body.get("user")); // sub, email, name, department, picture ...
res.sendRedirect(next);
}
@GetMapping("/auth/logout")
public void logout(HttpSession session, HttpServletResponse res) throws IOException {
session.invalidate();
res.sendRedirect(SSO + "/logout?redirect_uri=" + enc(appUrl.replaceAll("/$", "") + "/"));
}
// ---------- 3. user data (used by advantech-sso.js) ----------
@GetMapping("/auth/me")
public ResponseEntity<Object> me(HttpSession session) {
Object user = session.getAttribute("user");
return user == null ? ResponseEntity.status(401).body(Map.of("error", "not signed in")) : ResponseEntity.ok(user);
}
// ---------- your app ----------
@GetMapping("/api/hello")
public Map<String, Object> hello(@SessionAttribute("user") Map<String, Object> user) {
return Map.of("message", "Hello " + user.get("name"));
}
}
// ---------- 2. is the user signed in? (every request except the public ones) ----------
@Component
class RequireLogin extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest req) {
String p = req.getRequestURI();
return p.startsWith("/auth/") || p.equals("/login.html") || p.equals("/favicon.ico");
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
HttpSession s = req.getSession(false);
if (s != null && s.getAttribute("user") != null) { chain.doFilter(req, res); return; }
if (req.getRequestURI().startsWith("/api/")) { res.sendError(401); return; }
String next = req.getRequestURI() + (req.getQueryString() != null ? "?" + req.getQueryString() : "");
// try single sign-on first; login.html is shown only if that fails
res.sendRedirect("/auth/login?silent=1&next=" + URLEncoder.encode(next, StandardCharsets.UTF_8));
}
}
sso.client-id=${SSO_CLIENT_ID}
sso.client-secret=${SSO_CLIENT_SECRET}
sso.app-url=${APP_URL}
server.port=8080
server.servlet.session.cookie.same-site=lax
server.servlet.session.cookie.http-only=true
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
myapp/ main.go go.mod public/login.html public/index.html README.txt
# Go 1.22+ export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world go run .
Windows PowerShell: write $env:X="Y" instead of export X=Y.
For a local test, use this as your site's address: http://localhost:8080
// Advantech SSO + Go (standard library only, Go 1.22+)
//
// SSO_CLIENT_ID=svc_xxxxxxxx SSO_CLIENT_SECRET=xxxxxxxx \
// APP_URL=https://myapp.advantech.world go run . (login.html / index.html in ./public)
//
// Register APP_URL/auth/callback as the Redirect URI of the service.
// Sessions live in memory here; use a database or Redis when you run several instances.
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
)
const SSO = "https://ae-sso.advantech.com"
var (
clientID = os.Getenv("SSO_CLIENT_ID")
clientSecret = os.Getenv("SSO_CLIENT_SECRET")
appURL = strings.TrimRight(os.Getenv("APP_URL"), "/")
redirectURI = appURL + "/auth/callback"
)
type User map[string]any // sub, email, name, department, job_title, manager, picture ...
type session struct {
User User
Pending map[string]string // state -> page to return to (several tabs may sign in at once)
Expires time.Time
}
var (
mu sync.Mutex
sessions = map[string]*session{}
)
func randHex() string { b := make([]byte, 16); rand.Read(b); return hex.EncodeToString(b) }
func getSession(w http.ResponseWriter, r *http.Request) *session {
mu.Lock()
defer mu.Unlock()
if c, err := r.Cookie("sid"); err == nil {
if s := sessions[c.Value]; s != nil && time.Now().Before(s.Expires) {
return s
}
}
id, s := randHex(), &session{Expires: time.Now().Add(8 * time.Hour)}
sessions[id] = s
http.SetCookie(w, &http.Cookie{Name: "sid", Value: id, Path: "/", HttpOnly: true,
Secure: strings.HasPrefix(appURL, "https://"), SameSite: http.SameSiteLaxMode})
return s
}
func safeNext(v string) string {
if strings.HasPrefix(v, "/") && !strings.HasPrefix(v, "//") {
return v
}
return "/"
}
// ---------- 2. is the user signed in? ----------
func currentUser(w http.ResponseWriter, r *http.Request) User { return getSession(w, r).User } // nil = no
func requireLogin(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if currentUser(w, r) == nil {
if strings.HasPrefix(r.URL.Path, "/api/") {
http.Error(w, `{"error":"not signed in"}`, http.StatusUnauthorized)
return
}
// try single sign-on first; login.html is shown only if that fails
http.Redirect(w, r, "/auth/login?"+url.Values{"silent": {"1"}, "next": {r.URL.RequestURI()}}.Encode(), http.StatusFound)
return
}
h(w, r)
}
}
func main() {
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY
// service, the SSO returns straight away (no page, no click); otherwise it
// comes back with error=login_required and we show login.html.
http.HandleFunc("GET /auth/login", func(w http.ResponseWriter, r *http.Request) {
s := getSession(w, r)
state := randHex()
mu.Lock()
if s.Pending == nil || len(s.Pending) >= 5 {
s.Pending = map[string]string{}
}
s.Pending[state] = safeNext(r.URL.Query().Get("next"))
mu.Unlock()
q := url.Values{"client_id": {clientID}, "redirect_uri": {redirectURI},
"response_type": {"code"}, "state": {state}}
if r.URL.Query().Get("silent") == "1" {
q.Set("prompt", "none")
}
http.Redirect(w, r, SSO+"/authorize?"+q.Encode(), http.StatusFound)
})
http.HandleFunc("GET /auth/callback", func(w http.ResponseWriter, r *http.Request) {
s := getSession(w, r)
state := r.URL.Query().Get("state")
mu.Lock()
next, ok := s.Pending[state]
delete(s.Pending, state)
mu.Unlock()
if !ok { // unknown / reused state
http.Redirect(w, r, "/login.html?error=state", http.StatusFound)
return
}
if e := r.URL.Query().Get("error"); e != "" { // login_required (silent try) / access_denied
q := url.Values{"next": {next}}
if e != "login_required" {
q.Set("error", e)
}
http.Redirect(w, r, "/login.html?"+q.Encode(), http.StatusFound)
return
}
res, err := http.PostForm(SSO+"/token", url.Values{
"grant_type": {"authorization_code"}, "code": {r.URL.Query().Get("code")},
"redirect_uri": {redirectURI}, "client_id": {clientID}, "client_secret": {clientSecret}})
if err != nil || res.StatusCode != http.StatusOK {
http.Redirect(w, r, "/login.html?error=token", http.StatusFound)
return
}
defer res.Body.Close()
var body struct{ User User }
if json.NewDecoder(res.Body).Decode(&body) != nil {
http.Redirect(w, r, "/login.html?error=token", http.StatusFound)
return
}
mu.Lock()
s.User = body.User
mu.Unlock()
http.Redirect(w, r, next, http.StatusFound)
})
http.HandleFunc("GET /auth/logout", func(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("sid"); err == nil {
mu.Lock()
delete(sessions, c.Value)
mu.Unlock()
}
http.Redirect(w, r, SSO+"/logout?"+url.Values{"redirect_uri": {appURL + "/"}}.Encode(), http.StatusFound)
})
// ---------- 3. user data (used by advantech-sso.js) ----------
http.HandleFunc("GET /auth/me", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
u := currentUser(w, r)
if u == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"not signed in"}`))
return
}
json.NewEncoder(w).Encode(u)
})
// ---------- your app ----------
http.HandleFunc("GET /{$}", requireLogin(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
}))
http.HandleFunc("GET /api/hello", requireLogin(func(w http.ResponseWriter, r *http.Request) {
u := currentUser(w, r)
json.NewEncoder(w).Encode(map[string]any{"message": "Hello " + u["name"].(string), "department": u["department"]})
}))
http.Handle("GET /", http.FileServer(http.Dir("public")))
log.Fatal(http.ListenAndServe(":8080", nil))
}
module myapp go 1.22
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
https://myapp.advantech.world/Didn't work? See Something went wrong?
1) In the backend, copy the four routes /auth/login, /auth/callback, /auth/me and /auth/logout from the step 2 starter (using its file as is is fastest). 2) Paste these 3 lines into your sign-in page to get a “Sign in with Advantech account” button:
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css"> <script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" defer></script> <div data-sso="button"></div>
The button follows the browser's language by default (zh-TW / zh-CN / en / ja / ko). Language · Button options (language, style, divider)
No framework needed. Add these two lines to a page and you get a sign-in button, a user badge (photo + name + sign-out), field display and automatic redirect when signed out. The script only calls your own site's /auth/* and never touches the Client Secret.
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css"> <script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" defer></script>
Reference the files on the SSO directly, or download them and host them on your site (recommended for production).
advantech-sso.js advantech-sso.css login.html index.html
Save as public/login.html. The backend sends signed-out users to /login.html?next=original-url and failed sign-ins to /login.html?error=…. Signed-in users opening it go straight on to next.
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
Save as public/index.html. data-require-login sends signed-out users to sign in; the data itself must still be protected by the backend.
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
| Attribute | What it does |
|---|---|
| data-me / data-login / data-logout | On the <script> tag: change the backend paths (default /auth/me, /auth/login, /auth/logout). |
| data-login-text / data-logout-text | On the <script> tag: the badge's sign-in / sign-out texts (translated automatically by default). |
| data-require-login | On the <script> tag: when signed out, try single sign-on automatically and show the sign-in page only if needed. |
| data-login-page | On the <script> tag: this is the sign-in page; signed-in users go on to ?next=. |
| data-sso="button" | No redesign needed. Add the CSS and JS to the <head> of your existing sign-in page, then put <div data-sso="button"></div> where the button should go — a “Sign in with Advantech account” button appears. |
| data-sso="login" | Signs in on click (add data-next="/path" to choose the page after sign-in). |
| data-sso="logout" | Signs out on click (both your site and the SSO). |
| data-sso="user" | Renders a user badge: photo, name, email and a sign-out link; a sign-in button when signed out. On a dark background add class="adv-sso-on-dark". |
| data-sso-field="…" | Fills in a user field, e.g. name, email, department, job_title, manager.name. |
| data-sso-photo | On an <img>: shows the user's photo (hidden when there is none). |
| data-sso-show="in|out" | Shown only when signed in (in) or signed out (out). |
| data-sso-json | Shows the whole user JSON — handy while developing. |
| data-sso-error | Shown only when the URL has ?error= (error message on the sign-in page). |
const user = await AdvSSO.getUser(); // user JSON, or null when signed out
const me = await AdvSSO.requireLogin(); // redirects to sign-in when signed out
AdvSSO.login("/reports"); // sign in, then go to /reports
AdvSSO.logout();
document.addEventListener("advsso:user", (e) => {
if (e.detail) console.log(e.detail.name, e.detail.department, e.detail.picture);
});
| I want to… | How | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Know who is signed in, on the backend |
| ||||||||||||
| Require sign-in for a page or API |
| ||||||||||||
| Show the name or department on a page | <span data-sso-field="name"></span> <span data-sso-field="department"></span> | ||||||||||||
| Show the photo | <img data-sso-photo alt=""> or <img src="${user.picture}"> | ||||||||||||
| Put “photo + name + sign out” in the header | <div data-sso="user"></div> | ||||||||||||
| A sign-out button | <a href="/auth/logout">Logout</a> | ||||||||||||
| Get the manager or direct reports | user.manager.name, user.manager_chain, user.direct_reports — Organization data: manager, direct reports, team members | ||||||||||||
| Need more fields (e.g. mobile phone) but don't get them | The SSO administrator decides which fields each service gets — ask them to tick it under “Data this service may read” on the service page. User profile | ||||||||||||
| Users coming from another system shouldn't sign in again | Nothing to do — the starter already handles it (automatic single sign-on). Single sign-on across services (signed in to A, no sign-in again for B) |
| What you see | Fix |
|---|---|
| The SSO says “redirect_uri is not registered” | Your site's address and the registered Redirect URI are not exactly the same. http / https, domain, port and path must all match (e.g. http://localhost:5000/auth/callback). Check APP_URL, or ask the administrator to change the registered address. |
Back on the sign-in page with error=token in the URL | The Client ID or Client Secret is wrong. Check that the environment variables are set and have no extra spaces; restart the program after changing them. |
error=state in the URL | The session was lost during sign-in (the starter's sign-in page retries once automatically). If it still appears: APP_URL is https but you opened the site over http; the host differs (127.0.0.1 and localhost have separate cookies); the session secret changes on every restart (FLASK_SECRET_KEY / SESSION_SECRET); or another site on the same machine's localhost uses the same cookie name (cookies ignore ports). Open the site with exactly the APP_URL address and keep the session secret fixed. |
The SSO says “no access”, or error=access_denied | Your account is not on this service's allowlist (or is on the denylist). Ask the SSO administrator. |
| A field (e.g. mobile_phone) is missing | The service is not allowed that field, or the person has no value in Azure. Ask the administrator to check “Data this service may read” on the service page — it also has a preview. |
| The photo does not show | The person has no photo, the service is not allowed the photo, or your site sets a CSP whose img-src lacks https://ae-sso.advantech.com. |
| After sign-in you keep landing on the sign-in page | The backend did not keep the user in the session. Open /auth/me in the browser: you should see your data (200); 401 means the session was not kept (usually the same cause as error=state). |
| After signing out, signing in brings back the same user without a password | Your site and the SSO did sign out, but Microsoft still remembers you. The first sign-in after a sign-out shows Microsoft's “Pick an account” screen, where you can choose another account. To be asked for the password every time, ask the SSO administrator to turn on “Also sign out of Microsoft” under “Azure AD settings → Sign-out behavior”, or add microsoft=1 to the logout URL. |
How it works, every URL, the fields, HTML attributes and HTTP details. Not needed if you followed the steps above.
Every site uses the same flow (OAuth 2.0 Authorization Code). The Client Secret lives only on your backend; the browser never sees it.
/auth/login?silent=1 (see “Single sign-on across services” below); only if the user has not signed in to any service yet does it show /login.html./auth/login creates a random state and redirects to https://ae-sso.advantech.com/authorize./auth/callback?code=…&state=…; the backend checks the state and calls POST https://ae-sso.advantech.com/token with the code + Client Secret to get the user./auth/me to show the name, department and photo.Each example is a single runnable file with the same five routes, so the front-end kit works with all of them:
| Route | What it does |
|---|---|
| GET /auth/login | Creates the state and redirects to the SSO. ?next= is the page to return to; ?silent=1 adds prompt=none, which only passes when the user is already signed in to the SSO. |
| GET /auth/callback | Checks the state; on error (e.g. login_required) goes to the sign-in page, otherwise calls /token with the code + Client Secret and stores user in the session. |
| GET /auth/me | Returns the user JSON when signed in, 401 otherwise. |
| GET /auth/logout | Clears your session, signs out of the SSO and returns to your home page. |
| / , /api/* | Protected pages and APIs: signed-out page requests first go to /auth/login?silent=1&next=…, API requests get 401. |
Once a user signs in to any service, the SSO keeps them signed in for 8 hours (a cookie on the SSO's domain). When another service later sends the user to /authorize, the SSO immediately redirects back with a code — no Microsoft sign-in screen.
To need no click at all, when a service finds no session of its own it should not show its sign-in page first, but:
https://ae-sso.advantech.com/authorize?…&prompt=none (/auth/login?silent=1 in the examples).code; exchange it as usual. The user only sees the address bar flicker.error=login_required (no page is shown); only then show your login.html. Without access to the service the error is access_denied.# B: no session of its own → try silently GET https://ae-sso.advantech.com/authorize?client_id=svc_B&redirect_uri=https%3A%2F%2Fb.advantech.world%2Fauth%2Fcallback&response_type=code&state=RANDOM&prompt=none # already signed in (e.g. to A) → https://b.advantech.world/auth/callback?code=…&state=RANDOM # not signed in yet → https://b.advantech.world/auth/callback?error=login_required&state=RANDOM # no access to B → https://b.advantech.world/auth/callback?error=access_denied&state=RANDOM
<a href="https://b.advantech.world/reports">) — no token needs to be passed. B signs the user in automatically and lands on /reports./logout ends the SSO session but does not clear other services' own sessions; those expire on their own schedule.After sign-in, your site's own session is the source of truth — no need to ask the SSO on every request. In the examples:
| Language | Current user | Protect pages / APIs |
|---|---|---|
| Python / Flask | current_user() | @login_required |
| Node.js / Express | req.session.user | requireLogin |
| PHP | sso_user() | sso_require_login() |
| C# / ASP.NET Core | User.Identity.IsAuthenticated | .RequireAuthorization() |
| Java / Spring Boot | session.getAttribute("user") | RequireLogin filter |
| Go | currentUser(w, r) | requireLogin(h) |
/token also returns an access_token (valid 1 hour). To confirm the user still has access or to get fresh data, call GET https://ae-sso.advantech.com/userinfo while it is valid; after that, send the user through sign-in again (while their SSO session is valid (8 hours) they pass straight through without a password).sub as the user's key in your site (the Azure AD object ID, never changes); email addresses can change when someone is renamed.user in the POST /token response and the GET /userinfo response look like this. Which fields each service receives is set by an administrator under “Data this service may read” on the service page; sub, email, name and preferred_username are always sent. Fields appear only when Azure provides them.
Note: administrators can mask some fields of some people. A masked field is still present but its value is partly or fully * (e.g. j*******n@advantech.com, ******). Every person carries masked (true/false) and masked_fields (the hidden field names), so your code can tell directly. A signed-in person always sees their own data unmasked, but other people inside it (manager, reports …) may be masked. Identify people by sub.
{
"sub": "d3b9c1e2-…-azure-object-id",
"email": "john.chen@advantech.com",
"name": "John Chen",
"preferred_username": "john.chen@advantech.com",
"given_name": "John",
"family_name": "Chen",
"job_title": "Application Engineer",
"department": "AE",
"company": "Advantech",
"office_location": "Taipei",
"employee_id": "A12345",
"manager": { "name": "Mary Lin", "email": "mary.lin@advantech.com", "job_title": "Manager",
"masked": true, "masked_fields": ["mobile_phone"] },
"groups": ["AE Team", "All Employees"],
"picture": "https://ae-sso.advantech.com/photo/eyJ0Ijoi…",
"masked": false,
"masked_fields": []
}
| Field | What it does |
|---|---|
| sub | Unique user ID (Azure AD object ID) — use it as your key |
| masked, masked_fields | Every person (including each person inside manager, reports, group members …) carries these two fields: masked = whether any of this person's data is hidden (true/false); masked_fields = the names of the fields masked in this answer ([] when none). Use them to show "hidden" in your UI, or to avoid storing * as if it were real data. |
| email, preferred_username | Work email (lower case) |
| name | Display name |
| given_name, family_name | Given name / family name |
| job_title | Job title |
| department | Department |
| company | Company name |
| office_location | Office location |
| mobile_phone, business_phones | Mobile / business phones |
| employee_id | Employee ID |
| country, city | Country / city |
| manager | Manager's sub / name / email / job_title (taken from beta /me/profile when /me/manager is not permitted) |
| manager_chain | Manager chain: the direct manager first, up to the top (from beta /me/profile); without User.ReadBasic.All entries may carry only a sub |
| direct_reports | Direct reports' sub / name / email / job_title / department |
| groups | Names of the user's groups |
| picture | Photo URL for <img src> (absent when the user has no photo) |
| team_members, azure | People under the same manager (team_members), raw Microsoft Graph data (azure); off by default, an administrator must enable them |
Get the user again with the access_token:
curl -H "Authorization: Bearer $ACCESS_TOKEN" https://ae-sso.advantech.com/userinfo
On every sign-in the SSO reads the user's organization from Microsoft Graph: manager, manager_chain, direct reports (direct_reports) and people under the same manager (team_members). Which of them a service receives is set by an administrator under “Data this service may read” on the service page. When /me/manager is refused (403), the SSO takes the manager and manager chain from the position data of beta /me/profile, which needs only User.Read.
{
"sub": "…", "name": "Mary Lin", "email": "mary.lin@advantech.com",
"manager": { "sub": "…", "name": "David Wu", "email": "david.wu@advantech.com", "job_title": "Director" },
"manager_chain": [
{ "sub": "…", "name": "David Wu", "email": "david.wu@advantech.com", "job_title": "Director" },
{ "sub": "…", "name": "", "email": "", "job_title": "", "department": "" }
],
"direct_reports": [
{ "sub": "…", "name": "John Chen", "email": "john.chen@advantech.com", "job_title": "Engineer", "department": "AE" },
{ "sub": "…", "name": "Amy Lee", "email": "amy.lee@advantech.com", "job_title": "Engineer", "department": "AE" }
],
"team_members": [
{ "sub": "…", "name": "Kevin Huang", "email": "kevin.huang@advantech.com", "job_title": "Manager", "department": "FAE" }
]
}
Direct reports and team members need the Azure User.Read.All permission; with only User.Read these fields may be empty. The data is refreshed when the user signs in; administrators can also refresh it on the “Users” page with application permissions.
When a user signs in, the SSO fetches their 96×96 photo from Microsoft. Two ways to use it:
user.picture. It is a signed URL, valid for 7 days, that a browser can load directly without a token:
<img src="${user.picture}" alt="" width="96" height="96" style="border-radius:50%">
<!-- or let advantech-sso.js do it -->
<img data-sso-photo alt="">
GET https://ae-sso.advantech.com/userinfo/photo from the backend with the access_token; the response is the image itself (image/jpeg or image/png), or 404 when there is no photo.
curl -H "Authorization: Bearer $ACCESS_TOKEN" -o photo.jpg https://ae-sso.advantech.com/userinfo/photo
# Python
r = requests.get("https://ae-sso.advantech.com/userinfo/photo", headers={"Authorization": f"Bearer {access_token}"}, timeout=10)
if r.status_code == 200:
open(f"photos/{user['sub']}.jpg", "wb").write(r.content)
// Node.js
const r = await fetch("https://ae-sso.advantech.com/userinfo/photo", { headers: { Authorization: `Bearer ${accessToken}` } });
if (r.ok) await fs.promises.writeFile(`photos/${user.sub}.jpg`, Buffer.from(await r.arrayBuffer()));
https://ae-sso.advantech.com to img-src.picture, the front-end kit shows the user's initials instead.Any language that can make HTTP requests works. These are the complete requests:
# 1. browser → SSO
GET https://ae-sso.advantech.com/authorize?client_id=svc_xxxxxxxx&redirect_uri=https%3A%2F%2Fmyapp.advantech.world%2Fauth%2Fcallback&response_type=code&state=RANDOM
# 2. SSO → browser → your site
GET https://myapp.advantech.world/auth/callback?code=ONE_TIME_CODE&state=RANDOM
# 3. your backend → SSO (server to server)
curl -X POST https://ae-sso.advantech.com/token \
-d grant_type=authorization_code \
-d code=ONE_TIME_CODE \
-d redirect_uri=https://myapp.advantech.world/auth/callback \
-d client_id=svc_xxxxxxxx \
-d client_secret=$SSO_CLIENT_SECRET
# response
{ "access_token": "…", "token_type": "Bearer", "expires_in": 3600,
"user": { "sub": "…", "email": "…", "name": "…", "picture": "…" } }
# 4. later (optional, while the access_token is valid)
curl -H "Authorization: Bearer $ACCESS_TOKEN" https://ae-sso.advantech.com/userinfo
curl -H "Authorization: Bearer $ACCESS_TOKEN" https://ae-sso.advantech.com/userinfo/photo -o photo.jpg
# silent sign-in (single sign-on across services): add &prompt=none to step 1
# → ?code=… when already signed in, ?error=login_required otherwise
# other prompt values (select_account, login): /developers/advanced#prompt
# 5. sign out
GET https://ae-sso.advantech.com/logout?redirect_uri=https%3A%2F%2Fmyapp.advantech.world%2F
For servers that look up employees, managers, reports, groups and members with nobody signed in, see the People & Groups API: full guide, API reference and client libraries in 6 languages.
state and check it in the callback to prevent CSRF.code is single-use and valid for 60 seconds; redirect_uri must exactly match the one registered for the service.https://ae-sso.advantech.com/logout?redirect_uri=…; the redirect_uri must belong to a registered site.Advantech Single Sign-On