OAuthとは!?
例えば以下のようにサービスを利用されているとします
- ユーザー: 普段サービスを使っている人
- サービスA: 連携元のサービス(例: SNS、カレンダーアプリなど)
- サービスB: サービスAのデータを「連携」したい別サービス(例: 分析ツール、ダッシュボードアプリなど)
ゴールはシンプルで、
サービスBがユーザーの代わりにサービスAのAPIを呼べるようにしたい
でも、ユーザーのパスワードはサービスBに教えたくない
という状況を、安全に実現するのが OAuth 2.0 です。
流れ(Authorization Code フローのイメージ)
一般的によく使われるのが「Authorization Code フロー」と呼ばれるパターンです。
ざっくりした流れは次のとおりです。
| ステップ | ユーザー | サービスB | サービスA |
|---|---|---|---|
| ① | 「サービスAと連携」ボタンを押す | ← リクエスト受信 | |
| ② | サービスAのログイン画面へリダイレクト | ← リダイレクト受信 | |
| ③ | サービスAの画面でID/パスワード入力 | ← ログイン情報受信 | |
| ④ | 「このユーザーはOK」と判断 → 認可コード/トークン発行 |
||
| ⑤ | ← 認可コード/トークン受信 | ← 認可コード/トークン受信 「トークン」だけ保管 (パスワードは知らない) |
|
| ⑥ | トークンを使ってAPI呼び出し | ← APIリクエスト受信 |
- ユーザーの パスワードはサービスAのログイン画面にだけ入力される
- サービスBはパスワードを一切見ないし保存もしない
- 代わりに、「どの操作をどこまでしていいか」が書かれたアクセストークンだけを受け取る
このアクセストークンが、いわゆる「鍵」だと考えると分かりやすいです。
- 本物のパスワード = 家の「マスターキー」
- アクセストークン = 「この部屋だけ入ってOK」と書かれたカードキー
サービスBはマスターキー(パスワード)は持たず、カードキー(トークン)だけを使って、
許可された範囲のAPIだけを呼び出します。
なにが嬉しいのか
この「パスワードを教えずに、鍵だけ渡す」形にすることで、次のようなメリットがあります。
-
パスワードを預かるサービスが減る
→ どこか1つが情報漏えいしても、他サービスへの被害を抑えやすい -
権限を細かく制御できる
→ 「読み取り専用だけ」「このAPIだけ」など、トークンにスコープを付けて制限できる -
連携の解除がしやすい
→ パスワードを変えなくても、トークンだけ無効化すれば特定のサービスBとの連携だけを切れる
まとめると、OAuth 2.0 は、
「パスワード」ではなく「アクセス権の書かれた鍵(トークン)」だけを連携先サービスに渡す仕組み
だとイメージしておくと理解しやすいです。
サービスB(連携する側)のバックエンド処理イメージ(Laravel/PHP)
ざっくりした流れは次の4ステップです。
- ユーザーがサービスBの画面で「サービスAと連携」ボタンを押す
- サービスBが認可画面のURLにリダイレクトし、コールバックで 認可コード(
code)を受け取る - その認可コードを使って アクセストークン を取得する
- 取得したアクセストークンを付けて、サービスAの API をコール する
<?php
namespace App\Services;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\GuzzleException;
class ServiceAOAuthClient
{
private const MAX_RETRY = 3;
private const RETRY_INTERVAL_SEC = 2;
private HttpClient $http;
// ① 認可コードをもらうためのURLを組み立てる(ここにリダイレクトする)
public function buildAuthorizeUrl(): string
{
$query = http_build_query([
'response_type' => 'code',
'client_id' => config('services.service_a.client_id'),
'redirect_uri' => route('oauth.callback'),
'scope' => 'profile read',
]);
return config('services.service_a.authorize_url') . '?' . $query;
}
// ② まとめて実行(認可コードからアクセストークン取得 → API呼び出し)
public function send(string $authorizationCode): array
{
try {
for ($attempt = 1; $attempt <= self::MAX_RETRY; $attempt++) {
logger()->info('ServiceA連携開始', ['attempt' => $attempt]);
// 1. アクセストークン取得
$tokenResult = $this->getAccessToken($authorizationCode);
if ($tokenResult['result'] === false) {
if ($attempt < self::MAX_RETRY) {
sleep(self::RETRY_INTERVAL_SEC);
continue;
}
throw new \RuntimeException('アクセストークン取得に失敗しました');
}
// 2. API呼び出し
$apiResult = $this->callApi($tokenResult['access_token']);
if ($apiResult['result'] === false) {
if ($attempt < self::MAX_RETRY) {
sleep(self::RETRY_INTERVAL_SEC);
continue;
}
throw new \RuntimeException('API呼び出しに失敗しました');
}
// 成功したら結果を返して終了
return ['success' => true, 'result_data' => $apiResult['response_data'] ?? null];
}
return ['success' => false, 'result_data' => null];
} catch (\Exception $e) {
logger()->error('ServiceA連携エラー', ['message' => $e->getMessage()]);
return ['success' => false, 'result_data' => $e->getMessage()];
}
}
// 認可コードからアクセストークンを取得するイメージ
private function getAccessToken(string $authorizationCode): array
{
try {
$response = $this->http->post(config('services.service_a.token_url'), [
'form_params' => [
'grant_type' => 'authorization_code',
'code' => $authorizationCode,
'redirect_uri' => route('oauth.callback'),
'client_id' => config('services.service_a.client_id'),
'client_secret' => config('services.service_a.client_secret'),
],
]);
$data = json_decode((string) $response->getBody(), true);
$accessToken = $data['access_token'] ?? null;
return ['result' => true, 'access_token' => $accessToken];
} catch (GuzzleException $e) {
logger()->warning('アクセストークン取得失敗', ['message' => $e->getMessage()]);
return ['result' => false, 'access_token' => null];
}
}
// アクセストークンを付けてサービスAのAPIを叩くイメージ
private function callApi(string $accessToken): array
{
try {
$response = $this->http->get('/v1/profile', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
'Accept' => 'application/json',
],
]);
$data = json_decode((string) $response->getBody(), true);
return ['result' => true, 'response_data' => $data];
} catch (GuzzleException $e) {
logger()->warning('API呼び出し失敗', ['message' => $e->getMessage()]);
return ['result' => false, 'response_data' => null];
}
}
}
OAuth 1.0 と OAuth 2.0 のざっくり比較表
最後に、よく聞かれる「1.0 と 2.0 の違い」をざっくり表にしておきます。
| 観点 | OAuth 1.0 | OAuth 2.0 |
|---|---|---|
| リクエスト保護 | リクエストごとに秘密の鍵つきハッシュを付ける | HTTPS 前提+Bearerトークン(Authorization: Bearer ...) |
| 実装の難易度 | 署名生成が複雑で実装が重い | トークンをヘッダに乗せるだけでシンプル |
| フローの種類 | 固定的(リクエストトークン → アクセストークン) | Authorization Code / Client Credentials / Password など複数 |
| 主な利用シーン | あまり使われていない | Google / GitHub / Salesforce など、現在主流のAPI連携ほぼすべて |