@btcv/auth-provider
v0.2.0
Published
Plug-and-play OIDC/WebAuthn auth module for Next.js apps backed by me.btcv.fr
Maintainers
Readme
@btcv/auth-provider
Module d'authentification plug-and-play pour applications Next.js (App Router), façade au-dessus du serveur OIDC me.btcv.fr. Le module ne gère pas les flows de login/register : ils ont lieu sur le site d'auth externe. Il fournit :
- un middleware Next.js qui protège les routes et injecte les headers user
- un catch-all route handler (
/api/auth/[...auth]) qui gère login → callback PKCE → session → refresh → logout → proxy authentifié vers${authUrl}/api/* - un
<AuthProvider>+ hooks (useSession,useAccount,usePasskeys,useAuthGuard) - un
<AccountDialog>prêt à l'emploi (profil, sécurité/passkeys, sessions, providers) - des helpers serveur (
getSession,withAuth) pour RSC et route handlers
Installation
npm install @btcv/auth-providerPeers requis dans l'app consommatrice :
next >= 14react >= 18/react-dom >= 18@simplewebauthn/browser(uniquement si tu utilises les passkeys côté client)
Styles (Tailwind v4)
Le <AccountDialog> utilise des classes Tailwind utilitaires (md:h-140, w-[225px], etc.).
Tailwind v4 ne scanne pas node_modules par défaut — sans import explicite, le dialog
rend comme une mini-boîte sans dimensions.
Dans le CSS global de ton app :
@import "tailwindcss";
@import "@btcv/ui/styles.css";
@import "@btcv/auth-provider/styles.css"; /* classes du AccountDialog */styles.css est un fichier pré-compilé qui contient uniquement les classes utilitaires
utilisées par les composants du package. Les tokens (couleurs, radius, fonts) viennent
de @btcv/ui/styles.css que tu importes déjà.
AccountDialog depuis un DropdownMenuItem (Radix)
Si tu ouvres le dialog depuis un <DropdownMenuItem>, un setTimeout(0) est
nécessaire pour laisser le dropdown restaurer pointer-events sur <body> avant
que le dialog pose son propre lock — sinon la page reste bloquée après fermeture :
const [accountOpen, setAccountOpen] = useState(false);
<DropdownMenuItem
onSelect={() => setTimeout(() => setAccountOpen(true), 0)}
>
Mon compte
</DropdownMenuItem>
<AccountDialog open={accountOpen} onClose={() => setAccountOpen(false)} />Sans le setTimeout, le dropdown et le dialog se disputent la gestion du
pointer-events: none sur le body (conflit Radix Dropdown ↔ Dialog).
Configuration minimale
1. Variables d'environnement
AUTH_URL=https://me.btcv.fr
AUTH_CLIENT_ID=your-client-id
AUTH_CLIENT_SECRET=your-client-secret # optionnel (clients confidentiels)2. Route handler catch-all
app/api/auth/[...auth]/route.ts :
import { createAuthHandlers } from "@btcv/auth-provider/next";
export const { GET, POST, PATCH, DELETE } = createAuthHandlers({
authUrl: process.env.AUTH_URL!,
clientId: process.env.AUTH_CLIENT_ID!,
clientSecret: process.env.AUTH_CLIENT_SECRET,
afterSignInUrl: "/dashboard",
afterSignOutUrl: "/",
});Routes exposées automatiquement :
| Route | Rôle |
| --------------------------- | ---------------------------------------------------------- |
| GET /api/auth/login | Démarre le flow OAuth (PKCE) — ?redirect_uri= supporté |
| GET /api/auth/callback | Callback OAuth, échange le code, set les cookies |
| GET /api/auth/session | Retourne { user } à partir des cookies |
| POST /api/auth/refresh | Refresh silencieux du token |
| POST /api/auth/logout | Détruit la session locale + remote |
| * /api/auth/proxy/... | Proxy authentifié vers ${authUrl}/api/... (CSRF requis) |
3. Middleware
middleware.ts à la racine :
import { createAuthMiddleware } from "@btcv/auth-provider/next";
export default createAuthMiddleware({
authUrl: process.env.AUTH_URL!,
publicRoutes: ["/", "/about", "/pricing/*"],
});
export const config = {
matcher: ["/((?!_next|favicon.ico|.*\\..*).*)"],
};Comportement :
- Les routes
/api/auth/*passent toujours. - Si session valide → injecte
x-user-id,x-user-email,x-user-name,x-user-imagedans la requête (et strip ces headers s'ils viennent du client, anti-forgery). - Si access token expiré + refresh dispo → tente un refresh silencieux et forward les
Set-Cookie. - Sinon → redirect vers
/api/auth/login?redirect_uri=<pathname>. publicRoutesaccepte les wildcards en suffixe (/blog/*).
4. AuthProvider + AccountDialog (client)
"use client";
import { AuthProvider, AccountDialog } from "@btcv/auth-provider";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<AuthProvider authUrl={process.env.NEXT_PUBLIC_AUTH_URL!}>
{children}
</AuthProvider>
);
}Note :
<AuthProvider>n'a besoin que deauthUrl. Toutes les requêtes API passent parbasePath(défaut/api/auth, donc le proxy du route handler) — il n'y a pas deapiUrlséparé.
const [open, setOpen] = useState(false);
<AccountDialog open={open} onClose={() => setOpen(false)} defaultTab="profile" />Hooks client
import { useSession, useAccount, usePasskeys, useAuthGuard } from "@btcv/auth-provider";
const { user, isLoading, isAuthenticated } = useSession();
const { updateProfile, revokeSession } = useAccount();
const { passkeys, addPasskey, removePasskey } = usePasskeys();
useAuthGuard(); // redirige automatiquement si non auth (utile en CSR pur)Helpers serveur (RSC / route handlers)
import { getSession, withAuth } from "@btcv/auth-provider/server";
// dans un Server Component
const session = await getSession(); // SessionData | null
// wrapper pour les route handlers
export const GET = withAuth(async (req, { user }) => {
return Response.json({ hello: user.email });
});CSRF
Toute requête mutating (POST/PATCH/DELETE) à travers /api/auth/proxy/* doit inclure le header X-CSRF-Token égal à la valeur du cookie CSRF posé par /api/auth/session. Les hooks fournis (useAccount, usePasskeys, etc.) gèrent ça automatiquement — c'est seulement à savoir si tu appelles le proxy à la main.
Référence backend
Le contrat REST complet (endpoints existants vs. à créer côté me.btcv.fr, headers, payloads) vit dans api-specs.md. Toute modif de src/core/api.ts, src/core/tokens.ts ou des hooks doit y rester alignée.
Dev local du package
npm run dev # tsup watch
npm run build # bundle dist/
npm run typecheck
npm test # vitest watch
npm run test:run # vitest single run