手順どおりにコピー&ペーストするだけ。OAuth の知識も Azure の設定も不要です。
これを 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 には入れないでください。
テンプレートはそのまま動く小さなプロジェクトです。サインインページ、トップページ、バックエンドができています。ダウンロードして解凍 → 手順 1 の 2 つの値をコマンドに入れる → 実行。
テンプレート .zip をダウンロード (Python / Flask)
myapp/ flask_app.py requirements.txt public/login.html public/index.html README.txt
pip install -r requirements.txt export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world export FLASK_SECRET_KEY=change-me-to-a-long-random-string python flask_app.py
Windows PowerShell では export X=Y の代わりに $env:X="Y" と書きます。
ローカルで試すときのサイトのアドレス: http://localhost:5000
# Advantech SSO + Flask — pip install flask requests
#
# export SSO_CLIENT_ID=svc_xxxxxxxx
# export SSO_CLIENT_SECRET=xxxxxxxx
# export APP_URL=https://myapp.advantech.world # this site; register APP_URL/auth/callback as Redirect URI
# export FLASK_SECRET_KEY=$(python -c "import secrets;print(secrets.token_hex(32))")
# python flask_app.py (put login.html / index.html in ./public)
import os, secrets
from functools import wraps
from urllib.parse import urlencode
import requests
from flask import Flask, abort, jsonify, redirect, request, send_from_directory, session
SSO = "https://ae-sso.advantech.com"
CLIENT_ID = os.environ["SSO_CLIENT_ID"]
CLIENT_SECRET = os.environ["SSO_CLIENT_SECRET"]
APP_URL = os.environ["APP_URL"].rstrip("/")
REDIRECT_URI = APP_URL + "/auth/callback"
# the session is a signed cookie (max ~4 KB), so keep only the fields you use
KEEP = ("sub", "email", "name", "given_name", "family_name", "job_title", "department",
"company", "office_location", "employee_id", "manager", "picture")
app = Flask(__name__, static_folder="public", static_url_path="")
app.secret_key = os.environ["FLASK_SECRET_KEY"]
app.config.update(SESSION_COOKIE_SECURE=APP_URL.startswith("https://"),
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax")
def safe_next(v):
return v if v and v.startswith("/") and not v.startswith("//") else "/"
# ---------- 1. sign in ----------
# /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY
# service, the SSO returns straight away (no page, no click); otherwise it comes
# back with error=login_required and we show login.html.
@app.get("/auth/login")
def login():
state = secrets.token_urlsafe(16)
# remember several pending sign-ins (two tabs, a double click ...), newest 5
pending = dict(list(session.get("pending", {}).items())[-4:])
pending[state] = safe_next(request.args.get("next"))
session["pending"] = pending
q = {"client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI,
"response_type": "code", "state": state}
if request.args.get("silent") == "1":
q["prompt"] = "none"
return redirect(f"{SSO}/authorize?{urlencode(q)}")
@app.get("/auth/callback")
def callback():
pending = session.get("pending", {})
next_url = pending.pop(request.args.get("state", ""), None)
session["pending"] = pending
if next_url is None: # unknown / reused state
return redirect("/login.html?error=state")
if request.args.get("error"): # login_required (silent try) / access_denied
q = {"next": next_url}
if request.args["error"] != "login_required":
q["error"] = request.args["error"]
return redirect("/login.html?" + urlencode(q))
r = requests.post(f"{SSO}/token", data={
"grant_type": "authorization_code",
"code": request.args.get("code", ""),
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}, timeout=10)
if r.status_code != 200:
return redirect("/login.html?error=token")
user = r.json()["user"]
session["user"] = {k: user[k] for k in KEEP if k in user}
return redirect(next_url)
@app.get("/auth/logout")
def logout():
session.clear()
return redirect(f"{SSO}/logout?" + urlencode({"redirect_uri": APP_URL + "/"}))
# ---------- 2. is the user signed in? ----------
def current_user():
return session.get("user") # None = not signed in
def login_required(view):
@wraps(view)
def wrapper(*args, **kwargs):
if current_user() is None:
if request.path.startswith("/api/"):
abort(401)
# try single sign-on first; login.html is shown only if that fails
return redirect("/auth/login?" + urlencode({"silent": 1, "next": request.full_path.rstrip("?")}))
return view(*args, **kwargs)
return wrapper
# ---------- 3. user data (used by advantech-sso.js) ----------
@app.get("/auth/me")
def me():
u = current_user()
return (jsonify(u), 200) if u else (jsonify(error="not signed in"), 401)
# ---------- your app ----------
@app.get("/")
@login_required
def index():
return send_from_directory("public", "index.html")
@app.get("/api/hello")
@login_required
def hello():
u = current_user()
return {"message": f"Hello {u['name']}", "department": u.get("department")}
if __name__ == "__main__":
app.run(port=5000)
flask>=3.0 requests>=2.31
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
テンプレート .zip をダウンロード (Node.js / Express)
myapp/ express_app.mjs package.json public/login.html public/index.html README.txt
npm install export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world export SESSION_SECRET=change-me-to-a-long-random-string npm start
Windows PowerShell では export X=Y の代わりに $env:X="Y" と書きます。
ローカルで試すときのサイトのアドレス: http://localhost:3000
// Advantech SSO + Node.js / Express — npm i express express-session (Node 18+)
//
// SSO_CLIENT_ID=svc_xxxxxxxx SSO_CLIENT_SECRET=xxxxxxxx \
// APP_URL=https://myapp.advantech.world SESSION_SECRET=long-random-string \
// node express_app.mjs (put login.html / index.html in ./public)
//
// Register APP_URL/auth/callback as the Redirect URI of the service.
// express-session keeps sessions in memory: use a store (Redis, DB ...) in production.
import crypto from "node:crypto";
import express from "express";
import session from "express-session";
const SSO = "https://ae-sso.advantech.com";
const { SSO_CLIENT_ID: CLIENT_ID, SSO_CLIENT_SECRET: CLIENT_SECRET } = process.env;
const APP_URL = process.env.APP_URL.replace(/\/$/, "");
const REDIRECT_URI = `${APP_URL}/auth/callback`;
const app = express();
app.set("trust proxy", 1);
app.use(session({
secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false,
cookie: { httpOnly: true, secure: APP_URL.startsWith("https://"), sameSite: "lax", maxAge: 8 * 3600e3 },
}));
const safeNext = (v) => (typeof v === "string" && v.startsWith("/") && !v.startsWith("//") ? v : "/");
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY
// service, the SSO returns straight away (no page, no click); otherwise it comes
// back with error=login_required and we show login.html.
app.get("/auth/login", (req, res) => {
const state = crypto.randomBytes(16).toString("hex");
// remember several pending sign-ins (two tabs, a double click ...), newest 5
const pending = Object.entries(req.session.pending || {}).slice(-4);
req.session.pending = Object.fromEntries([...pending, [state, safeNext(req.query.next)]]);
const q = new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI,
response_type: "code", state });
if (req.query.silent === "1") q.set("prompt", "none");
res.redirect(`${SSO}/authorize?${q}`);
});
app.get("/auth/callback", async (req, res) => {
const pending = req.session.pending || {};
const next = Object.hasOwn(pending, String(req.query.state)) ? pending[String(req.query.state)] : null;
delete pending[String(req.query.state)];
if (next === null) return res.redirect("/login.html?error=state"); // unknown / reused state
if (req.query.error) { // login_required (silent try) / access_denied
const q = new URLSearchParams({ next });
if (req.query.error !== "login_required") q.set("error", String(req.query.error));
return res.redirect(`/login.html?${q}`);
}
const r = await fetch(`${SSO}/token`, {
method: "POST",
body: new URLSearchParams({ grant_type: "authorization_code", code: String(req.query.code || ""),
redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
});
if (!r.ok) return res.redirect("/login.html?error=token");
const { user } = await r.json();
req.session.regenerate(() => { // new session id after sign-in
req.session.user = user; // sub, email, name, department, picture ...
res.redirect(next);
});
});
app.get("/auth/logout", (req, res) => {
req.session.destroy(() => {
res.redirect(`${SSO}/logout?` + new URLSearchParams({ redirect_uri: `${APP_URL}/` }));
});
});
// ---------- 2. is the user signed in? ----------
function requireLogin(req, res, next) {
if (req.session.user) return next();
if (req.path.startsWith("/api/")) return res.status(401).json({ error: "not signed in" });
// try single sign-on first; login.html is shown only if that fails
res.redirect("/auth/login?" + new URLSearchParams({ silent: "1", next: req.originalUrl }));
}
// ---------- 3. user data (used by advantech-sso.js) ----------
app.get("/auth/me", (req, res) => {
if (!req.session.user) return res.status(401).json({ error: "not signed in" });
res.json(req.session.user);
});
// ---------- your app ----------
app.get("/", requireLogin, (req, res) => res.sendFile("index.html", { root: "public" }));
app.get("/api/hello", requireLogin, (req, res) => {
res.json({ message: `Hello ${req.session.user.name}`, department: req.session.user.department });
});
app.use(express.static("public")); // login.html etc.
app.listen(3000, () => console.log("http://localhost:3000"));
{
"name": "myapp",
"private": true,
"type": "module",
"scripts": { "start": "node express_app.mjs" },
"dependencies": {
"express": "^4.21.2",
"express-session": "^1.18.1"
}
}
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
myapp/ sso.php login.html index.html README.txt
# PHP 8+ with the curl extension export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world php -S localhost:8000 # on Apache/nginx: copy the files to the web root and set the 3 variables # (Apache: SetEnv SSO_CLIENT_ID ... nginx: fastcgi_param SSO_CLIENT_ID ...;)
Windows PowerShell では export X=Y の代わりに $env:X="Y" と書きます。
ローカルで試すときのサイトのアドレス: http://localhost:8000
PHP 版で登録する Redirect URI: https://myapp.advantech.world/sso.php?a=callback
<?php
// Advantech SSO + PHP (8.0+, no framework) — save as sso.php next to login.html / index.html
//
// Set SSO_CLIENT_ID, SSO_CLIENT_SECRET, APP_URL (e.g. https://myapp.advantech.world)
// in the web server environment (or edit the constants below).
// Register APP_URL/sso.php?a=callback as the Redirect URI of the service.
// In the HTML, point advantech-sso.js at this file:
// <script src=".../advantech-sso.js" data-me="/sso.php?a=me"
// data-login="/sso.php?a=login" data-logout="/sso.php?a=logout"></script>
//
// Protect any other PHP page with:
// require __DIR__ . '/sso.php';
// $user = sso_require_login(); // signs in silently, or shows login.html
// echo htmlspecialchars($user['name']);
const SSO = 'https://ae-sso.advantech.com';
define('CLIENT_ID', getenv('SSO_CLIENT_ID'));
define('CLIENT_SECRET', getenv('SSO_CLIENT_SECRET'));
define('APP_URL', rtrim(getenv('APP_URL'), '/'));
define('REDIRECT_URI', APP_URL . '/sso.php?a=callback');
session_set_cookie_params(['httponly' => true, 'secure' => str_starts_with(APP_URL, 'https://'), 'samesite' => 'Lax']);
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
function sso_safe_next($v) {
return is_string($v) && str_starts_with($v, '/') && !str_starts_with($v, '//') ? $v : '/';
}
// ---------- 2. is the user signed in? ----------
function sso_user() {
return $_SESSION['user'] ?? null; // null = not signed in
}
function sso_require_login() {
$u = sso_user();
if ($u === null) {
// try single sign-on first; login.html is shown only if that fails
header('Location: /sso.php?' . http_build_query(['a' => 'login', 'silent' => 1, 'next' => $_SERVER['REQUEST_URI']]));
exit;
}
return $u;
}
// only route when sso.php itself is requested (not when it is require'd)
if (realpath($_SERVER['SCRIPT_FILENAME']) !== __FILE__) return;
switch ($_GET['a'] ?? '') {
// ---------- 1. sign in ----------
// ?silent=1 adds prompt=none: if the user already signed in to ANY service,
// the SSO returns straight away (no page, no click); otherwise it comes back
// with error=login_required and we show login.html.
case 'login':
$state = bin2hex(random_bytes(16));
// remember several pending sign-ins (two tabs, a double click ...), newest 5
$pending = array_slice($_SESSION['pending'] ?? [], -4, null, true);
$pending[$state] = sso_safe_next($_GET['next'] ?? '/');
$_SESSION['pending'] = $pending;
$q = ['client_id' => CLIENT_ID, 'redirect_uri' => REDIRECT_URI,
'response_type' => 'code', 'state' => $state];
if (($_GET['silent'] ?? '') === '1') $q['prompt'] = 'none';
header('Location: ' . SSO . '/authorize?' . http_build_query($q));
exit;
case 'callback':
$state = (string)($_GET['state'] ?? '');
$next = $_SESSION['pending'][$state] ?? null;
unset($_SESSION['pending'][$state]);
if ($state === '' || $next === null) { // unknown / reused state
header('Location: /login.html?error=state'); exit;
}
if (!empty($_GET['error'])) { // login_required (silent try) / access_denied
$q = ['next' => $next];
if ($_GET['error'] !== 'login_required') $q['error'] = $_GET['error'];
header('Location: /login.html?' . http_build_query($q)); exit;
}
$ch = curl_init(SSO . '/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code', 'code' => $_GET['code'] ?? '',
'redirect_uri' => REDIRECT_URI, 'client_id' => CLIENT_ID, 'client_secret' => CLIENT_SECRET,
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) { header('Location: /login.html?error=token'); exit; }
session_regenerate_id(true);
$_SESSION['user'] = json_decode($body, true)['user']; // sub, email, name, department, picture ...
header('Location: ' . $next);
exit;
case 'logout':
$_SESSION = [];
session_destroy();
header('Location: ' . SSO . '/logout?' . http_build_query(['redirect_uri' => APP_URL . '/']));
exit;
// ---------- 3. user data (used by advantech-sso.js) ----------
case 'me':
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$u = sso_user();
if ($u === null) { http_response_code(401); echo '{"error":"not signed in"}'; exit; }
echo json_encode($u, JSON_UNESCAPED_UNICODE);
exit;
default:
http_response_code(404);
}
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-me="/sso.php?a=me" data-login="/sso.php?a=login" data-logout="/sso.php?a=logout" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-me="/sso.php?a=me" data-login="/sso.php?a=login" data-logout="/sso.php?a=logout" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
テンプレート .zip をダウンロード (C# / ASP.NET Core)
myapp/ MyApp.csproj Program.cs wwwroot/login.html wwwroot/index.html README.txt
# .NET 8 SDK export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world dotnet run --urls http://localhost:5000
Windows PowerShell では export X=Y の代わりに $env:X="Y" と書きます。
ローカルで試すときのサイトのアドレス: http://localhost:5000
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
// Advantech SSO + ASP.NET Core (.NET 8, minimal API)
//
// Environment variables: SSO_CLIENT_ID, SSO_CLIENT_SECRET, APP_URL (e.g. https://myapp.advantech.world)
// dotnet run (login.html / index.html in wwwroot/)
// Register APP_URL/auth/callback as the Redirect URI of the service.
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
const string SSO = "https://ae-sso.advantech.com";
var builder = WebApplication.CreateBuilder(args);
var clientId = builder.Configuration["SSO_CLIENT_ID"]!;
var clientSecret = builder.Configuration["SSO_CLIENT_SECRET"]!;
var appUrl = builder.Configuration["APP_URL"]!.TrimEnd('/');
var redirectUri = appUrl + "/auth/callback";
builder.Services.AddHttpClient();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.LoginPath = "/login.html"; // ?ReturnUrl=... is appended
o.ExpireTimeSpan = TimeSpan.FromHours(8);
o.Cookie.SameSite = SameSiteMode.Lax;
o.Events.OnRedirectToLogin = ctx => // APIs get 401 instead of a redirect
{
if (ctx.Request.Path.StartsWithSegments("/api") || ctx.Request.Path.StartsWithSegments("/auth/me"))
ctx.Response.StatusCode = 401;
else // try single sign-on first; login.html is shown only if that fails
ctx.Response.Redirect("/auth/login?silent=1&next=" + Uri.EscapeDataString(ctx.Request.Path + ctx.Request.QueryString));
return Task.CompletedTask;
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
static string SafeNext(string? v) => v is { Length: > 0 } && v.StartsWith('/') && !v.StartsWith("//") ? v : "/";
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY service,
// the SSO returns straight away (no page, no click); otherwise it comes back with
// error=login_required and we show login.html.
app.MapGet("/auth/login", (HttpContext ctx, string? next, string? silent) =>
{
var state = Convert.ToHexString(RandomNumberGenerator.GetBytes(16));
// one short-lived cookie per pending sign-in, so two tabs don't overwrite each other
var opts = new CookieOptions { HttpOnly = true, Secure = ctx.Request.IsHttps, SameSite = SameSiteMode.Lax, MaxAge = TimeSpan.FromMinutes(10) };
ctx.Response.Cookies.Append("sso_st_" + state, SafeNext(next), opts);
var p = new Dictionary<string, string?> {
["client_id"] = clientId, ["redirect_uri"] = redirectUri, ["response_type"] = "code", ["state"] = state };
if (silent == "1") p["prompt"] = "none";
var q = QueryString.Create(p);
return Results.Redirect($"{SSO}/authorize{q}");
});
app.MapGet("/auth/callback", async (HttpContext ctx, IHttpClientFactory http, string? code, string? state, string? error) =>
{
var key = "sso_st_" + (state ?? "");
var saved = string.IsNullOrEmpty(state) || state.Length != 32 ? null : ctx.Request.Cookies[key];
if (saved is null) return Results.Redirect("/login.html?error=state"); // unknown / reused state
ctx.Response.Cookies.Delete(key);
var next = SafeNext(saved);
if (!string.IsNullOrEmpty(error)) // login_required (silent try) / access_denied
return Results.Redirect("/login.html?next=" + Uri.EscapeDataString(next)
+ (error == "login_required" ? "" : "&error=" + Uri.EscapeDataString(error)));
var res = await http.CreateClient().PostAsync($"{SSO}/token", new FormUrlEncodedContent(new Dictionary<string, string> {
["grant_type"] = "authorization_code", ["code"] = code ?? "", ["redirect_uri"] = redirectUri,
["client_id"] = clientId, ["client_secret"] = clientSecret }));
if (!res.IsSuccessStatusCode) return Results.Redirect("/login.html?error=token");
var user = JsonNode.Parse(await res.Content.ReadAsStringAsync())!["user"]!;
var claims = new List<Claim> {
new("sub", (string)user["sub"]!),
new(ClaimTypes.Name, (string?)user["name"] ?? ""),
new(ClaimTypes.Email, (string?)user["email"] ?? ""),
new("sso_user", user.ToJsonString()), // the whole user JSON (department, picture ...)
};
await ctx.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)));
return Results.Redirect(next);
});
app.MapGet("/auth/logout", async (HttpContext ctx) =>
{
await ctx.SignOutAsync();
return Results.Redirect($"{SSO}/logout?redirect_uri={Uri.EscapeDataString(appUrl + "/")}");
});
// ---------- 2. is the user signed in? ctx.User.Identity.IsAuthenticated / .RequireAuthorization() ----------
static JsonNode? SsoUser(ClaimsPrincipal p) =>
p.Identity?.IsAuthenticated == true ? JsonNode.Parse(p.FindFirst("sso_user")?.Value ?? "{}") : null;
// ---------- 3. user data (used by advantech-sso.js) ----------
app.MapGet("/auth/me", (ClaimsPrincipal p) => Results.Content(SsoUser(p)!.ToJsonString(), "application/json"))
.RequireAuthorization();
// ---------- your app ----------
app.MapGet("/", () => Results.File(Path.Combine(app.Environment.WebRootPath, "index.html"), "text/html"))
.RequireAuthorization();
app.MapGet("/api/hello", (ClaimsPrincipal p) =>
{
var u = SsoUser(p)!;
return Results.Json(new { message = $"Hello {u["name"]}", department = (string?)u["department"] });
}).RequireAuthorization();
app.Run();
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
テンプレート .zip をダウンロード (Java / Spring Boot)
myapp/ pom.xml src/main/java/com/example/myapp/MyAppApplication.java src/main/java/com/example/myapp/SsoController.java src/main/resources/application.properties src/main/resources/static/login.html src/main/resources/static/index.html README.txt
# Java 17+ and Maven export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world mvn spring-boot:run
Windows PowerShell では export X=Y の代わりに $env:X="Y" と書きます。
ローカルで試すときのサイトのアドレス: http://localhost:8080
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.example.myapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
// Advantech SSO + Java / Spring Boot 3 (spring-boot-starter-web)
//
// application.properties:
// sso.client-id=${SSO_CLIENT_ID}
// sso.client-secret=${SSO_CLIENT_SECRET}
// sso.app-url=${APP_URL}
// server.servlet.session.cookie.same-site=lax
// Register APP_URL/auth/callback as the Redirect URI of the service.
// Put login.html / index.html in src/main/resources/static/.
package com.example.myapp;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClient;
import org.springframework.web.filter.OncePerRequestFilter;
@RestController
public class SsoController {
static final String SSO = "https://ae-sso.advantech.com";
@Value("${sso.client-id}") String clientId;
@Value("${sso.client-secret}") String clientSecret;
@Value("${sso.app-url}") String appUrl;
private final RestClient http = RestClient.create();
String redirectUri() { return appUrl.replaceAll("/$", "") + "/auth/callback"; }
static String enc(String s) { return URLEncoder.encode(s, StandardCharsets.UTF_8); }
static String safeNext(String v) { return v != null && v.startsWith("/") && !v.startsWith("//") ? v : "/"; }
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY service,
// the SSO returns straight away (no page, no click); otherwise it comes back with
// error=login_required and we show login.html.
@GetMapping("/auth/login")
public void login(@RequestParam(required = false) String next, @RequestParam(required = false) String silent,
HttpSession session, HttpServletResponse res) throws IOException {
byte[] b = new byte[16];
new SecureRandom().nextBytes(b);
String state = HexFormat.of().formatHex(b);
// remember several pending sign-ins (two tabs, a double click ...), newest 5
@SuppressWarnings("unchecked")
var pending = (java.util.LinkedHashMap<String, String>) session.getAttribute("pending");
if (pending == null) pending = new java.util.LinkedHashMap<>();
while (pending.size() >= 5) pending.remove(pending.keySet().iterator().next());
pending.put(state, safeNext(next));
session.setAttribute("pending", pending);
res.sendRedirect(SSO + "/authorize?client_id=" + enc(clientId) + "&redirect_uri=" + enc(redirectUri())
+ "&response_type=code&state=" + state + ("1".equals(silent) ? "&prompt=none" : ""));
}
@GetMapping("/auth/callback")
@SuppressWarnings("unchecked")
public void callback(@RequestParam(defaultValue = "") String code, @RequestParam(defaultValue = "") String state,
@RequestParam(required = false) String error,
HttpServletRequest req, HttpServletResponse res) throws IOException {
HttpSession session = req.getSession();
var pending = (Map<String, String>) session.getAttribute("pending");
String next = pending == null ? null : pending.remove(state);
if (next == null) { res.sendRedirect("/login.html?error=state"); return; } // unknown / reused state
if (error != null) { // login_required (silent try) / access_denied
res.sendRedirect("/login.html?next=" + enc(next) + ("login_required".equals(error) ? "" : "&error=" + enc(error)));
return;
}
var form = new LinkedMultiValueMap<String, String>();
form.add("grant_type", "authorization_code");
form.add("code", code);
form.add("redirect_uri", redirectUri());
form.add("client_id", clientId);
form.add("client_secret", clientSecret);
Map<String, Object> body;
try {
body = http.post().uri(SSO + "/token").contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(form).retrieve().body(Map.class);
} catch (Exception e) {
res.sendRedirect("/login.html?error=token");
return;
}
req.changeSessionId(); // new session id after sign-in
req.getSession().setAttribute("user", body.get("user")); // sub, email, name, department, picture ...
res.sendRedirect(next);
}
@GetMapping("/auth/logout")
public void logout(HttpSession session, HttpServletResponse res) throws IOException {
session.invalidate();
res.sendRedirect(SSO + "/logout?redirect_uri=" + enc(appUrl.replaceAll("/$", "") + "/"));
}
// ---------- 3. user data (used by advantech-sso.js) ----------
@GetMapping("/auth/me")
public ResponseEntity<Object> me(HttpSession session) {
Object user = session.getAttribute("user");
return user == null ? ResponseEntity.status(401).body(Map.of("error", "not signed in")) : ResponseEntity.ok(user);
}
// ---------- your app ----------
@GetMapping("/api/hello")
public Map<String, Object> hello(@SessionAttribute("user") Map<String, Object> user) {
return Map.of("message", "Hello " + user.get("name"));
}
}
// ---------- 2. is the user signed in? (every request except the public ones) ----------
@Component
class RequireLogin extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest req) {
String p = req.getRequestURI();
return p.startsWith("/auth/") || p.equals("/login.html") || p.equals("/favicon.ico");
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
HttpSession s = req.getSession(false);
if (s != null && s.getAttribute("user") != null) { chain.doFilter(req, res); return; }
if (req.getRequestURI().startsWith("/api/")) { res.sendError(401); return; }
String next = req.getRequestURI() + (req.getQueryString() != null ? "?" + req.getQueryString() : "");
// try single sign-on first; login.html is shown only if that fails
res.sendRedirect("/auth/login?silent=1&next=" + URLEncoder.encode(next, StandardCharsets.UTF_8));
}
}
sso.client-id=${SSO_CLIENT_ID}
sso.client-secret=${SSO_CLIENT_SECRET}
sso.app-url=${APP_URL}
server.port=8080
server.servlet.session.cookie.same-site=lax
server.servlet.session.cookie.http-only=true
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
myapp/ main.go go.mod public/login.html public/index.html README.txt
# Go 1.22+ export SSO_CLIENT_ID=svc_xxxxxxxx export SSO_CLIENT_SECRET=xxxxxxxx export APP_URL=https://myapp.advantech.world go run .
Windows PowerShell では export X=Y の代わりに $env:X="Y" と書きます。
ローカルで試すときのサイトのアドレス: http://localhost:8080
// Advantech SSO + Go (standard library only, Go 1.22+)
//
// SSO_CLIENT_ID=svc_xxxxxxxx SSO_CLIENT_SECRET=xxxxxxxx \
// APP_URL=https://myapp.advantech.world go run . (login.html / index.html in ./public)
//
// Register APP_URL/auth/callback as the Redirect URI of the service.
// Sessions live in memory here; use a database or Redis when you run several instances.
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
)
const SSO = "https://ae-sso.advantech.com"
var (
clientID = os.Getenv("SSO_CLIENT_ID")
clientSecret = os.Getenv("SSO_CLIENT_SECRET")
appURL = strings.TrimRight(os.Getenv("APP_URL"), "/")
redirectURI = appURL + "/auth/callback"
)
type User map[string]any // sub, email, name, department, job_title, manager, picture ...
type session struct {
User User
Pending map[string]string // state -> page to return to (several tabs may sign in at once)
Expires time.Time
}
var (
mu sync.Mutex
sessions = map[string]*session{}
)
func randHex() string { b := make([]byte, 16); rand.Read(b); return hex.EncodeToString(b) }
func getSession(w http.ResponseWriter, r *http.Request) *session {
mu.Lock()
defer mu.Unlock()
if c, err := r.Cookie("sid"); err == nil {
if s := sessions[c.Value]; s != nil && time.Now().Before(s.Expires) {
return s
}
}
id, s := randHex(), &session{Expires: time.Now().Add(8 * time.Hour)}
sessions[id] = s
http.SetCookie(w, &http.Cookie{Name: "sid", Value: id, Path: "/", HttpOnly: true,
Secure: strings.HasPrefix(appURL, "https://"), SameSite: http.SameSiteLaxMode})
return s
}
func safeNext(v string) string {
if strings.HasPrefix(v, "/") && !strings.HasPrefix(v, "//") {
return v
}
return "/"
}
// ---------- 2. is the user signed in? ----------
func currentUser(w http.ResponseWriter, r *http.Request) User { return getSession(w, r).User } // nil = no
func requireLogin(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if currentUser(w, r) == nil {
if strings.HasPrefix(r.URL.Path, "/api/") {
http.Error(w, `{"error":"not signed in"}`, http.StatusUnauthorized)
return
}
// try single sign-on first; login.html is shown only if that fails
http.Redirect(w, r, "/auth/login?"+url.Values{"silent": {"1"}, "next": {r.URL.RequestURI()}}.Encode(), http.StatusFound)
return
}
h(w, r)
}
}
func main() {
// ---------- 1. sign in ----------
// /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY
// service, the SSO returns straight away (no page, no click); otherwise it
// comes back with error=login_required and we show login.html.
http.HandleFunc("GET /auth/login", func(w http.ResponseWriter, r *http.Request) {
s := getSession(w, r)
state := randHex()
mu.Lock()
if s.Pending == nil || len(s.Pending) >= 5 {
s.Pending = map[string]string{}
}
s.Pending[state] = safeNext(r.URL.Query().Get("next"))
mu.Unlock()
q := url.Values{"client_id": {clientID}, "redirect_uri": {redirectURI},
"response_type": {"code"}, "state": {state}}
if r.URL.Query().Get("silent") == "1" {
q.Set("prompt", "none")
}
http.Redirect(w, r, SSO+"/authorize?"+q.Encode(), http.StatusFound)
})
http.HandleFunc("GET /auth/callback", func(w http.ResponseWriter, r *http.Request) {
s := getSession(w, r)
state := r.URL.Query().Get("state")
mu.Lock()
next, ok := s.Pending[state]
delete(s.Pending, state)
mu.Unlock()
if !ok { // unknown / reused state
http.Redirect(w, r, "/login.html?error=state", http.StatusFound)
return
}
if e := r.URL.Query().Get("error"); e != "" { // login_required (silent try) / access_denied
q := url.Values{"next": {next}}
if e != "login_required" {
q.Set("error", e)
}
http.Redirect(w, r, "/login.html?"+q.Encode(), http.StatusFound)
return
}
res, err := http.PostForm(SSO+"/token", url.Values{
"grant_type": {"authorization_code"}, "code": {r.URL.Query().Get("code")},
"redirect_uri": {redirectURI}, "client_id": {clientID}, "client_secret": {clientSecret}})
if err != nil || res.StatusCode != http.StatusOK {
http.Redirect(w, r, "/login.html?error=token", http.StatusFound)
return
}
defer res.Body.Close()
var body struct{ User User }
if json.NewDecoder(res.Body).Decode(&body) != nil {
http.Redirect(w, r, "/login.html?error=token", http.StatusFound)
return
}
mu.Lock()
s.User = body.User
mu.Unlock()
http.Redirect(w, r, next, http.StatusFound)
})
http.HandleFunc("GET /auth/logout", func(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("sid"); err == nil {
mu.Lock()
delete(sessions, c.Value)
mu.Unlock()
}
http.Redirect(w, r, SSO+"/logout?"+url.Values{"redirect_uri": {appURL + "/"}}.Encode(), http.StatusFound)
})
// ---------- 3. user data (used by advantech-sso.js) ----------
http.HandleFunc("GET /auth/me", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
u := currentUser(w, r)
if u == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"not signed in"}`))
return
}
json.NewEncoder(w).Encode(u)
})
// ---------- your app ----------
http.HandleFunc("GET /{$}", requireLogin(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
}))
http.HandleFunc("GET /api/hello", requireLogin(func(w http.ResponseWriter, r *http.Request) {
u := currentUser(w, r)
json.NewEncoder(w).Encode(map[string]any{"message": "Hello " + u["name"].(string), "department": u["department"]})
}))
http.Handle("GET /", http.FileServer(http.Dir("public")))
log.Fatal(http.ListenAndServe(":8080", nil))
}
module myapp go 1.22
<!doctype html>
<!--
Advantech SSO sign-in page. Save as public/login.html on your site.
Your backend sends signed-out visitors here (login.html?next=/page), and
failed sign-ins here with ?error=... . Change "My App" and the texts freely.
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登入 · My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-login-page defer></script>
</head>
<body class="adv-sso-page">
<main class="adv-sso-card">
<div class="adv-sso-card-logo">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="992" height="202">
</div>
<div class="adv-sso-card-body">
<h1 class="adv-sso-title">My App</h1>
<p class="adv-sso-lead">請使用 Advantech 公司帳號 (Microsoft) 登入。</p>
<p class="adv-sso-info" data-sso-error="state" hidden>登入逾時或被中斷了,請再按一次登入。</p>
<p class="adv-sso-error" data-sso-error="access_denied" hidden>你的帳號沒有這個網站的使用權限,請聯絡網站管理員。</p>
<p class="adv-sso-error" data-sso-error hidden>登入沒有成功,請再試一次。如果一直發生,請聯絡網站管理員。</p>
<button type="button" class="adv-sso-btn adv-sso-block" data-sso="login">
<svg class="adv-sso-ms" viewBox="0 0 21 21" aria-hidden="true"><path fill="#f25022" d="M1 1h9v9H1z"/><path fill="#7fba00" d="M11 1h9v9h-9z"/><path fill="#00a4ef" d="M1 11h9v9H1z"/><path fill="#ffb900" d="M11 11h9v9h-9z"/></svg>
使用 Advantech 帳號登入
</button>
<p class="adv-sso-foot">Advantech Single Sign-On</p>
</div>
</main>
</body>
</html>
<!doctype html>
<!--
A protected page. Save as public/index.html on your site.
data-require-login sends signed-out visitors to sign in automatically.
(Protect the data on the server too - see the backend examples.)
-->
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="https://ae-sso.advantech.com/assets/css/advantech-sso.css">
<script src="https://ae-sso.advantech.com/assets/js/advantech-sso.js" data-require-login defer></script>
<style>
body { margin: 0; font-family: var(--adv-sso-font); color: var(--adv-sso-ink); background: var(--adv-sso-paper); }
main { max-width: 860px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--adv-sso-line); border-radius: 6px; padding: 20px 24px; margin-bottom: 20px; }
.profile { display: flex; gap: 20px; align-items: center; }
.profile img { width: 96px; height: 96px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
dt { color: var(--adv-sso-muted); }
dd { margin: 0; }
pre { background: #0f2236; color: #e6eef7; padding: 14px; border-radius: 4px; overflow: auto; font-size: 13px; }
</style>
</head>
<body>
<header class="adv-sso-header">
<img src="https://ae-sso.advantech.com/static/logo.svg" alt="Advantech" width="157" height="32">
<div data-sso="user" class="adv-sso-on-dark"></div>
</header>
<main data-sso-show="in">
<section class="card profile">
<img class="adv-sso-avatar" data-sso-photo alt="" hidden>
<div>
<h1 style="margin:0">你好,<span data-sso-field="name"></span></h1>
<div data-sso-field="email"></div>
</div>
</section>
<section class="card">
<dl>
<dt>部門</dt><dd data-sso-field="department"></dd>
<dt>職稱</dt><dd data-sso-field="job_title"></dd>
<dt>公司</dt><dd data-sso-field="company"></dd>
<dt>辦公室</dt><dd data-sso-field="office_location"></dd>
<dt>主管</dt><dd data-sso-field="manager.name"></dd>
</dl>
</section>
<section class="card">
<h2 style="margin-top:0">/auth/me</h2>
<pre data-sso-json></pre>
</section>
</main>
</body>
</html>
https://myapp.advantech.world/うまくいかない場合は うまくいかない?
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>
ボタンの文字は既定でブラウザーの言語に従います (繁中 / 簡中 / 英 / 日 / 韓)。 言語 · ボタンのオプション (言語、スタイル、区切り線)
フレームワークは不要です。ページにこの 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
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>
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>
| 属性 | 説明 |
|---|---|
| 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-error | URL に ?error= があるときだけ表示 (サインインページのエラー表示)。 |
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);
});
| やりたいこと | 方法 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| バックエンドで「今だれか」を知る |
| ||||||||||||
| ページや API をサインイン必須にする |
| ||||||||||||
| ページに名前や部署を表示 | <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=token | Client 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 の詳細。上の手順どおりなら読む必要はありません。
すべてのサイトが同じ流れ (OAuth 2.0 Authorization Code) を使います。Client Secret はバックエンドだけにあり、ブラウザーには渡りません。
/auth/login?silent=1 でそのままサインインを試みます (下の「サービス間のシングルサインオン」参照)。どのサービスにもまだサインインしていない場合だけ /login.html を表示します。/auth/login がランダムな state を作り、https://ae-sso.advantech.com/authorize にリダイレクト。/auth/callback?code=…&state=… に戻す。バックエンドは state を確認し、code と Client Secret で POST https://ae-sso.advantech.com/token を呼んでユーザー情報を取得。/auth/me で名前・部署・写真を表示。どの例もそのまま動く 1 ファイルで、同じ 5 つのルートを持つので、フロントエンドキットはどれとも組み合わせられます:
| ルート | 説明 |
|---|---|
| GET /auth/login | state を作って SSO にリダイレクト。?next= はサインイン後に戻るページ。?silent=1 は prompt=none を付け、SSO にサインイン済みのときだけ通過します。 |
| GET /auth/callback | state を確認し、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。 |
どれかのサービスでサインインすると、SSO は 8 時間サインイン状態を保持します (SSO ドメインの cookie)。その後、別のサービスがユーザーを /authorize に送ると、SSO はすぐに code を付けて戻し、Microsoft のサインイン画面は出ません。
クリックも不要にするには、サービスは自分のセッションがないとき、先にサインインページを出さずに次のようにします:
https://ae-sso.advantech.com/authorize?…&prompt=none にリダイレクト (例では /auth/login?silent=1)。code 付きで戻るので、通常どおり交換します。ユーザーにはアドレスバーが一瞬変わるだけです。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
<a href="https://b.advantech.world/reports">)。トークンを渡す必要はなく、B が自動でサインインして /reports を表示します。/logout は SSO のサインイン状態を終了しますが、各サービス自身のセッションは消しません。それぞれの有効期限で切れます。サインイン後の判定基準は自サイトのセッションです。リクエストのたびに SSO に問い合わせる必要はありません。各例での書き方:
| 言語 | 現在のユーザー | ページ / API の保護 |
|---|---|---|
| Python / Flask | current_user() | @login_required |
| Node.js / Express | req.session.user | requireLogin |
| PHP | sso_user() | sso_require_login() |
| C# / ASP.NET Core | User.Identity.IsAuthenticated | .RequireAuthorization() |
| Java / Spring Boot | session.getAttribute("user") | RequireLogin filter |
| Go | currentUser(w, r) | requireLogin(h) |
/token は access_token (有効期間 1 時間) も返します。ユーザーがまだアクセスできるか確認したいときや最新の情報が必要なときは、有効期間内に GET https://ae-sso.advantech.com/userinfo を呼びます。期限切れ後はもう一度サインインしてもらいます (SSO のセッション (8 時間) が有効ならパスワード入力なしで通過します)。sub (Azure AD のオブジェクト ID、変わらない) を使ってください。メールアドレスは改名で変わることがあります。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 だけでは空になることがあります。データはサインイン時に更新され、管理者は「ユーザー」ページからアプリケーション権限で更新することもできます。
ユーザーがサインインすると、SSO は Microsoft から 96×96 の写真を取得します。使い方は 2 通り:
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="">
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()));
img-src に https://ae-sso.advantech.com を追加してください。picture がない場合、フロントエンドキットはイニシャルを表示します。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 リファレンス、6 言語のクライアント ライブラリをご覧ください。
state を作り、コールバックで照合してください (CSRF 対策)。code は 1 回限り、60 秒間有効です。redirect_uri はサービスに登録したものと完全に一致させてください。https://ae-sso.advantech.com/logout?redirect_uri=… にリダイレクトします。redirect_uri は登録済みサイトの URL である必要があります。Advantech Single Sign-On