はじめに
OAuth 1.0 と 2.0 の違いを、自分で実装した 4 つの API 連携で比べます。OAuth は、利用者のパスワードを渡さずに、アプリへ API の利用を許可する仕組みです。実装上の最大の違いは、1.0a がリクエストごとに署名を計算するのに対し、2.0 は取得したトークンを送り、その更新を管理する点です。
きっかけは、OAuth 1.0a を使う Zaim API の検証です。Zaim API は 2026-12-07 に提供終了するため、本記事のコードは同じ API では再現できなくなります。署名の組み立て方は OAuth 1.0a に共通なので、ほかの API を連携するときの参考として読んでください。
本記事のまとめ
- OAuth 1.0a は、メソッド、URL、パラメータから署名を作り、リクエストのたびに付ける。
- OAuth 1.0a のトークン取得は、request token(許可を得るまでの一時的なトークン)、利用者の許可、access token の 3 段階である。
- OAuth 2.0 は認可コードを交換してトークンを得る。実装の中心は
refresh_tokenの保存と更新になる。 - Withings のように、OAuth 2.0 のトークン取得に独自の nonce(1 回だけ使う値)と署名の形を用意する API もある。

OAuth 1.0a と 2.0 の違いを 4 つの実装で比べる
比べると、差が出るのはリクエストごとの処理と、トークンを保つ方法です。
| 観点 | Zaim | FatSecret | Withings | Google Health |
| 方式 | OAuth 1.0a | OAuth 1.0a | OAuth 2.0 | OAuth 2.0 |
| トークン取得 | 3 段階を自分で実装 | 取得処理は持たず、保存済みの access token を読む | 認可コードを交換 | 認可コードを交換 |
| 交換時の追加要件 | oauth_verifier | 対象外(交換処理を持たない) | client secret か、nonce と署名(私の実装は署名) | access_type=offline と prompt=consent を認可 URL に付ける |
| 長期保存するもの | access token と token secret | access token と token secret | refresh_token | refresh_token |
仕様は、OAuth 1.0 が RFC 5849、OAuth 2.0 が RFC 6749 です。RFC 5849 は RFC 6749 に置き換えられた扱いですが、Zaim や FatSecret のように OAuth 1.0a の API は実在します。
OAuth 1.0a のトークン取得は 3 段階で進む
OAuth 1.0a では、request token を取り、利用者が許可し、access token に交換します。Zaim では、次の順に進めて GET /v2/home/user/verify の 200 まで確かめました。
- Consumer Key と Consumer Secret で署名し、
oauth_callbackを付けて request token を受け取る。 - 利用者がブラウザで認可 URL を開いて許可し、
oauth_verifierを受け取る。 - request token とその secret で署名し、
oauth_verifierを付けて access token に交換する。

2 段目と 3 段目のあいだで、利用者の操作を受け取る必要があります。oauth_callback は戻り先の指定で、RFC 5849 では、戻り先を受けられないときに oob を指定できます。Zaim では oob が通り、許可後の画面に oauth_verifier が表示されたので、手で写しました。ほかの API で通るかどうかは、それぞれ確かめてください。
Zaim の 1 段目を実装すると次のようになります。oauth_callback は本文ではなく、署名の対象になる oauth パラメータへ入れます。認証系のレスポンスは JSON ではなくフォーム形式で返るため、parse_qsl で読みます。
header = authorization_header(
args.method, REQUEST_TOKEN_URL, {},
cred["consumer_key"], cred["consumer_secret"],
extra_oauth={"oauth_callback": args.callback},
)
status, text = http(args.method, REQUEST_TOKEN_URL, {}, header)
# 認証系のレスポンスは JSON ではなく form-urlencoded で返る。
data = dict(urllib.parse.parse_qsl(text))OAuth 1.0a の署名はこう組み立てる
署名は、メソッド、URL、並べ替えたパラメータをつないだ文字列を、2 つの secret から作った鍵で HMAC-SHA1 にかけた値です。実装は次のとおりです。別の API 連携用に書いた署名をそのまま流用して通りました。Zaim のために足したのは、oauth_callback と oauth_verifier を oauth パラメータへ入れる処理だけです。
def oauth_quote(value):
# 既定の safe は "/" なので、そのままでは署名が通らない。
return urllib.parse.quote(str(value), safe="~-._")
normalized = "&".join(
f"{oauth_quote(k)}={oauth_quote(v)}" for k, v in sorted(all_params)
)
signature_base = "&".join(
[method.upper(), oauth_quote(base_url), oauth_quote(normalized)]
)
signing_key = f"{oauth_quote(consumer_secret)}&{oauth_quote(token_secret)}"
signature = base64.b64encode(
hmac.new(signing_key.encode("utf-8"), signature_base.encode("utf-8"), hashlib.sha1).digest()
).decode("ascii")RFC 5849 の決まりのうち、実装で誤りやすいのは次の 4 点です。
- エンコード: 英数字と
-._~以外をすべて変換する。Python のquoteは既定で/を残すため、safeの指定が要る。 - 並べ替え: クエリ、本文、oauth パラメータを合わせ、エンコードしてから名前の順に並べる。掲載コードはエンコード前に並べているので、エンコードで順序が変わる名前には対応していない。
- 署名の鍵: token secret が空でも、間の
&は残す。request token の取得がこの形になる。 - 対象の URL: クエリを除いたベース URL を使う。クエリはパラメータとして別に数える。
oauth_nonce はリクエストごとの乱数、oauth_timestamp は送信時刻です。RFC 5849 での nonce は、サーバーが同じリクエストの再送を見分けるための値とされています。時刻のずれの許容幅は API が決めるもので、Zaim で測ると過去 5 分、未来 10 分でした。

OAuth 2.0 はトークンの更新が実装の中心になる
OAuth 2.0 にはリクエストごとの署名が無く、代わりに refresh_token の保存と更新を実装します。Google Health の認可コードの交換は、フォームを POST するだけです。
response = session.post(
TOKEN_URL,
data={
"grant_type": "authorization_code",
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": redirect_uri,
},
timeout=timeout_seconds,
)RFC 6749 では、認可エンドポイントとトークンエンドポイントで TLS が必須です。API の呼び出しでは RFC 6750 の Bearer トークン(持っているだけで使えるトークン)をヘッダーで送るので、計算の処理は要りません。代わりに、トークンの管理で次の点が必要でした。
- Google Health では、認可 URL に
access_type=offlineを付けないとrefresh_tokenが返らない。Google の公式ドキュメントでは、返るのは初回の認可だけとされている。私の実装はprompt=consentも付けている。 - 認可コードを使い切ってから保存に失敗するとトークンを失うので、保存先の権限を交換の前に確かめている。
ただし、OAuth 2.0 でも署名の形を用意する API があります。Withings の公式リファレンスでは、トークンの交換と更新を client secret でも、先に取得した nonce と署名でも呼べます。私の実装は署名の形で、client secret の形は試していません。更新のたびに新しい refresh_token も返るので、保存し直す処理が要ります。比べるときは、OAuth の仕様と API ごとの追加要件を分けて確認してください。

OAuth 1.0a と 2.0 のセキュリティの違い
OAuth 1.0a は API の呼び出しで secret を送らず署名で正しさを示し、OAuth 2.0 はトークンを TLS で守ります。どちらが常に安全というより、何に頼って守るかが違います。仕様上の違いは次のとおりです。
| 観点 | OAuth 1.0a | OAuth 2.0 |
| トークンが漏れたとき | token secret が無ければ署名を作れない | Bearer トークンは持っていれば使える |
| TLS | トークンの取得では必須(secret が平文で返るため)。署名は通信内容を秘匿しないので、守るリソースにも使うべきとされる | トークンの送信に必須 |
| 改ざんの検知 | 署名の範囲だけ。本文の多くとヘッダーの多くは対象外 | Bearer トークン自体では検知しない |
| 再送への備え | nonce と timestamp。サーバーが使用済みの nonce を記録して働く | Bearer トークン自体には無い。有効期間を 1 時間以内にする推奨がある |
OAuth 2.0 の推奨は RFC 6749 の後も更新されています。2025 年の RFC 9700 の要点は次のとおりです。
- 認可コードの横取りを防ぐ PKCE は、secret を安全に持てないアプリで必須、それ以外でも推奨である。
- CSRF(利用者に意図しないリクエストを送らせる攻撃)を防ぐ必要がある。手段は 1 回限りの
stateか PKCE である。 - secret を安全に持てないアプリの
refresh_tokenは、更新のたびに新しくするか、送信者に結び付ける。
新しく作るなら OAuth 2.0 を選び、これらの推奨に沿ってください。私の 2 つの OAuth 2.0 の実装は、初回の認可をローカルで行うだけです。state は付けていますが戻り値を照合しておらず、PKCE も使っていません。
OAuth 1.0a の API を連携するときの進め方
OAuth 1.0a の API に当たったら、署名の問題とトークンの問題を分けて確かめると進めやすくなります。順番は次のとおりです。
- 認証の要らないエンドポイントがあれば呼び、通信とレスポンスの形を確かめる。
oauth_callback=oobで request token を取る。通らなければ、ローカルに立てた受信用サーバーや登録済みの URL を試す。- access token に交換し、利用者情報を返す読み取り専用のエンドポイントで 200 を確かめる。
- 署名が通らないときは、サーバーの時刻とのずれ、エンコード、並べ替えの順に確かめる。
Zaim の登録フォームのチェックボックスは 3 つですが、認可画面では 5 項目に分かれ、「書き込む」に家計簿の削除が含まれていました。読み取りだけで足りるなら、書き込みの権限を付けない判断ができるよう、認可画面まで確認してください。
まとめ
OAuth 1.0a は署名の計算が、OAuth 2.0 はトークンの保存と更新が、実装の中心です。分かったことは次のとおりです。
- OAuth 1.0a は、エンコード、並べ替え、署名の鍵の作り方(token secret が空でも
&を残す)を仕様どおりに実装すれば、ほかの API へ流用できる。 - トークンの取得は、1.0a が 3 段階で、2.0 は認可コードの交換になる。
- API の呼び出しの保護は、1.0a が署名に、2.0 が TLS とトークンの管理に頼る。1.0a もトークンの取得には TLS が要る。
- API ごとの追加要件は、仕様とは別に確認する。
Google や Withings の認可の要件は変わることがあるので、最新は各社の公式ドキュメントで確認してください。

