fix: survive an unconfigured identity provider
Auth.js refuses to build a provider with no issuer, and that refusal takes down the whole auth layer — including reading a session that already exists. A missing or misspelled AUTH_AUTHENTIK_ISSUER would therefore lock everyone out of an otherwise healthy application, and explain itself only as a stack trace in the logs. The provider is now registered only when its three settings are present. Sessions stay readable either way, and the sign-in page says which variables are missing instead of offering a button that fails. Found by the first CI run, which has no .env to inherit from: every signed-in test failed at once, looking exactly like a broken cookie. The local suite had been passing on variables Playwright was quietly inheriting from the development environment — so the E2E server is now given explicit placeholders rather than whatever happens to be around. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
+12
-1
@@ -1,7 +1,7 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { auth, signIn } from '@/lib/auth';
|
import { auth, isAuthentikConfigured, signIn } from '@/lib/auth';
|
||||||
|
|
||||||
export const metadata = { title: 'Connexion — ITA ITO' };
|
export const metadata = { title: 'Connexion — ITA ITO' };
|
||||||
|
|
||||||
@@ -49,6 +49,16 @@ export default async function LoginPage({
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{!isAuthentikConfigured ? (
|
||||||
|
<p
|
||||||
|
role="alert"
|
||||||
|
className="mt-8 rounded-[var(--radius-md)] border border-[var(--danger)] bg-[var(--danger-tint)] px-4 py-3 text-sm text-[var(--danger)]"
|
||||||
|
>
|
||||||
|
La connexion n’est pas configurée : <span className="font-mono">AUTH_AUTHENTIK_ID</span>,{' '}
|
||||||
|
<span className="font-mono">AUTH_AUTHENTIK_SECRET</span> et{' '}
|
||||||
|
<span className="font-mono">AUTH_AUTHENTIK_ISSUER</span> doivent être renseignés.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
<form
|
<form
|
||||||
className="mt-8"
|
className="mt-8"
|
||||||
action={async () => {
|
action={async () => {
|
||||||
@@ -65,6 +75,7 @@ export default async function LoginPage({
|
|||||||
Se connecter avec {providerName}
|
Se connecter avec {providerName}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
+19
-1
@@ -19,6 +19,22 @@ import { groupsFromClaim, roleFromGroups, type Role } from './roles';
|
|||||||
|
|
||||||
const ADMIN_GROUP = process.env.AUTHENTIK_ADMIN_GROUP?.trim() || 'horaires-admins';
|
const ADMIN_GROUP = process.env.AUTHENTIK_ADMIN_GROUP?.trim() || 'horaires-admins';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Authentik is configured well enough to sign anyone in.
|
||||||
|
*
|
||||||
|
* Auth.js refuses to build a provider that has no issuer, and that refusal
|
||||||
|
* takes down the whole auth layer — including reading a session that already
|
||||||
|
* exists. A missing or misspelled variable would therefore lock everyone out
|
||||||
|
* of an application that is otherwise perfectly healthy, and say so only as a
|
||||||
|
* stack trace in the logs. Registering the provider only when it can work
|
||||||
|
* keeps sessions readable and lets the sign-in page explain itself.
|
||||||
|
*/
|
||||||
|
export const isAuthentikConfigured = Boolean(
|
||||||
|
process.env.AUTH_AUTHENTIK_ID &&
|
||||||
|
process.env.AUTH_AUTHENTIK_SECRET &&
|
||||||
|
process.env.AUTH_AUTHENTIK_ISSUER,
|
||||||
|
);
|
||||||
|
|
||||||
/** Enough to explain a read-only account, not enough to bloat the cookie. */
|
/** Enough to explain a read-only account, not enough to bloat the cookie. */
|
||||||
const MAX_REPORTED_GROUPS = 20;
|
const MAX_REPORTED_GROUPS = 20;
|
||||||
|
|
||||||
@@ -45,7 +61,9 @@ declare module '@auth/core/jwt' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextAuth = NextAuth({
|
const nextAuth = NextAuth({
|
||||||
providers: [Authentik({ name: process.env.AUTHENTIK_DISPLAY_NAME?.trim() || 'Loxi' })],
|
providers: isAuthentikConfigured
|
||||||
|
? [Authentik({ name: process.env.AUTHENTIK_DISPLAY_NAME?.trim() || 'Loxi' })]
|
||||||
|
: [],
|
||||||
// The application sits behind a reverse proxy; the forwarded host is the
|
// The application sits behind a reverse proxy; the forwarded host is the
|
||||||
// real one.
|
// real one.
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ export default defineConfig({
|
|||||||
AUTH_URL: BASE_URL,
|
AUTH_URL: BASE_URL,
|
||||||
AUTH_SECRET: E2E_AUTH_SECRET,
|
AUTH_SECRET: E2E_AUTH_SECRET,
|
||||||
AUTHENTIK_ADMIN_GROUP: 'horaires-admins',
|
AUTHENTIK_ADMIN_GROUP: 'horaires-admins',
|
||||||
|
// Placeholders, never contacted: the suite mints its own session cookie.
|
||||||
|
// They exist so the provider builds and the sign-in page renders its
|
||||||
|
// normal button — CI has no .env to inherit these from.
|
||||||
|
AUTH_AUTHENTIK_ID: 'e2e-client-id',
|
||||||
|
AUTH_AUTHENTIK_SECRET: 'e2e-client-secret',
|
||||||
|
AUTH_AUTHENTIK_ISSUER: 'https://auth.example.test/application/o/e2e/',
|
||||||
// The translation service is stubbed per-test; never called for real.
|
// The translation service is stubbed per-test; never called for real.
|
||||||
TRANSLATION_API_URL: '',
|
TRANSLATION_API_URL: '',
|
||||||
TRANSLATION_API_KEY: '',
|
TRANSLATION_API_KEY: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user