【初心者向け】プログラミングの「OAuth」仕組み解説

OAuthの基本的な仕組みと動作原理を初心者向けに解説。認証と認可の違い、実装方法、セキュリティのポイントを分かりやすく紹介します。

Learning Next 運営
32 分で読めます

みなさん、Webサイトで「Googleでログイン」「Twitterでログイン」というボタンを見たことはありませんか?

パスワードを新しく作ることなく、既存のアカウントでログインできる便利な機能ですよね。 実は、この仕組みの裏側では「OAuth」という技術が使われています。

この記事では、OAuthの基本的な仕組みから実装方法まで、初心者でも理解できるよう詳しく解説します。 現代のWeb開発で欠かせないOAuthについて、しっかりと理解を深めましょう。

OAuthとは何か

OAuthは「Open Authorization」の略で、第三者のアプリケーションに対して、ユーザーの代わりにリソースへのアクセス権限を安全に委譲するための仕組みです。

簡単に言うと、「パスワードを教えることなく、他のサービスの情報を使う許可を与える仕組み」です。 例えば、新しいアプリでGoogleアカウントを使ってログインする際、Googleにパスワードを直接教えるのではなく、一時的な許可証をもらってアクセスします。

身近な例で理解する

OAuthを身近な例で説明すると、以下のようになります。

  • 従来の方法: 友人に家の鍵を渡して留守番を頼む
  • OAuth: 友人に一時的な入館カードを渡して、必要な部屋だけにアクセスしてもらう

入館カードは期限付きで、必要がなくなったら無効にできます。 これがOAuthの基本的な考え方です。

認証と認可の違い

OAuthを理解する前に、「認証」と「認可」の違いを理解することが重要です。

認証(Authentication)

認証は「その人が本当にその人であるかを確認すること」です。

// 認証の例
function authenticate(username, password) {
const user = database.findUser(username);
if (user && user.password === hashPassword(password)) {
return {
success: true,
userId: user.id,
username: user.username
};
}
return { success: false, error: 'Invalid credentials' };
}

身分証明書の確認のようなものです。

認可(Authorization)

認可は「その人が特定のリソースにアクセスする権限があるかを確認すること」です。

// 認可の例
function authorize(userId, resource, action) {
const permissions = database.getUserPermissions(userId);
return permissions.some(permission =>
permission.resource === resource &&
permission.actions.includes(action)
);
}
// 使用例
if (authorize(userId, 'user_profile', 'read')) {
// プロフィール情報を表示
return getUserProfile(userId);
} else {
return { error: 'Access denied' };
}

OAuthは主に「認可」の仕組みであり、「この人にこのデータへのアクセスを許可するか」を管理します。

OAuthの基本的な流れ

OAuthの動作を、具体的な流れで説明しましょう。

登場人物の整理

OAuthには以下の4つの主要な登場人物がいます。

// OAuth の登場人物
const oauthActors = {
"リソースオーナー": {
"説明": "データの所有者(ユーザー)",
"例": "Googleアカウントを持っている人"
},
"クライアント": {
"説明": "データにアクセスしたいアプリケーション",
"例": "新しいWebサービス"
},
"認可サーバー": {
"説明": "アクセス許可を管理するサーバー",
"例": "Google の認証サーバー"
},
"リソースサーバー": {
"説明": "実際のデータを保持するサーバー",
"例": "Google のユーザー情報API"
}
};

基本的な認可フロー

最も一般的な「認可コードフロー」を例に説明します。

sequenceDiagram
participant User as ユーザー
participant Client as クライアントアプリ
participant AuthServer as 認可サーバー
participant ResourceServer as リソースサーバー
User->>Client: 1. ログインボタンをクリック
Client->>AuthServer: 2. 認可リクエスト
AuthServer->>User: 3. ログイン画面を表示
User->>AuthServer: 4. ログイン情報を入力
AuthServer->>User: 5. 権限の許可画面を表示
User->>AuthServer: 6. 許可ボタンをクリック
AuthServer->>Client: 7. 認可コードを送信
Client->>AuthServer: 8. アクセストークンを要求
AuthServer->>Client: 9. アクセストークンを送信
Client->>ResourceServer: 10. トークンでAPI呼び出し
ResourceServer->>Client: 11. ユーザーデータを返送

実装例での理解

実際のコードを使って、流れを理解してみましょう。

// Step 1: 認可リクエストの生成
function generateAuthorizationUrl() {
const baseUrl = 'https://accounts.google.com/oauth/authorize';
const params = new URLSearchParams({
'client_id': 'your_client_id',
'redirect_uri': 'https://yourapp.com/callback',
'scope': 'openid email profile',
'response_type': 'code',
'state': generateRandomState() // CSRF対策
});
return `${baseUrl}?${params.toString()}`;
}
// Step 2: 認可コードの処理
async function handleCallback(authorizationCode, state) {
// state の検証(セキュリティ対策)
if (!isValidState(state)) {
throw new Error('Invalid state parameter');
}
// アクセストークンの取得
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
'client_id': 'your_client_id',
'client_secret': 'your_client_secret',
'code': authorizationCode,
'grant_type': 'authorization_code',
'redirect_uri': 'https://yourapp.com/callback'
})
});
const tokens = await tokenResponse.json();
return tokens;
}
// Step 3: APIの呼び出し
async function getUserInfo(accessToken) {
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return await response.json();
}

このように、段階的に許可を得てデータにアクセスします。

OAuth 2.0の主要な仕組み

OAuth 2.0には、様々な認可フローが用意されています。

認可コードフロー

最も安全で推奨される方法です。

# 認可コードフローの実装例
import requests
import secrets
class OAuthClient:
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self.auth_url = "https://accounts.google.com/oauth/authorize"
self.token_url = "https://oauth2.googleapis.com/token"
def get_authorization_url(self, scope="openid email profile"):
"""認可URLを生成"""
state = secrets.token_urlsafe(32)
params = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'scope': scope,
'response_type': 'code',
'state': state
}
# stateを保存(セッションなどに)
self.save_state(state)
query_string = '&'.join([f"{k}={v}" for k, v in params.items()])
return f"{self.auth_url}?{query_string}", state
def exchange_code_for_token(self, authorization_code, state):
"""認可コードをアクセストークンに交換"""
# state の検証
if not self.verify_state(state):
raise ValueError("Invalid state parameter")
data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'code': authorization_code,
'grant_type': 'authorization_code',
'redirect_uri': self.redirect_uri
}
response = requests.post(self.token_url, data=data)
return response.json()

インプリシットフロー

SPAなどクライアントサイドでの使用に適していますが、セキュリティ上の理由で現在は推奨されていません。

クライアントクレデンシャルフロー

サーバー間通信で使用されるフローです。

// クライアントクレデンシャルフローの例
async function getClientCredentialsToken() {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
'client_id': 'your_client_id',
'client_secret': 'your_client_secret',
'grant_type': 'client_credentials',
'scope': 'https://www.googleapis.com/auth/cloud-platform'
})
});
const tokenData = await response.json();
return tokenData.access_token;
}

トークンの種類と管理

OAuthでは、複数の種類のトークンが使用されます。

アクセストークン

実際のAPIアクセスに使用されるトークンです。

// アクセストークンの使用例
class APIClient {
constructor(accessToken) {
this.accessToken = accessToken;
}
async makeAuthenticatedRequest(url) {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Token expired or invalid');
}
throw new Error(`API request failed: ${response.status}`);
}
return await response.json();
}
}

リフレッシュトークン

アクセストークンの更新に使用されます。

# リフレッシュトークンを使った更新
def refresh_access_token(refresh_token):
"""リフレッシュトークンを使ってアクセストークンを更新"""
data = {
'client_id': 'your_client_id',
'client_secret': 'your_client_secret',
'refresh_token': refresh_token,
'grant_type': 'refresh_token'
}
response = requests.post('https://oauth2.googleapis.com/token', data=data)
token_data = response.json()
return {
'access_token': token_data['access_token'],
'expires_in': token_data.get('expires_in'),
'refresh_token': token_data.get('refresh_token', refresh_token)
}
# 自動更新機能付きのクライアント
class AutoRefreshOAuthClient:
def __init__(self, access_token, refresh_token, expires_at):
self.access_token = access_token
self.refresh_token = refresh_token
self.expires_at = expires_at
def ensure_valid_token(self):
"""トークンの有効性を確認し、必要に応じて更新"""
import time
if time.time() >= self.expires_at - 60: # 1分前に更新
token_data = refresh_access_token(self.refresh_token)
self.access_token = token_data['access_token']
self.expires_at = time.time() + token_data['expires_in']
if 'refresh_token' in token_data:
self.refresh_token = token_data['refresh_token']

セキュリティの重要なポイント

OAuthを安全に実装するためのポイントを紹介します。

CSRF攻撃の防止

stateパラメータを使用してCSRF攻撃を防ぎます。

// CSRF攻撃防止の実装
class SecureOAuthClient {
constructor() {
this.pendingStates = new Map();
}
generateState() {
const state = crypto.randomUUID();
const timestamp = Date.now();
// 5分間有効なstateを保存
this.pendingStates.set(state, timestamp);
// 古いstateを清理
this.cleanupExpiredStates();
return state;
}
verifyState(state) {
const timestamp = this.pendingStates.get(state);
if (!timestamp) {
return false;
}
// 5分以内かチェック
const isValid = Date.now() - timestamp < 5 * 60 * 1000;
if (isValid) {
this.pendingStates.delete(state);
}
return isValid;
}
cleanupExpiredStates() {
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
for (const [state, timestamp] of this.pendingStates.entries()) {
if (timestamp < fiveMinutesAgo) {
this.pendingStates.delete(state);
}
}
}
}

PKCEの使用

公開クライアント(SPAやモバイルアプリ)では、PKCEを使用します。

// PKCE(Proof Key for Code Exchange)の実装
function generatePKCE() {
// code_verifier の生成
const codeVerifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));
// code_challenge の生成
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
return crypto.subtle.digest('SHA-256', data).then(hash => {
const codeChallenge = base64URLEncode(new Uint8Array(hash));
return {
codeVerifier,
codeChallenge,
codeChallengeMethod: 'S256'
};
});
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
// 使用例
async function startPKCEFlow() {
const pkce = await generatePKCE();
// 認可リクエストにcode_challengeを追加
const authUrl = new URL('https://accounts.google.com/oauth/authorize');
authUrl.searchParams.append('client_id', 'your_client_id');
authUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.append('scope', 'openid email');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('code_challenge', pkce.codeChallenge);
authUrl.searchParams.append('code_challenge_method', pkce.codeChallengeMethod);
// code_verifierを保存
sessionStorage.setItem('code_verifier', pkce.codeVerifier);
// 認可ページにリダイレクト
window.location.href = authUrl.toString();
}

トークンの安全な保存

トークンは適切に保存し、管理する必要があります。

# 安全なトークン保存の例
import os
import json
from cryptography.fernet import Fernet
class SecureTokenStorage:
def __init__(self, storage_path='tokens.enc'):
self.storage_path = storage_path
self.key = self._get_or_create_key()
self.cipher = Fernet(self.key)
def _get_or_create_key(self):
"""暗号化キーの取得または生成"""
key_path = 'token_key.key'
if os.path.exists(key_path):
with open(key_path, 'rb') as f:
return f.read()
else:
key = Fernet.generate_key()
with open(key_path, 'wb') as f:
f.write(key)
return key
def save_tokens(self, user_id, tokens):
"""トークンを暗号化して保存"""
data = self._load_all_tokens()
data[user_id] = tokens
encrypted_data = self.cipher.encrypt(json.dumps(data).encode())
with open(self.storage_path, 'wb') as f:
f.write(encrypted_data)
def load_tokens(self, user_id):
"""特定ユーザーのトークンを復号化して取得"""
data = self._load_all_tokens()
return data.get(user_id)
def _load_all_tokens(self):
"""すべてのトークンデータを復号化"""
if not os.path.exists(self.storage_path):
return {}
with open(self.storage_path, 'rb') as f:
encrypted_data = f.read()
decrypted_data = self.cipher.decrypt(encrypted_data)
return json.loads(decrypted_data.decode())

実践的な実装例

実際のWebアプリケーションでOAuthを実装する例を示します。

Express.js での実装

// Express.js を使ったOAuth実装
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: true
}));
// OAuth設定
const oauth_config = {
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: 'http://localhost:3000/auth/callback',
scope: 'openid email profile'
};
// ログインエンドポイント
app.get('/auth/login', (req, res) => {
const state = require('crypto').randomBytes(16).toString('hex');
req.session.oauth_state = state;
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.append('client_id', oauth_config.client_id);
authUrl.searchParams.append('redirect_uri', oauth_config.redirect_uri);
authUrl.searchParams.append('scope', oauth_config.scope);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('state', state);
res.redirect(authUrl.toString());
});
// コールバックエンドポイント
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
// state検証
if (state !== req.session.oauth_state) {
return res.status(400).send('Invalid state parameter');
}
try {
// アクセストークンの取得
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: oauth_config.client_id,
client_secret: oauth_config.client_secret,
code: code,
grant_type: 'authorization_code',
redirect_uri: oauth_config.redirect_uri
})
});
const tokens = await tokenResponse.json();
// ユーザー情報の取得
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
});
const user = await userResponse.json();
// セッションに保存
req.session.user = user;
req.session.tokens = tokens;
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth error:', error);
res.status(500).send('Authentication failed');
}
});
// 保護されたルート
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
return res.redirect('/auth/login');
}
res.json({
message: 'Welcome to your dashboard!',
user: req.session.user
});
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});

まとめ

OAuthは、現代のWeb開発において欠かせない重要な技術です。 パスワードを共有することなく、安全にデータアクセスの許可を管理できる優れた仕組みです。

OAuthを理解し実装するために、以下のポイントを押さえておきましょう。

  • 基本概念の理解: 認証と認可の違い、4つの登場人物の役割
  • 適切なフローの選択: アプリケーションタイプに応じた認可フロー
  • セキュリティ対策: CSRF防止、PKCE、トークンの安全な管理
  • 実装のベストプラクティス: エラーハンドリング、トークンの自動更新
  • 最新仕様への対応: OAuth 2.1やOpenID Connectなどの新しい仕様

最初は複雑に感じるかもしれませんが、基本的な流れを理解すれば、安全で便利な認証・認可システムを構築できます。 実際のプロジェクトでOAuthを実装する際は、セキュリティを最優先に考え、テストを十分に行うことが重要です。

ぜひ、この記事を参考に、OAuthを活用したアプリケーション開発に挑戦してみてください。 きっと、ユーザーにとって便利で安全なサービスを提供できるようになりますよ。

関連記事