範例與疑難排解 開發人員

上次更新:2026年8月16日

範例與疑難排解

片段範例便於對照協定;完整可執行指令碼見《Python 最小可執行範例》。請將常數換成你的實際值。

探索文件

curl -sS https://api-passport.swaymoon.com/.well-known/openid-configuration | jq .

請確認 authorization_endpointtoken_endpointuserinfo_endpointjwks_uriid_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_verifiercode_challenge
invalid_clientBasic 憑證錯誤、對非機密用戶端誤用了 Basic / secret,或 JWT 斷言驗簽失敗核對類型。機密核對 client_id/client_secret。非機密改用表單 client_id。JWT 核對 kid、公鑰、audiss/subexp 與 ES256
invalid_grantcode 已使用或已過期,或 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,並確認 algES256
前端暴露 client_secret將機密用戶端用於純 SPA改為建立非機密用戶端,或僅在後端保存金鑰
私鑰 JWK(含 d)出現在前端或 git把私鑰當成公鑰提交,或把金鑰檔提交進倉庫入口只接受公鑰。輪換 kid 並作廢已洩漏的私鑰
JWT 斷言 invalid_clientaud 不是 token_endpointkid 未登記,或斷言用了 RS256 / 已過期使用探索文件中的權杖 URL。頭裡的 kid 必須已登記。演算法必須是 ES256
非機密用戶端沒有 refresh_token預期行為Access Token 過期後重新走授權碼 + PKCE

聯調提示

  • 根據探索文件取得端點,避免硬編碼路徑。
  • 完整本機指令碼:《Python 最小可執行範例》。
  • 登入和授權確認在瀏覽器中完成;機密與 JWT 斷言用戶端的換取權杖與 UserInfo 應在伺服器端進行。
  • 正式環境的 Issuer 和前端網域分別為 api-passport.swaymoon.compassport.swaymoon.com

取得協助

  • 使用者文件:關閉開發人員模式後,檢視「指南」和「法律」分類。
  • 開發者入口網站:develop.swaymoon.com
  • 聯絡方式:hello@swaymoon.com(請勿透過公共管道傳送明文 client_secret;描述問題時可將其打碼)