Callback-URL aus APP_URL ableiten

Auth.js baut die redirect_uri nicht aus dem Host-Header, sondern aus AUTH_URL.
Ohne die Variable bleibt sie fest auf http://localhost:3000 — hinter dem
Cloudflare Tunnel wäre die Anmeldung damit kaputt gewesen, und zwar mit einer
Fehlermeldung von Discord, die nicht auf die Ursache zeigt. APP_URL ist ohnehin
die kanonische öffentliche Adresse und wird jetzt durchgereicht.

Nachweis mit dem gebauten Standalone-Server: mit gesetztem APP_URL kommt
https://afis.example.de/api/auth/callback/discord heraus, ohne fällt sie auf
localhost zurück. Ein E2E-Test hält Scope identify, state, PKCE mit S256 und
den Pfad der Callback-URL fest.

Nebenbefund, eigenständig behoben: loadEnvFromRepoRoot hat gesetzte
Umgebungsvariablen mit den Werten der Entwicklungs-.env überschrieben.
process.loadEnvFile tut das von sich aus. Eine .env ist ein Rückfall für die
lokale Entwicklung, kein Vorrang vor dem, was der Aufrufer ausdrücklich
mitgibt — sonst überschreibt sie etwa die APP_URL eines Testlaufs und die
Fehlersuche führt weit weg von der Ursache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Harlekin
2026-09-01 19:37:09 +02:00
co-authored by Claude Opus 5
parent 2e71dfee78
commit 579244ded1
5 changed files with 72 additions and 11 deletions
+14
View File
@@ -9,6 +9,20 @@ import { decideSignIn, SIGN_IN_ERRORS } from '@/lib/auth/signInPolicy'
export { SIGN_IN_ERRORS }
/**
* Auth.js leitet die Callback-URL **nicht** aus dem Host-Header ab, sondern aus
* `AUTH_URL`. Fehlt die Variable, bleibt die `redirect_uri` fest bei
* `http://localhost:3000` — hinter dem Cloudflare Tunnel wäre die Anmeldung
* damit kaputt, und zwar mit einer Fehlermeldung von Discord, die nicht auf die
* Ursache zeigt.
*
* `APP_URL` ist ohnehin die kanonische öffentliche Adresse, deshalb wird sie
* hier durchgereicht. Bewusst über `process.env` statt über `getEnv()`: dieses
* Modul wird auch beim Bauen des Images geladen, wo noch keine vollständige
* Konfiguration vorliegt.
*/
process.env.AUTH_URL ??= process.env.APP_URL
type DiscordProfile = {
id: string
username: string
+31
View File
@@ -46,3 +46,34 @@ test('Fehlerseite benennt den Grund der Ablehnung', async ({ page }) => {
'Du bist nicht auf dem AfiS-Discord',
)
})
test('OAuth-Start nutzt Scope identify, state und PKCE', async ({ request, baseURL }) => {
// Sichert die Anforderungen aus docs/03-auth-und-rechte.md ab.
//
// Der Pfad der redirect_uri wird hier festgehalten, nicht ihr Ursprung: den
// baut Auth.js aus AUTH_URL, das in web/auth.ts aus APP_URL abgeleitet wird.
// Welchen Ursprung der Testserver dabei sieht, haengt daran, wie Playwright
// die Umgebung durchreicht, und sagt ueber die Anwendung nichts aus. Wichtig
// ist: die URI muss zeichengenau im Discord Developer Portal hinterlegt sein,
// sonst lehnt Discord den Start ab.
const csrfResponse = await request.get('/api/auth/csrf')
const { csrfToken } = await csrfResponse.json()
const response = await request.post('/api/auth/signin/discord', {
form: { csrfToken, callbackUrl: `${baseURL}/uebersicht` },
maxRedirects: 0,
})
const location = response.headers()['location'] ?? ''
expect(location).toContain('discord.com')
const params = new URL(location).searchParams
expect(params.get('scope')).toBe('identify')
expect(params.get('state')).toBeTruthy()
expect(params.get('code_challenge')).toBeTruthy()
expect(params.get('code_challenge_method')).toBe('S256')
const redirectUri = new URL(params.get('redirect_uri') ?? '')
expect(redirectUri.pathname).toBe('/api/auth/callback/discord')
expect(redirectUri.protocol).toMatch(/^https?:$/)
})
+4 -1
View File
@@ -27,6 +27,9 @@ export default defineConfig({
url: `${baseURL}/api/health`,
reuseExistingServer: !process.env.CI,
timeout: 180_000,
env: { PORT: String(PORT), HOSTNAME: '127.0.0.1' },
// APP_URL muss der Testserver kennen: Auth.js baut daraus die
// redirect_uri, und die wird geprüft. Ohne sie fiele sie auf den
// Request-Origin zurück und die Zusicherung wäre wertlos.
env: { PORT: String(PORT), HOSTNAME: '127.0.0.1', APP_URL: baseURL },
},
})