Merge branch 'feature/SUPPORT-8845_fix_logout' into hotfix/1.9.5

# Conflicts:
#	backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java
#	backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java
This commit is contained in:
Eduard Tihomirov 2025-01-14 15:57:10 +03:00
commit f44dad90a5
7 changed files with 124 additions and 67 deletions

View file

@ -31,7 +31,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;

View file

@ -31,9 +31,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);
}
@PostMapping(value = "/esia/refresh")

View file

@ -0,0 +1,19 @@
package ru.micord.ervu.security.esia.exception;
/**
* @author Adel Kalimullin
*/
public class EsiaException extends RuntimeException{
public EsiaException(String message, Throwable cause) {
super(message, cause);
}
public EsiaException(Throwable cause) {
super(cause);
}
public EsiaException(String message) {
super(message);
}
}

View file

@ -14,12 +14,14 @@ import java.time.format.DateTimeFormatter;
import java.util.Arrays;
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 ervu.service.okopf.OkopfService;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.token.EsiaTokensStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -49,6 +51,8 @@ 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
*/
@ -118,7 +122,7 @@ public class EsiaAuthService {
return buildUrl(url, params);
}
catch (Exception e) {
throw new RuntimeException(e);
throw new EsiaException(e);
}
}
@ -148,7 +152,18 @@ 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) {
if (error != null && !error.equals("null")) {
return new ResponseEntity<>(
"Произошла неизвестная ошибка. Обратитесь к системному администратору",
HttpStatus.FORBIDDEN
);
}
String esiaAccessTokenStr = null;
String prnOid = null;
Long expiresIn = null;
boolean hasRole = false;
try {
String clientId = esiaConfig.getClientId();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
@ -202,26 +217,21 @@ public class EsiaAuthService {
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();
boolean hasRole = ulDataService.checkRole(esiaAccessTokenStr);
EsiaAccessToken esiaAccessToken = ulDataService.readToken(esiaAccessTokenStr);
String prnOid = esiaAccessToken.getSbj_id();
String ervuId = getErvuId(esiaAccessTokenStr, prnOid);
Long expiresIn = tokenResponse.getExpires_in();
prnOid = esiaAccessToken.getSbj_id();
expiresIn = tokenResponse.getExpires_in();
EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
EsiaTokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn);
Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuId, hasRole);
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());
context.setAuthentication(jwtAuthentication);
SecurityContextHolder.setContext(context);
}
catch (Exception e) {
throw new EsiaException(e);
}
try {
hasRole = ulDataService.checkRole(esiaAccessTokenStr);
String ervuId = getErvuId(esiaAccessTokenStr, prnOid);
createTokenAndAddCookie(response, prnOid, ervuId, hasRole, expiresIn);
if (!hasRole) {
LOGGER.error("The user with id = " + prnOid + " does not have the required role");
return new ResponseEntity<>(
@ -232,7 +242,14 @@ public class EsiaAuthService {
return ResponseEntity.ok("Authentication successful");
}
catch (Exception e) {
throw new RuntimeException(e);
createTokenAndAddCookie(response, prnOid, null, hasRole , expiresIn);
String messageId = getMessageId(e);
String messageWithId = String.format("[%s] %s", messageId, e.getMessage());
LOGGER.error(messageWithId, e);
return new ResponseEntity<>(
"Произошла ошибка " + messageId + ". Обратитесь к системному администратору",
HttpStatus.FORBIDDEN
);
}
}
@ -297,19 +314,10 @@ public class EsiaAuthService {
EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
EsiaTokensStore.addRefreshToken(prnOid, esiaNewRefreshToken, expiresIn);
String ervuId = getErvuId(esiaAccessTokenStr, prnOid);
Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuId, true);
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());
context.setAuthentication(jwtAuthentication);
SecurityContextHolder.setContext(context);
createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuId, true, expiresIn);
}
catch (Exception e) {
throw new RuntimeException(e);
throw new EsiaException(e);
}
}
@ -335,13 +343,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());
}
}
@ -360,7 +368,7 @@ public class EsiaAuthService {
return buildUrl(url, params);
}
catch (Exception e) {
throw new RuntimeException(e);
throw new EsiaException(e);
}
}
@ -378,12 +386,12 @@ public class EsiaAuthService {
String ervuId = ervuOrgResponse.getData().getOrgId_ERVU();
if (!StringUtils.hasText(ervuId)) {
throw new RuntimeException("No ervuId for prnOid = " + prnOid);
throw new EsiaException("No ervuId for prnOid = " + prnOid);
}
return ervuId;
}
catch (Exception e) {
throw new RuntimeException(e);
throw new EsiaException(e);
}
}
@ -442,4 +450,24 @@ public class EsiaAuthService {
employee.setOrgOid(employeeModel.getOrgOid());
return employee;
}
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,
Boolean hasRole, Long expiresIn) {
Token token = jwtTokenService.createAccessToken(userId, expiresIn, ervuId, hasRole);
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);
}
}

View file

@ -48,22 +48,20 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
throw new BadCredentialsException("Authentication Failed.", e);
}
if (jwtTokenService.isValid(token)) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(
REFERENCE_REQUEST);
if (request == null) {
throw new IllegalStateException("No request found in request attributes");
}
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") || token.getHasRole()) {
UsernamePasswordAuthenticationToken pwdToken =
UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null,
Collections.emptyList()
);
if (jwtTokenService.isValid(token) && token.getHasRole()) {
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(

View file

@ -30,4 +30,12 @@ public final class SecurityUtil {
.map(a -> ((JwtAuthentication) a).getUserIdsPair())
.orElse(null);
}
public static String getCurrentUsername() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
return auth.getName();
}
return null;
}
}

View file

@ -34,18 +34,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: {
@ -61,8 +51,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;
}