From c6af9ba05e1ed794b03c0133c22c375475147e9e Mon Sep 17 00:00:00 2001 From: Eduard Tihomirov Date: Thu, 9 Jan 2025 16:45:16 +0300 Subject: [PATCH 1/4] Cherry pick SUPPORT-8682 --- .../esia/controller/EsiaController.java | 8 ++- .../esia/service/EsiaAuthService.java | 72 ++++++++++++------- .../webbpm/jwt/util/SecurityUtil.java | 9 +++ .../ts/modules/security/guard/auth.guard.ts | 30 ++++---- 4 files changed, 78 insertions(+), 41 deletions(-) diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java b/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java index 2a3ab21..a0d3282 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/controller/EsiaController.java @@ -38,9 +38,11 @@ public class EsiaController { return esiaAuthService.generateAuthCodeUrl(); } - @GetMapping(value = "/esia/auth", params = "code") - public ResponseEntity esiaAuth(@RequestParam("code") String code, HttpServletRequest request, HttpServletResponse response) { - return esiaAuthService.getEsiaTokensByCode(code, request, response); + @GetMapping(value = "/esia/auth") + public ResponseEntity esiaAuth(@RequestParam(value = "code", required = false) String code, + @RequestParam(value = "error", required = false) String error, HttpServletRequest request, + HttpServletResponse response) { + return esiaAuthService.getEsiaTokensByCode(code, error, request, response); } @RequestMapping(value = "/esia/refresh") diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java index 4fb22ae..bf4da8c 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java @@ -1,5 +1,6 @@ package ru.micord.ervu.security.esia.service; +import java.lang.invoke.MethodHandles; import java.net.URI; import java.net.URL; import java.net.URLEncoder; @@ -12,13 +13,17 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.context.SecurityContext; @@ -42,11 +47,15 @@ import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper; import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; import ru.micord.ervu.security.webbpm.jwt.model.Token; +import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.getCurrentUsername; + /** * @author Eduard Tihomirov */ @Service public class EsiaAuthService { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final Long EXPIRES_IN = 3600L; @Autowired private ObjectMapper objectMapper; @@ -143,8 +152,16 @@ public class EsiaAuthService { return uriBuilder.toString(); } - public ResponseEntity getEsiaTokensByCode(String esiaAuthCode, HttpServletRequest request, HttpServletResponse response) { +public ResponseEntity getEsiaTokensByCode(String esiaAuthCode, String error, + HttpServletRequest request, HttpServletResponse response) { try { + if (error != null && !error.equals("null")) { + createTokenAndAddCookie(response, null, null, EXPIRES_IN); + return new ResponseEntity<>( + "Произошла неизвестная ошибка. Обратитесь к системному администратору", + HttpStatus.FORBIDDEN + ); + } String clientId = esiaConfig.getClientId(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); @@ -202,21 +219,18 @@ public class EsiaAuthService { EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); EsiaTokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); - Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId()); - int expiry = tokenResponse.getExpires_in().intValue(); - securityHelper.addAccessCookies(response, token.getValue(), expiry); - UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = - new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - JwtAuthentication jwtAuthentication = new JwtAuthentication(usernamePasswordAuthenticationToken, - esiaAccessToken.getSbj_id(), token.getValue()); - authenticationManager.authenticate(jwtAuthentication); - context.setAuthentication(jwtAuthentication); - SecurityContextHolder.setContext(context); + createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuIdResponse.getErvuId(), expiresIn); return ResponseEntity.ok("Authentication successful"); } catch (Exception e) { - throw new RuntimeException(e); + createTokenAndAddCookie(response, null, null, EXPIRES_IN); + String messageId = getMessageId(e); + String messageWithId = String.format("[%s] %s", messageId, e.getMessage()); + LOGGER.error(messageWithId, e); + return new ResponseEntity<>( + "Произошла ошибка " + messageId + ". Обратитесь к системному администратору", + HttpStatus.FORBIDDEN + ); } } @@ -276,17 +290,7 @@ public class EsiaAuthService { EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); EsiaTokensStore.addRefreshToken(prnOid, esiaNewRefreshTokenStr, expiresIn); Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); - Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId()); - int expiry = tokenResponse.getExpires_in().intValue(); - securityHelper.addAccessCookies(response, token.getValue(), expiry); - UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = - new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - JwtAuthentication jwtAuthentication = new JwtAuthentication(usernamePasswordAuthenticationToken, - esiaAccessToken.getSbj_id(), token.getValue()); - authenticationManager.authenticate(jwtAuthentication); - context.setAuthentication(jwtAuthentication); - SecurityContextHolder.setContext(context); + createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuIdResponse.getErvuId(), expiresIn); } catch (Exception e) { throw new RuntimeException(e); @@ -372,4 +376,24 @@ public class EsiaAuthService { person.setDocument(document); return person; } + + private String getMessageId(Exception exception) { + return Integer.toUnsignedString(Objects + .hashCode(getCurrentUsername()), 36) + + "-" + + Integer.toUnsignedString(exception.hashCode(), 36); + } + + private void createTokenAndAddCookie(HttpServletResponse response, String userId, String ervuId, + Long expiresIn) { + Token token = jwtTokenService.createAccessToken(userId, expiresIn, ervuId); + securityHelper.addAccessCookies(response, token.getValue(), expiresIn.intValue()); + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + JwtAuthentication authentication = new JwtAuthentication(usernamePasswordAuthenticationToken, + userId, token.getValue()); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java index 15c3c7a..ef8555e 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java @@ -4,6 +4,7 @@ import java.util.Optional; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.util.WebUtils; import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; @@ -31,4 +32,12 @@ public final class SecurityUtil { }) .orElse(null); } + + public static String getCurrentUsername() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.isAuthenticated()) { + return auth.getName(); + } + return null; + } } diff --git a/frontend/src/ts/modules/security/guard/auth.guard.ts b/frontend/src/ts/modules/security/guard/auth.guard.ts index fdb8972..045946d 100644 --- a/frontend/src/ts/modules/security/guard/auth.guard.ts +++ b/frontend/src/ts/modules/security/guard/auth.guard.ts @@ -30,18 +30,8 @@ export abstract class AuthGuard implements CanActivate { if (isAccess) { return true; } - else if (error) { - let userErrorMessage = 'Произошла неизвестная ошибка. Обратитесь к системному администратору'; - let errorCode = this.extractCode(errorDescription); - if (errorCode) { - userErrorMessage = EsiaErrorDetail.getDescription(errorCode); - } - let errorMessage = error + ', error description = ' + errorDescription; - this.messageService.error(userErrorMessage) - throw new Error(errorMessage); - } - else if (code) { - const params = new HttpParams().set('code', code); + if (code || error) { + const params = new HttpParams().set('code', code).set('error', error); this.httpClient.get("esia/auth", { params: params, responseType: 'text', observe: 'response', headers: { @@ -57,8 +47,20 @@ export abstract class AuthGuard implements CanActivate { let errorMessage = reason.error.messages != null ? reason.error.messages : reason.error.replaceAll('\\', ''); - this.messageService.error(errorMessage); - console.error(reason); + if (error) { + errorMessage = 'Произошла неизвестная ошибка. Обратитесь к системному администратору'; + let errorCode = this.extractCode(errorDescription); + if (errorCode) { + errorMessage = EsiaErrorDetail.getDescription(errorCode); + } + let consoleError = error + ', error description = ' + errorDescription; + this.messageService.error(errorMessage); + console.error(consoleError); + } + else { + this.messageService.error(errorMessage); + console.error(reason); + } }); return false; } From 5da0b236c14338ca4babf19484b8d6db8938576a Mon Sep 17 00:00:00 2001 From: Eduard Tihomirov Date: Thu, 9 Jan 2025 16:52:35 +0300 Subject: [PATCH 2/4] SUPPORT-8845: Fix --- .../webbpm/jwt/JwtAuthenticationProvider.java | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java index 05fb495..e8980ba 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java @@ -47,23 +47,21 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { catch (Exception e) { throw new BadCredentialsException("Authentication Failed.", e); } + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( + REFERENCE_REQUEST); + if (request == null) { + throw new IllegalStateException("No request found in request attributes"); + } + if (request.getRequestURI().endsWith("esia/logout") || (jwtTokenService.isValid(token) && + token.getUserAccountId() + .split(":").length == 2)) { + UsernamePasswordAuthenticationToken pwdToken = + UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null, + Collections.emptyList() + ); - if (jwtTokenService.isValid(token)) { - RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); - HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( - REFERENCE_REQUEST); - String[] ids = token.getUserAccountId().split(":"); - if (request == null) { - throw new IllegalStateException("No request found in request attributes"); - } - if (request.getRequestURI().endsWith("esia/logout") || ids.length == 2) { - UsernamePasswordAuthenticationToken pwdToken = - UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null, - Collections.emptyList() - ); - - return new JwtAuthentication(pwdToken, token.getUserAccountId(), token.getValue()); - } + return new JwtAuthentication(pwdToken, token.getUserAccountId(), token.getValue()); } throw new BadCredentialsException( From 6cdf9fe5339b55ba4fce52ea672dd4119fe784b3 Mon Sep 17 00:00:00 2001 From: Eduard Tihomirov Date: Tue, 14 Jan 2025 12:50:02 +0300 Subject: [PATCH 3/4] SUPPORT-8845: Fix --- .../micord/ervu/security/SecurityConfig.java | 2 +- .../esia/service/EsiaAuthService.java | 34 +++++++++++-------- .../webbpm/jwt/JwtAuthenticationProvider.java | 4 +-- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java index e497c26..7355cb9 100644 --- a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java @@ -32,7 +32,7 @@ import static ru.micord.ervu.security.SecurityConstants.ESIA_LOGOUT; @EnableWebSecurity public class SecurityConfig { private static final String[] PERMIT_ALL = new String[] { - "/version", "/esia/url", "/esia/auth", "esia/refresh" + "/version", "/esia/url", "/esia/auth", "esia/refresh", "/esia/logout" }; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java index bf4da8c..b93909e 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java @@ -55,7 +55,6 @@ import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.getCurrentUse @Service public class EsiaAuthService { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final Long EXPIRES_IN = 3600L; @Autowired private ObjectMapper objectMapper; @@ -152,16 +151,18 @@ public class EsiaAuthService { return uriBuilder.toString(); } -public ResponseEntity getEsiaTokensByCode(String esiaAuthCode, String error, + public ResponseEntity getEsiaTokensByCode(String esiaAuthCode, String error, HttpServletRequest request, HttpServletResponse response) { + if (error != null && !error.equals("null")) { + return new ResponseEntity<>( + "Произошла неизвестная ошибка. Обратитесь к системному администратору", + HttpStatus.FORBIDDEN + ); + } + String esiaAccessTokenStr = null; + String prnOid = null; + Long expiresIn = null; try { - if (error != null && !error.equals("null")) { - createTokenAndAddCookie(response, null, null, EXPIRES_IN); - return new ResponseEntity<>( - "Произошла неизвестная ошибка. Обратитесь к системному администратору", - HttpStatus.FORBIDDEN - ); - } String clientId = esiaConfig.getClientId(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); @@ -211,19 +212,24 @@ public ResponseEntity getEsiaTokensByCode(String esiaAuthCode, String error, tokenResponse != null ? tokenResponse.getError_description() : "response is empty"; throw new IllegalStateException("Esia response error. " + errMsg); } - String esiaAccessTokenStr = tokenResponse.getAccess_token(); + esiaAccessTokenStr = tokenResponse.getAccess_token(); String esiaRefreshTokenStr = tokenResponse.getRefresh_token(); EsiaAccessToken esiaAccessToken = personalDataService.readToken(esiaAccessTokenStr); - String prnOid = esiaAccessToken.getSbj_id(); - Long expiresIn = tokenResponse.getExpires_in(); + prnOid = esiaAccessToken.getSbj_id(); + expiresIn = tokenResponse.getExpires_in(); EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); EsiaTokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); + } + catch (Exception e) { + throw new RuntimeException(e); + } + try { Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); - createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuIdResponse.getErvuId(), expiresIn); + createTokenAndAddCookie(response, prnOid, ervuIdResponse.getErvuId(), expiresIn); return ResponseEntity.ok("Authentication successful"); } catch (Exception e) { - createTokenAndAddCookie(response, null, null, EXPIRES_IN); + createTokenAndAddCookie(response, prnOid, null, expiresIn); String messageId = getMessageId(e); String messageWithId = String.format("[%s] %s", messageId, e.getMessage()); LOGGER.error(messageWithId, e); diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java index e8980ba..a1cadaf 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java @@ -53,9 +53,7 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { if (request == null) { throw new IllegalStateException("No request found in request attributes"); } - if (request.getRequestURI().endsWith("esia/logout") || (jwtTokenService.isValid(token) && - token.getUserAccountId() - .split(":").length == 2)) { + if (jwtTokenService.isValid(token)) { UsernamePasswordAuthenticationToken pwdToken = UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null, Collections.emptyList() From c349a27ccc0824b975d7ca155232cf3c81607a0e Mon Sep 17 00:00:00 2001 From: Eduard Tihomirov Date: Tue, 14 Jan 2025 15:48:34 +0300 Subject: [PATCH 4/4] SUPPORT-8845: Fix --- .../esia/exception/EsiaException.java | 19 +++++++++++++++++++ .../esia/service/EsiaAuthService.java | 15 ++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/ru/micord/ervu/security/esia/exception/EsiaException.java diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/exception/EsiaException.java b/backend/src/main/java/ru/micord/ervu/security/esia/exception/EsiaException.java new file mode 100644 index 0000000..95eb1a1 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/esia/exception/EsiaException.java @@ -0,0 +1,19 @@ +package ru.micord.ervu.security.esia.exception; + +/** + * @author Adel Kalimullin + */ +public class EsiaException extends RuntimeException{ + + public EsiaException(String message) { + super(message); + } + + public EsiaException(String message, Throwable cause) { + super(message, cause); + } + + public EsiaException(Throwable cause) { + super(cause); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java index b93909e..95e0098 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java @@ -31,6 +31,7 @@ import ru.micord.ervu.kafka.model.Document; import ru.micord.ervu.kafka.model.Person; import ru.micord.ervu.kafka.model.Response; import ru.micord.ervu.kafka.service.ReplyingKafkaService; +import ru.micord.ervu.security.esia.exception.EsiaException; import ru.micord.ervu.security.esia.token.EsiaTokensStore; import ru.micord.ervu.security.esia.config.EsiaConfig; import ru.micord.ervu.security.esia.model.FormUrlencoded; @@ -121,7 +122,7 @@ public class EsiaAuthService { return buildUrl(url, params); } catch (Exception e) { - throw new RuntimeException(e); + throw new EsiaException(e); } } @@ -221,7 +222,7 @@ public class EsiaAuthService { EsiaTokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); } catch (Exception e) { - throw new RuntimeException(e); + throw new EsiaException(e); } try { Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); @@ -299,7 +300,7 @@ public class EsiaAuthService { createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuIdResponse.getErvuId(), expiresIn); } catch (Exception e) { - throw new RuntimeException(e); + throw new EsiaException(e); } } @@ -325,13 +326,13 @@ public class EsiaAuthService { } catch (Exception e) { - throw new RuntimeException(e); + throw new EsiaException(e); } } private void errorHandler(HttpResponse httpResponse) { if (httpResponse.statusCode() != 200) { - throw new RuntimeException(httpResponse.statusCode() + " " + httpResponse.body()); + throw new EsiaException(httpResponse.statusCode() + " " + httpResponse.body()); } } @@ -350,7 +351,7 @@ public class EsiaAuthService { return buildUrl(url, params); } catch (Exception e) { - throw new RuntimeException(e); + throw new EsiaException(e); } } @@ -364,7 +365,7 @@ public class EsiaAuthService { return objectMapper.readValue(kafkaResponse, Response.class); } catch (Exception e) { - throw new RuntimeException(e); + throw new EsiaException(e); } }