fix(oauth): déduire l'URI de redirection du domaine public réel

Sans APP_URL, Facebook et TikTok recevaient
http://localhost:5555/api/{facebook,tiktok}/callback : une URI que les
fournisseurs refusent, donc aucune page connectable derrière un reverse
proxy. Le repli localhost n'est plus utilisé que hors requête HTTP ; en
service, la base publique est reconstruite depuis X-Forwarded-Proto /
X-Forwarded-Host (trust proxy est déjà activé).

- `resolvePublicBaseUrl(req)` centralise la résolution (APP_URL prioritaire)
- l'URI de redirection est déduite de la requête au démarrage du flux, à
  l'échange du code et dans l'affichage des paramètres : les trois restent
  identiques, comme l'exige OAuth
- docker-compose ne force plus APP_URL à localhost, qui rendait l'auto
  détection inopérante
- cookie de session en `secure: 'auto'` à défaut d'APP_URL, pour ne pas
  émettre un cookie non sécurisé derrière un proxy HTTPS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Db2EazEcvnqgTTeg8oGTif
This commit is contained in:
Claude committed 2026-09-11 18:01:28 +00:00
1 parent 94e961d0f1
commit 3af7f9418b
9 files changed
+119 -27

No files matched your search

+13 -7
View File
@@ -21,7 +21,9 @@
* chaîne se renouvelle toute seule.
*/
import type { Request } from 'express';
import { storage } from '../storage';
import { resolvePublicBaseUrl } from '../utils/public_url';
const GRAPH_VERSION = process.env.FACEBOOK_GRAPH_VERSION?.trim() || 'v19.0';
const GRAPH_URL = `https://graph.facebook.com/${GRAPH_VERSION}`;
@@ -99,12 +101,16 @@ export class FacebookOAuthService {
}
}
getRedirectUri(): string {
/**
* URI de redirection OAuth. `req` permet de la déduire du domaine réellement
* utilisé quand `APP_URL` n'est pas défini : sans lui on renverrait
* `http://localhost:5555`, que Facebook rejette.
*/
getRedirectUri(req?: Request): string {
if (process.env.FACEBOOK_REDIRECT_URI) {
return process.env.FACEBOOK_REDIRECT_URI;
}
const base = (process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, '');
return `${base}/api/facebook/callback`;
return `${resolvePublicBaseUrl(req)}/api/facebook/callback`;
}
getScopes(): string {
@@ -112,12 +118,12 @@ export class FacebookOAuthService {
}
/** URL d'autorisation à ouvrir pour connecter les pages d'un compte Facebook. */
async buildAuthorizationUrl(state: string): Promise<string> {
async buildAuthorizationUrl(state: string, req?: Request): Promise<string> {
const { appId } = await this.getCredentials();
const params = new URLSearchParams({
client_id: appId,
redirect_uri: this.getRedirectUri(),
redirect_uri: this.getRedirectUri(req),
state,
scope: this.getScopes(),
response_type: 'code',
@@ -131,13 +137,13 @@ export class FacebookOAuthService {
* L'étape intermédiaire (token court) n'est jamais conservée : seul le token
* longue durée rend les tokens de page permanents.
*/
async exchangeCodeForUserToken(code: string): Promise<FacebookLongLivedToken> {
async exchangeCodeForUserToken(code: string, req?: Request): Promise<FacebookLongLivedToken> {
const { appId, appSecret } = await this.getCredentials();
const shortLived = await this.graphGet<{ access_token?: string }>('/oauth/access_token', {
client_id: appId,
client_secret: appSecret,
redirect_uri: this.getRedirectUri(),
redirect_uri: this.getRedirectUri(req),
code,
}, "échange du code d'autorisation");