Advantech

3 ステップ、5 分で Advantech サインイン

手順どおりにコピー&ペーストするだけ。OAuth の知識も Azure の設定も不要です。

1SSO 管理者に Client ID と Client Secret をもらう

これを SSO 管理者に送ってください。Client ID と Client Secret の 2 つが返ってきます。

Advantech SSO にサービスを追加してください
サービス名:(サイトの名前)
Redirect URI: https://myapp.advantech.world/auth/callback

PHP 版は Redirect URI が異なります。手順 2 の PHP タブを見てください。Client Secret はパスワードです。バックエンドの環境変数だけに置き、Web ページや Git には入れないでください。

2言語を選び、テンプレートをダウンロードして実行

テンプレートはそのまま動く小さなプロジェクトです。サインインページ、トップページ、バックエンドができています。ダウンロードして解凍 → 手順 1 の 2 つの値をコマンドに入れる → 実行。

Python / Flask

テンプレート .zip をダウンロード (Python / Flask)

中身

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

実行コマンド (svc_xxxxxxxx / xxxxxxxx を手順 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 では export X=Y の代わりに $env:X="Y" と書きます。

ローカルで試すときのサイトのアドレス: http://localhost:5000

テンプレートのすべてのファイル (バックエンド + フロントエンド HTML。クリックで表示)

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

テンプレート .zip をダウンロード (Node.js / Express)

中身

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

実行コマンド (svc_xxxxxxxx / xxxxxxxx を手順 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 では export X=Y の代わりに $env:X="Y" と書きます。

ローカルで試すときのサイトのアドレス: http://localhost:3000

テンプレートのすべてのファイル (バックエンド + フロントエンド HTML。クリックで表示)

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

テンプレート .zip をダウンロード (PHP)

中身

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

実行コマンド (svc_xxxxxxxx / xxxxxxxx を手順 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 では export X=Y の代わりに $env:X="Y" と書きます。

ローカルで試すときのサイトのアドレス: http://localhost:8000

PHP 版で登録する Redirect URI: https://myapp.advantech.world/sso.php?a=callback

テンプレートのすべてのファイル (バックエンド + フロントエンド HTML。クリックで表示)

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

テンプレート .zip をダウンロード (C# / ASP.NET Core)

中身

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

実行コマンド (svc_xxxxxxxx / xxxxxxxx を手順 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 では export X=Y の代わりに $env:X="Y" と書きます。

ローカルで試すときのサイトのアドレス: http://localhost:5000

テンプレートのすべてのファイル (バックエンド + フロントエンド HTML。クリックで表示)

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

テンプレート .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

実行コマンド (svc_xxxxxxxx / xxxxxxxx を手順 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 では export X=Y の代わりに $env:X="Y" と書きます。

ローカルで試すときのサイトのアドレス: http://localhost:8080

テンプレートのすべてのファイル (バックエンド + フロントエンド HTML。クリックで表示)

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

テンプレート .zip をダウンロード (Go)

中身

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

実行コマンド (svc_xxxxxxxx / xxxxxxxx を手順 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 では export X=Y の代わりに $env:X="Y" と書きます。

ローカルで試すときのサイトのアドレス: http://localhost:8080

テンプレートのすべてのファイル (バックエンド + フロントエンド HTML。クリックで表示)

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>

3サイトを開いて確認

  1. サイトのトップページを開く: https://myapp.advantech.world/
  2. サインインページに移動します (他のサービスでサインイン済みならそのまま入れます)。「Advantech アカウントでサインイン」を押します。
  3. トップページに戻って名前と写真が表示されたら完了です!

うまくいかない場合は うまくいかない?

サイトとサインインページがすでにある場合

1) バックエンドに、手順 2 のテンプレートから /auth/login、/auth/callback、/auth/me、/auth/logout の 4 つのルートをコピーします (テンプレートのファイルをそのまま使うのが最速)。 2) サインインページにこの 3 行を貼ると「Advantech アカウントでサインイン」ボタンが表示されます:

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

ボタンの文字は既定でブラウザーの言語に従います (繁中 / 簡中 / 英 / 日 / 韓)。 言語 · ボタンのオプション (言語、スタイル、区切り線)

フロントエンドキット (HTML / CSS / JS)

フレームワークは不要です。ページにこの 2 行を加えるだけで、サインインボタン、ユーザーバッジ (写真 + 名前 + サインアウト)、項目表示、未サインイン時の自動リダイレクトが使えます。スクリプトは自サイトの /auth/* だけを呼び、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>

SSO 上のファイルを直接参照するか、ダウンロードして自サイトに置いてください (本番では自サイトに置くことを推奨)。

advantech-sso.js advantech-sso.css login.html index.html

サインインページ login.html

public/login.html として保存します。バックエンドは未サインインのユーザーを /login.html?next=元の URL に、失敗時は /login.html?error=… に送ります。サインイン済みのユーザーは 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>

保護されたページ index.html

public/index.html として保存します。data-require-login で未サインインのユーザーは自動的にサインインへ。データ自体はバックエンドで保護してください。

<!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 属性

属性説明
data-me / data-login / data-logout<script> に付けて、バックエンドのパスを変更 (既定は /auth/me、/auth/login、/auth/logout)。
data-login-text / data-logout-text<script> に付けて、ユーザーバッジのサインイン / サインアウトの文字を変更 (既定は言語に応じて自動翻訳)。
data-require-login<script> に付けると、未サインインならまずシングルサインオンを試み、必要なときだけサインインページを表示。
data-login-page<script> に付けて、このページがサインインページであることを示す。サインイン済みなら ?next= へ。
data-sso="button"デザインの変更は不要です。既存のサインインページの <head> に CSS と JS を加え、ボタンを置きたい場所に <div data-sso="button"></div> を追加すると、「Advantech アカウントでサインイン」ボタンが表示されます。
data-sso="login"クリックでサインイン (data-next="/path" でサインイン後のページを指定可)。
data-sso="logout"クリックでサインアウト (サイトと SSO の両方)。
data-sso="user"ユーザーバッジ (写真・名前・メール・サインアウトリンク) を表示。未サインイン時はサインインボタン。暗い背景では class="adv-sso-on-dark" を追加。
data-sso-field="…"ユーザー項目を表示。例: name、email、department、job_title、manager.name。
data-sso-photo<img> に付けてユーザーの写真を表示 (写真がなければ非表示)。
data-sso-show="in|out"サインイン中 (in) または未サインイン (out) のときだけ表示。
data-sso-jsonユーザー JSON 全体を表示 (開発中に便利)。
data-sso-errorURL に ?error= があるときだけ表示 (サインインページのエラー表示)。

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);
});

やりたいこと

やりたいこと方法
バックエンドで「今だれか」を知る
Python / Flaskcurrent_user()
Node.js / Expressreq.session.user
PHPsso_user()
C# / ASP.NET CoreUser.Identity.IsAuthenticated
Java / Spring Bootsession.getAttribute("user")
GocurrentUser(w, r)
ユーザー JSON (sub、email、name、department…) が得られます。ユーザー ID には sub を使います。
ページや API をサインイン必須にする
Python / Flask@login_required
Node.js / ExpressrequireLogin
PHPsso_require_login()
C# / ASP.NET Core.RequireAuthorization()
Java / Spring BootRequireLogin filter
GorequireLogin(h)
ページに名前や部署を表示<span data-sso-field="name"></span> <span data-sso-field="department"></span>
写真を表示<img data-sso-photo alt=""> または <img src="${user.picture}">
ヘッダーに「写真 + 名前 + サインアウト」<div data-sso="user"></div>
サインアウトボタン<a href="/auth/logout">Logout</a>
上司や部下を取得user.manager.name, user.manager_chain, user.direct_reports — 組織データ: 上司・部下・同じ上司のメンバー
もっと項目 (携帯番号など) がほしいが取れない各サービスが取得できる項目は SSO 管理者が設定します。サービスページの「このサービスが取得できるデータ」でチェックを入れてもらってください。 ユーザー情報 (profile)
他のシステムから来たユーザーに再サインインさせたくない何もしなくて大丈夫です。テンプレートで対応済み (自動シングルサインオン)。 サービス間のシングルサインオン (A でサインインしたら B では不要)

うまくいかない?

表示されたもの解決方法
SSO に「redirect_uri が登録されていない」と出るサイトのアドレスと登録した Redirect URI が完全に一致していません。http / https、ドメイン、ポート、パスすべて同じにしてください (例: http://localhost:5000/auth/callback)。APP_URL を確認するか、管理者に登録アドレスを直してもらってください。
サインインページに戻り、URL に error=tokenClient ID か Client Secret が間違っています。環境変数が設定されているか、余分な空白がないか確認し、変更後はプログラムを再起動してください。
URL に error=stateサインイン中にセッションが失われました (テンプレートのサインインページは 1 回自動で再試行します)。それでも出る場合のよくある原因: APP_URL が https なのに http で開いている、ホストが違う (127.0.0.1 と localhost は別の cookie)、再起動のたびにセッションの鍵が変わる (FLASK_SECRET_KEY / SESSION_SECRET)、同じ PC の localhost 上の別サイトが同名の cookie を使っている (cookie はポートを区別しません)。APP_URL とまったく同じアドレスで開き、セッションの鍵を固定してください。
SSO に「権限がない」と出る、または error=access_deniedアカウントがこのサービスの許可リストにない (または拒否リストにある) ためです。SSO 管理者に連絡してください。
項目 (例: mobile_phone) が含まれないサービスにその項目が許可されていないか、Azure に値がありません。管理者にサービスページの「このサービスが取得できるデータ」を確認してもらってください (プレビューもあります)。
写真が表示されない写真が未設定、サービスに写真の権限がない、またはサイトの CSP の img-src に https://ae-sso.advantech.com がありません。
サインイン後、何度もサインインページに戻されるバックエンドがユーザーをセッションに保存できていません。ブラウザーで /auth/me を開いてください。自分のデータ (200) が出れば正常、401 ならセッションが保存されていません (たいてい error=state と同じ原因)。
サインアウト後にサインインすると、パスワードなしで同じユーザーに戻るサイトと SSO はサインアウトしていますが、Microsoft がまだ覚えています。サインアウト後の最初のサインインでは Microsoft の「アカウントを選択」画面が出るので、別のアカウントを選べます。毎回パスワードを求めたい場合は、SSO 管理者に「Azure AD 設定 → サインアウトの動作」で「Microsoft からもサインアウト」をオンにしてもらうか、ログアウト URL に microsoft=1 を付けてください。

詳細リファレンス

仕組み、すべての URL、項目の説明、HTML 属性、HTTP の詳細。上の手順どおりなら読む必要はありません。

この SSO の URL

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 写真 (Bearer アクセストークン)
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

サインインの流れ

すべてのサイトが同じ流れ (OAuth 2.0 Authorization Code) を使います。Client Secret はバックエンドだけにあり、ブラウザーには渡りません。

  1. ユーザーがサイトを開く。バックエンドはセッションがないので、まず /auth/login?silent=1 でそのままサインインを試みます (下の「サービス間のシングルサインオン」参照)。どのサービスにもまだサインインしていない場合だけ /login.html を表示します。
  2. サインインボタン → バックエンドの /auth/login がランダムな state を作り、https://ae-sso.advantech.com/authorize にリダイレクト。
  3. ユーザーが Microsoft でサインイン (他のサービスでサインイン済みならそのまま通過)。
  4. SSO が /auth/callback?code=…&state=… に戻す。バックエンドは state を確認し、code と Client Secret で POST https://ae-sso.advantech.com/token を呼んでユーザー情報を取得。
  5. バックエンドがユーザー情報を自分のセッションに保存。以降はセッションを見るだけで判定できます。
  6. フロントエンドは /auth/me で名前・部署・写真を表示。

サインインページがすでにある場合: 1 行でボタンを追加

デザインの変更は不要です。既存のサインインページの <head> に CSS と JS を加え、ボタンを置きたい場所に <div data-sso="button"></div> を追加すると、「Advantech アカウントでサインイン」ボタンが表示されます。

<!-- 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>

表示例 (プレビュー。ボタンを押してもサインインしません):

ボタンはサイトのバックエンドの /auth/login にリンクします (Client Secret はバックエンドに置く必要があります)。そのため、バックエンドには「バックエンドの例」の /auth/login と /auth/callback が必要です。パスが違う場合は <script> の data-login か、ボタンの data-href で指定します。

属性説明
data-next="/path"サインイン後に戻るページ (既定は URL の ?next= か ?returnUrl=、なければトップ)。
data-text="…"ボタンの文字を変更。
data-lang="ja"このボタンだけの言語 (zh-TW / zh-CN / en / ja / ko)。指定しない場合は下の「言語」のルールに従います。
data-variant="outline"outline = 白地に青枠のサブボタン。
data-width="auto"auto = 文字の幅に合わせる。既定は全幅。
data-divider="top|bottom"ボタンの上 (top) か下 (bottom) に「または」の区切り線を追加。ID / パスワードのフォームの横に置くときに便利。
data-href="…"ボタンのリンク先を直接指定 (例: PHP の /sso.php?a=login)。

ページにボタンしかない場合、スクリプトは /auth/me を呼ばないので、既存のサインインページに影響しません。

言語

ボタン、「または」の区切り線、ユーザーバッジのサインイン / サインアウトの文字は自動で翻訳されます。既定ではブラウザーの言語に従います (繁体字・簡体字中国語、英語、日本語、韓国語。それ以外は英語)。言語を固定・切り替えるには次のいずれかを使います (上ほど優先):

  1. ボタン単位: data-lang
  2. コードで切り替え (サイトの言語メニューなど): AdvSSO.setLang("ja") ですぐ再描画。AdvSSO.setLang("auto") でブラウザーの言語に戻ります。
  3. サイト全体で固定: script タグに data-lang、またはスクリプトの前に window.AdvSSOConfig。"page" にすると <html lang> に従います。
  4. 他の言語の追加や文字の変更: AdvSSOConfig.texts または 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>

バックエンドの例

どの例もそのまま動く 1 ファイルで、同じ 5 つのルートを持つので、フロントエンドキットはどれとも組み合わせられます:

ルート説明
GET /auth/loginstate を作って SSO にリダイレクト。?next= はサインイン後に戻るページ。?silent=1 は prompt=none を付け、SSO にサインイン済みのときだけ通過します。
GET /auth/callbackstate を確認し、error (例: login_required) があればサインインページへ。なければ code と Client Secret で /token を呼び、user をセッションに保存。
GET /auth/meサインイン中ならユーザー JSON、そうでなければ 401。
GET /auth/logoutサイトのセッションを消し、SSO からもサインアウトしてサイトのトップに戻る。
/ , /api/*保護されたページと API: 未サインインのページ要求はまず /auth/login?silent=1&next=… へ、API は 401。

言語を選び、テンプレートをダウンロードして実行

サービス間のシングルサインオン (A でサインインしたら B では不要)

どれかのサービスでサインインすると、SSO は 8 時間サインイン状態を保持します (SSO ドメインの cookie)。その後、別のサービスがユーザーを /authorize に送ると、SSO はすぐに code を付けて戻し、Microsoft のサインイン画面は出ません。

クリックも不要にするには、サービスは自分のセッションがないとき、先にサインインページを出さずに次のようにします:

  1. https://ae-sso.advantech.com/authorize?…&prompt=none にリダイレクト (例では /auth/login?silent=1)。
  2. SSO にサインイン済み → すぐに code 付きで戻るので、通常どおり交換します。ユーザーにはアドレスバーが一瞬変わるだけです。
  3. まだサインインしていない → error=login_required 付きで戻る (画面は出ません)。このときだけ login.html を表示します。サービスの権限がない場合は error=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

バックエンドはどうやってサインイン状態を知るのか

サインイン後の判定基準は自サイトのセッションです。リクエストのたびに SSO に問い合わせる必要はありません。各例での書き方:

言語現在のユーザーページ / API の保護
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)

ユーザー情報 (profile)

POST /token の応答の user と GET /userinfo の応答は次の形式です。各サービスが受け取る項目は、管理者がサービスページの「このサービスが取得できるデータ」で設定します。sub、email、name、preferred_username は常に含まれます。Azure が提供する項目だけが含まれます。

注意: 管理者は特定の人の特定の項目をマスキングできます。マスキングされた項目は存在しますが、値の一部または全部が * になります (例: j*******n@advantech.com、******)。各人に masked (true/false) と masked_fields (隠された項目名) が付くので、プログラムで直接判定できます。サインインした本人の自分のデータはマスキングされませんが、その中の他の人 (上司、部下など) はマスキングされることがあります。人の識別には 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": []
}
項目説明
subユーザーの一意な ID (Azure AD オブジェクト ID)。キーとして使用
masked, masked_fieldsすべての人 (manager、部下、グループ メンバーなどの中の各人を含む) にこの 2 項目があります: masked = この人のデータが隠されているか (true/false)、masked_fields = この応答でマスキングされた項目名の一覧 (ない場合は [])。UI で「非表示」を示したり、* を本物のデータとして保存しないために使えます。
email, preferred_username会社のメール (小文字)
name表示名
given_name, family_name名 / 姓
job_title役職
department部署
company会社名
office_locationオフィス
mobile_phone, business_phones携帯 / 会社の電話
employee_id社員番号
country, city国 / 都市
manager上司の sub / name / email / job_title (/me/manager の権限がない場合は beta /me/profile から取得)
manager_chain上司のチェーン: 直属の上司が先頭、最上位まで (beta /me/profile から)。User.ReadBasic.All がないと sub だけの場合あり
direct_reports直属の部下の sub / name / email / job_title / department
groups所属グループ名
picture<img src> にそのまま使える写真の URL (写真がなければ含まれない)
team_members, azure同じ上司のメンバー (team_members)、Microsoft Graph の生データ (azure)。既定では無効で、管理者が有効にする必要があります

access_token でユーザー情報を再取得:

curl -H "Authorization: Bearer $ACCESS_TOKEN" https://ae-sso.advantech.com/userinfo

組織データ: 上司・部下・同じ上司のメンバー

サインインのたびに、SSO は Microsoft Graph から組織情報 (manager、manager_chain、直属の部下 direct_reports、同じ上司のメンバー team_members) を取得します。どれをサービスに返すかは、管理者がサービスページの「このサービスが取得できるデータ」で設定します。/me/manager が拒否された (403) 場合は、beta /me/profile の職位データから上司と上司のチェーンを取得します (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" }
  ]
}

部下と同じ上司のメンバーには Azure の User.Read.All 権限が必要です。User.Read だけでは空になることがあります。データはサインイン時に更新され、管理者は「ユーザー」ページからアプリケーション権限で更新することもできます。

写真 (photo)

ユーザーがサインインすると、SSO は Microsoft から 96×96 の写真を取得します。使い方は 2 通り:

  1. いちばん簡単: user.picture を使います。署名付き URL で 7 日間有効、トークンなしでブラウザーが直接読み込めます:
    <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. 自サーバーに保存する場合: バックエンドから access_token 付きで GET https://ae-sso.advantech.com/userinfo/photo を呼ぶと、画像そのもの (image/jpeg または image/png) が返ります。写真がなければ 404。
    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()));

その他の言語: HTTP を直接呼ぶ

HTTP リクエストを送れる言語なら何でも接続できます。リクエストの全内容:

# 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

ユーザー・グループ API

誰もサインインしていない状態でサーバーから社員、上司、部下、グループ、メンバーを参照するには、「ユーザー・グループ API」のガイド、API リファレンス、6 言語のクライアント ライブラリをご覧ください。

ユーザー・グループ API ガイド →

注意事項

Advantech Single Sign-On