@otmc/authenticator
v0.1.7
Published
TypeScript SDK for OTMC Authenticator (OAuth 2.0 + OpenID Connect) with optional React integration
Maintainers
Readme
@otmc/authenticator
SDK TypeScript dành cho OTMC Authenticator (OAuth 2.0 + OpenID Connect). SDK sử dụng Authorization Code Flow kèm PKCE S256, không cần client secret trong browser.
Yêu cầu
- React 18 trở lên và Vite.
- Một public OIDC client đã được đăng ký trong OTMC Authenticator.
- Server Authenticator đang chạy tại
http://localhost:5007.
Cài đặt
Trong React app, chạy:
npm install @otmc/authenticatorCấu hình môi trường
Tạo file .env trong thư mục React app:
VITE_AUTH_AUTHORITY=http://localhost:5007
VITE_AUTH_CLIENT_ID=react-appThay react-app bằng clientId đã đăng ký trong Authenticator. Không đặt client secret vào React app; flow PKCE không sử dụng client secret trong browser. redirectUri và postLogoutRedirectUri được lấy từ window.location nên không cần cấu hình thủ công — chỉ cần đảm bảo origin của app (ví dụ http://localhost:5173) đã được đăng ký trong Authenticator.
Khởi tạo AuthProvider
Trong src/main.tsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from '@otmc/authenticator/react';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider
config={{
authority: import.meta.env.VITE_AUTH_AUTHORITY,
clientId: import.meta.env.VITE_AUTH_CLIENT_ID,
redirectUri: `${window.location.origin}/`,
postLogoutRedirectUri: window.location.origin,
scope: 'openid profile email',
storage: 'session',
}}
>
<App />
</AuthProvider>
</React.StrictMode>,
);Trong khi phát triển, redirectUri phải khớp chính xác URI đã đăng ký. Nếu Authenticator đăng ký http://localhost:5173/, origin http://localhost:5173 sẽ tự động tạo redirect URI có dấu / cuối như ví dụ.
Sử dụng trong component
import { useAuth, ProtectedRoute } from '@otmc/authenticator/react';
function App() {
return (
<ProtectedRoute
fallback={<div>Đang kiểm tra phiên đăng nhập...</div>}
unauthenticated={<LoginPage />}
>
<Dashboard />
</ProtectedRoute>
);
}
function LoginPage() {
const { login, error } = useAuth();
return (
<main>
<h1>Đăng nhập</h1>
{error && <p role="alert">{error.message}</p>}
<button onClick={() => void login()}>Đăng nhập bằng OTMC Authenticator</button>
</main>
);
}
function Dashboard() {
const { user, logout, getAccessToken } = useAuth();
const callProtectedApi = async () => {
const token = await getAccessToken();
const response = await fetch('http://localhost:5007/api/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) throw new Error('Không thể gọi API được bảo vệ.');
};
return (
<main>
<h1>Xin chào {user?.name ?? user?.email}</h1>
<button onClick={() => void callProtectedApi()}>Gọi API</button>
<button onClick={() => void logout()}>Đăng xuất</button>
</main>
);
}AuthProvider tự động xử lý redirect callback khi trình duyệt quay lại từ Authenticator. Khi callback hoàn tất, người dùng được đăng nhập và ứng dụng hiển thị phần được bảo vệ.
Các API React chính
useAuth() trả về:
{
user,
isLoading,
isAuthenticated,
error,
state,
client,
login,
logout,
handleCallback,
getAccessToken,
getRoles,
hasRole,
hasAnyRole,
refreshToken,
fetch,
}login(): chuyển trình duyệt đến trang đăng nhập của Authenticator.logout(): kết thúc phiên và chuyển vềpostLogoutRedirectUri.getAccessToken(): lấy access token hiện tại; SDK có thể tự refresh khi token hết hạn.getRoles(): trả về danh sách role trong claimrolescủa access token (đọc từ JWT, không cần gọi API).hasRole(role): kiểm tra user có đúng role đã cho (không phân biệt hoa/thường).hasAnyRole(roles): kiểm tra user có ít nhất một trong các role đã cho.fetch(url, options): gọi API có gắn Bearer token và thử refresh một lần khi API trả về401.ProtectedRoute: chỉ là lớp bảo vệ giao diện. Backend vẫn phải kiểm tra và xác thực token ở mọi API được bảo vệ.
Giao diện đăng nhập dựng sẵn (AuthenticatorLogin)
SDK cung cấp component AuthenticatorLogin — màn hình xin quyền truy cập với branding OTMC (giao diện dark) và tùy biến qua CSS variables.
import { AuthenticatorLogin, ProtectedRoute } from '@otmc/authenticator/react';
import appIcon from './assets/app.ico';
function LoginPage() {
return <AuthenticatorLogin appName="My App" appLogo={appIcon} />;
}
function App() {
return (
<ProtectedRoute unauthenticated={<LoginPage />}>
<Dashboard />
</ProtectedRoute>
);
}Props chính:
| Prop | Kiểu | Mặc định | Mô tả |
|------|------|----------|-------|
| appName | string | — | Tên ứng dụng hiển thị trên card (bắt buộc). |
| appLogo | string | — | URL/path logo app. |
| className | string | — | Class bổ sung cho phần tử gốc. |
| onLogin | () => void \| Promise<void> | — | Callback tùy chọn; nếu bỏ trống component tự gọi auth.login(). |
Tùy biến màu sắc bằng CSS variables --otmc-bg, --otmc-card, --otmc-border, --otmc-text, --otmc-muted, --otmc-footer, --otmc-primary, --otmc-accent, --otmc-button-bg, --otmc-button-text, --otmc-error, --otmc-error-bg, --otmc-card-shadow — đặt trên :root hoặc phần tử cha để ghi đè giá trị mặc định.
Hướng dẫn chi tiết: docs/features/AuthenticatorLogin.md.
Phân quyền theo role
Access token do Authenticator cấp đã chứa claim roles (ví dụ ["Administrator", "Manager"]). SDK đọc trực tiếp từ token — không cần gọi API bổ sung.
Bảo vệ route bằng roles
ProtectedRoute nhận thêm prop roles: user đã đăng nhập nhưng thiếu role sẽ thấy trang từ chối truy cập thay vì nội dung được bảo vệ.
import { AccessDenied, ProtectedRoute } from '@otmc/authenticator/react';
function App() {
return (
<ProtectedRoute
unauthenticated={<LoginPage />}
roles={['Administrator', 'Manager']}
unauthorized={<AccessDenied requiredRoles={['Administrator', 'Manager']} />}
>
<Dashboard />
</ProtectedRoute>
);
}Nếu bỏ unauthorized, ProtectedRoute tự render <AccessDenied requiredRoles={roles} /> — trang từ chối truy cập dựng sẵn có nút Logout.
| Prop | Kiểu | Mặc định | Mô tả |
|------|------|----------|-------|
| roles | string[] | — | User phải có ít nhất một role (không phân biệt hoa/thường). |
| unauthorized | ReactNode | <AccessDenied requiredRoles={roles} /> | Nội dung hiển thị khi thiếu quyền. |
Kiểm tra role trong component
function Dashboard() {
const { hasAnyRole } = useAuth();
const canManage = await hasAnyRole(['Administrator', 'Manager']);
// hoặc: await hasRole('Administrator')
}Component AccessDenied
Trang "Access denied" dựng sẵn (cùng style với AuthenticatorLogin, có icon khóa, danh sách role yêu cầu dạng badge và nút Logout).
| Prop | Kiểu | Mặc định | Mô tả |
|------|------|----------|-------|
| appName | string | 'This application' | Tên ứng dụng hiển thị. |
| message | string | — | Thông báo tùy chỉnh thay cho mô tả mặc định. |
| requiredRoles | string[] | — | Hiển thị các role yêu cầu dưới dạng badge. |
| showLogout | boolean | true | Hiển thị nút Logout. |
| onLogout | () => void \| Promise<void> | — | Callback tùy chọn; mặc định gọi auth.logout(). |
| className | string | — | Class bổ sung cho phần tử gốc. |
Core helpers (không cần React)
Khi dùng SDK ở vanilla JS/TS:
import { getRolesFromToken, hasAnyRole } from '@otmc/authenticator';
const roles = getRolesFromToken(accessToken); // ['Administrator', 'Manager']
hasAnyRole(roles, ['Administrator', 'Manager']); // trueLưu ý: phân quyền phía client chỉ phục vụ trải nghiệm người dùng. API backend vẫn phải tự kiểm tra claim
rolestrong access token — backend có thể đọc roles quaValidateAccessToken(claimsRoles) hoặc endpoint/api/users/:id/roles.
Gọi API bằng auth.fetch
Có thể dùng helper của SDK thay cho fetch thủ công:
const { fetch: authFetch } = useAuth();
const response = await authFetch('http://localhost:5007/api/protected');
const data = await response.json();Hiển thị avatar người dùng
Access token/ID token và endpoint /oauth/userinfo trả về claim picture chứa URL avatar. Giá trị có thể là URL tuyệt đối (https://...) hoặc đường dẫn tương đối (/avatars/...) — trong trường hợp tương đối cần ghép với authority.
import { useAuth, resolveAvatarUrl } from '@otmc/authenticator/react';
function Dashboard() {
const { user, fetch } = useAuth();
const authority = import.meta.env.VITE_AUTH_AUTHORITY;
const [avatarUrl, setAvatarUrl] = React.useState<string | null>(() =>
resolveAvatarUrl(user?.picture, authority) ?? resolveAvatarUrl((user?.profile as any)?.picture, authority),
);
React.useEffect(() => {
let cancelled = false;
fetch(`${authority}/oauth/userinfo`)
.then((res) => (res.ok ? res.json() : null))
.then((info) => {
const url = resolveAvatarUrl(info?.picture, authority);
if (!cancelled && url) setAvatarUrl(url);
})
.catch(() => {});
return () => { cancelled = true; };
}, [fetch]);
const displayName = user?.name ?? user?.preferred_username ?? user?.sub ?? '';
return (
<main>
<div className="user-identity">
{avatarUrl ? (
<img src={avatarUrl} alt={displayName} width={40} height={40} />
) : (
<span>{displayName.slice(0, 2).toUpperCase()}</span>
)}
<h1>Xin chào {displayName}</h1>
</div>
</main>
);
}Lưu ý: avatar URL tương đối được phục vụ bởi Authenticator; luôn ghép với
authoritynhư ví dụ. Backend vẫn phải xác thực token ở mọi API được bảo vệ.
Component Avatar dựng sẵn
SDK cung cấp component Avatar — chỉ cần render, SDK tự lấy avatar từ user.picture và /oauth/userinfo:
import { Avatar, useAuth } from '@otmc/authenticator/react';
function Dashboard() {
const { user } = useAuth();
return (
<main>
<Avatar size={48} />
<h1>Xin chào {user?.name}</h1>
</main>
);
}