@pr2core/librairy-pr2core
v1.0.0
Published
Module de sécurité OIDC/Keycloak pour NestJS — équivalent TypeScript d'un module de sécurité Java
Downloads
13
Readme
librairy-pr2core-mindef
Module de securite OIDC/Keycloak pour NestJS.
Equivalent TypeScript du module Java Spring Boot module-security-mindef.
Fournit : authentification OIDC via Keycloak, gestion de session serveur, guards bases sur les roles/autorisations en base locale, logging OAuth2 filtre.
Prerequis
- Node.js >= 18
- NestJS >= 9
- TypeORM >= 0.3
- Keycloak avec un realm et un client OIDC configure
Installation
npm install @pr2core/librairy-pr2corePeer dependencies
La librairie declare ces peer dependencies (a installer dans votre projet) :
npm install @nestjs/common @nestjs/core @nestjs/passport @nestjs/typeorm passport typeormDependencies additionnelles necessaires
npm install express-session passport-openidconnect
npm install -D @types/express-session @types/passportConfiguration complete
Etape 1 : Creer les entites TypeORM
La librairie s'appuie sur 3 interfaces que vos entites doivent implementer. Les roles et autorisations sont geres en base locale, pas dans Keycloak.
identifiant.entity.ts — Utilisateur local (lie au preferred_username Keycloak)
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { IdentifiantInterface } from '@pr2core/librairy-pr2core';
import { Profil } from './profil.entity';
@Entity('identifiant')
export class Identifiant implements IdentifiantInterface {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
login: string; // DOIT correspondre au preferred_username Keycloak
@Column({ default: true })
actif: boolean;
@Column({ default: false })
verrouille: boolean;
@ManyToMany(() => Profil, { eager: true })
@JoinTable({
name: 'identifiant_profil',
joinColumn: { name: 'identifiant_id' },
inverseJoinColumn: { name: 'profil_id' },
})
profils: Profil[];
}profil.entity.ts — Role utilisateur
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { ProfilInterface } from '@pr2core/librairy-pr2core';
import { Autorisation } from './autorisation.entity';
@Entity('profil')
export class Profil implements ProfilInterface {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
nom: string; // Ex: 'ADMIN', 'USER'
@ManyToMany(() => Autorisation, { eager: true })
@JoinTable({
name: 'profil_autorisation',
joinColumn: { name: 'profil_id' },
inverseJoinColumn: { name: 'autorisation_id' },
})
autorisations: Autorisation[];
}autorisation.entity.ts — Permission fine
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { AutorisationInterface } from '@pr2core/librairy-pr2core';
@Entity('autorisation')
export class Autorisation implements AutorisationInterface {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
code: string; // Ex: 'READ_USERS', 'WRITE_DOCUMENTS'
@Column()
libelle: string;
}Schema de la base :
identifiant (id, login, actif, verrouille)
└── identifiant_profil (identifiant_id, profil_id)
└── profil (id, nom)
└── profil_autorisation (profil_id, autorisation_id)
└── autorisation (id, code, libelle)Etape 2 : Implementer le repository d'identifiants
// identifiant.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IdentifiantRepositoryInterface } from '@pr2core/librairy-pr2core';
import { Identifiant } from './entities/identifiant.entity';
@Injectable()
export class IdentifiantRepository implements IdentifiantRepositoryInterface {
constructor(
@InjectRepository(Identifiant)
private readonly repo: Repository<Identifiant>,
) {}
async findByLogin(login: string) {
return this.repo.findOne({
where: { login },
relations: ['profils', 'profils.autorisations'],
});
}
}Etape 3 : Rendre les repositories TypeORM accessibles globalement
Important : Le module de securite instancie votre
IdentifiantRepositoryviauseClass. Pour que l'injection@InjectRepository(Identifiant)fonctionne dans ce contexte, les repositories TypeORM doivent etre disponibles globalement.
Creez un module global qui exporte TypeORM :
// database-entities.module.ts
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Identifiant } from './entities/identifiant.entity';
import { Profil } from './entities/profil.entity';
import { Autorisation } from './entities/autorisation.entity';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([Identifiant, Profil, Autorisation])],
exports: [TypeOrmModule],
})
export class DatabaseEntitiesModule {}Etape 4 : Importer le module de securite
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SecurityPr2coreMindefModule } from '@pr2core/librairy-pr2core';
import { DatabaseEntitiesModule } from './database-entities.module';
import { IdentifiantRepository } from './repositories/identifiant.repository';
import { Identifiant } from './entities/identifiant.entity';
import { Profil } from './entities/profil.entity';
import { Autorisation } from './entities/autorisation.entity';
@Module({
imports: [
// Charger le .env AVANT le module de securite
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRoot({
type: 'postgres', // ou 'better-sqlite3', 'mysql', etc.
// ... votre config DB
entities: [Identifiant, Profil, Autorisation],
synchronize: false, // true uniquement en dev
}),
// OBLIGATOIRE : rendre les repositories TypeORM globaux
DatabaseEntitiesModule,
// Module de securite
SecurityPr2coreMindefModule.forRoot({
config: {
keycloak: {
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
serverUrl: process.env.KEYCLOAK_SERVER_URL!,
realm: process.env.KEYCLOAK_REALM!,
registrationId: 'keycloak',
scope: 'openid',
userNameAttribute: 'preferred_username',
},
backend: {
url: process.env.BACKEND_URL!, // Ex: http://localhost:3000
redirectUri: '/api/auth/callback', // DOIT correspondre a la route du controller
},
frontend: {
url: process.env.FRONTEND_URL!, // Ex: http://localhost:4200
},
session: {
secret: process.env.SESSION_SECRET!,
maxAge: 86400000, // 24h
},
},
identifiantRepository: IdentifiantRepository,
}),
],
})
export class AppModule {}Attention :
ConfigModule.forRoot()doit etre importe avantSecurityPr2coreMindefModule.forRoot()dans le tableauimportspour que lesprocess.env.*soient disponibles.
Etape 5 : Configurer la session Express dans main.ts
import { NestFactory } from '@nestjs/core';
import * as session from 'express-session';
import * as passport from 'passport';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Session Express (necessaire pour Passport avec sessions)
app.use(
session({
secret: process.env.SESSION_SECRET || 'changeme',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 86400000, // 24h
httpOnly: true,
secure: false, // Mettre true en production avec HTTPS
sameSite: 'lax',
},
}),
);
// Initialisation Passport
app.use(passport.initialize());
app.use(passport.session());
// CORS si frontend sur un port/domaine different
app.enableCors({
origin: process.env.FRONTEND_URL,
credentials: true, // Necessaire pour envoyer les cookies de session
});
await app.listen(3000);
}
bootstrap();Note :
cookie.securedoit etrefalseen developpement (HTTP). En production (HTTPS), passez-le atrue.
Variables d'environnement
# Keycloak
KEYCLOAK_SERVER_URL=https://keycloak.example.com
KEYCLOAK_REALM=mon-realm
KEYCLOAK_CLIENT_ID=mon-client
KEYCLOAK_CLIENT_SECRET=secret-du-client
# URLs
BACKEND_URL=http://localhost:3000
FRONTEND_URL=http://localhost:4200
# Session
SESSION_SECRET=un-secret-tres-long-et-aleatoireConfiguration Keycloak
1. Client OIDC
| Parametre | Valeur |
|---|---|
| Client ID | mon-client |
| Client type | OpenID Connect |
| Client authentication | ON (confidential) |
| Standard flow | ON (Authorization Code Flow) |
| Valid redirect URIs | http://localhost:3000/* (dev) |
| Valid post logout redirect URIs | http://localhost:4200/* (dev) |
| Web origins | http://localhost:3000, http://localhost:4200 |
Le Client Secret se trouve dans l'onglet Credentials du client.
2. Utilisateurs
Creer les utilisateurs dans le realm Keycloak. Le champ Username doit correspondre
exactement au champ login de la table identifiant en base locale.
La librairie utilise le claim preferred_username (configurable via userNameAttribute)
pour faire la correspondance avec findByLogin().
Les roles sont geres en base locale, pas dans Keycloak. Keycloak sert uniquement a authentifier l'utilisateur.
Utilisation dans les controleurs
import { Controller, Get } from '@nestjs/common';
import { Public, Roles, CurrentUser, CustomOidcUser } from '@pr2core/librairy-pr2core';
@Controller('api/documents')
export class DocumentsController {
// Route publique — accessible sans authentification
@Public()
@Get('public')
publicEndpoint() {
return { message: 'accessible sans authentification' };
}
// Route protegee — authentification requise
@Get('mine')
myDocuments(@CurrentUser() user: CustomOidcUser) {
return {
login: user.login, // string
roles: user.roles, // string[] (noms des profils)
authorities: user.authorities, // string[] (codes des autorisations)
};
}
// Route avec role requis
@Roles('ADMIN')
@Get('admin')
adminOnly(@CurrentUser() user: CustomOidcUser) {
return { message: `Bienvenue ${user.login}` };
}
}Decorateurs disponibles
| Decorateur | Description |
|---|---|
| @Public() | Marque la route comme accessible sans authentification |
| @Roles('ADMIN', 'USER') | Exige un des roles (noms de profils en base) |
| @CurrentUser() | Injecte l'objet CustomOidcUser depuis la session |
Objet CustomOidcUser
| Propriete / Methode | Type | Description |
|---|---|---|
| login | string | Login de l'utilisateur |
| roles | string[] | Noms des profils en base |
| authorities | string[] | Codes des autorisations en base |
| hasRole(role) | boolean | Verifie si l'utilisateur a le role |
| hasAuthority(code) | boolean | Verifie si l'utilisateur a l'autorisation |
| identifiant | IdentifiantInterface | Entite complete en base |
| oidcClaims | Record<string, unknown> | Claims OIDC bruts du provider |
Endpoints fournis
Tous les endpoints sont montes automatiquement par le module :
| Methode | Route | Auth | Description |
|---------|-------|------|-------------|
| GET | /api/auth/login | Non | Redirige vers Keycloak |
| GET | /api/auth/callback | Non | Callback OAuth2 (gere par Passport) |
| GET | /api/auth/me | Oui | Retourne l'utilisateur connecte |
| GET | /api/auth/check | Oui | Statut d'authentification |
| POST | /api/auth/logout | Non | Deconnexion (session + Keycloak RP-Initiated Logout) |
Flow d'authentification
Navigateur Backend NestJS Keycloak
| | |
|-- GET /api/auth/login --> |
| |-- 302 redirect ------>|
| | |
|<---------- Page de login Keycloak ---------|
|-- credentials ----->| |
| |<-- 302 + auth code --|
| | |
|<-- GET /api/auth/callback (code) --> |
| |-- exchange code ----->|
| |<-- tokens -----------|
| | |
| |-- findByLogin(preferred_username)
| |-- session.save() |
|<-- 302 redirect vers FRONTEND_URL |
| | |
|-- GET /api/auth/me (cookie session) --> |
|<-- 200 { login, roles, authorities } -- |Regles de securite personnalisees
Pour ajouter des regles de securite propres a votre application :
import { Injectable } from '@nestjs/common';
import { SecurityRulesSupplier, SecurityRule } from '@pr2core/librairy-pr2core';
@Injectable()
export class AppSecurityRules implements SecurityRulesSupplier {
getRules(): SecurityRule[] {
return [
{ path: '/api/public/**', access: 'permitAll' },
{ path: '/api/internal/**', access: 'authenticated' },
];
}
}Puis passez-les au module :
SecurityPr2coreMindefModule.forRoot({
config: { /* ... */ },
identifiantRepository: IdentifiantRepository,
additionalRulesSuppliers: [AppSecurityRules],
})Correspondance Java -> NestJS
| Java (Spring) | NestJS (librairy-pr2core-mindef) |
|---|---|
| SecurityConfiguration | SecurityPr2coreMindefModule.forRoot() |
| ClientRegistrationConfig | SecurityPr2coreMindefConfig.keycloak |
| CustomOidcUserService | CustomOidcUserService |
| CustomOidcUser (record) | CustomOidcUser (class) |
| OAuth2LoggingFilter | OAuth2LoggingMiddleware |
| OAuth2LoginSuccessHandler | AuthController.callback() |
| OidcPr2coreSecurityRules | OidcPr2coreMindefSecurityRules |
| TokenRefreshController | AuthController |
| @PreAuthorize("hasRole()") | @Roles() |
| @AuthenticationPrincipal | @CurrentUser() |
| .permitAll() | @Public() |
| SecurityRulesSupplier | SecurityRulesSupplier |
| application.yml (security.*) | SecurityPr2coreMindefConfig |
API exportee
// Module principal
export { SecurityPr2coreMindefModule, SecurityPr2coreMindefModuleOptions } from '...';
// Configuration
export { SecurityPr2coreMindefConfig, SECURITY_PR2CORE_MINDEF_CONFIG, buildKeycloakUrls } from '...';
// Interfaces (a implementer par vos entites)
export { IdentifiantInterface } from '...';
export { ProfilInterface } from '...';
export { AutorisationInterface } from '...';
export { IdentifiantRepositoryInterface, IDENTIFIANT_REPOSITORY } from '...';
export { SecurityRule, SecurityRulesSupplier, SECURITY_RULES_SUPPLIERS } from '...';
// Modele utilisateur
export { CustomOidcUser } from '...';
// Services
export { CustomOidcUserService } from '...';
// Guards
export { OidcAuthGuard } from '...';
export { OidcCallbackGuard } from '...';
export { RolesGuard } from '...';
// Decorateurs
export { Public, IS_PUBLIC_KEY } from '...';
export { Roles, ROLES_KEY } from '...';
export { CurrentUser } from '...';
// Regles de securite
export { OidcPr2coreMindefSecurityRules } from '...';
// Middleware
export { OAuth2LoggingMiddleware } from '...';Tests
npm test # 93 tests, 100% coverage
npm run test:cov # Avec rapport de couverture