GTM APIにOAuth 2.0で接続する方法
Published on 2025年12月6日
メモはじめに
Google Tag Manager (GTM) APIにOAuth 2.0で接続しようとしたら、思った以上にハマりどころが多かったのでまとめました。
この記事では、認証の設定からAPIでデータを取得するまでの全手順と、実際につまずいたポイントを共有します。
この記事の対象読者
- GTM APIを初めて触る開発者
- OAuth 2.0の実装経験はあるが、Google APIは初めての方
- 「なぜか認証が通らない」で困っている方
検証の目的
- OAuth 2.0を使用してGTM APIへの認証が可能か
- 認証後、GTMアカウント情報を取得できるか
- リフレッシュトークンを使用した継続的なアクセスが可能か
事前準備:GCPコンソールでの設定
1. Tag Manager APIの有効化
GCPコンソールで以下の手順を実施します:
- 「APIとサービス」→「ライブラリ」を開く
- 「Tag Manager API」を検索
- 「有効にする」をクリック
⚠️ これを忘れると、認証は通ってもAPI呼び出し時に
403 Forbiddenエラーになります。
2. OAuth同意画面の設定
「APIとサービス」→「OAuth同意画面」で以下を設定:
- ユーザーの種類: 内部 or 外部(テスト段階では内部推奨)
- スコープ: Tag Manager APIのスコープを追加
https://www.googleapis.com/auth/tagmanager.readonlyhttps://www.googleapis.com/auth/tagmanager.edit.containers
3. OAuthクライアントIDの作成
「認証情報」→「認証情報を作成」→「OAuthクライアントID」
- アプリケーションの種類: ウェブアプリケーション
- 承認済みのリダイレクトURI:
http://localhost/callback
⚠️ リダイレクトURIは完全一致が必要です。末尾のスラッシュの有無でもエラーになります。
OAuth 2.0 認証フローの全体像
┌──────────────┐ ①認可URL生成 ┌──────────────┐
│ │ ────────────────────> │ │
│ アプリ │ │ Google │
│ │ <──────────────────── │ 認証サーバー│
└──────────────┘ ②認可コード返却 └──────────────┘
│
│ ③認可コードでトークン交換
v
┌──────────────┐ ④API呼び出し ┌──────────────┐
│ │ ────────────────────> │ │
│ アプリ │ │ GTM API │
│ │ <──────────────────── │ │
└──────────────┘ ⑤レスポンス └──────────────┘
重要なパラメータ
パラメータ
値
説明
access_type
offline
リフレッシュトークンを取得するために必須
prompt
consent
毎回同意画面を表示(再認可時もリフレッシュトークンを取得)
scope
GTMスコープ
必要な権限を指定
redirect_uri
コールバックURL
GCPコンソールの設定と完全一致させる
Step 1:googleapis パッケージの導入
GTM APIを呼び出す方法はいくつかありますが、今回は googleapis を選択しました。
選定理由:
google.tagmanager()でタイプセーフにAPIを呼び出せる- 認証も
google.auth.OAuth2で統合的に扱える - APIレスポンスの型定義が含まれている
npm install googleapis dotenv
Step 2:プロトタイプで動作確認
まずは最小構成で動くものを作ります。
import { google } from 'googleapis';
const CLIENT_ID = 'your-client-id.apps.googleusercontent.com';
const CLIENT_SECRET = 'your-client-secret';
const REFRESH_TOKEN = 'your-refresh-token';
async function main() {
const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET);
oauth2Client.setCredentials({ refresh_token: REFRESH_TOKEN });
const tagmanager = google.tagmanager({
version: 'v2',
auth: oauth2Client,
});
const res = await tagmanager.accounts.list();
console.log(res.data);
}
main();
実行結果:
{
"account": [
{
"path": "accounts/xxxxxxxxxx",
"accountId": "xxxxxxxxxx",
"name": "My GTM Account"
}
]
}
GTM APIへの疎通を確認できました。
Step 3:リフレッシュトークン取得フローの実装
新規ユーザーの場合、ブラウザでの同意フローが必要です。
認可URLの生成
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
scope: ['https://www.googleapis.com/auth/tagmanager.readonly'],
redirect_uri: 'http://localhost/callback',
});
console.log('以下のURLをブラウザで開いてください:');
console.log(authUrl);
コールバックサーバーの実装
import http from 'http';
import { URL } from 'url';
async function waitForAuthCode(): Promise<string> {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
if (!req.url) {
res.statusCode = 400;
res.end('Bad Request');
return;
}
const url = new URL(req.url, 'http://localhost');
if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
const error = url.searchParams.get('error');
if (error) {
res.statusCode = 400;
res.end(`Error: ${error}`);
server.close();
reject(new Error(`OAuth Error: ${error}`));
return;
}
if (!code) {
res.statusCode = 400;
res.end('No code in query');
server.close();
reject(new Error('No authorization code received'));
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('認可が完了しました。ターミナルに戻ってください。');
server.close();
resolve(code);
} else {
res.statusCode = 404;
res.end('Not Found');
}
});
server.listen(80, () => {
console.log('🌐 http://localhost/callback を待ち受け中...');
});
});
}
トークンへの交換
const code = await waitForAuthCode();
const { tokens } = await oauth2Client.getToken({
code,
redirect_uri: 'http://localhost/callback',
});
console.log('refresh_token:', tokens.refresh_token); // これを保存!
Step 4:環境変数対応
認証情報のハードコードはセキュリティリスクです。環境変数に移行しましょう。
.env ファイル:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost/callback
GOOGLE_REFRESH_TOKEN=your-refresh-token
⚠️
.envは必ず.gitignoreに追加してください!
# .gitignore に追加
echo ".env" >> .gitignore
コードでの読み込み:
import 'dotenv/config';
const CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
if (!CLIENT_ID || !CLIENT_SECRET) {
throw new Error('環境変数が未設定です');
}
つまずいたポイント5選
1. redirect_uri_mismatch エラー
原因: GCPコンソールのURIとコード内のURIが不一致
解決策:
- 末尾スラッシュを確認(
/callbackvs/callback/) - ポート番号を確認(
:80の有無) - プロトコルを確認(
httpvshttps)
2. リフレッシュトークンが返ってこない
原因: 同一ユーザーで既に同意済みの場合、2回目以降は返されない
解決策:
- Googleアカウントのセキュリティ設定 でアプリのアクセス権を取り消し
- 再度同意フローを実行
3. ポート80でのリッスンに失敗
原因: ポート80は特権ポートのため、root権限が必要
解決策:
sudo npx tsx scripts/gtm-prototype.ts
または、リダイレクトURIを http://localhost:3000/callback に変更。
4. invalid_grant エラー
原因: リフレッシュトークンが無効化された
解決策: 再度同意フローを実行し、新しいトークンを取得。
5. 403 Forbidden エラー
原因: Tag Manager APIが有効化されていない
解決策: GCPコンソールの「APIとサービス」→「ライブラリ」から有効化。
完成したコード
全体のソースコード
import { google } from 'googleapis';
import http from 'http';
import { URL } from 'url';
import 'dotenv/config';
const CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || 'http://localhost/callback';
const SCOPES = ['https://www.googleapis.com/auth/tagmanager.readonly'];
const REFRESH_TOKEN = process.env.GOOGLE_REFRESH_TOKEN || null;
async function waitForAuthCode(): Promise<string> {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
if (!req.url) {
res.statusCode = 400;
res.end('Bad Request');
return;
}
const url = new URL(req.url, 'http://localhost');
if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
const error = url.searchParams.get('error');
if (error) {
res.statusCode = 400;
res.end(`Error: ${error}`);
server.close();
reject(new Error(`OAuth Error: ${error}`));
return;
}
if (!code) {
res.statusCode = 400;
res.end('No code in query');
server.close();
reject(new Error('No authorization code received'));
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('認可が完了しました。ターミナルに戻ってください。');
server.close();
resolve(code);
} else {
res.statusCode = 404;
res.end('Not Found');
}
});
server.listen(80, () => {
console.log('🌐 http://localhost/callback を待ち受け中...');
});
server.on('error', (err) => reject(err));
});
}
async function ensureRefreshToken(oauth2Client: any): Promise<string> {
if (REFRESH_TOKEN) return REFRESH_TOKEN;
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
scope: SCOPES,
redirect_uri: REDIRECT_URI,
});
console.log('🔑 ブラウザで以下のURLを開いてください:');
console.log(authUrl);
const code = await waitForAuthCode();
const { tokens } = await oauth2Client.getToken({ code, redirect_uri: REDIRECT_URI });
if (!tokens.refresh_token) {
throw new Error('refresh_token が返されませんでした');
}
console.log('✅ refresh_token:', tokens.refresh_token);
return tokens.refresh_token;
}
async function main() {
if (!CLIENT_ID || !CLIENT_SECRET) {
throw new Error('環境変数が未設定です');
}
const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
const refreshToken = await ensureRefreshToken(oauth2Client);
oauth2Client.setCredentials({ refresh_token: refreshToken });
await oauth2Client.getAccessToken();
const tagmanager = google.tagmanager({ version: 'v2', auth: oauth2Client });
const res = await tagmanager.accounts.list();
console.log('✅ GTM accounts:');
console.log(JSON.stringify(res.data, null, 2));
}
main().catch((error) => {
console.error('❌ Error:', error);
process.exit(1);
});
まとめ
項目
結果
OAuth 2.0認証
✅ 成功
リフレッシュトークン取得
✅ 成功
GTMアカウント一覧取得
✅ 成功
コンテナ・タグ一覧取得
✅ 成功
ポイント:
access_type=offline+prompt=consentでリフレッシュトークンを確実に取得redirect_uriは完全一致が必要googleapisパッケージでタイプセーフに実装可能
参考リンク
同じところでつまずいた方、他にもハマりポイントがあった方は、ぜひコメントで共有してください!