+ Pourquoi ce compte est-il en lecture seule ?
+
+
+
+
- Groupe donnant l’accès en écriture
+ - {expected || '(non configuré)'}
+
+
+
- Groupes reçus d’Authentik
+ -
+ {claimMissing ? 'aucun — le claim « groups » est absent' : groups.join(', ')}
+
+
+
+
+
+ {claimMissing ? (
+ <>
+ Authentik n’envoie aucun groupe. Dans le provider OAuth2/OpenID, vérifiez que le scope{' '}
+ profile est bien sélectionné et que{' '}
+ Include claims in id_token est activé.
+ >
+ ) : (
+ <>
+ Authentik envoie bien des groupes, mais pas celui attendu. Ajoutez ce compte au groupe{' '}
+ {expected}, ou corrigez{' '}
+ AUTHENTIK_ADMIN_GROUP pour qu’il corresponde à l’un
+ des groupes ci-dessus.
+ >
+ )}
+
+
+
+ Le rôle est fixé à la connexion : après correction, déconnectez-vous et reconnectez-vous.
+
+
+ );
+}
diff --git a/lib/auth/index.ts b/lib/auth/index.ts
index 68b21e2..ad72983 100644
--- a/lib/auth/index.ts
+++ b/lib/auth/index.ts
@@ -19,6 +19,9 @@ import { groupsFromClaim, roleFromGroups, type Role } from './roles';
const ADMIN_GROUP = process.env.AUTHENTIK_ADMIN_GROUP?.trim() || 'horaires-admins';
+/** Enough to explain a read-only account, not enough to bloat the cookie. */
+const MAX_REPORTED_GROUPS = 20;
+
declare module 'next-auth' {
interface Session {
user: {
@@ -26,6 +29,10 @@ declare module 'next-auth' {
name?: string | null;
image?: string | null;
role: Role;
+ /** The groups the identity provider sent, so the UI can explain itself. */
+ groups: string[];
+ /** The group that would grant write access. */
+ adminGroup: string;
};
}
}
@@ -33,6 +40,7 @@ declare module 'next-auth' {
declare module '@auth/core/jwt' {
interface JWT {
role?: Role;
+ groups?: string[];
}
}
@@ -48,12 +56,27 @@ const nextAuth = NextAuth({
// `profile` is only present on the request that follows a sign-in, so
// the group membership is resolved once and carried in the token.
if (profile) {
- token.role = roleFromGroups(groupsFromClaim(profile.groups), ADMIN_GROUP);
+ const groups = groupsFromClaim(profile.groups);
+ token.groups = groups.slice(0, MAX_REPORTED_GROUPS);
+ token.role = roleFromGroups(groups, ADMIN_GROUP);
+
+ if (token.role !== 'admin') {
+ // The two failure modes look identical from the outside and are
+ // fixed in completely different places, so say which one it is.
+ // Group names are not secrets.
+ console.info(
+ `[auth] ${token.email ?? 'inconnu'} est en lecture seule. ` +
+ `Groupes reçus : ${groups.length > 0 ? groups.join(', ') : '(aucun — le claim « groups » est absent)'}. ` +
+ `Groupe attendu : ${ADMIN_GROUP}.`,
+ );
+ }
}
return token;
},
session({ session, token }) {
session.user.role = token.role ?? 'viewer';
+ session.user.groups = token.groups ?? [];
+ session.user.adminGroup = ADMIN_GROUP;
return session;
},
},