Advantech

3 steps, 5 minutes to Advantech sign-in

Follow along and copy-paste. No OAuth knowledge and no Azure setup needed.

1Get a Client ID and Client Secret from the SSO administrator

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.

2Pick your language, download the starter and run it

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.

Python / Flask

Download starter .zip (Python / Flask)

What's inside

myapp/
flask_app.py
requirements.txt
public/login.html
public/index.html
README.txt

Commands (replace svc_xxxxxxxx / xxxxxxxx with the values from step 1)

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

Every file in the starter (backend + front-end HTML; click to view)

flask_app.py
# 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)
requirements.txt
flask>=3.0
requests>=2.31
public/login.html
<!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>
public/index.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>

Node.js / Express

Download starter .zip (Node.js / Express)

What's inside

myapp/
express_app.mjs
package.json
public/login.html
public/index.html
README.txt

Commands (replace svc_xxxxxxxx / xxxxxxxx with the values from step 1)

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

Every file in the starter (backend + front-end HTML; click to view)

express_app.mjs
// 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"));
package.json
{
  "name": "myapp",
  "private": true,
  "type": "module",
  "scripts": { "start": "node express_app.mjs" },
  "dependencies": {
    "express": "^4.21.2",
    "express-session": "^1.18.1"
  }
}
public/login.html
<!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>
public/index.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>

PHP

Download starter .zip (PHP)

What's inside

myapp/
sso.php
login.html
index.html
README.txt

Commands (replace svc_xxxxxxxx / xxxxxxxx with the values from step 1)

# 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

Every file in the starter (backend + front-end HTML; click to view)

sso.php
<?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);
}
login.html
<!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>
index.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>

C# / ASP.NET Core

Download starter .zip (C# / ASP.NET Core)

What's inside

myapp/
MyApp.csproj
Program.cs
wwwroot/login.html
wwwroot/index.html
README.txt

Commands (replace svc_xxxxxxxx / xxxxxxxx with the values from step 1)

# .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

Every file in the starter (backend + front-end HTML; click to view)

MyApp.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
Program.cs
// 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();
wwwroot/login.html
<!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>
wwwroot/index.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>

Java / Spring Boot

Download starter .zip (Java / Spring Boot)

What's inside

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

Commands (replace svc_xxxxxxxx / xxxxxxxx with the values from step 1)

# 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

Every file in the starter (backend + front-end HTML; click to view)

pom.xml
<?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>
src/main/java/com/example/myapp/MyAppApplication.java
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);
    }
}
src/main/java/com/example/myapp/SsoController.java
// 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));
    }
}
src/main/resources/application.properties
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
src/main/resources/static/login.html
<!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>
src/main/resources/static/index.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>

Go

Download starter .zip (Go)

What's inside

myapp/
main.go
go.mod
public/login.html
public/index.html
README.txt

Commands (replace svc_xxxxxxxx / xxxxxxxx with the values from step 1)

# 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

Every file in the starter (backend + front-end HTML; click to view)

main.go
// 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))
}
go.mod
module myapp

go 1.22
public/login.html
<!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>
public/index.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>

3Open your site and test

  1. Open your site's home page: https://myapp.advantech.world/
  2. You are sent to the sign-in page (or straight in if you already signed in to another service). Click “Sign in with Advantech account”.
  3. Back on the home page you see your name and photo — done!

Didn't work? See Something went wrong?

Already have a site and a sign-in page?

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)

Front-end kit (HTML / CSS / JS)

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

Sign-in page login.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>

Protected page index.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>

HTML attributes

AttributeWhat it does
data-me / data-login / data-logoutOn the <script> tag: change the backend paths (default /auth/me, /auth/login, /auth/logout).
data-login-text / data-logout-textOn the <script> tag: the badge's sign-in / sign-out texts (translated automatically by default).
data-require-loginOn the <script> tag: when signed out, try single sign-on automatically and show the sign-in page only if needed.
data-login-pageOn 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-photoOn 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-jsonShows the whole user JSON — handy while developing.
data-sso-errorShown only when the URL has ?error= (error message on the sign-in page).

JavaScript API

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…

I want to…How
Know who is signed in, on the backend
Python / Flaskcurrent_user()
Node.js / Expressreq.session.user
PHPsso_user()
C# / ASP.NET CoreUser.Identity.IsAuthenticated
Java / Spring Bootsession.getAttribute("user")
GocurrentUser(w, r)
You get the user JSON (sub, email, name, department …). Use sub as the user ID.
Require sign-in for a page or API
Python / Flask@login_required
Node.js / ExpressrequireLogin
PHPsso_require_login()
C# / ASP.NET Core.RequireAuthorization()
Java / Spring BootRequireLogin filter
GorequireLogin(h)
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 reportsuser.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 themThe 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 againNothing 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)

Something went wrong?

What you seeFix
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 URLThe 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 URLThe 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_deniedYour account is not on this service's allowlist (or is on the denylist). Ask the SSO administrator.
A field (e.g. mobile_phone) is missingThe 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 showThe 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 pageThe 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 passwordYour 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.

Advanced reference

How it works, every URL, the fields, HTML attributes and HTTP details. Not needed if you followed the steps above.

This SSO's URLs

Authorize URL
https://ae-sso.advantech.com/authorize
Token URL
https://ae-sso.advantech.com/token
Userinfo URL
https://ae-sso.advantech.com/userinfo
Photo URL
https://ae-sso.advantech.com/userinfo/photo Photo (Bearer access token)
Logout URL
https://ae-sso.advantech.com/logout
JS
https://ae-sso.advantech.com/assets/js/advantech-sso.js
CSS
https://ae-sso.advantech.com/assets/css/advantech-sso.css

Sign-in flow

Every site uses the same flow (OAuth 2.0 Authorization Code). The Client Secret lives only on your backend; the browser never sees it.

  1. The user opens your site; the backend sees no session → first tries /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.
  2. The sign-in button → your backend's /auth/login creates a random state and redirects to https://ae-sso.advantech.com/authorize.
  3. The user signs in with Microsoft (or passes straight through if already signed in to another service).
  4. The SSO redirects to /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.
  5. The backend stores the user in its own session; from then on every request just checks the session.
  6. The front end calls /auth/me to show the name, department and photo.

Already have a sign-in page? Add the button with one line

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.

<!-- 1. in <head> -->
<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>

<!-- 2. where the button should appear -->
<div data-sso="button"></div>

<!-- e.g. under an existing username / password form -->
<form method="post" action="/login"> … </form>
<div data-sso="button" data-divider="top"></div>

What it looks like (preview — the button does not sign in):

The button links to your backend's /auth/login (the Client Secret must stay on the backend), so the backend still needs the /auth/login and /auth/callback routes from the “Backend examples”. For other paths use data-login on the <script> or data-href on the button.

AttributeWhat it does
data-next="/path"Page to return to after sign-in (default: ?next= or ?returnUrl= from the URL, else the home page).
data-text="…"Custom button text.
data-lang="ja"Language of this one button (zh-TW / zh-CN / en / ja / ko). Without it the “Language” rules below apply.
data-variant="outline"outline = secondary button, white with a blue border.
data-width="auto"auto = as wide as the text; full width by default.
data-divider="top|bottom"Adds an “or” divider above (top) or below (bottom) the button — handy next to a username/password form.
data-href="…"Sets the button's link directly (e.g. /sso.php?a=login for PHP).

When the page has only the button, the script never calls /auth/me, so your existing sign-in page is unaffected.

Language

The button, the “or” divider and the badge's sign-in / sign-out texts are translated automatically. By default they follow the browser's language (Traditional and Simplified Chinese, English, Japanese, Korean; anything else shows English). To fix or switch the language use any of these (top wins):

  1. One button: data-lang
  2. Switch from code (e.g. your site's language menu): AdvSSO.setLang("ja") re-renders immediately; AdvSSO.setLang("auto") goes back to the browser's language.
  3. Fix it for the whole site: data-lang on the script tag, or window.AdvSSOConfig before the script. "page" follows <html lang> instead.
  4. Add another language or change texts: AdvSSOConfig.texts or AdvSSO.addTexts().
<!-- 1. one button -->
<div data-sso="button" data-lang="ja"></div>

<!-- 2. your own language menu -->
<select onchange="AdvSSO.setLang(this.value)">
  <option value="auto">Auto</option><option value="zh-TW">繁體中文</option>
  <option value="en">English</option><option value="ja">日本語</option>
</select>

<!-- 3. the whole site (either line) -->
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-lang="zh-TW" defer></script>
<script>window.AdvSSOConfig = { lang: "zh-TW" };</script>   <!-- before advantech-sso.js -->

<!-- 4. another language / your own wording -->
<script>
window.AdvSSOConfig = {
  texts: {
    th: { button: "เข้าสู่ระบบด้วยบัญชี Advantech", or: "หรือ", signIn: "เข้าสู่ระบบ", signOut: "ออกจากระบบ" },
    en: { button: "Continue with Advantech" }
  }
};
</script>

Backend examples

Each example is a single runnable file with the same five routes, so the front-end kit works with all of them:

RouteWhat it does
GET /auth/loginCreates 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/callbackChecks 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/meReturns the user JSON when signed in, 401 otherwise.
GET /auth/logoutClears 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.

Pick your language, download the starter and run it

Single sign-on across services (signed in to A, no sign-in again for B)

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:

  1. Redirect to https://ae-sso.advantech.com/authorize?…&prompt=none (/auth/login?silent=1 in the examples).
  2. Signed in to the SSO → it comes straight back with a code; exchange it as usual. The user only sees the address bar flicker.
  3. Not signed in yet → it comes back with 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

How does the backend know the user is signed in?

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:

LanguageCurrent userProtect pages / APIs
Python / Flaskcurrent_user()@login_required
Node.js / Expressreq.session.userrequireLogin
PHPsso_user()sso_require_login()
C# / ASP.NET CoreUser.Identity.IsAuthenticated.RequireAuthorization()
Java / Spring Bootsession.getAttribute("user")RequireLogin filter
GocurrentUser(w, r)requireLogin(h)

User profile

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": []
}
FieldWhat it does
subUnique user ID (Azure AD object ID) — use it as your key
masked, masked_fieldsEvery 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_usernameWork email (lower case)
nameDisplay name
given_name, family_nameGiven name / family name
job_titleJob title
departmentDepartment
companyCompany name
office_locationOffice location
mobile_phone, business_phonesMobile / business phones
employee_idEmployee ID
country, cityCountry / city
managerManager's sub / name / email / job_title (taken from beta /me/profile when /me/manager is not permitted)
manager_chainManager chain: the direct manager first, up to the top (from beta /me/profile); without User.ReadBasic.All entries may carry only a sub
direct_reportsDirect reports' sub / name / email / job_title / department
groupsNames of the user's groups
picturePhoto URL for <img src> (absent when the user has no photo)
team_members, azurePeople 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

Organization data: manager, direct reports, team members

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.

Photo

When a user signs in, the SSO fetches their 96×96 photo from Microsoft. Two ways to use it:

  1. Easiest: use 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="">
  2. To store it on your server: call 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()));

Other languages: plain HTTP

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

People & Groups API

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.

People & Groups API guide →

Notes

Advantech Single Sign-On