Produits
MySSO
Un fournisseur d'identité maison, branché sur mes propres applications
- Rôle
- Conception et développement
- Contexte
- Projet personnel en production
- Stack
- Express, TypeScript, Next.js 16, PostgreSQL, Prisma, OIDC · RS256

Vue d'ensemble
Implémentation SSO personnalisée avec support OpenID Connect et OAuth2, PKCE, gestion du consentement utilisateur et signature JWT RSA. Il sert de fournisseur OpenID Connect à mes applications : Quarto en production, MagicCards en dev et staging.
Points clés
- 01OpenID Connect et OAuth2 avec PKCE
- 02Écran de consentement listant les permissions demandées
- 03Jetons JWT signés en RS256, clé publique sur /jwks.json, refresh tokens en rotation
- 04Tableau de bord : sessions actives, applications autorisées et révocation
- 05Clients réels : Quarto en production, MagicCards en dev et staging
Architecture
Sous le capot
Un fournisseur d'identité OAuth 2.0 / OpenID Connect maison. Un backend Express unique émet et vérifie tous les jetons, signés en RS256 avec une clé RSA. Un frontend Next.js porte les écrans que l'utilisateur voit : connexion, consentement, tableau de bord et console d'administration. PostgreSQL, via Prisma, est la seule source de vérité : sessions, codes d'autorisation, consentements et journal d'audit y vivent tous.
- étapes dans le flux code + PKCE
- 16étapes dans le flux code + PKCE
- routes API en quatre routeurs
- 44routes API en quatre routeurs
- tables PostgreSQL, toutes autour de User
- 11tables PostgreSQL, toutes autour de User
- pages Next.js, dont 6 d'administration
- 16pages Next.js, dont 6 d'administration
- durée de vie d'un access token
- 15mindurée de vie d'un access token
- durée de vie d'un code, usage unique
- 60sdurée de vie d'un code, usage unique
Qui parle à qui
Clients
Applis clientes OIDC
Quarto en production, MagicCards en dev · client_id, PKCE, redirect_uri déclarée
Navigateur
Suit les redirections de /authorize · cookies HttpOnly
Frontend MySSO
Next.js 16 · React 19 · login, consentement, dashboard, admin
GET /authorize · POST /token · GET /userinfo · fetch en Bearer ou cookie
Express · middlewares
helmet
CSP, HSTS, X-Frame-Options DENY
CORS
Liste blanche ALLOWED_ORIGINS
Rate limit
120 req/min, puis limites par route
Parsers
Cookies, corps JSON limité à 10 kB
authMiddleware ajouté route par route, requireScope('admin') pour l'admin
Express · routeurs
authRoutes
/auth/* · /authorize · /token · /userinfo · /jwks.json · découverte
userRoutes
Profil, mot de passe, sessions, consentements
adminRoutes
Scope admin · comptes, applis, scopes, audit, réglages
Outils
/health avec latence base · Swagger sur /api-docs
Prisma · JwtService · SecurityLogger · EmailService
Données et services
PostgreSQL
Supabase en production · Docker en local · Prisma 6
Clés RSA 2048
Clé publique servie sur /jwks.json
Journal d'audit
Table AuditLog, lue par la console admin
SMTP
nodemailer, repli sur la console
Principes de conception
01
Un seul émetteur de jetons
Seul le backend Express signe des JWT. La clé privée ne le quitte jamais ; la clé publique est publiée sur /jwks.json.
02
Un frontend sans logique d'auth
Les pages Next.js appellent l'API. Mot de passe, consentement, émission de code : toute décision se prend côté Express.
03
Une base, rien en mémoire
Codes, refresh tokens, sessions et consentements sont en base. Le serveur est sans état, à part les compteurs de rate limit.
04
Des applis clientes déclarées
Chaque client a ses redirectUris et ses scopes autorisés. Une redirection hors liste est refusée avant même la connexion.
05
Les admins sont un scope
Un utilisateur isAdmin reçoit le scope admin dans son access token ; requireScope('admin') protège tout /admin/*.
06
Tout est tracé
Connexions, échecs, émissions de jetons, révocations et actions admin sont écrits dans la table AuditLog.
Flux OpenID Connect · code d'autorisation + PKCE
- 01App cliente → NavigateurRedirige vers /authorize
- 02Navigateur → BackendGET /authorize · client_id, state, nonce, code_challenge
- 03Backend → FrontendPas de session → /login?returnTo
- 04Navigateur → FrontendEmail et mot de passe
- 05Frontend → BackendPOST /auth/login
- 06Backend → PostgreSQLVérification argon2 · création de la Session
- 07Backend → FrontendCookies HttpOnly + jetons
- 08Navigateur → BackendRetour sur /authorize avec le cookie
- 09Backend → FrontendPas de consentement → /consent
- 10Navigateur → Frontend« Autoriser »
- 11Frontend → BackendGET /authorize?approved=true
- 12Backend → PostgreSQLUserConsent + AuthCode
- 13Backend → App clienteRedirection vers redirect_uri?code&state
- 14App cliente → BackendPOST /token · code + code_verifier
- 15Backend → App clienteaccess_token · refresh_token · id_token
- 16App cliente → BackendGET /userinfo en Bearer
Un utilisateur déjà connecté saute les étapes 3 à 7, un consentement déjà donné saute 9 à 11. À l'échange, le backend vérifie que le code existe, n'a pas servi, n'a pas expiré et que la redirect_uri est identique, puis contrôle PKCE. L'id_token n'est émis que si le scope openid a été consenti.
Sessions et révocation
Chaque refresh fait tourner le refresh token sur la même Session. Le scope admin est recalculé à chaque refresh, et un compte désactivé est bloqué immédiatement car le middleware relit son état en base. Chaque connexion crée une Session avec IP et user-agent, que l'utilisateur liste et révoque depuis son tableau de bord. Changer son mot de passe ferme toutes les sessions.
Jetons et durées de vie
| Jeton | Format | Durée | Côté serveur |
|---|---|---|---|
| Access token | JWT RS256 · sub, email, scope | 15 min | Vérifié par signature et compte actif |
| Refresh token | JWT RS256 · jti | 90 j | Table RefreshToken, rotation à chaque usage |
| ID token | JWT RS256 · aud, nonce | 1 h | Émis seulement avec le scope openid |
| Code d'autorisation | UUID v4 | 60 s | Table AuthCode, supprimé à l'usage |
| Lien de réinitialisation | Jeton aléatoire | 1 h | Usage unique |
| Lien de vérification | Jeton aléatoire | 24 h | Table EmailVerificationToken |
Scopes
| Scope | Donne accès à |
|---|---|
| openid | Émission de l'id_token |
| profile | Infos de profil dans /userinfo |
| Adresse email | |
| username | preferred_username |
| admin | Routes /admin/* · jamais demandé par un client, ajouté au login si isAdmin |
Modèle de données · 11 tables
| Table | Contenu |
|---|---|
| User | Email, username, hash, isAdmin, isDisabled, emailVerified |
| Session · RefreshToken | Sessions avec IP et user-agent, refresh tokens liés |
| AuthCode | Codes de 60 s avec nonce et code_challenge |
| UserConsent | Scopes accordés, un par utilisateur et par client |
| Client | clientId public, secret en argon2id, redirectUris, scopes autorisés |
| PasswordResetToken · EmailVerificationToken | Liens envoyés par email, suppression en cascade |
| Scope · AuditLog · SystemSetting | Catalogue de scopes, journal d'audit, réglages admin |
AuditLog garde un userId libre, sans clé étrangère, pour survivre à la suppression du compte. Treize migrations Prisma, de novembre 2025 à avril 2026.
API · quatre routeurs
| Routeur | Routes | Protection |
|---|---|---|
| Compte | /auth/register · login · refresh · logout · reset · verify | Limites par IP, refresh token |
| OAuth 2.0 / OIDC | /authorize · /token · /userinfo · /jwks.json · /.well-known | Cookie ou Bearer, public pour la découverte |
| Utilisateur | /user/consents · sessions · profile · password · account | authMiddleware |
| Administration | /admin/users · clients · scopes · audit-logs · settings | Scope admin |
Sécurité, couche par couche
| Couche | Mécanisme |
|---|---|
| En-têtes HTTP | helmet : CSP 'self', HSTS un an + preload, X-Frame-Options DENY, nosniff |
| Mots de passe | argon2id, 64 Mo, 3 itérations ; même message pour email inconnu ou mot de passe faux |
| Secrets clients | 32 octets aléatoires, stockés en argon2id, affichés une seule fois |
| Jetons | RS256 uniquement, issuer vérifié, refresh à usage unique |
| Redirections | redirect_uri comparée à la liste déclarée du client |
| Cookies | HttpOnly, secure en production, SameSite=strict pour le refresh |
| Traçabilité | Connexions, échecs, jetons, révocations et actions admin journalisés |
CI/CD et environnements
PR
CI GitHub Actions
develop
Intégration, CI
main
Production
La CI tourne sur Node 18 et 20 avec un service Postgres 15 : migrations Prisma, build TypeScript du backend, build Next.js du frontend, puis démarrage du serveur et contrôle de /health.
Stack complète
| Techno | Version | Rôle |
|---|---|---|
| Node.js · TypeScript | 18–20 · 5.3 | Runtime et langage |
| Express | 4.18 | Serveur HTTP et routage |
| jsonwebtoken | 9 | Signature et vérification RS256 |
| argon2 | 0.31 | Hash des mots de passe et des secrets |
| helmet · cors · cookie-parser | 8 · 2.8 · 1.4 | En-têtes, CORS, cookies |
| express-rate-limit | 8.3 | Limites de débit |
| nodemailer | 8 | Emails de vérification et de réinitialisation |
| swagger-jsdoc · swagger-ui-express | 6 · 5 | Documentation d'API |
| Techno | Version | Rôle |
|---|---|---|
| Prisma | 6.19 | ORM, migrations |
| PostgreSQL | 15 · 16 | CI et local ; Supabase en production |
| Techno | Version | Rôle |
|---|---|---|
| Next.js (App Router) | 16.2 | Connexion, consentement, dashboard, admin |
| React | 19.2 | Interface |
| Tailwind CSS | 4 | Styles, thème clair et sombre |
| Techno | Version | Rôle |
|---|---|---|
| Vercel | — | Frontend my-sso.louis-bertrand.fr |
| Render | — | Backend, selon le dépôt |
| Supabase | — | PostgreSQL managé, pooler |
| GitHub Actions · Docker Compose | — | CI · Postgres local |
Relevé dans le code, branche main, v3.4.0, 20 octobre 2026
Captures
Survolez ou parcourez au clavier pour agrandir une capture




Projet suivant OCR
Sudoku OCR →