UNPKG

@passkey-fas/webauthn-sdk

Version:

Official JavaScript SDK for FaS (FIDO2 as Service) Platform - Easy passwordless authentication integration

453 lines (365 loc) 14.2 kB
# @passkey-fas/webauthn-sdk **SDK Chính thức** cho FaS (FIDO2 as Service) Platform - Tích hợp xác thực WebAuthn/Passkey trong vài phút ## 🚀 **Bắt Đầu Nhanh** ### **1. Thiết Lập Dự Án** 1. Đăng ký tại [https://fas-l450.onrender.com](https://fas-l450.onrender.com) 2. Tạo project và lấy thông tin xác thực: ```json { "clientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "clientSecret": "z9y8x7w6-v5u4-3210-9876-543210fedcba" } ``` 3. Thêm domain vào **Allowed Origins**: `https://yourapp.com`, `http://localhost:3000` ### **2. Cài Đặt** ```bash npm install @passkey-fas/webauthn-sdk @simplewebauthn/browser ``` ### **3. Triển Khai** #### **🔧 Phát Triển Localhost:** ```javascript import FaSSDK from '@passkey-fas/webauthn-sdk'; // ✅ OK cho phát triển localhost với CORS setup const fas = new FaSSDK({ clientId: 'your-client-id', clientSecret: 'your-client-secret', // ⚠️ Chỉ dành cho localhost! apiBase: 'https://fas-l450.onrender.com/api/webauthn' }); // Đăng ký passkey const result = await fas.registerPasskey('user@example.com', 'Nguyễn Văn A'); console.log('Đã đăng ký:', result.user); // Đăng nhập bằng passkey const auth = await fas.authenticatePasskey('user@example.com'); console.log('Đã xác thực:', auth.user); ``` #### **🏭 Triển Khai Production:** ```javascript import FaSSDK from '@passkey-fas/webauthn-sdk'; // ✅ AN TOÀN CHO PRODUCTION - Không để lộ clientSecret const fas = new FaSSDK({ clientId: 'your-client-id', // Vẫn cần cho WebAuthn RP ID apiBase: '/api/auth', // Endpoints proxy backend của bạn useProxy: true // Route qua backend của bạn }); // API giống nhau, chỉ khác routing const result = await fas.registerPasskey('user@example.com', 'Nguyễn Văn A'); console.log('Đã đăng ký:', result.user); ``` ## 🔐 **Bảo Mật Production** ### **🏗️ Kiến Trúc FaS & Mô Hình Bảo Mật** **FaS sử dụng `clientSecret` để:** 1. **Xác thực project** qua headers `X-Client-ID`/`X-Client-Secret` 2. **CORS validation** với `allowedOrigins` từ cài đặt project 3. **Phân tách multi-tenant** - mỗi project có dữ liệu riêng biệt **2 cách triển khai an toàn:** #### **Lựa chọn 1: Tích hợp trực tiếp (Có rủi ro)** ```javascript // Frontend gọi trực tiếp FaS với clientSecret const fas = new FaSSDK({ clientSecret: process.env.REACT_APP_FAS_CLIENT_SECRET // ⚠️ Bị lộ! }); ``` **Rủi ro:** Secret hiển thị trong browser, nhưng được bảo vệ bởi CORS #### **Lựa chọn 2: Backend Proxy (An toàn nhất)** ```javascript // Frontend gọi backend, backend gọi FaS const fas = new FaSSDK({ apiBase: '/api/auth', // Backend của bạn useProxy: true }); ``` **Ưu điểm:** Secret hoàn toàn ẩn, có thể thêm logic tùy chỉnh #### **Production với Backend Proxy:** **Frontend (SDK với chế độ proxy):** ```javascript import FaSSDK from '@passkey-fas/webauthn-sdk'; const fas = new FaSSDK({ clientId: 'your-client-id', // Vẫn cần cho WebAuthn RP ID apiBase: '/api/auth', // Route tới backend của bạn useProxy: true // Bật chế độ proxy }); // API giống hệt, chỉ khác routing const user = await fas.registerPasskey('user@example.com', 'Nguyễn Văn A'); const auth = await fas.authenticatePasskey('user@example.com'); ``` #### **Triển Khai Backend (Express.js):** ```javascript // routes/auth.js const express = require('express'); const axios = require('axios'); const router = express.Router(); const FAS_API = 'https://fas-l450.onrender.com/api/webauthn'; const FAS_HEADERS = { 'X-Client-ID': process.env.FAS_CLIENT_ID, 'X-Client-Secret': process.env.FAS_CLIENT_SECRET, // ✅ An toàn trên server 'Content-Type': 'application/json' }; // Proxy bắt đầu đăng ký router.post('/register/start', async (req, res) => { try { const { email, fullname } = req.body; const response = await axios.post(`${FAS_API}/public-register/start`, { email, fullname }, { headers: FAS_HEADERS }); res.json(response.data); } catch (error) { res.status(500).json({ error: error.response?.data?.error || error.message }); } }); // Proxy hoàn tất đăng ký router.post('/register/finish', async (req, res) => { try { const { email, credential } = req.body; const response = await axios.post(`${FAS_API}/public-register/finish`, { email, credential }, { headers: FAS_HEADERS }); res.json(response.data); } catch (error) { res.status(500).json({ error: error.response?.data?.error || error.message }); } }); // Proxy bắt đầu xác thực router.post('/authenticate/start', async (req, res) => { try { const { email } = req.body; const response = await axios.post(`${FAS_API}/public-authenticate/start`, { email }, { headers: FAS_HEADERS }); res.json(response.data); } catch (error) { res.status(500).json({ error: error.response?.data?.error || error.message }); } }); // Proxy hoàn tất xác thực router.post('/authenticate/finish', async (req, res) => { try { const { email, credential } = req.body; const response = await axios.post(`${FAS_API}/public-authenticate/finish`, { email, credential }, { headers: FAS_HEADERS }); res.json(response.data); } catch (error) { res.status(500).json({ error: error.response?.data?.error || error.message }); } }); module.exports = router; ``` ## ⚛️ **Tích Hợp React** ### **Mẫu Hook:** ```javascript // hooks/usePasskey.js import { useState, useCallback } from 'react'; import FaSSDK from '@passkey-fas/webauthn-sdk'; export const usePasskey = () => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Thiết lập production - chọn một cách tiếp cận: const fas = new FaSSDK({ clientId: process.env.REACT_APP_FAS_CLIENT_ID, // Lựa chọn 1: Trực tiếp (có rủi ro nhưng được CORS bảo vệ) clientSecret: process.env.REACT_APP_FAS_CLIENT_SECRET, // Lựa chọn 2: Backend proxy (an toàn nhất) // apiBase: '/api/auth', // useProxy: true }); const register = useCallback(async (email, fullname) => { setLoading(true); setError(null); try { // SDK tự động xử lý routing (trực tiếp hoặc proxy) const result = await fas.registerPasskey(email, fullname); setUser(result.user); localStorage.setItem('fas_token', result.token); return result; } catch (err) { setError(err.message); throw err; } finally { setLoading(false); } }, []); const login = useCallback(async (email) => { setLoading(true); setError(null); try { // SDK tự động xử lý routing (trực tiếp hoặc proxy) const result = await fas.authenticatePasskey(email); setUser(result.user); localStorage.setItem('fas_token', result.token); return result; } catch (err) { setError(err.message); throw err; } finally { setLoading(false); } }, []); return { user, loading, error, register, login }; }; // components/AuthForm.js const AuthForm = () => { const { register, login, user, loading, error } = usePasskey(); const [email, setEmail] = useState(''); if (user) { return <div>Chào mừng {user.fullname || user.email}!</div>; } return ( <div> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" /> <button onClick={() => register(email, 'Tên Người Dùng')} disabled={loading}> {loading ? 'Đang đăng ký...' : 'Đăng Ký Passkey'} </button> <button onClick={() => login(email)} disabled={loading}> {loading ? 'Đang đăng nhập...' : 'Đăng Nhập với Passkey'} </button> {error && <div style={{color: 'red'}}>Lỗi: {error}</div>} </div> ); }; ``` ## 🟡 **Vanilla JavaScript** ```html <!DOCTYPE html> <html> <head> <title>Demo Passkey</title> </head> <body> <input id="email" type="email" placeholder="Email" /> <button onclick="registerPasskey()">Đăng Ký</button> <button onclick="loginPasskey()">Đăng Nhập</button> <div id="result"></div> <script type="module"> import FaSSDK from 'https://unpkg.com/@passkey-fas/webauthn-sdk'; // Thiết lập production const fas = new FaSSDK({ clientId: 'your-client-id', // Chọn cách triển khai: // Lựa chọn 1: Tích hợp trực tiếp clientSecret: 'your-client-secret', // Lựa chọn 2: Backend proxy // apiBase: '/api/auth', // useProxy: true }); window.registerPasskey = async () => { const email = document.getElementById('email').value; try { const result = await fas.registerPasskey(email, 'Tên Người Dùng'); document.getElementById('result').innerHTML = `✅ Đã đăng ký: ${result.user.email}`; } catch (error) { document.getElementById('result').innerHTML = `❌ Lỗi: ${error.message}`; } }; window.loginPasskey = async () => { const email = document.getElementById('email').value; try { const result = await fas.authenticatePasskey(email); document.getElementById('result').innerHTML = `✅ Đã đăng nhập: ${result.user.email}`; } catch (error) { document.getElementById('result').innerHTML = `❌ Lỗi: ${error.message}`; } }; </script> </body> </html> ``` ## 🔧 **Xác Thực JWT Backend** ```javascript // middleware/auth.js const jwt = require('jsonwebtoken'); const validatePasskeyToken = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Cần có token' }); } try { const user = jwt.verify(token, process.env.FAS_JWT_SECRET); req.user = user; next(); } catch (error) { return res.status(401).json({ error: 'Token không hợp lệ' }); } }; // Route được bảo vệ app.get('/api/profile', validatePasskeyToken, (req, res) => { res.json({ user: req.user }); }); ``` ## 📋 **Biến Môi Trường** ```env # Frontend (.env) REACT_APP_FAS_CLIENT_ID=your-client-id # Tích hợp trực tiếp (có rủi ro) REACT_APP_FAS_CLIENT_SECRET=your-client-secret # Backend (.env) - Cho chế độ proxy FAS_CLIENT_ID=your-client-id FAS_CLIENT_SECRET=your-client-secret FAS_API_BASE=https://fas-l450.onrender.com/api/webauthn ``` ## 🚨 **Xử Lý Lỗi** ```javascript // Kiểm tra hỗ trợ browser if (!FaSSDK.isWebAuthnSupported()) { console.error('WebAuthn không được hỗ trợ'); // Hiển thị phương thức đăng nhập dự phòng } // Xử lý lỗi const handleError = (error) => { const messages = { 'NotAllowedError': 'Người dùng hủy hoặc hết thời gian', 'NotSupportedError': 'WebAuthn không được hỗ trợ', 'InvalidStateError': 'Passkey đã tồn tại', 'SecurityError': 'Yêu cầu HTTPS' }; return messages[error.name] || error.message; }; try { await fas.registerPasskey(email); } catch (error) { alert(handleError(error)); } ``` ## 📊 **Checklist Triển Khai Production** ### **🔒 BẮT BUỘC CHO TẤT CẢ TRIỂN KHAI:** - ✅ **HTTPS**: Bắt buộc cho WebAuthn - ✅ **Whitelist Domain**: Thêm domain của bạn trong FaS dashboard `allowedOrigins` - ✅ **CORS Headers**: FaS xác thực origin vs allowedOrigins - ✅ **Xác Thực Token**: Xác minh chữ ký JWT trên backend của bạn - ✅ **Xử Lý Lỗi**: Thông báo lỗi thân thiện với người dùng - ✅ **Hỗ Trợ Browser**: Kiểm tra tính khả dụng WebAuthn ### **🎯 CÁCH TIẾP CẬN TRIỂN KHAI:** #### **Lựa chọn 1: Tích hợp trực tiếp** - ✅ Thiết lập nhanh hơn, ít thành phần di chuyển - ⚠️ `clientSecret` hiển thị trong frontend bundle - ✅ Được bảo vệ bởi CORS + whitelist domain - ✅ Tốt cho ứng dụng nhỏ/vừa #### **Lựa chọn 2: Backend Proxy** - ✅ Bảo mật tối đa, secret hoàn toàn ẩn - ✅ Logic nghiệp vụ tùy chỉnh, giới hạn tỷ lệ - ⚠️ Thiết lập phức tạp hơn, độ trễ thêm - ✅ Tốt nhất cho ứng dụng doanh nghiệp ## 📚 **Tham Khảo API** ### **Phương thức FaSSDK:** - `registerPasskey(email, fullname?)` - Đăng ký passkey mới - `authenticatePasskey(email)` - Đăng nhập bằng passkey - `passwordlessLogin()` - Đăng nhập nhanh không cần email - `isAuthenticated()` - Kiểm tra trạng thái xác thực - `getAuthToken()` / `logout()` - Quản lý token ### **Phương thức tĩnh:** - `FaSSDK.isWebAuthnSupported()` - Kiểm tra hỗ trợ browser - `FaSSDK.getBrowserSupport()` - Thông tin hỗ trợ chi tiết ## 📞 **Hỗ Trợ** - 📖 **Tài liệu**: https://fas-l450.onrender.com/docs - 🐛 **Vấn đề**: GitHub Issues - 💬 **Email**: support@fas-platform.com **🎯 Xác thực WebAuthn/Passkey không cần cấu hình, sẵn sàng trong vài phút!**