コンテンツにスキップ
コンソール

OAuth2

サードパーティのアプリは、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>。

現在、自分で登録する画面はありません。概要のページの連絡先から申請し、次を伝えてください。

  • アプリ名と 1 文の説明(同意ページに表示され、ユーザーの token 一覧のメモにもなります)
  • redirect_uri の一覧:認可後に戻る URL。https:// の URL でも、アプリ独自の scheme(例:myapp://oauth/callback)でもかまいません。比較は完全に同じ文字列で、ワイルドカードやプレフィックス一致はありません
  • 必要な権限
  • client の種類:
    • 公開 client(モバイルアプリ、デスクトップアプリ):client_secret はなく、PKCE だけに依存します。
    • 機密 client(自前のサーバーがあるウェブサイト):token の交換時に client_secret を付けます。secret はサーバー側にだけ置いてください。
  1. アプリはランダムな code_verifier を作り、code_challenge = base64url(SHA-256(code_verifier))(= のパディングなし)を計算し、さらにランダムな state を作ります。どちらもユーザーの session に保存します。

  2. ユーザーを認可リクエストの URL へ誘導します。

  3. サーバーはパラメータを確認したあと、ユーザーを公式サイトのログインページ(未ログインのとき)または同意ページへ誘導します。この段階では認可コードは発行されません。

  4. ユーザーは同意ページで同意または拒否を押します。同意ページが同意または拒否のリクエストを送り、サーバーはユーザーをあなたの redirect_uri へ戻します。

    • 同意:redirect_uri?code=<認可コード>&state=<state>
    • 拒否:redirect_uri?error=access_denied&state=<state>
  5. アプリは state が手順 1 と同じことを確認してから、サーバーまたはネイティブアプリから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を参照してください。

ユーザーのブラウザをこの URL へ誘導します(トップレベルの遷移、またはシステムのブラウザを開きます。fetch は使わないでください)。

GEThttps://manager.exptech.com.tw/api/v1/oauth2/authorize
サーバーManager
クエリパラメーター
  • response_typestring必須

    固定で code

  • client_idstring必須

    申請時に受け取った client id

  • redirect_uristring必須

    登録したうちの 1 件と完全に同じ文字列

  • 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=<response_type>" \
--data-urlencode "client_id=<client_id>" \
--data-urlencode "redirect_uri=<redirect_uri>" \
--data-urlencode "code_challenge=<code_challenge>" \
--data-urlencode "code_challenge_method=<code_challenge_method>"
レスポンス

パラメータが正しい場合は公式のログインページまたは同意ページへ誘導します。誤りがある場合は error を付けて redirect_uri へ戻します

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

client_id または redirect_uri が不正です。アプリへは戻りません(zstd 圧縮の JSON)

{
"error": "invalid_redirect_uri"
}
試してみる
クエリパラメーター

トークンはこのブラウザから API へ直接送信されます。

client_id または redirect_uri に問題があるとき、サーバーはどの URL へも戻しません(オープンリダイレクトになるのを避けるため)。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)。無視はしません。

このリクエストは、ユーザーがボタンを押したときに公式の同意ページが送ります。ユーザーが公式サイトにログインしている必要があります。サードパーティのアプリが呼ぶ必要はなく、直接呼ぶこともできません。 ここに載せているのは、redirect_uri へ戻る動作を説明するためです。

POSThttps://manager.exptech.com.tw/api/v1/oauth2/authorize
サーバーManager
クエリパラメーター
  • 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"
レスポンス

同意:code を付けて redirect_uri へ戻します。拒否:error=access_denied を付けます

Location: https://app.example.com/oauth/callback?code=4mXq9vKpL2sTzW8yRb1nCe7uJ0hGdA5fYiOkN3wPtQx&state=af0ifjsldkj
  • 認可コードは 5 分間有効で、使えるのは 1 回だけです。
  • ユーザーが同意ページに長く留まり、ログインの期限が切れていると、ログインページへ戻され、ログイン後に再び同意ページへ戻ります。
  • サーバーが認可コードを作成できないときは、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=<grant_type>" \
--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実際に取得した権限。空白区切りで、展開後の名前。権限が 1 つもないときはこのフィールドを省略します

レスポンスに 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
ヘッダー
  • Authorization必須

    Bearer <access_token>。Bearer のうしろの空白は 1 つだけです

リクエスト例
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 が無効・取り消し済み。本文はありませんレスポンスボディなし

試してみる
ヘッダー

トークンはこのブラウザから API へ直接送信されます。

フィールド型説明
substringユーザー id(10 進数の文字列)。同じユーザーでは変わりません。アカウントの識別に使えます
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 を作り、誘導先の URL を返します
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 は、リクエストの Accept-Encoding に関係なく、常に Content-Encoding: zstd が付きます。比較的新しい Node.js の fetch は自動で展開します。curl は zstd に対応しているとき(curl --version の Features に zstd があるとき)だけ --compressed で展開できます。そうでなければ curl -s ... | zstd -d を使ってください。他の HTTP クライアントは zstd に対応しているか確認するか、自分で展開してください。
  • サーバー側から呼んでください。 これらのエンドポイントの CORS は公式サイトだけを許可し、サードパーティのウェブページ内の fetch はレスポンスを読めません。token の交換とユーザー情報は、自分のサーバーまたはネイティブアプリから呼んでください。フロントエンドだけのウェブページは、この 2 段階を完了するバックエンドが必要です。
  • token は期限切れにならず、refresh token もありません。 expires_in に依存しないでください(レスポンスにこのフィールドはありません)。token が無効になるのは、取り消されたときだけです。
  • 取り消しはユーザーがダッシュボードで行います。 認可後の token は、ユーザーのダッシュボードの API Token 一覧に出ます。メモはあなたのアプリ名で、ユーザーはいつでも削除できます。現在、アプリ自身が token を取り消す API はありません。必要なときは、ユーザーをダッシュボードの削除へ案内してください。token が取り消されると、HTTP は 401 になり、SSE 配信は token revoked を受け取って切断されます。
  • token_type は Bearer ですが、HTTP データ API は X-Api-Token header を使います。 Authorization: Bearer を使うのは SSE 配信とユーザー情報だけです。
  • ユーザー 1 人につき token は最大 25 個で、OAuth2 が発行したものも数に入ります。上限に達すると token の交換は 403 too_many_tokens になり、その時点で認可コードは無効です。ユーザーに不要な token を削除してもらってから、認可をやり直してください。
  • 頻度の制限。 OAuth2 エンドポイントは IP 単位で、毎秒約 4 回、短時間なら 5 回までです。超えると 429 を返します。
  • 利用量はユーザーに計上されます。 あなたのアプリがこの token でデータを取得すると、ユーザーの利用量とクォータに計上されます。購読が必要なリソースは、ユーザー自身が購読する必要があります。