例とトラブルシューティング デベロッパ
最終更新: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 が登録値と一致しない、または二重エンコード | 1 文字ずつ照合。アドレスバーに %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 のプロフィールが「空」に見える | 未認可のプロフィール細目は既定のプレースホルダ(ニックネーム Swaymoon ユーザー、アバター "")。フィールド欠落ではない | トークンの 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 |
連携のヒント
- エンドポイントはディスカバリ文書から取り、パスをハードコードしない。
- 完全なローカルスクリプト:「Python の最小実行例」。
- サインインと認可確認はブラウザで行う。機密および JWT アサーションクライアントのトークン交換と UserInfo はサーバーで行う。
- 本番の Issuer とフロントドメインはそれぞれ
api-passport.swaymoon.comとpassport.swaymoon.com。
サポート
- ユーザードキュメント:デベロッパモードをオフにし、「ガイド」と「法律」を見る。
- デベロッパポータル:develop.swaymoon.com
- 連絡先:hello@swaymoon.com(公開チャネルに平文の
client_secretを送らない。問題を説明するときはマスクする)