範例與疑難排解 開發人員
上次更新:2026年8月16日
範例與疑難排解
片段範例便於對照協定;完整可執行指令碼見《Python 最小可執行範例》。請將常數換成你的實際值。
探索文件
curl -sS https://api-passport.swaymoon.com/.well-known/openid-configuration | jq .
請確認 authorization_endpoint、token_endpoint、userinfo_endpoint、jwks_uri 和 id_token_signing_alg_values_supported 等欄位。
授權 URL(瀏覽器跳轉)
https://api-passport.swaymoon.com/oauth2/authorize
?response_type=code
&client_id=swm_YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20name%20picture%20email
&state=YOUR_STATE
&code_challenge=YOUR_CHALLENGE
&code_challenge_method=S256
拼 URL 時對查詢參數只編碼一次。若網址列裡出現 redirect_uri=http%253A%252F%252F…,屬於雙重編碼,伺服器端會按字面值校驗並失敗。
換取權杖:機密用戶端(curl)
ISSUER=https://api-passport.swaymoon.com
CLIENT_ID='swm_YOUR_CLIENT_ID'
CLIENT_SECRET='YOUR_SECRET'
CODE='...'
VERIFIER='...'
REDIRECT='https://app.example.com/callback'
curl -sS -u "$CLIENT_ID:$CLIENT_SECRET" \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT" \
--data-urlencode "code_verifier=$VERIFIER"
換取權杖:JWT 斷言用戶端(curl)
先在後端用私鑰簽發 client_assertion(不要把私鑰寫進命令列歷史)。Python 範例:
import time, uuid
from jwcrypto import jwk, jwt # pip install jwcrypto
key = jwk.JWK.from_json(open("private.jwk.json").read()) # 含 d 的私鑰,勿提交入口
now = int(time.time())
token = jwt.JWT(
header={"alg": "ES256", "kid": key["kid"], "typ": "JWT"},
claims={
"iss": "swm_YOUR_CLIENT_ID",
"sub": "swm_YOUR_CLIENT_ID",
"aud": "https://api-passport.swaymoon.com/oauth2/token",
"jti": str(uuid.uuid4()),
"iat": now,
"exp": now + 300,
},
)
token.make_signed_token(key)
assertion = token.serialize()
然後:
ISSUER=https://api-passport.swaymoon.com
CLIENT_ID='swm_YOUR_CLIENT_ID'
CODE='...'
VERIFIER='...'
REDIRECT='https://app.example.com/callback'
ASSERTION='...' # 上一步簽發的 JWT
curl -sS \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "code_verifier=$VERIFIER" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$ASSERTION"
aud 必須與探索文件裡的 token_endpoint 一致。重新整理權杖時把 grant_type 換成 refresh_token,並再簽一枚新的斷言(新的 jti / iat / exp)。
換取權杖:非機密用戶端(curl)
ISSUER=https://api-passport.swaymoon.com
CLIENT_ID='swm_YOUR_CLIENT_ID'
CODE='...'
VERIFIER='...'
REDIRECT='https://app.example.com/callback'
curl -sS \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "code_verifier=$VERIFIER"
UserInfo(curl)
curl -sS https://api-passport.swaymoon.com/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"
重新整理權杖(機密用戶端)
curl -sS -u "$CLIENT_ID:$CLIENT_SECRET" \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN"
常見問題
| 現象 | 可能原因 | 處理 |
|---|---|---|
| 授權報錯 / 無法跳轉至回呼位址 | redirect_uri 與登記值不一致,或雙重編碼 | 逐字元核對;網址列不應出現 %253A / %252F |
invalid_request / PKCE 相關錯誤 | 缺少 code_challenge,或 code_challenge_method 不是 S256 | 在授權與換取權杖流程中配套使用 code_verifier 和 code_challenge |
invalid_client | Basic 憑證錯誤、對非機密用戶端誤用了 Basic / secret,或 JWT 斷言驗簽失敗 | 核對類型。機密核對 client_id/client_secret。非機密改用表單 client_id。JWT 核對 kid、公鑰、aud、iss/sub、exp 與 ES256 |
invalid_grant | code 已使用或已過期,或 code_verifier 不匹配 | 重新發起授權,不要重複提交同一個 code |
| 同意頁未顯示預期權限 | 入口網站未設定該權限,或請求中未包含對應的 scope | 檢查應用程式的選用權限和 scope 參數 |
回呼 error=invalid_scope | 授權請求的 scope 超出該用戶端已登記範圍 | 在入口網站「功能」中勾選對應權限,或縮小請求中的 scope |
UserInfo 沒有 email | 權杖未含 email(使用者取消了選用電子郵件,或請求未申請) | 檢查權杖回應中的 scope;已授予 email 時應始終有該欄位 |
| UserInfo 資料像「空的」 | 未授權資料細項時返回預設佔位值(如暱稱 搖月使用者、大頭貼 ""),不是欄位缺失 | 按權杖 scope 判斷是否為真實資料;見《權杖與使用者資訊》 |
| 使用者選了隱私郵件 | email 可能為 @privaterelay.swaymoon.com 中繼位址 | 按普通電子郵件處理即可;真實電子郵件不會提供給你方 |
sub 與其他應用程式中的值不一致 | 這是 pairwise 機制的預期行為 | 僅在目前 client_id 或團隊範圍內使用 sub |
| ID Token 驗簽失敗 | 未使用 ES256,或快取的 JWKS 已過期 | 重新取得 /oauth2/jwks,並確認 alg 為 ES256 |
前端暴露 client_secret | 將機密用戶端用於純 SPA | 改為建立非機密用戶端,或僅在後端保存金鑰 |
私鑰 JWK(含 d)出現在前端或 git | 把私鑰當成公鑰提交,或把金鑰檔提交進倉庫 | 入口只接受公鑰。輪換 kid 並作廢已洩漏的私鑰 |
JWT 斷言 invalid_client | aud 不是 token_endpoint、kid 未登記,或斷言用了 RS256 / 已過期 | 使用探索文件中的權杖 URL。頭裡的 kid 必須已登記。演算法必須是 ES256 |
非機密用戶端沒有 refresh_token | 預期行為 | Access Token 過期後重新走授權碼 + PKCE |
聯調提示
- 根據探索文件取得端點,避免硬編碼路徑。
- 完整本機指令碼:《Python 最小可執行範例》。
- 登入和授權確認在瀏覽器中完成;機密與 JWT 斷言用戶端的換取權杖與 UserInfo 應在伺服器端進行。
- 正式環境的 Issuer 和前端網域分別為
api-passport.swaymoon.com和passport.swaymoon.com。
取得協助
- 使用者文件:關閉開發人員模式後,檢視「指南」和「法律」分類。
- 開發者入口網站:develop.swaymoon.com
- 聯絡方式:hello@swaymoon.com(請勿透過公共管道傳送明文
client_secret;描述問題時可將其打碼)