OAuth 1.0aと2.0の違いを実装で比べる|署名とトークンの扱い

「OAuth 1.0a vs 2.0」「Signatures and Tokens」の文字と、署名(Signature)と更新(Refresh)の 2 つのレーンを描いたイラスト 認証・IAM

はじめに

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 もある。
Zaim の連携を許可する画面。プロフィール情報の参照、家計簿の参照、入力、削除、永続的なアクセスの 5 項目が並んでいる
Zaim の認可画面。利用者がここで許可すると、アプリは API を使えるようになる

OAuth 1.0a と 2.0 の違いを 4 つの実装で比べる

比べると、差が出るのはリクエストごとの処理と、トークンを保つ方法です。

観点ZaimFatSecretWithingsGoogle Health
方式OAuth 1.0aOAuth 1.0aOAuth 2.0OAuth 2.0
トークン取得3 段階を自分で実装取得処理は持たず、保存済みの access token を読む認可コードを交換認可コードを交換
交換時の追加要件oauth_verifier対象外(交換処理を持たない)client secret か、nonce と署名(私の実装は署名)access_type=offline と prompt=consent を認可 URL に付ける
長期保存するものaccess token と token secretaccess token と token secretrefresh_tokenrefresh_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 まで確かめました。

  1. Consumer Key と Consumer Secret で署名し、oauth_callback を付けて request token を受け取る。
  2. 利用者がブラウザで認可 URL を開いて許可し、oauth_verifier を受け取る。
  3. request token とその secret で署名し、oauth_verifier を付けて access token に交換する。
アプリ、利用者のブラウザ、API サーバーの間の往復を示す図。request token の取得、利用者の許可と oauth_verifier の受け取り、access token への交換の順に進み、署名の鍵は段階ごとに変わる
OAuth 1.0a のトークン取得の流れ。段階によって署名の鍵が変わる

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_verifier の値は OAUTH-VERIFIER と書いた黒帯で伏せてある
oob で許可すると、完了画面に oauth_verifier が表示される。値は黒帯で伏せた

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 はリクエストごとに署名を計算して access token と token secret を保存し、OAuth 2.0 は計算なしで Bearer トークンを送り refresh_token を保存することを並べた図
リクエストごとの処理と、長期保存するものの違い

OAuth 1.0a と 2.0 のセキュリティの違い

OAuth 1.0a は API の呼び出しで secret を送らず署名で正しさを示し、OAuth 2.0 はトークンを TLS で守ります。どちらが常に安全というより、何に頼って守るかが違います。仕様上の違いは次のとおりです。

観点OAuth 1.0aOAuth 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 に当たったら、署名の問題とトークンの問題を分けて確かめると進めやすくなります。順番は次のとおりです。

  1. 認証の要らないエンドポイントがあれば呼び、通信とレスポンスの形を確かめる。
  2. oauth_callback=oob で request token を取る。通らなければ、ローカルに立てた受信用サーバーや登録済みの URL を試す。
  3. access token に交換し、利用者情報を返す読み取り専用のエンドポイントで 200 を確かめる。
  4. 署名が通らないときは、サーバーの時刻とのずれ、エンコード、並べ替えの順に確かめる。

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 の認可の要件は変わることがあるので、最新は各社の公式ドキュメントで確認してください。