npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@otmc/authenticator

v0.1.7

Published

TypeScript SDK for OTMC Authenticator (OAuth 2.0 + OpenID Connect) with optional React integration

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/authenticator

Cấ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-app

Thay 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 claim roles củ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']); // true

Lư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 roles trong access token — backend có thể đọc roles qua ValidateAccessToken (claims Roles) 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 authority như 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>
  );
}