Implement OAuth 2.1 PKCE Cho React SPA Với AI: Đúng Chuẩn Ngay Lần Đầu
OAuth 2.1 PKCE là tiêu chuẩn bảo mật cho SPA hiện đại. Dùng AI để implement đúng cách không đơn giản — đây là cách prompt để AI generate security-critical code chính xác.
SPA Security Vẫn Là Chủ Đề "Khó Nhai"
OAuth 2.1 PKCE (Proof Key for Code Exchange) là tiêu chuẩn bảo mật hiện tại cho Single Page Applications. Không còn implicit flow, không còn client secrets trong browser — chỉ có PKCE. Nhưng implement đúng cách không phải chuyện đơn giản, nhất là khi tích hợp với Keycloak hay các OAuth server tự host.
Tôi đã thử dùng Claude Code để implement OAuth 2.1 PKCE cho một React SPA. Kết quả khá ấn tượng — nhưng quan trọng hơn là cách prompt để AI generate đúng code cho security-critical features.
Vấn Đề Khi Prompt Naively
Nếu bạn chỉ prompt "implement OAuth PKCE login", AI thường generate code thiếu sót nghiêm trọng:
// Trước khi dùng AI đúng cách: output từ naive prompt
async function login() {
const authUrl = new URL(`${AUTH_SERVER}/authorize`);
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid');
window.location.href = authUrl.toString();
// Thiếu: code_verifier, code_challenge, state param!
}
Không có PKCE challenge = không phải PKCE thật sự. Không có state param = CSRF vulnerable. Đây là code trông đúng nhưng thực ra không bảo mật.
Prompt Đúng Cách: Feed Security Requirements Trước
Trick quan trọng nhất: list từng security requirement cụ thể trước khi ask code. Tôi dùng prompt kiểu này:
"Implement OAuth 2.1 PKCE flow cho React SPA tích hợp Keycloak. Requirements bắt buộc theo spec: (1) Generate cryptographically randomcode_verifiervới entropy đủ lớn dùngcrypto.getRandomValues, (2) Tạocode_challengebằng SHA-256 hash của verifier rồi base64url encode, (3) Randomstateparameter để prevent CSRF, (4) Lưu verifier và state trongsessionStoragekhông phảilocalStorage, (5) Validate state khi handle callback, (6) Clear verifier ngay sau khi exchange code thành công."
Với prompt này, AI generate code như sau:
// Với AI assistant (proper prompt): PKCEAuthService
class PKCEAuthService {
private readonly VERIFIER_KEY = 'pkce_verifier';
private readonly STATE_KEY = 'oauth_state';
// Cryptographically secure code verifier (43-128 chars per RFC 7636)
private generateVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// SHA-256 code challenge
private async generateChallenge(verifier: string): Promise<string> {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
async initiateLogin(config: OAuthConfig): Promise<void> {
const verifier = this.generateVerifier();
const challenge = await this.generateChallenge(verifier);
const state = crypto.randomUUID(); // Random state để prevent CSRF
// sessionStorage: cleared khi tab đóng, không persist cross-tab
sessionStorage.setItem(this.VERIFIER_KEY, verifier);
sessionStorage.setItem(this.STATE_KEY, state);
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
response_type: 'code',
scope: 'openid profile email',
code_challenge: challenge,
code_challenge_method: 'S256',
state,
});
window.location.href = `${config.authorizationEndpoint}?${params}`;
}
async handleCallback(code: string, returnedState: string): Promise<TokenSet> {
const storedState = sessionStorage.getItem(this.STATE_KEY);
const verifier = sessionStorage.getItem(this.VERIFIER_KEY);
// Validate state trước tiên — prevent CSRF
if (!storedState || returnedState !== storedState) {
throw new Error('Invalid state — possible CSRF attack');
}
if (!verifier) throw new Error('Code verifier not found');
// Clear ngay trước khi exchange
sessionStorage.removeItem(this.VERIFIER_KEY);
sessionStorage.removeItem(this.STATE_KEY);
return this.exchangeCodeForToken(code, verifier, config);
}
}
Wrap Vào React Hook
Sau khi có service class, tôi ask AI tiếp: "Wrap PKCEAuthService thành React hook với loading state, error handling, và auto-detect callback URL":
// useAuth.ts — AI-generated React hook
export function useAuth(config: OAuthConfig) {
const [status, setStatus] = useState<AuthStatus>('checking');
const [user, setUser] = useState<User | null>(null);
const service = useMemo(() => new PKCEAuthService(), []);
// Auto-detect nếu đang ở callback URL
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (code && state) {
setStatus('exchanging');
service.handleCallback(code, state)
.then(tokens => fetchUser(tokens.access_token))
.then(userData => { setUser(userData); setStatus('authenticated'); })
.catch(() => setStatus('error'))
.finally(() => {
// Clean up URL sau callback
window.history.replaceState({}, '', window.location.pathname);
});
} else {
setStatus('idle');
}
}, [service]);
const login = () => service.initiateLogin(config);
return { status, user, login };
}
Lesson Học Được
Dùng AI cho security-critical code không phải là "prompt rồi dùng luôn". Workflow đúng:
- Research spec trước: Hiểu OAuth 2.1 RFC và PKCE spec trước khi prompt
- Prompt với requirements cụ thể: List từng security requirement rõ ràng, không để AI tự suy đoán
- Review output kỹ: Đọc và hiểu code AI generate, đặc biệt phần crypto và storage
- Hỏi AI về attack vectors: "Code này còn thiếu security check nào?" — AI thường tìm được issues
- Test adversarial cases: Thử CSRF, token replay, state manipulation
AI không thay thế hiểu biết về security. Nhưng nếu bạn đã hiểu spec và biết cách prompt, nó giúp bạn implement đúng nhanh hơn gấp nhiều lần — và ít sót edge case hơn so với tự viết từ đầu.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!