Sécurisation via Keycloak #2

Closed
opened 2025-11-03 20:19:15 +01:00 by ronan.quintin · 0 comments

Besoin

Actuellement l'application n'est pas du tout sécurisé. Il n'y a aucune possibilité de s'authentifier d'aucune manière. Afin de sécuriser nous allons introduire un composant Keycloak.

En dev on fait tourner un composant Keycloak qu'on rajoute dans la stack docker compose lancée par spring boot.

Elle est intégré dans le front react et dans le back spring boot. Il est indispensable de pouvoir créer

Spécifications

Docker

Dans le fichier "compose" du back rajouter un service keycloak. Voici un exemple de déclaration du service :

compose.yml

  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    container_name: keryloo-keycloak
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_DB: mysql
      KC_DB_URL: jdbc:mysql://mysql:3306/keycloak
      KC_DB_USERNAME: root
      KC_DB_PASSWORD: root
      KC_HOSTNAME: localhost
      KC_HOSTNAME_PORT: 8081
      KC_HTTP_PORT: 8081
      KC_HTTP_ENABLED: true
      KC_HOSTNAME_STRICT: false
    depends_on:
      - mysql
    ports:
      - "8081:8081"
    volumes:
      - ./keycloak-import:/opt/keycloak/data/import:ro
    command:
      - start-dev
      - --import-realm
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8081/ || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 10

Le dossier local "keycloak-import" contient la déclaration du realm de base. Il y'a deux fichier :

  • un "master-realm-admin-client.json" qui contient les définition de base, et notamment la création de l'admin qui permet de créer les user depuis l'interface de Keryloo
  • un keryloo-realm : qui contient les users à proprement parler.

Voici des exemples de ce qui est attendu :

master-realm.json

{
  "realm": "master",
  "clients": [
    {
      "clientId": "keryloo-admin",
      "name": "keryloo Admin Client",
      "description": "Client for keryloo administrative operations via Keycloak Admin API",
      "enabled": true,
      "publicClient": false,
      "standardFlowEnabled": false,
      "implicitFlowEnabled": false,
      "directAccessGrantsEnabled": false,
      "serviceAccountsEnabled": true,
      "authorizationServicesEnabled": false,
      "secret": "keryloo-admin-secret-2025",
      "redirectUris": [],
      "webOrigins": [],
      "notBefore": 0,
      "bearerOnly": false,
      "consentRequired": false,
      "protocol": "openid-connect",
      "attributes": {
        "use.refresh.tokens": "false",
        "client_credentials.use_refresh_token": "false"
      }
    }
  ],
  "clientScopeMappings": {
    "realm-management": [
      {
        "client": "keryloo-admin",
        "roles": [
          "admin"
        ]
      }
    ]
  },
  "roles": {
    "client": {
      "keryloo-admin": []
    }
  }
}

keryloo-realm.json

{
  "realm": "keryloo",
  "enabled": true,
  "displayName": "keryloo",
  "sslRequired": "external",
  "registrationAllowed": false,
  "loginWithEmailAllowed": true,
  "duplicateEmailsAllowed": false,
  "resetPasswordAllowed": true,
  "editUsernameAllowed": false,
  "bruteForceProtected": true,
  "permanentLockout": false,
  "maxFailureWaitSeconds": 900,
  "minimumQuickLoginWaitSeconds": 60,
  "waitIncrementSeconds": 60,
  "quickLoginCheckMilliSeconds": 1000,
  "maxDeltaTimeSeconds": 43200,
  "failureFactor": 30,
  "defaultRoles": [
    "offline_access",
    "uma_authorization"
  ],
  "requiredCredentials": ["password"],
  "passwordPolicy": "length(8)",
  "users": [
    {
      "username": "john.doe",
      "email": "john.doe@example.com",
      "enabled": true,
      "emailVerified": true,
      "firstName": "John",
      "lastName": "Doe",
      "credentials": [
        {
          "type": "password",
          "value": "password123",
          "temporary": false
        }
      ],
      "realmRoles": ["default-roles-keryloo"],
      "groups": []
    },
    {
      "username": "jane.smith",
      "email": "jane.smith@example.com",
      "enabled": true,
      "emailVerified": true,
      "firstName": "Jane",
      "lastName": "Smith",
      "credentials": [
        {
          "type": "password",
          "value": "password123",
          "temporary": false
        }
      ],
      "realmRoles": ["default-roles-keryloo"],
      "groups": []
    }
  ],
  "clients": [
    {
      "clientId": "keryloo-front",
      "name": "keryloo Frontend",
      "description": "React frontend application",
      "enabled": true,
      "publicClient": true,
      "standardFlowEnabled": true,
      "implicitFlowEnabled": false,
      "directAccessGrantsEnabled": true,
      "serviceAccountsEnabled": false,
      "authorizationServicesEnabled": false,
      "redirectUris": [
        "http://localhost:5173/*",
        "http://localhost:3000/*"
      ],
      "webOrigins": [
        "http://localhost:5173",
        "http://localhost:3000",
        "+"
      ],
      "notBefore": 0,
      "bearerOnly": false,
      "consentRequired": false,
      "protocol": "openid-connect",
      "attributes": {
        "pkce.code.challenge.method": "S256",
        "access.token.lifespan": "300",
        "display.on.consent.screen": "false",
        "oauth2.device.authorization.grant.enabled": "false",
        "oidc.ciba.grant.enabled": "false"
      },
      "protocolMappers": [
        {
          "name": "email",
          "protocol": "openid-connect",
          "protocolMapper": "oidc-usermodel-property-mapper",
          "consentRequired": false,
          "config": {
            "userinfo.token.claim": "true",
            "user.attribute": "email",
            "id.token.claim": "true",
            "access.token.claim": "true",
            "claim.name": "email",
            "jsonType.label": "String"
          }
        }
      ]
    }
  ],
  "scopeMappings": [],
  "clientScopeMappings": {},
  "roles": {
    "realm": [],
    "client": {}
  },
  "groups": [],
  "defaultDefaultClientScopes": [
    "role_list",
    "profile",
    "email",
    "roles",
    "web-origins"
  ],
  "defaultOptionalClientScopes": [
    "offline_access",
    "address",
    "phone",
    "microprofile-jwt"
  ],
  "browserSecurityHeaders": {
    "contentSecurityPolicyReportOnly": "",
    "xContentTypeOptions": "nosniff",
    "xRobotsTag": "none",
    "xFrameOptions": "SAMEORIGIN",
    "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
    "xXSSProtection": "1; mode=block",
    "strictTransportSecurity": "max-age=31536000; includeSubDomains"
  },
  "smtpServer": {},
  "eventsEnabled": false,
  "eventsListeners": ["jboss-logging"],
  "enabledEventTypes": [],
  "adminEventsEnabled": false,
  "adminEventsDetailsEnabled": false,
  "internationalizationEnabled": false,
  "supportedLocales": [],
  "browserFlow": "browser",
  "registrationFlow": "registration",
  "directGrantFlow": "direct grant",
  "resetCredentialsFlow": "reset credentials",
  "clientAuthenticationFlow": "clients",
  "dockerAuthenticationFlow": "docker auth",
  "attributes": {},
  "userManagedAccessAllowed": false
}

On doit prévoir les 3 rôles suivants :

  • administrateur : accède à toutes les fonctions
  • bailleur : peut ajouter des locataires, des propriétées, créer des baux
  • locataire : peut voir ses échéances et quittances de loyer, les charges associées
  • caution solidaire : peut voir les mêmes informations que le locataire dont il est caution

Dans un premier temps on ne créera pas d'habilitations, on les affinera / appliquera ensuite quand toutes les fonctionnalités seront développés

Intégration dans le back

L'intégration dans le back est assez simple, il faut d'abord rajouter les bonnes dépendances dans le pom :

pom.xml

<!-- Security -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
		</dependency>
		
		<!-- keycloack integration -->
		<dependency>
			<groupId>org.keycloak</groupId>
			<artifactId>keycloak-admin-client</artifactId>
			<version>26.0.5</version>
		</dependency>

Puis rajouter le point d'accès vers le serveur d'authentification dans le fichier "application-dev.yml"

application-dev.yml

spring:
  security:
    oauth2.resourceserver.jwt:
      issuer-uri: http://localhost:8081/realms/keryloo
      jwk-set-uri: http://localhost:8081/realms/keryloo/protocol/openid-connect/certs

keycloak:
  realm: keryloo
  auth-server-url: http://localhost:8081
  ssl-required: external
  bearer-only: true
  adminClientId: ${KEYCLOAK_ADMIN_CLIENT_ID}
  adminClientSecret: ${KEYCLOAK_ADMIN_CLIENT_SECRET}

Front

Dans le front il faudra rajouter un fichier .env pour le dev qui contient les informations de base

VITE_API_BASE_URL=http://localhost:8081/api
VITE_KEYCLOAK_URL=http://localhost:8080
VITE_KEYCLOAK_REALM=keryloo
VITE_KEYCLOAK_CLIENT_ID=keryloo-front

Installer la dépendance "keycloak-js"

Et créer un fichier pour configurer la connexion a keycloak

"src/config/keycloak.ts"

import Keycloak from 'keycloak-js';

// Configuration du client Keycloak depuis les variables d'environnement
const keycloakConfig = {
  url: import.meta.env.VITE_KEYCLOAK_URL,
  realm: import.meta.env.VITE_KEYCLOAK_REALM,
  clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
};

// Validation de la configuration
if (!keycloakConfig.url || !keycloakConfig.realm || !keycloakConfig.clientId) {
  throw new Error('Keycloak configuration is missing. Please check your environment variables.');
}

console.log('Keycloak configuration:', {
  url: keycloakConfig.url,
  realm: keycloakConfig.realm,
  clientId: keycloakConfig.clientId,
});

const keycloak = new Keycloak(keycloakConfig);

export default keycloak;

Modifier ensuite le fichier de configuration de l'instance axios pour gérer les bearer

src/api/axiosInstance.ts

import axios from 'axios';
import keycloak from '../config/keycloak';

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;

if (!API_BASE_URL) {
  throw new Error('API_BASE_URL is not defined. Please check your environment variables.');
}

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Intercepteur pour ajouter le token
api.interceptors.request.use(
  async (config) => {
    if (keycloak.token) {
      await keycloak.updateToken(5);
      config.headers.Authorization = `Bearer ${keycloak.token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

export default api;

Page de login

Prévoir de générer une page de login keryloo sympa que l'on mettre dans le realm

# Besoin Actuellement l'application n'est pas du tout sécurisé. Il n'y a aucune possibilité de s'authentifier d'aucune manière. Afin de sécuriser nous allons introduire un composant Keycloak. En dev on fait tourner un composant Keycloak qu'on rajoute dans la stack docker compose lancée par spring boot. Elle est intégré dans le front react et dans le back spring boot. Il est indispensable de pouvoir créer # Spécifications ## Docker Dans le fichier "compose" du back rajouter un service keycloak. Voici un exemple de déclaration du service : #### compose.yml ```yaml keycloak: image: quay.io/keycloak/keycloak:24.0 container_name: keryloo-keycloak environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KC_DB: mysql KC_DB_URL: jdbc:mysql://mysql:3306/keycloak KC_DB_USERNAME: root KC_DB_PASSWORD: root KC_HOSTNAME: localhost KC_HOSTNAME_PORT: 8081 KC_HTTP_PORT: 8081 KC_HTTP_ENABLED: true KC_HOSTNAME_STRICT: false depends_on: - mysql ports: - "8081:8081" volumes: - ./keycloak-import:/opt/keycloak/data/import:ro command: - start-dev - --import-realm healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8081/ || exit 1"] interval: 10s timeout: 5s retries: 10 ``` Le dossier local "keycloak-import" contient la déclaration du realm de base. Il y'a deux fichier : - un "master-realm-admin-client.json" qui contient les définition de base, et notamment la création de l'admin qui permet de créer les user depuis l'interface de Keryloo - un keryloo-realm : qui contient les users à proprement parler. Voici des exemples de ce qui est attendu : #### master-realm.json ```json { "realm": "master", "clients": [ { "clientId": "keryloo-admin", "name": "keryloo Admin Client", "description": "Client for keryloo administrative operations via Keycloak Admin API", "enabled": true, "publicClient": false, "standardFlowEnabled": false, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "secret": "keryloo-admin-secret-2025", "redirectUris": [], "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "protocol": "openid-connect", "attributes": { "use.refresh.tokens": "false", "client_credentials.use_refresh_token": "false" } } ], "clientScopeMappings": { "realm-management": [ { "client": "keryloo-admin", "roles": [ "admin" ] } ] }, "roles": { "client": { "keryloo-admin": [] } } } ``` #### keryloo-realm.json ```json { "realm": "keryloo", "enabled": true, "displayName": "keryloo", "sslRequired": "external", "registrationAllowed": false, "loginWithEmailAllowed": true, "duplicateEmailsAllowed": false, "resetPasswordAllowed": true, "editUsernameAllowed": false, "bruteForceProtected": true, "permanentLockout": false, "maxFailureWaitSeconds": 900, "minimumQuickLoginWaitSeconds": 60, "waitIncrementSeconds": 60, "quickLoginCheckMilliSeconds": 1000, "maxDeltaTimeSeconds": 43200, "failureFactor": 30, "defaultRoles": [ "offline_access", "uma_authorization" ], "requiredCredentials": ["password"], "passwordPolicy": "length(8)", "users": [ { "username": "john.doe", "email": "john.doe@example.com", "enabled": true, "emailVerified": true, "firstName": "John", "lastName": "Doe", "credentials": [ { "type": "password", "value": "password123", "temporary": false } ], "realmRoles": ["default-roles-keryloo"], "groups": [] }, { "username": "jane.smith", "email": "jane.smith@example.com", "enabled": true, "emailVerified": true, "firstName": "Jane", "lastName": "Smith", "credentials": [ { "type": "password", "value": "password123", "temporary": false } ], "realmRoles": ["default-roles-keryloo"], "groups": [] } ], "clients": [ { "clientId": "keryloo-front", "name": "keryloo Frontend", "description": "React frontend application", "enabled": true, "publicClient": true, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, "authorizationServicesEnabled": false, "redirectUris": [ "http://localhost:5173/*", "http://localhost:3000/*" ], "webOrigins": [ "http://localhost:5173", "http://localhost:3000", "+" ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "protocol": "openid-connect", "attributes": { "pkce.code.challenge.method": "S256", "access.token.lifespan": "300", "display.on.consent.screen": "false", "oauth2.device.authorization.grant.enabled": "false", "oidc.ciba.grant.enabled": "false" }, "protocolMappers": [ { "name": "email", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "email", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "email", "jsonType.label": "String" } } ] } ], "scopeMappings": [], "clientScopeMappings": {}, "roles": { "realm": [], "client": {} }, "groups": [], "defaultDefaultClientScopes": [ "role_list", "profile", "email", "roles", "web-origins" ], "defaultOptionalClientScopes": [ "offline_access", "address", "phone", "microprofile-jwt" ], "browserSecurityHeaders": { "contentSecurityPolicyReportOnly": "", "xContentTypeOptions": "nosniff", "xRobotsTag": "none", "xFrameOptions": "SAMEORIGIN", "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", "xXSSProtection": "1; mode=block", "strictTransportSecurity": "max-age=31536000; includeSubDomains" }, "smtpServer": {}, "eventsEnabled": false, "eventsListeners": ["jboss-logging"], "enabledEventTypes": [], "adminEventsEnabled": false, "adminEventsDetailsEnabled": false, "internationalizationEnabled": false, "supportedLocales": [], "browserFlow": "browser", "registrationFlow": "registration", "directGrantFlow": "direct grant", "resetCredentialsFlow": "reset credentials", "clientAuthenticationFlow": "clients", "dockerAuthenticationFlow": "docker auth", "attributes": {}, "userManagedAccessAllowed": false } ``` On doit prévoir les 3 rôles suivants : - administrateur : accède à toutes les fonctions - bailleur : peut ajouter des locataires, des propriétées, créer des baux - locataire : peut voir ses échéances et quittances de loyer, les charges associées - caution solidaire : peut voir les mêmes informations que le locataire dont il est caution Dans un premier temps on ne créera pas d'habilitations, on les affinera / appliquera ensuite quand toutes les fonctionnalités seront développés ## Intégration dans le back L'intégration dans le back est assez simple, il faut d'abord rajouter les bonnes dépendances dans le pom : #### pom.xml ```xml <!-- Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <!-- keycloack integration --> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-client</artifactId> <version>26.0.5</version> </dependency> ``` Puis rajouter le point d'accès vers le serveur d'authentification dans le fichier "application-dev.yml" #### application-dev.yml ```yaml spring: security: oauth2.resourceserver.jwt: issuer-uri: http://localhost:8081/realms/keryloo jwk-set-uri: http://localhost:8081/realms/keryloo/protocol/openid-connect/certs keycloak: realm: keryloo auth-server-url: http://localhost:8081 ssl-required: external bearer-only: true adminClientId: ${KEYCLOAK_ADMIN_CLIENT_ID} adminClientSecret: ${KEYCLOAK_ADMIN_CLIENT_SECRET} ``` ## Front Dans le front il faudra rajouter un fichier .env pour le dev qui contient les informations de base ```bash VITE_API_BASE_URL=http://localhost:8081/api VITE_KEYCLOAK_URL=http://localhost:8080 VITE_KEYCLOAK_REALM=keryloo VITE_KEYCLOAK_CLIENT_ID=keryloo-front ``` Installer la dépendance "keycloak-js" Et créer un fichier pour configurer la connexion a keycloak #### "src/config/keycloak.ts" ```typescript import Keycloak from 'keycloak-js'; // Configuration du client Keycloak depuis les variables d'environnement const keycloakConfig = { url: import.meta.env.VITE_KEYCLOAK_URL, realm: import.meta.env.VITE_KEYCLOAK_REALM, clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID, }; // Validation de la configuration if (!keycloakConfig.url || !keycloakConfig.realm || !keycloakConfig.clientId) { throw new Error('Keycloak configuration is missing. Please check your environment variables.'); } console.log('Keycloak configuration:', { url: keycloakConfig.url, realm: keycloakConfig.realm, clientId: keycloakConfig.clientId, }); const keycloak = new Keycloak(keycloakConfig); export default keycloak; ``` Modifier ensuite le fichier de configuration de l'instance axios pour gérer les bearer #### src/api/axiosInstance.ts ```typescript import axios from 'axios'; import keycloak from '../config/keycloak'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; if (!API_BASE_URL) { throw new Error('API_BASE_URL is not defined. Please check your environment variables.'); } const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); // Intercepteur pour ajouter le token api.interceptors.request.use( async (config) => { if (keycloak.token) { await keycloak.updateToken(5); config.headers.Authorization = `Bearer ${keycloak.token}`; } return config; }, (error) => Promise.reject(error) ); export default api; ``` ## Page de login Prévoir de générer une page de login keryloo sympa que l'on mettre dans le realm
ronan.quintin referenced this issue from a commit 2025-11-03 20:36:41 +01:00
ronan.quintin referenced this issue from a commit 2025-11-11 21:18:03 +01:00
#2
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: ronan.quintin/Keryloo#2
No description provided.