Làm việc với AI

Dùng bản tóm tắt tối ưu cho ChatGPT, Claude, Grok hoặc Cursor.

Mở bản Markdown cho AI
Hãy đọc docs/AI-CONTEXT.md trước và tích hợp ứng dụng hiện tại với PHP SSO Service theo OAuth 2.0 Authorization Code Flow và OpenID Connect. Kiểm tra framework và session system của ứng dụng client; triển khai login route, callback route, state validation, PKCE nếu là public client, token exchange, UserInfo request và tạo session nội bộ. Dùng biến môi trường cho SSO_BASE_URL, CLIENT_ID, CLIENT_SECRET và REDIRECT_URI. Không sửa source code của SSO Service, không commit hoặc log secret/token, không thay đổi OAuth contract, và bổ sung hướng dẫn cấu hình cùng kiểm thử.
Tài liệu Code Mẫu

Ví Dụ Code

Hướng dẫn tích hợp OAuth2 cho các kịch bản thực tế khác nhau.

Ứng Dụng Web Truyền Thống

Website PHP truyền thống với session server-side.

Tệp: login.php

<?php
session_start();
require_once 'config.php';

// Tạo state token
$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;

// Xây dựng URL phân quyền
$auth_url = APP_URL . '/authorize.php?' . http_build_query([
    'client_id' => CLIENT_ID,
    'response_type' => 'code',
    'redirect_uri' => REDIRECT_URI,
    'scope' => 'openid profile email',
    'state' => $state
]);

header('Location: ' . $auth_url);
exit;

Tệp: callback.php

<?php
session_start();
require_once 'config.php';

// Kiểm tra state
if (!hash_equals($_SESSION['oauth_state'] ?? '', $_GET['state'] ?? '')) {
    die('Lỗi: State không khớp');
}

// Trao đổi code lấy token
$token_response = file_get_contents(APP_URL . '/api/token', false, 
    stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/x-www-form-urlencoded',
            'content' => http_build_query([
                'grant_type' => 'authorization_code',
                'code' => $_GET['code'],
                'client_id' => CLIENT_ID,
                'client_secret' => CLIENT_SECRET,
                'redirect_uri' => REDIRECT_URI
            ])
        ]
    ])
);

$tokens = json_decode($token_response, true);
if (!$tokens || isset($tokens['error'])) {
    die('Lỗi: Không lấy được token');
}

// Lưu token vào session
$_SESSION['access_token'] = $tokens['access_token'];
$_SESSION['refresh_token'] = $tokens['refresh_token'] ?? null;

// Lấy thông tin người dùng
$userinfo = file_get_contents(APP_URL . '/api/userinfo', false,
    stream_context_create([
        'http' => [
            'header' => 'Authorization: Bearer ' . $tokens['access_token']
        ]
    ])
);

$user = json_decode($userinfo, true);
$_SESSION['user'] = $user;

header('Location: dashboard.php');
exit;

Single Page Application (React)

Ứng dụng React với PKCE flow (bảo mật hơn cho SPA).

Tệp: useOAuth.js

import { useState } from 'react';

export function useOAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const generatePKCE = async () => {
    const verifier = generateRandomString(43);
    const challenge = await sha256(verifier);
    localStorage.setItem('pkce_verifier', verifier);
    return challenge;
  };

  const login = async () => {
    setLoading(true);
    try {
      const state = generateRandomString(16);
      const challenge = await generatePKCE();
      
      localStorage.setItem('oauth_state', state);
      
      const authUrl = `${process.env.REACT_APP_AUTH_URL}/authorize.php?` + 
        new URLSearchParams({
          client_id: process.env.REACT_APP_CLIENT_ID,
          response_type: 'code',
          redirect_uri: window.location.origin + '/callback',
          scope: 'openid profile email',
          state,
          code_challenge: challenge,
          code_challenge_method: 'S256'
        });
      
      window.location.href = authUrl;
    } catch (err) {
      setError(err.message);
      setLoading(false);
    }
  };

  return { user, loading, error, login };
}

Mobile API (Native/Flutter)

Ứng dụng di động gọi API backend.

Tệp: lib/services/auth_service.dart

import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthService {
  final _appAuth = FlutterAppAuth();
  final _storage = FlutterSecureStorage();
  
  static const _clientId = 'mobile_app';
  static const _redirectUrl = 'com.myapp:/oauth-callback';
  static const _discoveryUrl = 'https://auth.example.com';

  Future<bool> login() async {
    try {
      final result = await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(
          _clientId,
          _redirectUrl,
          discoveryUrl: _discoveryUrl,
          scopes: ['openid', 'profile', 'email'],
          promptValues: ['login'],
        ),
      );

      if (result != null) {
        await _storage.write(
          key: 'access_token',
          value: result.accessToken,
        );
        await _storage.write(
          key: 'refresh_token',
          value: result.refreshToken ?? '',
        );
        return true;
      }
      return false;
    } catch (e) {
      print('Lỗi OAuth: $e');
      return false;
    }
  }
}

Microservices (Node.js)

Service-to-service authentication với client credentials flow.

Tệp: auth-client.js

const axios = require('axios');

class ServiceAuthClient {
  constructor(clientId, clientSecret, authUrl) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.authUrl = authUrl;
    this.accessToken = null;
    this.tokenExpiry = null;
  }

  async getToken() {
    if (this.accessToken && this.tokenExpiry > Date.now()) {
      return this.accessToken;
    }

    try {
      const response = await axios.post(
        `${this.authUrl}/api/token`,
        {
          grant_type: 'client_credentials',
          client_id: this.clientId,
          client_secret: this.clientSecret,
          scope: 'service:read service:write'
        }
      );

      this.accessToken = response.data.access_token;
      this.tokenExpiry = Date.now() + response.data.expires_in * 1000;
      
      return this.accessToken;
    } catch (error) {
      throw new Error(`Lỗi lấy token: ${error.message}`);
    }
  }

  async request(method, url, data = null) {
    const token = await this.getToken();
    
    return axios({
      method,
      url,
      data,
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });
  }
}

module.exports = ServiceAuthClient;

Background Job (Python)

Python script cho công việc background.

Tệp: jobs/data_sync.py

import requests
import json
from datetime import datetime, timedelta

class OAuthClient:
    def __init__(self, client_id, client_secret, auth_url):
        self.client_id = client_id
        self.client_secret = client_secret
        self.auth_url = auth_url
        self.token = None
        self.token_expiry = None

    def get_token(self):
        if self.token and self.token_expiry > datetime.now():
            return self.token

        try:
            response = requests.post(
                f'{self.auth_url}/api/token',
                data={
                    'grant_type': 'client_credentials',
                    'client_id': self.client_id,
                    'client_secret': self.client_secret
                }
            )
            response.raise_for_status()
            
            data = response.json()
            self.token = data['access_token']
            self.token_expiry = datetime.now() + timedelta(
                seconds=data['expires_in']
            )
            
            return self.token
        except requests.RequestException as e:
            raise Exception(f'Lỗi OAuth: {e}')

    def get_user_data(self, user_id):
        token = self.get_token()
        
        response = requests.get(
            f'{self.auth_url}/api/users/{user_id}',
            headers={'Authorization': f'Bearer {token}'}
        )
        response.raise_for_status()
        return response.json()

Public API Client

Client công khai với giới hạn tỷ lệ.

Tệp: api-client.js

class PublicAPIClient {
  constructor(apiUrl, clientId, clientSecret) {
    this.apiUrl = apiUrl;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.rateLimitRemaining = null;
    this.rateLimitReset = null;
  }

  async request(endpoint, method = 'GET', data = null) {
    try {
      const options = {
        method,
        headers: {
          'Authorization': `Basic ${btoa(
            this.clientId + ':' + this.clientSecret
          )}`,
          'Content-Type': 'application/json'
        }
      };

      if (data) {
        options.body = JSON.stringify(data);
      }

      const response = await fetch(
        `${this.apiUrl}${endpoint}`,
        options
      );

      this.rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
      this.rateLimitReset = response.headers.get('X-RateLimit-Reset');

      if (response.status === 429) {
        throw new Error(
          `Vượt giới hạn tỷ lệ. Thử lại sau ${this.rateLimitReset}s`
        );
      }

      if (!response.ok) {
        throw new Error(`API Error: ${response.statusText}`);
      }

      return await response.json();
    } catch (error) {
      console.error('Lỗi API:', error);
      throw error;
    }
  }

  async getUsers() {
    return this.request('/api/users');
  }

  async getUserData(userId) {
    return this.request(`/api/users/${userId}`);
  }

  getRateLimitStatus() {
    return {
      remaining: this.rateLimitRemaining,
      resetIn: this.rateLimitReset
    };
  }
}

Error Handling Pattern

Xử lý lỗi toàn cầu cho tất cả trường hợp.

Tệp: error-handler.js

class OAuthErrorHandler {
  static getErrorMessage(error) {
    if (error.error === 'invalid_token') {
      return 'Token không hợp lệ hoặc hết hạn. Vui lòng đăng nhập lại.';
    }

    if (error.error === 'expired_token') {
      return 'Token đã hết hạn. Đang làm mới...';
    }

    if (error.error === 'access_denied') {
      return 'Bạn đã từ chối cấp quyền.';
    }

    if (error.error === 'invalid_scope') {
      return 'Quyền hạn không được hỗ trợ.';
    }

    if (error.error === 'invalid_client') {
      return 'Ứng dụng không hợp lệ. Liên hệ với admin.';
    }

    if (error.error === 'invalid_request') {
      return 'Yêu cầu không hợp lệ: ' + (error.error_description || '');
    }

    return 'Lỗi xác thực. Vui lòng thử lại.';
  }

  static async handleTokenRefresh(refreshToken, authUrl, clientId) {
    try {
      const response = await fetch(`${authUrl}/api/token`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
          client_id: clientId
        })
      });

      if (!response.ok) {
        throw new Error('Refresh token hết hạn');
      }

      return await response.json();
    } catch (error) {
      console.error('Lỗi làm mới token:', error);
      return null;
    }
  }
}

module.exports = OAuthErrorHandler;

Mẹo: Sử dụng error handler này cho tất cả OAuth operations để cung cấp trải nghiệm người dùng tốt hơn.

Khắc Phục Sự Cố →

Cập nhật gần nhất: Tháng 10, 2025
Lên đầu trang