はじめに
以前に、FlutterアプリからGoogleカレンダーにアクセスを実装する過程でOAuth2.0の仕組みを使ったが、OAuth2.0についてしっかり理解していなかったので、まとめておきたいと思う。
別プロジェクトで、Fitbitアプリと連携してウェアラブルデータを取り込む開発をした際にOAuth2.0に触れる機会があったため。
OAuth 2.0 は認可(Authorization)の仕組みであり、認証(Authentication)ではない。ただし、認可を行う前に認証が必要になることが多い。しかし、それは OAuth 2.0 の仕様ではなく、認可サーバー側の実装によるものである。正式に認証を扱うプロトコルとして OpenID Connect(OIDC)があり、OAuth 2.0 を拡張して認証機能を提供する。
OAuth2.0を実装しようとすると、その手前で認証も実装することが多く、認証+認可となるため、フローが多くなる。

OAuth 2.0 の全体の流れ
OAuth 2.0 は以下のステップで進行する。
(前半)
- クライアントアプリが認可サーバーに認可リクエストを送信
- ユーザーが認可を許可
- 認可サーバーが認可コードを発行
流れの画像はこちらのサイトがわかりやすかったので転載。

(後半)
- クライアントアプリが認可コードを使ってアクセストークンを取得
- アクセストークンを使って API にアクセス
- アクセストークンの有効期限切れ時にリフレッシュトークンを使用

クライアントアプリが認可サーバーに認可リクエストを送信
認可サーバーとは?
- OAuth 2.0 の認可プロセスを管理するサーバー で、クライアントアプリのリクエストを処理し、適切な認可コードやアクセストークンを発行する。
- 各サービス(Google, Fitbit, GitHub など)が独自の認可サーバーを提供しており、そのエンドポイント(URL)は サービスごとに異なる。OAuth 2.0 はオープンなプロトコルであり、各サービスが独自に実装できるため、Google, Fitbit, Facebook などは、OAuth 2.0 の基本仕様に従いつつ、自社の API に合わせたカスタマイズを行い、認可サーバーのエンドポイントURLを設定している。
サービス | 認可エンドポイント URL |
Google OAuth 2.0 | https://accounts.google.com/o/oauth2/auth |
Fitbit OAuth 2.0 | https://www.fitbit.com/oauth2/authorize |
GitHub OAuth 2.0 | https://github.com/login/oauth/authorize |
Facebook OAuth 2.0 | https://www.facebook.com/v12.0/dialog/oauth |
例えばGoogle の OAuth 2.0 を使う場合、リクエスト先はhttps://accounts.google.com/o/oauth2/auth
となる。
GET https://accounts.google.com/o/oauth2/auth
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&scope=email profile
&state=SECURE_RANDOM_STRING
&access_type=offline
&prompt=consent
認可コード取得に必要なパラメーター:response_type / client_id / redirect_url / scope / state
下記、OAuth2.0認可リクエストには、幾つかのパラメーターが付与されている。
GET https://authorization-server.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&scope=read write
&state=SECURE_RANDOM_STRING
#レスポンス
https://your-app.com/callback?code=AUTH_CODE&state=SECURE_RANDOM_STRING
パラメーター | 例 | 必須か? | 説明 | なぜ必要か? |
response_type | code | ✅ 必須 | 認可コードフローを使うことを指定 | 認可サーバーが認可コードを発行するため |
client_id | YOUR_CLIENT_ID | ✅ 必須 | クライアントアプリを識別する一意な ID | 正規のアプリからのリクエストであることを確認するため |
redirect_uri | https://your-app.com/callback | ⭕ 推奨(多くのサーバーでは必須) | 認可コードを受け取るリダイレクト先の URL | 認可コードを適切なクライアントに送るため(事前登録が必要) |
scope | read write | ⭕ 推奨(必須なケースもあり) | クライアントがリクエストするアクセス権限の範囲 | 最小権限の原則に従い、適切なデータアクセスを制限するため |
state | SECURE_RANDOM_STRING | ⭕ 推奨(CSRF 対策のためほぼ必須) | CSRF 攻撃を防ぐためのランダムな値 | 認可リクエストとレスポンスの整合性を確保し、攻撃を防ぐため |
response_type
- どのタイプの値を返すかを指定
response_type=code
と、codeを指定すると、認可コード取得を可コードフローを指定したことになり、認可サーバーは、認可が成功すると 認可コードをクライアントアプリに渡す。その場合、クライアントアプリはこのコードを使って、後でアクセストークンを取得する。
client_id
- 認可サーバーに登録されたクライアントアプリの ID。クライアントごとに一意なIDが割り当てられる
- 不正なクライアント(未登録のアプリ)からのリクエストを拒否するために必要
redirect_uri
- 認可コードを送信するリダイレクト先 URL
- クライアントアプリの設定時に 事前に登録しておく必要があり、認可コードをURLに付加してリダイレクトする。
- 実際には、認可サーバーは Web ブラウザに認可レスポンスを返すため、クライアントアプリケーションは直接参照できない。そのため、認可サーバーは 302 という HTTP ステータスコードを返し、
Location
ヘッダーでリダイレクト先を指定する。Web ブラウザはこの 302 を受け取り、指定されたredirect_uri
に自動的に遷移する。
HTTP/1.1 302 Found
Location: https://your-app.com/callback?code=AUTH_CODE&state=SECURE_RANDOM_STRING

scope
- クライアントアプリが取得したい データのアクセス範囲 を指定
state
- CSRF対策として用いられるランダムな値
- クライアントアプリが事前にランダムな
state
を生成し、セッション情報として保存するので、攻撃者は正しいstate
を知らないため、偽の OAuth リクエストを作成できない。
ユーザーが認可を許可
上記の設定でクライアントアプリが認可サーバーのエンドポイントに認可リクエストを送ると、認可サーバーはユーザーのブラウザを認可画面にリダイレクトする。
具体的には下記のような画面が表示される。
・認証:本人確認(ログインなど)
・認可:権限(googleカレンダーの予定を取得する権限を与えるかどうかなど)

上記はダミー画面ではあるが、googleにサインインして(認証)googleカレンダーへのアクセス許可をユーザーにリクエストする画面だとこのようになる。

ユーザーが「許可」ボタンを押すと、認可サーバーは事前に指定された redirect_uri
にユーザーをリダイレクトし、認可コードをURLに付加する。
認可コードのフラグメント識別子
下記のように、#_=_
(フラグメント識別子)がリダイレクトURLの認可コードの最後に付加されることがある。
フラグメント識別子 は、OAuth 2.0 の標準仕様ではなく、一部の認可サーバー(特に Facebook OAuth 2.0 の古いバージョン)で使われていたもの。昔、Facebook の OAuth では 「ログイン後のリダイレクトを正しく処理するため」 に #_=_
を追加する仕様になっていた。しかし、実際には #_=_
がなくても問題なく動くことがほとんどであったため、現在は不要だが名残として残っていることがある。
誤って、フラグメント識別子付きの認可コードを用いてアクセストークンを取得しようとすると、もちろん取得できずにエラーになるので、#_=_
付きでリダイレクトURLが返却された場合は#_=_
は含まずに認可コード部分のみを使用する必要がある。
https://myapp.com/callback?code=d62d6f5bdc13df79d9a5f#_=_
クライアントアプリが認可コードを使ってアクセストークンを取得
認可コードを受け取ったクライアントアプリは、認可サーバーの トークンエンドポイント に対して、アクセストークンを取得するためのリクエスト を送る。
POST https://authorization-server.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE(認可コード)
&redirect_uri=https://your-app.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
パラメーター | 値の例 | 説明 |
grant_type | authorization_code | 認可コードフローであることを示す |
code | AUTH_CODE | 取得した認可コード |
redirect_uri | https://your-app.com/callback | 認可コード取得時に使用した redirect_uri (一致しないと拒否される) |
client_id | YOUR_CLIENT_ID | クライアントアプリを識別する ID |
client_secret | YOUR_CLIENT_SECRET | クライアントアプリの秘密鍵(セキュアな環境でのみ送信) |
redirect_url
- 攻撃者が不正なリダイレクト URI を使って認可コードを横取りするのを防ぐため、
redirect_uri
が、認可コード発行時のredirect_uri
と一致しない場合、認可サーバーはリクエストを拒否する
client_secret
- 認可コードの取得(最初のリクエスト)では
client_secret
は不要。なぜなら、ユーザーが ブラウザで操作 しながら認可画面を通過するため、「ユーザーの意思」が確認できるから。 - しかし、アクセストークンの取得(トークンリクエスト)は「バックエンドのAPIリクエスト」。つまり、ここでは クライアントが本物かどうかの確認 をする必要があるため、client_secretも必要
- client_secretが不要の場合、攻撃者が認可コードを盗んだ場合に、自分のclientidでアクセストークンをリクエストし、アクセストークンを取得できてしまう。
クライアントアプリのリクエストが正しい場合、認可サーバーは アクセストークンを発行 し、レスポンスとして返す。
{
"access_token": "ACCESS_TOKEN_VALUE",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN_VALUE",
"scope": "read write"
}
アクセストークンを使って API にアクセス
取得したアクセストークンを API リクエストの Authorization
ヘッダーに含めて送信 することで、認証されたリクエストとしてリソースサーバー(API)にアクセスできる。
# APIリクエストの例
GET https://api.example.com/userinfo
Authorization: Bearer ACCESS_TOKEN_VALUE
アクセストークンの有効期限切れ時にリフレッシュトークンを使用
OAuth 2.0 では、アクセストークン(Access Token)には有効期限があり、期限が切れたら新しいアクセストークンを取得しなければならない。
その際、「リフレッシュトークン(Refresh Token)」を使用して、新しいアクセストークンを取得できる仕組みを採用しており、これにより、ユーザーは再ログインせずに、継続してサービスを利用できる。
アクセストークンの有効期限が切れた場合の動作
クライアントアプリが API にリクエスト
これは、実際にはユーザーがアプリを操作してデータを取得しようとしたタイミングで発生する(例:SNSアプリで新しい自分の投稿を取得しようとタイムラインをリロードする場合)
GET https://api.example.com/userinfo
Authorization: Bearer ACCESS_TOKEN_VALUE
API サーバーが「トークンの有効期限が切れている」とエラーを返す
{
"error": "invalid_token",
"error_description": "Access token expired"
}
クライアントアプリは、リフレッシュトークンを使って新しいアクセストークンをリクエスト
POST https://authorization-server.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN_VALUE
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
認可サーバーが新しいアクセストークンを発行し、クライアントアプリは、新しいアクセストークンを使ってAPIに再リクエストする
{
"access_token": "NEW_ACCESS_TOKEN_VALUE",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "NEW_REFRESH_TOKEN_VALUE"
}
なぜリフレッシュトークンが必要なのか?
- アクセストークンが短時間で失効するため、安全性が向上。アクセストークンが漏洩しても、短時間しか有効でないため、被害を最小限に抑えられる。
- リフレッシュトークンを使えば、ユーザーの再ログイン不要。リフレッシュトークンは長期間有効(数日〜数ヶ月) なため、アクセストークンが期限切れになっても、ユーザーの手間なしで更新できる。
例えば、Fitbitでユーザーが睡眠データを確認する場合の例だと、ユーザーが Fitbit を使い続けている限り、一度認証が完了すれば、アクセストークンがリフレッシュトークンにより自動的に更新され続けるため、ユーザーが再認可する必要はなく、シームレスに最新のデータを取得できる
ユーザー視点
- 昨日 Fitbit アプリで睡眠データを確認 → 問題なく表示
- 24時間後、再びアプリを開いて今日の睡眠データを確認しようとする
- ユーザーは特に何も意識しないが、バックグラウンドではアクセストークンの更新が発生している(内部的に)
- 結果として、ユーザーは最新のデータを取得できる
つまり、リフレッシュトークンは、OAuth 2.0 において「セキュリティ」と「利便性」を両立させるための仕組みであると言える。
仮にリフレッシュトークンも無効の場合には、OAuth 2.0 の仕様上、サーバーは単に「リフレッシュトークンが無効です」というエラーレスポンス(例:400 Bad Request
や invalid_grant
)返すだけなので、クライアントアプリの開発者が、リフレッシュトークンが無効だった場合にログイン画面を表示する処理を組み込む必要がある。
リフレッシュトークンのセキュリティ対策
リフレッシュトークンは長期間有効なため、セキュリティリスクを減らすために以下の対策が取られることが多い。
✅ リフレッシュトークンを暗号化・サーバー側に安全に保管する
✅ 一定期間(数ヶ月など)使用されなかったリフレッシュトークンは無効化する
✅ ユーザーがログアウトしたらリフレッシュトークンも無効にする
✅ リフレッシュトークンを定期的に更新する(トークンローテーション)
トークンローテーションは、アクセストークンの有効期間以上の期間を空けてユーザーがアプリにアクセスしてデータを取得しようとすると、アクセストークンが有効期限状態になるので、リフレッシュトークンが新しいアクセストークンを発行する。そのタイミングで、新しいリフレッシュトークンも発行され、古いリフレッシュトークンは使えなくなる。
OAuth1.0認証と何が変わったか?
一見すると、OAuth 2.0 では、「認可コード取得」「アクセストークン取得」「リフレッシュトークン取得」などの二重三重のやり取りが増えたように見える。しかし、OAuth 2.0 は OAuth 1.0 の問題点を改善し、よりシンプルでセキュアな認可フローを提供するように設計された。
OAuth1.0の主な課題
- クライアント側で「署名(Signature)」を生成する必要があり、実装が難しかった。OAuth 2.0 では、シンプルな「Bearer トークン(アクセストークン)」方式を採用し、リクエストの認証が簡単になった。
- アクセストークンの有効期限の概念がなかった。もしアクセストークンが漏洩した場合、攻撃者がずっとそのトークンを使って API にアクセスできる
- モバイルアプリやシングルページアプリ(SPA)には適さなかった。OAuth 1.0 では、クライアントアプリが「クライアントシークレット(client_secret)」を常に使用する必要があったが、モバイルアプリや JavaScript を使うシングルページアプリ(SPA)では、クライアントシークレットを安全に管理するのが難しい。
- サーバー間のやり取りが複雑で、多くのリクエストが必要だった。
署名とはここでは、署名ベース文字列(リクエストの内容(HTTP メソッド、URL、パラメータ)を結合したもの)と、署名キー(署名を作成するための秘密鍵:client_secretとaccess_token_secretを結合)を使って、HMAC-SHA1 ハッシュを生成する必要があり、煩雑だった。
確かに、言われてみれば、twitterのOAuth1.0APIを使うときは、OAuth2.0APIの時よりも多くの環境変数を指定していた気がする。その中にaccess_token_secretもあった。