1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package com.github.talbotgui.psl.socle.securite.service;
23
24 import java.nio.charset.StandardCharsets;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Base64;
28 import java.util.Date;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.regex.Pattern;
33
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36 import org.springframework.beans.factory.annotation.Value;
37 import org.springframework.security.core.userdetails.User;
38 import org.springframework.security.core.userdetails.UserDetails;
39 import org.springframework.stereotype.Service;
40 import org.springframework.util.StringUtils;
41
42 import com.fasterxml.jackson.core.JsonProcessingException;
43 import com.fasterxml.jackson.core.type.TypeReference;
44 import com.fasterxml.jackson.databind.ObjectMapper;
45 import com.github.talbotgui.psl.socle.commun.oidc.OidcClient;
46 import com.github.talbotgui.psl.socle.commun.oidc.dto.ReponseTokenOIDC;
47 import com.github.talbotgui.psl.socle.commun.securite.JwtService;
48 import com.github.talbotgui.psl.socle.commun.utils.RegexUtils;
49 import com.github.talbotgui.psl.socle.securite.apiclient.dto.ReponseJwtDto;
50 import com.github.talbotgui.psl.socle.securite.apiclient.dto.RequeteCreationTokenOidcDto;
51 import com.github.talbotgui.psl.socle.securite.client.SpClient;
52 import com.github.talbotgui.psl.socle.securite.client.dto.InformationSpCompteDto;
53 import com.github.talbotgui.psl.socle.securite.client.dto.InformationSpUsagerDto;
54 import com.github.talbotgui.psl.socle.securite.exception.SecuriteException;
55
56 import io.jsonwebtoken.Claims;
57 import io.jsonwebtoken.lang.DateFormats;
58
59 @Service
60 public class OidcServiceImpl implements OidcService {
61
62 private static final Logger LOGGER = LoggerFactory.getLogger(OidcServiceImpl.class);
63
64
65 private static final String PATTERN_MOT_DE_PASSE_GLOBAL = "^[a-zA-Z0-9 \\.!#$%&'\"*+\\/=?^_`{|}~\\-@]*$";
66
67
68
69 private static final List<String> PATTERNS_CRITERES_MOT_DE_PASSE = Arrays.asList("^.*[a-z].*$", "^.*[A-Z].*$", "^.*[0-9].*$",
70 "^.*[ \\.!#$%&'\"*+\\/=?^_`{|}~\\-@].*$");
71
72
73
74
75
76 private static final String REGEX_VALIDATION_EMAIL = RegexUtils.EMAIL;
77
78
79 private final String clientId;
80
81
82 private final String clientSecret;
83
84 private final JwtService jwtService;
85 private final OidcClient oidcClient;
86 private final SpClient spClient;
87
88
89 public OidcServiceImpl(@Value("${oidc.clientId}") String clientId, @Value("${oidc.clientSecret}") String clientSecret, JwtService jwtService,
90 OidcClient oidcClient, SpClient spClient) {
91 super();
92 this.clientId = clientId;
93 this.clientSecret = clientSecret;
94 this.jwtService = jwtService;
95 this.oidcClient = oidcClient;
96 this.spClient = spClient;
97
98
99 if (LOGGER.isTraceEnabled()) {
100 LOGGER.warn("Le niveau de log TRACE pour cette classe ne doit EN AUCUN CAS étre actif en production !! (données sensibles/personnelles)");
101 }
102 }
103
104
105
106
107
108
109
110 private InformationSpUsagerDto chargerDonneesUsagerEtCompte(String accessTokenSP) {
111
112
113 InformationSpUsagerDto donneesUsager;
114 try {
115 donneesUsager = this.spClient.chargerDonneesPersonnelles(this.clientId, this.clientSecret, accessTokenSP);
116 } catch (Exception e) {
117
118 LOGGER.debug("Erreur à la recherche des données de l'usager connecté", e);
119 LOGGER.warn("Erreur de chargement des données personnelles : {}", e.getMessage());
120 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "cduec1");
121 }
122
123
124 if ((donneesUsager == null) || !StringUtils.hasLength(donneesUsager.getEmail())) {
125 LOGGER.warn("Erreur de chargement des données personnelles : les données sont incomplètes");
126 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "cduec2");
127 }
128
129 try {
130
131 InformationSpCompteDto donneesCompte = this.spClient.chargerDonneesCompte(this.clientId, this.clientSecret, accessTokenSP);
132
133
134 donneesUsager.setAccountType(donneesCompte.getAccountType());
135 donneesUsager.setFranceConnect(donneesCompte.getFranceConnect());
136 donneesUsager.setEmailTechnique(donneesCompte.getEmail());
137 donneesUsager.setUuidSp(donneesCompte.getSub());
138
139 } catch (Exception e) {
140
141 LOGGER.debug("Erreur à la recherche des données du compte connecté", e);
142 LOGGER.warn("Erreur de chargement des données du compte : {}", e.getMessage());
143 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "cduec3");
144 }
145 return donneesUsager;
146 }
147
148 @Override
149 public InformationSpUsagerDto chargerDonneesUsagerEtCompteAvecUnTokenPsl(String tokenPSL) {
150
151 Claims claimsPsl = this.jwtService.validerToken(tokenPSL);
152 if (claimsPsl == null) {
153 LOGGER.warn("Token PSL invalide");
154 throw new SecuriteException(SecuriteException.DONNEES_USAGER_INDISPONIBLES, "1");
155 }
156
157
158 String emailDansSubject = claimsPsl.getSubject();
159 String accessTokenOidcExistant = claimsPsl.get(JwtService.CLEF_CLAIMS_ACCESS_TOKEN_OIDC, String.class);
160
161
162 if (JwtService.EMAIL_UTILISATEUR_ANONYMOUS.equals(emailDansSubject)) {
163 return new InformationSpUsagerDto(JwtService.EMAIL_UTILISATEUR_ANONYMOUS, "", "");
164 }
165
166
167 else {
168 return this.chargerDonneesUsagerEtCompte(accessTokenOidcExistant);
169 }
170 }
171
172
173
174
175
176
177
178
179
180
181 private ReponseTokenOIDC creerLeToken(RequeteCreationTokenOidcDto requete) {
182
183
184 ReponseTokenOIDC reponseOidc;
185 try {
186 reponseOidc = this.oidcClient.creerAccessToken(requete.grantType(), requete.code(), requete.redirectUri(), requete.codeVerifier(),
187 this.clientId, this.clientSecret, requete.refreshToken());
188 } catch (Exception e) {
189
190 LOGGER.warn("Erreur de création du token OIDC", e);
191 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "c1");
192 }
193
194
195 if ((reponseOidc == null) || !StringUtils.hasLength(reponseOidc.accessToken()) || !StringUtils.hasLength(reponseOidc.refreshToken())) {
196 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "c2");
197 }
198
199
200 InformationSpUsagerDto donneesUsager = this.chargerDonneesUsagerEtCompte(reponseOidc.accessToken());
201
202
203 return this.creerTokenPslApartirDuTokenOIDC(reponseOidc, donneesUsager);
204 }
205
206 @Override
207 public ReponseTokenOIDC creerOuRaffrachirLeToken(RequeteCreationTokenOidcDto requete) {
208
209
210 if (!"refresh_token".equals(requete.grantType())) {
211 return this.creerLeToken(requete);
212 } else {
213 return this.raffraichirLeToken(requete);
214 }
215 }
216
217
218
219
220
221
222
223 private ReponseJwtDto creerTokenPsl(UserDetails userDetails) {
224
225
226 String token = this.jwtService.genererNouveauToken(userDetails);
227
228
229 return new ReponseJwtDto(token);
230 }
231
232
233
234
235
236
237
238
239 private ReponseTokenOIDC creerTokenPslApartirDuTokenOIDC(ReponseTokenOIDC reponseOidc, InformationSpUsagerDto donneesUsager) {
240
241 Date dateExpirationRefreshOidcExistant = new Date(new Date().getTime() + (Integer.parseInt(reponseOidc.expiresIn()) * 1000));
242 Date dateExpirationAccessOidcExistant = new Date(new Date().getTime() + (Integer.parseInt(reponseOidc.refreshExpiresIn()) * 1000));
243
244
245 String emailSP = this.extraireEmailDeLaPartiePubliqueDuToken(reponseOidc.accessToken());
246
247
248 Map<String, Object> claims = new HashMap<>(Map.of(
249 Claims.SUBJECT, emailSP,
250 JwtService.CLEF_CLAIMS_FC, donneesUsager.getFranceConnect(),
251 JwtService.CLEF_CLAIMS_ACCOUNT_TYPE, donneesUsager.getAccountType()
252 ));
253
254
255 Map<String, Object> donneesAchiffrer = Map.of(
256 JwtService.CLEF_CLAIMS_UUID_SP, donneesUsager.getUuidSp(),
257 JwtService.CLEF_CLAIMS_ACCESS_TOKEN_OIDC, reponseOidc.accessToken(),
258 JwtService.CLEF_CLAIMS_REFRESH_TOKEN_OIDC, reponseOidc.refreshToken(),
259 JwtService.CLEF_CLAIMS_EXPIRATION_ACCESSTOKEN_OIDC, DateFormats.formatIso8601(dateExpirationAccessOidcExistant),
260 JwtService.CLEF_CLAIMS_EXPIRATION_REFRESHTOKEN_OIDC, DateFormats.formatIso8601(dateExpirationRefreshOidcExistant)
261 );
262
263
264 UserDetails utilisateur = new User(emailSP, "", new ArrayList<>());
265 String nouveauToken = this.jwtService.genererNouveauToken(utilisateur, claims, donneesAchiffrer);
266
267
268
269
270
271
272
273 return new ReponseTokenOIDC(nouveauToken, reponseOidc.expiresIn(), reponseOidc.refreshExpiresIn(), nouveauToken, "PSL", "",
274 reponseOidc.sessionState(), reponseOidc.scope());
275 }
276
277
278
279
280
281
282
283 private String extraireEmailDeLaPartiePubliqueDuToken(String token) {
284
285
286 String[] tokenDecoupe = token.split("\\.");
287 if (tokenDecoupe.length != 3) {
288 LOGGER.warn("Token mal formaté");
289 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "e1");
290 }
291
292
293 String partiePubliqueTokenDecode = new String(Base64.getDecoder().decode(tokenDecoupe[1].getBytes(StandardCharsets.UTF_8)),
294 StandardCharsets.UTF_8);
295
296
297 Map<String, Object> resultat;
298 try {
299 resultat = new ObjectMapper().readValue(partiePubliqueTokenDecode, new TypeReference<Map<String, Object>>() {
300 });
301 } catch (JsonProcessingException e) {
302 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "e2");
303 }
304
305
306 Object email = resultat.get("email");
307 if (email == null) {
308 LOGGER.warn("Pas d'email dans le JSON de la partie publique du token");
309 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "e3");
310 }
311
312
313 return resultat.get("email").toString();
314 }
315
316
317
318
319
320
321
322
323
324
325 private ReponseTokenOIDC raffraichirLeToken(RequeteCreationTokenOidcDto requete) {
326 String tokenPSL = requete.refreshToken();
327
328
329 Claims claimsPsl = this.jwtService.validerToken(tokenPSL);
330 if (claimsPsl == null) {
331 LOGGER.warn("Token PSL invalide");
332 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "r1");
333 }
334
335
336 String emailUtilisateur = claimsPsl.getSubject();
337 String refreshTokenOidcExistant = claimsPsl.get(JwtService.CLEF_CLAIMS_REFRESH_TOKEN_OIDC, String.class);
338 String motDePasseEventuel = claimsPsl.get(JwtService.CLEF_CLAIMS_MOT_DE_PASSE, String.class);
339 Date dateExpirationAccessOidcExistant = claimsPsl.get(JwtService.CLEF_CLAIMS_EXPIRATION_ACCESSTOKEN_OIDC, Date.class);
340 Date dateExpirationRefreshOidcExistant = claimsPsl.get(JwtService.CLEF_CLAIMS_EXPIRATION_REFRESHTOKEN_OIDC, Date.class);
341
342
343 if (StringUtils.hasLength(emailUtilisateur) && JwtService.EMAIL_UTILISATEUR_ANONYMOUS.equals(emailUtilisateur)) {
344 ReponseJwtDto tokenAnonyme = this.sauthentifierEnAnonyme();
345 return new ReponseTokenOIDC(tokenAnonyme.token(), null, null, tokenAnonyme.token(), null, null, null, null);
346 }
347
348
349 if (StringUtils.hasLength(motDePasseEventuel)) {
350 ReponseJwtDto tokenAnonyme = this.sauthentifierAvecUnNomDutilisateurEtUnMotDePasse(emailUtilisateur, motDePasseEventuel);
351 return new ReponseTokenOIDC(tokenAnonyme.token(), null, null, tokenAnonyme.token(), null, null, null, null);
352 }
353
354
355 if (!StringUtils.hasLength(refreshTokenOidcExistant) || (dateExpirationAccessOidcExistant == null)
356 || (dateExpirationRefreshOidcExistant == null)) {
357 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "r2");
358 }
359 if (dateExpirationRefreshOidcExistant.before(new Date())) {
360 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "r3");
361 }
362
363
364 ReponseTokenOIDC reponseOidc;
365 try {
366 reponseOidc = this.oidcClient.creerAccessToken(requete.grantType(), requete.code(), requete.redirectUri(), requete.codeVerifier(),
367 this.clientId, this.clientSecret, refreshTokenOidcExistant);
368 LOGGER.trace("RefreshToken réalisé : {}", reponseOidc);
369 } catch (Exception e) {
370 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, e, "r4");
371 }
372
373 if ((reponseOidc == null) || !StringUtils.hasLength(reponseOidc.accessToken()) || !StringUtils.hasLength(reponseOidc.refreshToken())) {
374 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "r5");
375 }
376
377
378 String emailAccessTokenSP = this.extraireEmailDeLaPartiePubliqueDuToken(reponseOidc.accessToken());
379 String emailIdTokenSP = this.extraireEmailDeLaPartiePubliqueDuToken(reponseOidc.idToken());
380
381
382 if ((emailUtilisateur == null) || !emailAccessTokenSP.equals(emailUtilisateur) || !emailIdTokenSP.equals(emailUtilisateur)) {
383 throw new SecuriteException(SecuriteException.ACCESSTOKEN_NON_ENREGISTRABLE, "r6");
384 }
385
386
387 InformationSpUsagerDto donneesUsager = this.chargerDonneesUsagerEtCompte(reponseOidc.accessToken());
388
389
390 return this.creerTokenPslApartirDuTokenOIDC(reponseOidc, donneesUsager);
391 }
392
393 @Override
394 public ReponseJwtDto sauthentifierAvecUnNomDutilisateurEtUnMotDePasse(String nomUtilisateur, String motDePasse) {
395
396
397 if (!StringUtils.hasLength(nomUtilisateur)) {
398 throw new SecuriteException(SecuriteException.DONNEES_AUTHENTIFICATION_INVALIDE, "1");
399 }
400 if (!StringUtils.hasLength(motDePasse)) {
401 throw new SecuriteException(SecuriteException.DONNEES_AUTHENTIFICATION_INVALIDE, "3");
402 }
403
404
405 nomUtilisateur = nomUtilisateur.trim();
406
407
408 if (!Pattern.matches(REGEX_VALIDATION_EMAIL, nomUtilisateur)) {
409 throw new SecuriteException(SecuriteException.DONNEES_AUTHENTIFICATION_INVALIDE, "2");
410 }
411 this.validerMotDePasse(motDePasse);
412
413
414 nomUtilisateur = nomUtilisateur.trim().toUpperCase();
415
416
417 return this.creerTokenPsl(new User(nomUtilisateur, motDePasse, new ArrayList<>()));
418 }
419
420 @Override
421 public ReponseJwtDto sauthentifierEnAnonyme() {
422 return this.creerTokenPsl(JwtService.USER_ANONYME);
423 }
424
425
426
427
428
429
430
431
432
433
434
435
436 private void validerMotDePasse(String motDePasse) {
437
438 if (motDePasse.length() < 8) {
439 throw new SecuriteException(SecuriteException.DONNEES_AUTHENTIFICATION_INVALIDE, "4-1");
440 }
441
442
443 if (!Pattern.matches(PATTERN_MOT_DE_PASSE_GLOBAL, motDePasse)) {
444 throw new SecuriteException(SecuriteException.DONNEES_AUTHENTIFICATION_INVALIDE, "4-2");
445 }
446
447
448 long nbCriteresRespectes = PATTERNS_CRITERES_MOT_DE_PASSE.stream().filter(p -> Pattern.matches(p, motDePasse)).count();
449 if (nbCriteresRespectes < 3) {
450 throw new SecuriteException(SecuriteException.DONNEES_AUTHENTIFICATION_INVALIDE, "4-3");
451 }
452 }
453 }