Ajouter l'authentification à une application monopage React
Ce guide de démarrage rapide ajoute la connexion KeenAuth à une application monopage React créée avec Vite, à l'aide du SDK navigateur @keenauth/js. L'application connecte les utilisateurs par redirection, appelle une API protégée avec un jeton d'accès lié par DPoP, puis déconnecte les utilisateurs. Vous le testez également hors ligne. Le SDK est décrit dans SDK JavaScript pour navigateur.
Dans une application monopage, les jetons se trouvent dans le navigateur. Le SDK ne les conserve qu'en mémoire et les lie à une clé non extractible grâce à DPoP, si bien qu'un jeton volé ne peut pas être utilisé sans cette clé. Si votre application dispose d'un serveur, le guide de démarrage rapide Next.js conserve au contraire tous les jetons sur le serveur (backend for frontend), ce qui constitue le choix le plus robuste.
L'application expose les chemins suivants :
| Chemin | Rôle |
|---|---|
/ |
Un bouton de connexion, ou l'utilisateur, un bouton qui appelle votre API et un bouton de déconnexion |
/callback |
L'URI de redirection. Le SDK termine la connexion ici, puis l'application renvoie l'utilisateur à la page d'où il est parti |
Avant de commencer
- Un environnement KeenAuth. Son émetteur ressemble à
https://your-environment.eu.keenauth.app. - Une application monopage dans l'environnement (Créer une application) avec
app_typedéfini surspaettoken_endpoint_auth_methoddéfini surnone(un client public n'a pas de secret). Dans un environnementdevelopment, enregistrez :- l'URI de redirection
http://localhost/callback(un URI de redirection de bouclage est enregistré sans port et correspond à n'importe quel port) ; - l'URI de redirection après déconnexion
http://localhost/; - l'origine web autorisée
http://localhost:5173, afin que le navigateur puisse appeler le point de terminaison de jetons.
- l'URI de redirection
- Une API enregistrée dans l'environnement (Créer une API) avec l'autorisation
messages:read. Sonidentifier, par exemplehttps://api.example.com, est l'audience de ses jetons d'accès. Le guide de démarrage rapide Express crée une API qui accepte ces jetons. - Node.js 22.18 ou version ultérieure.
1. Installer
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @keenauth/js
Le SDK n'est pas encore publié. Tant qu'il ne l'est pas, l'exemple renvoie au code source du SDK dans son package.json. L'exemple épingle vite@8.3.4, @vitejs/plugin-react@6.1.2, react@19.3.1 et react-dom@19.3.1.
2. Configurer
L'application lit sa configuration uniquement à partir de variables d'environnement. Placez-les dans .env.local, que Vite charge, et ne validez jamais ce fichier dans un dépôt. Vite intègre chaque variable VITE_ dans le JavaScript que le navigateur télécharge ; n'y placez donc aucun secret. Cette application n'en a aucun.
| Variable | Obligatoire | Signification |
|---|---|---|
VITE_KEENAUTH_ISSUER |
oui | L'émetteur de votre environnement |
VITE_KEENAUTH_CLIENT_ID |
oui | Le client_id de l'application monopage |
VITE_KEENAUTH_API_AUDIENCE |
oui | L'identifiant de votre API dans KeenAuth |
VITE_API_URL |
non | URL de base de votre API. Vide signifie l'origine propre de l'application |
API_PROXY_TARGET |
non | Développement uniquement : destination vers laquelle le serveur de développement transmet /api ; par défaut http://localhost:8080 |
loadConfig lit et vérifie ces valeurs. Il arrête l'application avec une erreur qui nomme la variable lorsqu'une valeur manque :
export function loadConfig(env: Readonly<Record<string, unknown>>): AppConfig {
const need = (name: string): string => {
const v = env[name];
if (typeof v !== "string" || v.trim() === "") throw new Error(`${name} is required (see .env.example)`);
return v.trim();
};
3. Créer le client SDK
export function createAuth(config: AppConfig, origin: string, inject: Injectables = {}): KeenAuth {
return new KeenAuth({
issuer: config.issuer,
clientId: config.clientId,
redirectUri: `${origin}${CALLBACK_PATH}`,
postLogoutRedirectUri: `${origin}/`,
// The access token is for this API: its aud is the API identifier (RFC 8707).
resource: config.apiAudience,
// messages:read is a scope of that API; the example API requires it on /api/messages.
scope: "openid profile email offline_access messages:read",
...inject,
});
}
resource est l'identifiant de votre API (RFC 8707). Le SDK l'envoie dans la requête d'autorisation, lors de l'échange du code et à chaque rafraîchissement, et l'audience du jeton d'accès est votre API. Sans ce paramètre, le jeton d'accès ne vise pas votre API, et celle-ci le rejette. Le SDK vérifie chaque option lors de la création du client ; un émetteur ou un resource invalide provoque une exception KeenAuthError avec le code config_invalid.
Créez un seul client pour la page et transmettez-le à un provider :
const config = loadConfig(import.meta.env);
const auth = createAuth(config, window.location.origin);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AuthProvider auth={auth}>
<App apiUrl={config.apiUrl} />
</AuthProvider>
</StrictMode>,
);
4. Se connecter et traiter le callback
Le bouton de connexion lance la connexion par redirection : flux de code d'autorisation avec PKCE (S256), state et nonce. appState est le chemin vers lequel revenir.
<button onClick={() => auth.signInRedirect({ appState: window.location.pathname })}>Sign in</button>
Lorsque KeenAuth redirige vers /callback, le provider termine la connexion. Le SDK vérifie state, iss et le jeton d'identité, puis échange le code avec une preuve DPoP. Une réponse d'autorisation ne peut être utilisée qu'une seule fois, et StrictMode de React exécute deux fois les effets en développement ; completeSignIn conserve donc le premier résultat :
export function completeSignIn(auth: KeenAuth, href: string): Promise<string | null> {
const url = new URL(href);
if (url.pathname !== CALLBACK_PATH || !(url.searchParams.has("code") || url.searchParams.has("error"))) {
return Promise.resolve(null);
}
let done = completions.get(auth);
if (!done) {
done = auth.handleRedirectCallback(href).then((r) => safeReturnPath(r.appState));
completions.set(auth, done);
}
return done;
}
Le provider supprime ensuite code et state de la barre d'adresse. Seul un chemin situé dans votre application sert de chemin de retour, afin qu'un appState forgé ne puisse pas envoyer l'utilisateur vers un autre site :
export function safeReturnPath(path: string | undefined): string {
return path !== undefined && /^\/(?![/\\])/.test(path) ? path : "/";
}
Servez également index.html pour /callback en production (le serveur de développement Vite le fait déjà).
5. Appeler votre API
authorizedFetch envoie Authorization: DPoP <token> avec une preuve fraîche liée au jeton, et rafraîchit le jeton lorsqu'il est sur le point d'expirer :
export async function fetchMessages(auth: KeenAuth, apiUrl: string): Promise<unknown> {
const res = await auth.authorizedFetch(`${apiUrl}/api/messages`, { headers: { Accept: "application/json" } });
if (!res.ok) throw new Error(`The API answered ${res.status}`);
return res.json();
}
En développement, le serveur de développement Vite transmet /api à votre API ; le navigateur appelle donc l'API sur l'origine propre de l'application, et l'API n'a pas besoin de CORS :
proxy: { "/api": env.API_PROXY_TARGET || "http://localhost:8080" },
L'API vérifie la preuve DPoP par rapport à l'URL que le navigateur a appelée. Lorsque vous exécutez l'API du guide Express derrière ce proxy, définissez son PUBLIC_BASE_URL sur http://localhost:5173. En production, servez l'API sur la même origine ou activez CORS pour l'origine de votre application, et définissez VITE_API_URL.
6. Se déconnecter
<button onClick={() => auth.signOut()}>Sign out</button>
signOut supprime les jetons, révoque le jeton de rafraîchissement et envoie le navigateur vers le point de terminaison de déconnexion de votre environnement, qui le renvoie vers /.
7. Lancer l'application
Démarrez l'API du guide Express avec PUBLIC_BASE_URL=http://localhost:5173 et KEENAUTH_AUDIENCE défini sur l'identifiant de votre API, puis lancez l'application :
cp .env.example .env.local # then fill it in
npm run dev
Ouvrez http://localhost:5173, cliquez sur Sign in, connectez-vous auprès de KeenAuth, puis cliquez sur Call the API.
Le SDK ne conserve les jetons qu'en mémoire. Après un rechargement de la page, l'utilisateur est déconnecté dans l'application et doit se reconnecter ; la session KeenAuth permet généralement de le faire par une redirection, sans invite.
8. Tester sans réseau
Les tests n'ont besoin ni de réseau ni d'un compte KeenAuth. Un faux fetch joue le rôle du serveur d'autorisation et de l'API, et le jeton d'identité est signé avec une clé créée pour le test. Node exécute directement les tests TypeScript.
assert.deepEqual(await fetchMessages(s.auth, ""), { messages: ["hello"] });
const call = s.seen.find((c) => c.url === `${ORIGIN}/api/messages`)!;
assert.equal(call.headers.get("Authorization"), "DPoP access-token-1");
npm test
npm run typecheck
npm run deps # pinned versions, lockfile sources and licences
npm run build
Les tests vérifient que la configuration est validée, que le SDK refuse un émetteur ou un resource invalide, que la connexion envoie PKCE S256 et votre API comme resource, que le callback n'est traité qu'une fois et envoie resource avec le code, qu'un state forgé est refusé avant toute requête de jeton, que seul un chemin situé dans l'application est utilisé après la connexion, qu'aucun jeton n'est écrit dans le stockage, et que l'appel à l'API transporte le jeton lié par DPoP ainsi que sa preuve.
La connexion complète avec un vrai navigateur nécessite un environnement KeenAuth. Elle ne fait pas partie des tests hors ligne.
Étapes suivantes
- SDK JavaScript pour navigateur : toutes les options, l'endroit où les jetons sont conservés et les limites actuelles.
- SDK React : composants intégrés de connexion, d'inscription et de gestion du compte.
- Protéger une API Express : l'API que cette application appelle.