Merge branch 'develop' of 10.10.31.70:/ervu-lkrp-ul into develop

This commit is contained in:
m.epshtein 2025-05-14 16:25:39 +03:00
commit f049f1cb62
6 changed files with 57 additions and 41 deletions

View file

@ -2,4 +2,7 @@ package ru.micord.ervu.security;
public class SecurityConstants { public class SecurityConstants {
public static final String ESIA_LOGOUT = "/esia/logout"; public static final String ESIA_LOGOUT = "/esia/logout";
public static final String AUTH_TOKEN = "auth_token";
public static final String AUTH_MARKER = "webbpm.ervu-lkrp-ul";
public static final String PRNS_UUID = "prns_uuid_ul";
} }

View file

@ -9,6 +9,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.MessageSourceAccessor;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.ExpiringState; import ru.micord.ervu.security.esia.model.ExpiringState;
import ru.micord.ervu.security.esia.model.ExpiringToken; import ru.micord.ervu.security.esia.model.ExpiringToken;
@ -105,19 +106,24 @@ public class EsiaAuthInfoStore {
}); });
} }
public static boolean containsState(String prnsUUID, String state) { public static void validateState(String prnsUUID, String state) {
List<ExpiringState> states = PRNS_UUID_STATE_MAP.get(prnsUUID); List<ExpiringState> states = PRNS_UUID_STATE_MAP.get(prnsUUID);
if (states == null) { if (states == null) {
return false; throw new EsiaException("State invalid. No state found");
} }
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
states.removeIf(expiringState -> expiringState.getExpiryTime() < currentTime);
StringBuilder statesStringBuilder = new StringBuilder();
for (ExpiringState expiringState : states) { for (ExpiringState expiringState : states) {
if (expiringState.getState().equals(state)) { if (expiringState.getState().equals(state)) {
return true; if (expiringState.getExpiryTime() < currentTime) {
throw new EsiaException("State invalid. State : " + state + " expired at : " + expiringState.getExpiryTime());
}
return;
} }
statesStringBuilder.append(expiringState.getState(), 0, 8).append(", ");
} }
return false; throw new EsiaException("State invalid. Backend states :" + statesStringBuilder + " cookie state :" + state);
} }
public static void removeState(String prnsUUID) { public static void removeState(String prnsUUID) {

View file

@ -65,6 +65,8 @@ import ru.micord.ervu.security.webbpm.jwt.model.Token;
import ru.cg.webbpm.modules.core.runtime.api.LocalizedException; import ru.cg.webbpm.modules.core.runtime.api.LocalizedException;
import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils; import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils;
import static ru.micord.ervu.security.SecurityConstants.PRNS_UUID;
/** /**
* @author Eduard Tihomirov * @author Eduard Tihomirov
*/ */
@ -73,7 +75,6 @@ public class EsiaAuthService {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor( private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor(
"messages/common_errors_messages"); "messages/common_errors_messages");
private static final String PRNS_UUID = "prns_uuid";
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@Autowired @Autowired
@ -102,7 +103,6 @@ public class EsiaAuthService {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
ZonedDateTime dt = ZonedDateTime.now(); ZonedDateTime dt = ZonedDateTime.now();
String timestamp = dt.format(formatter); String timestamp = dt.format(formatter);
String state = UUID.randomUUID().toString();
String prnsUUID = UUID.randomUUID().toString(); String prnsUUID = UUID.randomUUID().toString();
Cookie oldPrnsCookie = WebUtils.getCookie(request, PRNS_UUID); Cookie oldPrnsCookie = WebUtils.getCookie(request, PRNS_UUID);
if (oldPrnsCookie != null) { if (oldPrnsCookie != null) {
@ -119,12 +119,14 @@ public class EsiaAuthService {
parameters.put("scope", scope); parameters.put("scope", scope);
parameters.put("scope_org", scopeOrg); parameters.put("scope_org", scopeOrg);
parameters.put("timestamp", timestamp); parameters.put("timestamp", timestamp);
parameters.put("state", state); parameters.put("state", "%s");
parameters.put("redirect_uri", esiaConfig.getRedirectUrl()); parameters.put("redirect_uri", esiaConfig.getRedirectUrl());
String clientSecret = signMap(parameters); SignResponse signResponse = signMap(parameters);
String state = signResponse.getState();
String clientSecret = signResponse.getSignature();
EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime(), esiaConfig.getEsiaLoginAttemptsCount()); EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime(), esiaConfig.getEsiaLoginAttemptsCount());
ResponseCookie prnsCookie = securityHelper.createCookie(PRNS_UUID, prnsUUID, "/") ResponseCookie prnsCookie = securityHelper.createAccessCookie(PRNS_UUID, prnsUUID)
.maxAge(esiaConfig.getEsiaStateCookieLifeTime()) .maxAge(esiaConfig.getEsiaStateCookieLifeTime())
.build(); .build();
securityHelper.addResponseCookie(response, prnsCookie); securityHelper.addResponseCookie(response, prnsCookie);
@ -184,16 +186,12 @@ public class EsiaAuthService {
Long expiresIn = null; Long expiresIn = null;
boolean hasRole = false; boolean hasRole = false;
long timeSignSecret = 0, timeRequestAccessToken = 0, timeVerifySecret = 0; long timeSignSecret = 0, timeRequestAccessToken = 0, timeVerifySecret = 0;
String verifyStateResult = verifyStateFromCookie(request, state, response); verifyStateFromCookie(request, state, response);
if (verifyStateResult != null) {
throw new EsiaException(verifyStateResult);
}
try { try {
String clientId = esiaConfig.getClientId(); String clientId = esiaConfig.getClientId();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
ZonedDateTime dt = ZonedDateTime.now(); ZonedDateTime dt = ZonedDateTime.now();
String timestamp = dt.format(formatter); String timestamp = dt.format(formatter);
String newState = UUID.randomUUID().toString();
String redirectUrl = esiaConfig.getRedirectUrl(); String redirectUrl = esiaConfig.getRedirectUrl();
String scope = esiaConfig.getEsiaScopes(); String scope = esiaConfig.getEsiaScopes();
String scopeOrg = esiaConfig.getEsiaOrgScopes(); String scopeOrg = esiaConfig.getEsiaOrgScopes();
@ -203,13 +201,15 @@ public class EsiaAuthService {
parameters.put("scope", scope); parameters.put("scope", scope);
parameters.put("scope_org", scopeOrg); parameters.put("scope_org", scopeOrg);
parameters.put("timestamp", timestamp); parameters.put("timestamp", timestamp);
parameters.put("state", newState); parameters.put("state", "%s");
parameters.put("redirect_uri", redirectUrl); parameters.put("redirect_uri", redirectUrl);
parameters.put("code", esiaAuthCode); parameters.put("code", esiaAuthCode);
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
String clientSecret = signMap(parameters); SignResponse signResponse = signMap(parameters);
timeSignSecret = System.currentTimeMillis() - startTime; timeSignSecret = System.currentTimeMillis() - startTime;
String newState = signResponse.getState();
String clientSecret = signResponse.getSignature();
String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl(); String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl();
String postBody = new FormUrlencoded() String postBody = new FormUrlencoded()
.setParameter("client_id", clientId) .setParameter("client_id", clientId)
@ -303,7 +303,6 @@ public class EsiaAuthService {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
ZonedDateTime dt = ZonedDateTime.now(); ZonedDateTime dt = ZonedDateTime.now();
String timestamp = dt.format(formatter); String timestamp = dt.format(formatter);
String state = UUID.randomUUID().toString();
String redirectUrl = esiaConfig.getRedirectUrl(); String redirectUrl = esiaConfig.getRedirectUrl();
String scope = esiaConfig.getEsiaScopes(); String scope = esiaConfig.getEsiaScopes();
String scopeOrg = esiaConfig.getEsiaOrgScopes(); String scopeOrg = esiaConfig.getEsiaOrgScopes();
@ -313,11 +312,13 @@ public class EsiaAuthService {
parameters.put("scope", scope); parameters.put("scope", scope);
parameters.put("scope_org", scopeOrg); parameters.put("scope_org", scopeOrg);
parameters.put("timestamp", timestamp); parameters.put("timestamp", timestamp);
parameters.put("state", state); parameters.put("state", "%s");
parameters.put("redirect_uri", esiaConfig.getRedirectUrl()); parameters.put("redirect_uri", esiaConfig.getRedirectUrl());
parameters.put("refresh_token", refreshToken); parameters.put("refresh_token", refreshToken);
String clientSecret = signMap(parameters); SignResponse signResponse = signMap(parameters);
String state = signResponse.getState();
String clientSecret = signResponse.getSignature();
String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl(); String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl();
String postBody = new FormUrlencoded() String postBody = new FormUrlencoded()
.setParameter("client_id", clientId) .setParameter("client_id", clientId)
@ -372,7 +373,7 @@ public class EsiaAuthService {
} }
} }
private String signMap(Map<String, String> paramsToSign) { private SignResponse signMap(Map<String, String> paramsToSign) {
try { try {
StringBuilder toSign = new StringBuilder(); StringBuilder toSign = new StringBuilder();
for (String s : paramsToSign.values()) { for (String s : paramsToSign.values()) {
@ -391,7 +392,7 @@ public class EsiaAuthService {
.build() .build()
.send(request, HttpResponse.BodyHandlers.ofString()); .send(request, HttpResponse.BodyHandlers.ofString());
errorHandler(response); errorHandler(response);
return response.body(); return objectMapper.readValue(response.body(), SignResponse.class);
} }
catch (Exception e) { catch (Exception e) {
@ -601,17 +602,18 @@ public class EsiaAuthService {
} }
} }
private String verifyStateFromCookie(HttpServletRequest request, String state, HttpServletResponse response) { private void verifyStateFromCookie(HttpServletRequest request, String state, HttpServletResponse response) {
Cookie cookie = WebUtils.getCookie(request, PRNS_UUID); Cookie cookie = WebUtils.getCookie(request, PRNS_UUID);
if (cookie == null) { if (cookie == null) {
return "State invalid. Cookie not found"; throw new EsiaException("State invalid. Cookie not found");
} }
String prnsUUID = cookie.getValue(); String prnsUUID = cookie.getValue();
if (!EsiaAuthInfoStore.containsState(prnsUUID, state)) { try {
return "State invalid. State from ESIA not equals with state before"; EsiaAuthInfoStore.validateState(prnsUUID, state);
}
finally {
EsiaAuthInfoStore.removeState(prnsUUID);
securityHelper.clearAccessCookie(response, PRNS_UUID);
} }
EsiaAuthInfoStore.removeState(prnsUUID);
securityHelper.clearCookie(response, PRNS_UUID, "/");
return null;
} }
} }

View file

@ -14,8 +14,9 @@ import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST; import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST;
import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.AUTH_MARKER; import static ru.micord.ervu.security.SecurityConstants.AUTH_MARKER;
import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.AUTH_TOKEN; import static ru.micord.ervu.security.SecurityConstants.AUTH_TOKEN;
import static ru.micord.ervu.security.SecurityConstants.PRNS_UUID;
public final class SecurityHelper { public final class SecurityHelper {
@Value("${cookie.path:#{null}}") @Value("${cookie.path:#{null}}")
@ -36,9 +37,7 @@ public final class SecurityHelper {
} }
public void clearAccessCookies(HttpServletResponse response) { public void clearAccessCookies(HttpServletResponse response) {
ResponseCookie emptyAuthToken = createCookie(AUTH_TOKEN, null, accessCookiePath) clearCookie(response, AUTH_TOKEN, accessCookiePath);
.maxAge(0).build();
addResponseCookie(response, emptyAuthToken);
ResponseCookie emptyAuthMarker = createCookie(AUTH_MARKER, null, "/") ResponseCookie emptyAuthMarker = createCookie(AUTH_MARKER, null, "/")
.maxAge(0) .maxAge(0)
@ -46,6 +45,7 @@ public final class SecurityHelper {
.httpOnly(false) .httpOnly(false)
.build(); .build();
addResponseCookie(response, emptyAuthMarker); addResponseCookie(response, emptyAuthMarker);
clearCookie(response, PRNS_UUID, accessCookiePath);
} }
public void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { public void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) {
@ -92,4 +92,12 @@ public final class SecurityHelper {
.maxAge(0).build(); .maxAge(0).build();
addResponseCookie(response, emptyCookie); addResponseCookie(response, emptyCookie);
} }
public ResponseCookie.ResponseCookieBuilder createAccessCookie(String name, String value) {
return createCookie(name, value, accessCookiePath);
}
public void clearAccessCookie(HttpServletResponse response, String name) {
clearCookie(response, name, accessCookiePath);
}
} }

View file

@ -11,10 +11,8 @@ import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication;
import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; import ru.micord.ervu.security.webbpm.jwt.UserIdsPair;
import static ru.micord.ervu.security.SecurityConstants.AUTH_TOKEN;
public final class SecurityUtil { public final class SecurityUtil {
public static final String AUTH_TOKEN = "auth_token";
public static final String AUTH_MARKER = "webbpm.ervu-lkrp-ul";
private SecurityUtil() { private SecurityUtil() {
//empty //empty

View file

@ -7,7 +7,7 @@ import {AuthenticationService} from "../authentication.service";
import {EsiaErrorDetail} from "../EsiaErrorDetail"; import {EsiaErrorDetail} from "../EsiaErrorDetail";
import {AccessChecker} from "../AccessChecker"; import {AccessChecker} from "../AccessChecker";
@Injectable({providedIn:'root'}) @Injectable({providedIn: 'root'})
export abstract class AuthGuard implements CanActivate { export abstract class AuthGuard implements CanActivate {
private cspTimeout: number; private cspTimeout: number;
@ -68,7 +68,7 @@ export abstract class AuthGuard implements CanActivate {
responseType: 'text', responseType: 'text',
observe: 'response', observe: 'response',
headers: { headers: {
"Error-intercept-skip":"true" "Error-intercept-skip": "true"
} }
}) })
.toPromise() .toPromise()
@ -95,8 +95,7 @@ export abstract class AuthGuard implements CanActivate {
console.error(reason); console.error(reason);
} }
return false return false
}) });
.finally(() => this.progressIndicationService.hideProgressBar());
} }
private checkAccess(): boolean { private checkAccess(): boolean {