feat: SuperOAuth token introspection middleware + /api/profile route

This commit is contained in:
2026-03-14 06:40:43 +01:00
parent b771f4d1c3
commit 4f3c0e6433
3 changed files with 77 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import express from "express";
import cors from "cors"; import cors from "cors";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { AppDataSource } from "./config/data-source"; import { AppDataSource } from "./config/data-source";
import { requireAuth, AuthenticatedRequest } from "./middleware/auth.middleware";
dotenv.config(); dotenv.config();
@@ -16,6 +17,12 @@ app.get("/api/health", (_req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() }); res.json({ status: "ok", timestamp: new Date().toISOString() });
}); });
// Route protégée — valide l'intégration SuperOAuth end-to-end
app.get("/api/profile", requireAuth, (req, res) => {
const { user } = req as AuthenticatedRequest;
res.json({ success: true, data: { user } });
});
AppDataSource.initialize() AppDataSource.initialize()
.then(() => { .then(() => {
console.log("Database connected"); console.log("Database connected");

View File

@@ -0,0 +1,69 @@
import { Request, Response, NextFunction } from "express";
export interface AuthenticatedUser {
id: string;
email: string | null;
nickname: string;
isActive: boolean;
linkedProviders: string[];
}
export interface AuthenticatedRequest extends Request {
user: AuthenticatedUser;
}
/**
* Middleware d'authentification — Token Introspection via SuperOAuth
*
* Valide le Bearer token auprès de SuperOAuth (POST /api/auth/token/validate).
* Aucun secret JWT partagé — SuperOAuth garde le contrôle total.
*
* Flow :
* 1. Extraire le Bearer token du header Authorization
* 2. Appeler SuperOAuth /api/auth/token/validate
* 3. Si valid → attacher req.user et continuer
* 4. Sinon → 401
*/
export const requireAuth = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
res.status(401).json({ success: false, error: "UNAUTHORIZED", message: "Access token required" });
return;
}
const superOAuthUrl = process.env.SUPER_OAUTH_URL;
if (!superOAuthUrl) {
console.error("SUPER_OAUTH_URL not configured");
res.status(500).json({ success: false, error: "INTERNAL_ERROR", message: "Auth service not configured" });
return;
}
try {
const response = await fetch(`${superOAuthUrl}/api/auth/token/validate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
const data = await response.json() as {
success: boolean;
data?: { valid: boolean; user?: AuthenticatedUser };
error?: string;
};
if (!response.ok || !data.data?.valid || !data.data.user) {
res.status(401).json({ success: false, error: data.error ?? "INVALID_TOKEN", message: "Invalid or expired token" });
return;
}
(req as AuthenticatedRequest).user = data.data.user;
next();
} catch {
res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE", message: "Authentication service unreachable" });
}
};

View File

@@ -29,6 +29,7 @@ services:
DB_USER: ${DB_USER:-originsdigital} DB_USER: ${DB_USER:-originsdigital}
DB_PASSWORD: ${DB_PASSWORD} DB_PASSWORD: ${DB_PASSWORD}
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
SUPER_OAUTH_URL: ${SUPER_OAUTH_URL:-https://superoauth.tetardtek.com}
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy