<!DOCTYPE html>
<html>
<head>
<title>サードパーティアプリケーションの例</title>
<style>
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
.user-info { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }
.login-section { text-align: center; padding: 40px; }
.btn { padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
.btn-primary { background: #007bff; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<h1>私のサードパーティアプリケーション</h1>
<!-- ロード状態 -->
<div id="loading">
<p>ログイン状態を確認中...</p>
</div>
<!-- ログイン済みユーザー情報 -->
<div id="user-info" class="user-info hidden">
<h2>おかえりなさい!</h2>
<p><strong>ユーザー名:</strong><span id="username"></span></p>
<p><strong>メールアドレス:</strong><span id="email"></span></p>
<p><strong>アカウント残高:</strong><span id="balance"></span></p>
<div style="margin-top: 20px;">
<button class="btn btn-success" onclick="refreshBalance()">残高を更新</button>
<button class="btn btn-primary" onclick="goToTopup()">アカウントチャージ</button>
<button class="btn btn-secondary" onclick="logout()">ログアウト</button>
</div>
</div>
<!-- 未ログイン状態 -->
<div id="login-section" class="login-section hidden">
<h2>アカウント情報を表示するにはログインしてください</h2>
<p>ワンクリックログインでアカウントに素早くアクセス</p>
<button class="btn btn-primary" onclick="oneClickLogin()">
🚀 ワンクリックログイン
</button>
</div>
</div>
<script>
// OAuth設定
const OAUTH_CONFIG = {
authServer: 'https://your-domain.com',
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET', // 本番環境ではバックエンドで処理すべき
redirectUri: window.location.origin + '/oauth/callback.html',
scope: 'profile balance'
};
class OAuthManager {
constructor() {
this.accessToken = localStorage.getItem('oauth_access_token');
this.refreshToken = localStorage.getItem('oauth_refresh_token');
this.tokenExpiresAt = localStorage.getItem('oauth_token_expires_at');
this.isRefreshing = false; // 同時リフレッシュ防止
this.init();
}
async init() {
// URLに認証コードがあるか確認
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
if (code) {
await this.handleAuthCallback(code, state);
// URLをクリーンアップ
window.history.replaceState({}, document.title, window.location.pathname);
} else if (this.accessToken) {
try {
// トークンが期限切れか確認し、期限切れの場合はリフレッシュを試みる
if (this.isTokenExpired()) {
await this.refreshTokenIfNeeded();
}
await this.fetchUserInfo();
this.showUserInfo();
} catch (error) {
console.log('トークンが期限切れか無効です。再ログインが必要です');
this.clearTokens();
this.showLoginSection();
}
} else {
this.showLoginSection();
}
document.getElementById('loading').classList.add('hidden');
}
// アクセストークンが期限切れか確認
isTokenExpired() {
if (!this.tokenExpiresAt) return false;
const expiryTime = parseInt(this.tokenExpiresAt);
const bufferTime = 5 * 60 * 1000; // 5分間のバッファ時間
return Date.now() > (expiryTime - bufferTime);
}
// 必要に応じてトークンを自動リフレッシュ
async refreshTokenIfNeeded() {
if (!this.refreshToken || this.isRefreshing) {
return false;
}
this.isRefreshing = true;
try {
const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: OAUTH_CONFIG.clientId,
client_secret: OAUTH_CONFIG.clientSecret,
})
});
if (response.ok) {
const tokenData = await response.json();
this.updateTokens(tokenData);
return true;
} else {
throw new Error('トークンのリフレッシュに失敗しました');
}
} catch (error) {
console.error('トークンのリフレッシュに失敗しました:', error);
this.clearTokens();
return false;
} finally {
this.isRefreshing = false;
}
}
// トークン情報を更新
updateTokens(tokenData) {
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
localStorage.setItem('oauth_access_token', this.accessToken);
localStorage.setItem('oauth_refresh_token', this.refreshToken);
localStorage.setItem('oauth_token_expires_at', this.tokenExpiresAt.toString());
}
oneClickLogin() {
const state = this.generateState();
localStorage.setItem('oauth_state', state);
const authUrl = `${OAUTH_CONFIG.authServer}/api/oauth2/authorize?` +
`client_id=${OAUTH_CONFIG.clientId}&` +
`redirect_uri=${encodeURIComponent(OAUTH_CONFIG.redirectUri)}&` +
`response_type=code&` +
`scope=${encodeURIComponent(OAUTH_CONFIG.scope)}&` +
`state=${state}&` +
`auto_authorize=true`; // 自動認証を有効にして、真のワンクリックログインを実現
// ポップアップで認証ページを開く
const popup = window.open(authUrl, 'oauth_login', 'width=500,height=600,scrollbars=yes');
// ポップアップのクローズを監視
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
// 認証が取得されたか確認
setTimeout(() => this.init(), 1000);
}
}, 1000);
}
async handleAuthCallback(code, state) {
const savedState = localStorage.getItem('oauth_state');
if (state !== savedState) {
console.error('Stateパラメータが一致しません');
return;
}
try {
const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: OAUTH_CONFIG.clientId,
client_secret: OAUTH_CONFIG.clientSecret,
redirect_uri: OAUTH_CONFIG.redirectUri
})
});
const tokenData = await response.json();
if (tokenData.access_token) {
this.updateTokens(tokenData);
localStorage.removeItem('oauth_state');
await this.fetchUserInfo();
this.showUserInfo();
}
} catch (error) {
console.error('アクセストークンの取得に失敗しました:', error);
}
}
// 自動リフレッシュ付きAPIリクエストメソッド
async apiRequest(url, options = {}) {
// トークンを確認してリフレッシュ
if (this.isTokenExpired()) {
const refreshed = await this.refreshTokenIfNeeded();
if (!refreshed) {
throw new Error('トークンのリフレッシュができません');
}
}
const headers = {
'Authorization': `Bearer ${this.accessToken}`,
...options.headers
};
const response = await fetch(url, {
...options,
headers
});
// 401エラーを受け取った場合、トークンをリフレッシュしてもう一度試す
if (response.status === 401 && !options._retry) {
const refreshed = await this.refreshTokenIfNeeded();
if (refreshed) {
return this.apiRequest(url, { ...options, _retry: true });
}
}
return response;
}
async fetchUserInfo() {
const response = await this.apiRequest(`${OAUTH_CONFIG.authServer}/api/oauth2/userinfo`);
if (!response.ok) {
throw new Error('ユーザー情報の取得に失敗しました');
}
this.userInfo = await response.json();
}
showUserInfo() {
document.getElementById('username').textContent = this.userInfo.username;
document.getElementById('email').textContent = this.userInfo.email;
document.getElementById('balance').textContent = this.userInfo.balance_formatted;
document.getElementById('user-info').classList.remove('hidden');
document.getElementById('login-section').classList.add('hidden');
}
showLoginSection() {
document.getElementById('user-info').classList.add('hidden');
document.getElementById('login-section').classList.remove('hidden');
}
async refreshBalance() {
try {
await this.fetchUserInfo();
document.getElementById('balance').textContent = this.userInfo.balance_formatted;
alert('残高が更新されました');
} catch (error) {
alert('リフレッシュに失敗しました。再ログインしてください');
this.clearTokens();
this.showLoginSection();
}
}
async goToTopup() {
try {
const response = await this.apiRequest(`${OAUTH_CONFIG.authServer}/api/oauth2/topup`);
const data = await response.json();
if (data.topup_url) {
window.open(data.topup_url, '_blank');
}
} catch (error) {
console.error('チャージリンクの取得に失敗しました:', error);
}
}
logout() {
this.clearTokens();
this.showLoginSection();
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiresAt = null;
this.userInfo = null;
localStorage.removeItem('oauth_access_token');
localStorage.removeItem('oauth_refresh_token');
localStorage.removeItem('oauth_token_expires_at');
}
generateState() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
}
// グローバル関数
let oauthManager;
function oneClickLogin() {
oauthManager.oneClickLogin();
}
function refreshBalance() {
oauthManager.refreshBalance();
}
function goToTopup() {
oauthManager.goToTopup();
}
function logout() {
oauthManager.logout();
}
// ページ読み込み完了後に初期化
document.addEventListener('DOMContentLoaded', () => {
oauthManager = new OAuthManager();
});
</script>
</body>
</html>