跳到內容
主控台

OAuth2

第三方 App 可以用 OAuth2 授權碼流程(Authorization Code + PKCE),在使用者同意後取得一個代表該使用者的 API Token。取得的 token 和使用者在主控台自己建立的 token 相同,用量計入該使用者的帳號與配額。

所有 OAuth2 端點都在 https://manager.exptech.com.tw。

  • 授權請求(GET):不需要。使用者在官方網站登入並同意。
  • 換取 token:client_id、PKCE code_verifier;機密 client 另需 client_secret。
  • 使用者資訊:Authorization: Bearer <access_token>。

目前沒有自助註冊介面。請透過關於頁的聯絡方式申請,並提供:

  • App 名稱與一句說明(會顯示在同意頁,也會成為使用者 token 列表中的備註)
  • redirect_uri 清單:授權後導回的網址,可以是 https:// 網址或 App 的自訂 scheme(例如 myapp://oauth/callback)。比對方式是完全相同的字串,不支援萬用字元或前綴比對
  • 需要的權限
  • client 類型:
    • 公開 client(手機 App、桌面 App):沒有 client_secret,只靠 PKCE。
    • 機密 client(有自己伺服器的網站):換 token 時要帶 client_secret,secret 只能放在伺服器端。
  1. App 產生隨機的 code_verifier,計算 code_challenge = base64url(SHA-256(code_verifier))(不含 = 填充),再產生一個隨機 state。兩者都存在使用者的 session 裡。

  2. 把使用者導向授權請求網址。

  3. 伺服器檢查參數後,把使用者導向官方網站的登入頁(未登入時)或同意頁。這一步不會發出授權碼。

  4. 使用者在同意頁按下同意或拒絕。同意頁會送出同意或拒絕的請求,伺服器再把使用者導回你的 redirect_uri:

    • 同意:redirect_uri?code=<授權碼>&state=<state>
    • 拒絕:redirect_uri?error=access_denied&state=<state>
  5. App 確認 state 與第 1 步相同,然後從伺服器或原生 App 呼叫換取 token,用 code 與 code_verifier 換到 access_token。

  6. (選用)呼叫取得使用者資訊,取得使用者 id 與電子郵件。

  7. 用 access_token 呼叫 API,帶法與一般 API Token 相同:SSE 串流用 Authorization: Bearer 或 query token,需要 token 的 HTTP API 用 X-Api-Token header。見 API Token。

把使用者的瀏覽器導向這個網址(頂層導向或開啟系統瀏覽器,不要用 fetch)。

GEThttps://manager.exptech.com.tw/api/v1/oauth2/authorize
伺服器Manager
Query 參數
  • response_typestring必填

    固定為 code

  • client_idstring必填

    申請時取得的 client id

  • redirect_uristring必填

    必須與註冊的其中一筆完全相同

  • code_challengestring必填

    base64url(SHA-256(code_verifier)),不含填充

  • code_challenge_methodstring必填

    固定為 S256,不支援 plain

  • scopestring選填

    以空白分隔的權限名稱或群組,例如 eq.report trem.rts。省略時 token 沒有任何權限

  • statestring選填

    防 CSRF 的隨機值,會原樣帶回 redirect_uri。強烈建議使用

請求範例
curl -i --compressed -G "https://manager.exptech.com.tw/api/v1/oauth2/authorize" \
--data-urlencode "response_type=code" \
--data-urlencode "client_id=<client_id>" \
--data-urlencode "redirect_uri=<redirect_uri>" \
--data-urlencode "code_challenge=<code_challenge>" \
--data-urlencode "code_challenge_method=S256"
回應

參數正確:導向官方登入頁或同意頁;參數錯誤:導回 redirect_uri 並帶 error

Location: https://app.example.com/oauth/callback?error=invalid_scope&state=af0ifjsldkj

client_id 或 redirect_uri 錯誤,不會導回 App(zstd 壓縮的 JSON)

{
"error": "invalid_redirect_uri"
}
試試看
Query 參數

Token 只會從你的瀏覽器直接送到 API。

client_id 或 redirect_uri 有問題時,伺服器不會導回任何網址(避免成為開放導向),而是直接回 400 JSON,使用者會停在錯誤頁。其他錯誤都會導回 redirect_uri,並在 query 帶上 error 與 state:

error 條件
unsupported_response_type response_type 不是 code
invalid_request 沒帶 code_challenge,或 code_challenge_method 不是 S256
invalid_scope scope 含有不存在的權限名稱
  • 實際發出的權限 = 請求的 scope ∩ client 註冊的權限。請求了 client 沒有註冊的權限不會報錯,只是不會發出。請以換 token 回應中的 scope 為準。
  • 群組(trem.*、eq.*)會展開成各個權限。
  • 不存在的名稱會讓整個請求失敗(invalid_scope),不會略過。

這個請求由官方同意頁在使用者按下按鈕時送出,需要使用者在官方網站的登入狀態。第三方 App 不需要、也無法直接呼叫。 列在這裡是為了說明導回 redirect_uri 的行為。

POSThttps://manager.exptech.com.tw/api/v1/oauth2/authorize
伺服器Manager
Query 參數
  • query 參數string

    與授權請求相同(response_type、client_id、redirect_uri、code_challenge、code_challenge_method、scope、state)

請求本文
  • actionstring選填

    表單欄位。值為 deny 時拒絕,其他值或省略都視為同意

請求範例
curl -i -X POST "https://manager.exptech.com.tw/api/v1/oauth2/authorize"
回應

同意:導回 redirect_uri 並帶 code;拒絕:帶 error=access_denied

Location: https://app.example.com/oauth/callback?code=4mXq9vKpL2sTzW8yRb1nCe7uJ0hGdA5fYiOkN3wPtQx&state=af0ifjsldkj
  • 授權碼 5 分鐘內有效,只能使用一次。
  • 使用者在同意頁停留太久、登入已過期時,會被導回登入頁,登入後再回到同意頁。
  • 伺服器無法建立授權碼時,導回 redirect_uri?error=server_error。

用授權碼換取 access_token。請求內容是 application/x-www-form-urlencoded 表單,不接受 JSON。

POSThttps://manager.exptech.com.tw/api/v1/oauth2/token
伺服器Manager
請求本文
  • grant_typestring必填

    固定為 authorization_code

  • codestring必填

    導回 redirect_uri 時拿到的授權碼

  • redirect_uristring必填

    必須與授權請求時的值完全相同

  • client_idstring必填

    必須與授權請求時的值相同

  • code_verifierstring必填

    第 1 步產生的 PKCE 原文

  • client_secretstring選填

    機密 client 必填;公開 client 不需要

請求範例
curl --compressed "https://manager.exptech.com.tw/api/v1/oauth2/token" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=<code>" \
--data-urlencode "redirect_uri=<redirect_uri>" \
--data-urlencode "client_id=<client_id>" \
--data-urlencode "code_verifier=<code_verifier>"
回應

成功(zstd 壓縮的 JSON)

{
"access_token": "et_3q2-7wAAexampleexampleexampleexampleexample",
"token_type": "Bearer",
"scope": "trem.rts eq.report"
}

授權碼無效、已使用、已過期,或 client_id、redirect_uri、PKCE 不符

{
"error": "invalid_grant"
}

client 不存在,或 client_secret 錯誤

{
"error": "invalid_client"
}

使用者的 token 已達 25 個

{
"error": "too_many_tokens"
}
欄位類型說明
access_tokenstringAPI Token(et_ 開頭)。只會在這個回應出現一次,請存在伺服器端
token_typestring固定為 Bearer
scopestring實際取得的權限,以空白分隔、展開後的名稱。沒有任何權限時省略這個欄位

回應沒有 expires_in 與 refresh_token:token 不會過期,直到使用者撤銷。

  • 授權碼在伺服器開始比對時就會作廢:得到 invalid_grant、too_many_tokens 或 server_error 之後,同一個授權碼不能再用,需要重新授權。invalid_request、unsupported_grant_type、invalid_client 發生在比對之前,授權碼仍可在有效期內重試。
  • 完整錯誤清單見錯誤碼。

用 access_token 取得授權使用者的 id 與電子郵件。不檢查 token 的權限,只要 token 有效即可。

GEThttps://manager.exptech.com.tw/api/v1/oauth2/userinfo
伺服器Manager
Header
  • Authorization必填

    Bearer <access_token>。Bearer 後面只能有一個空白

請求範例
curl --compressed "https://manager.exptech.com.tw/api/v1/oauth2/userinfo" \
-H "Authorization: Bearer <access_token>"
回應

成功(zstd 壓縮的 JSON)

{
"sub": "123",
"email": "[email protected]"
}

沒有 Bearer token,或 token 無效、已撤銷。沒有內容沒有回應本文

試試看
Header

Token 只會從你的瀏覽器直接送到 API。

欄位類型說明
substring使用者 id(十進位數字字串)。同一個使用者固定不變,可用來識別帳號
emailstring使用者的電子郵件。少數情況下可能是空字串

以 Node.js 伺服器串起第 1–6 步的完整流程(以下程式只在伺服器端執行):

import crypto from "node:crypto";
const AUTH_BASE = "https://manager.exptech.com.tw";
const CLIENT_ID = "your-client-id";
const REDIRECT_URI = "https://app.example.com/oauth/callback";
// 第 1、2 步:產生 PKCE 與 state,回傳要導向的網址
export function buildAuthorizeUrl(session) {
session.codeVerifier = crypto.randomBytes(32).toString("base64url");
session.state = crypto.randomBytes(16).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(session.codeVerifier)
.digest("base64url");
const url = new URL("/api/v1/oauth2/authorize", AUTH_BASE);
url.search = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: "eq.report trem.rts",
state: session.state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
}).toString();
return url.toString();
}
// 第 5、6 步:使用者回到 REDIRECT_URI 之後
export async function handleCallback(session, query) {
if (query.error) throw new Error(`授權失敗:${query.error}`);
if (query.state !== session.state) throw new Error("state 不符");
const tokenRes = await fetch(new URL("/api/v1/oauth2/token", AUTH_BASE), {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: query.code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: session.codeVerifier,
// 機密 client 才需要:
// client_secret: process.env.EXPTECH_CLIENT_SECRET,
}),
});
const token = await tokenRes.json();
if (!tokenRes.ok) throw new Error(`換取 token 失敗:${tokenRes.status} ${token.error}`);
const userRes = await fetch(new URL("/api/v1/oauth2/userinfo", AUTH_BASE), {
headers: { Authorization: `Bearer ${token.access_token}` },
});
if (!userRes.ok) throw new Error(`取得使用者資訊失敗:${userRes.status}`);
const user = await userRes.json();
return { accessToken: token.access_token, scope: token.scope ?? "", userId: user.sub };
}
  • 回應是 zstd 壓縮。 換取 token、使用者資訊與錯誤 JSON 一律帶 Content-Encoding: zstd,不論請求的 Accept-Encoding。較新版本 Node.js 的 fetch 會自動解壓;curl 要有 zstd 支援(curl --version 的 Features 有 zstd)才能用 --compressed 解開,否則改用 curl -s ... | zstd -d。其他 HTTP 用戶端請確認支援 zstd,或自行解壓。
  • 要在伺服器端呼叫。 這些端點的 CORS 只允許官方網站,第三方網頁裡的 fetch 讀不到回應。請從你的伺服器或原生 App 呼叫換取 token 與使用者資訊;純前端網頁需要一個後端來完成這兩步。
  • token 不會過期,也沒有 refresh token。 不要依賴 expires_in(回應沒有這個欄位)。token 失效只有一種情況:被撤銷。
  • 撤銷由使用者在主控台操作。 授權後的 token 會出現在使用者主控台的 API Token 列表,備註是你的 App 名稱,使用者可以隨時刪除。目前沒有讓 App 自己撤銷 token 的 API;需要時請引導使用者到主控台刪除。token 被撤銷後,HTTP 會得到 401,SSE 串流會收到 token revoked 並斷線。
  • token_type 是 Bearer,但 HTTP 資料 API 要用 X-Api-Token header。 SSE 串流與使用者資訊才用 Authorization: Bearer。
  • 每位使用者最多 25 個 token,OAuth2 發出的也算。達到上限時換取 token 會得到 403 too_many_tokens,此時授權碼已作廢,請引導使用者刪除不用的 token 後重新授權。
  • 頻率限制。 OAuth2 端點以 IP 計算,每秒約 4 次,可短暫突發 5 次,超過回 429。
  • 用量算在使用者身上。 你的 App 用這個 token 取用資料,會計入使用者的用量與配額;需要訂閱的資源,也要使用者自己訂閱。