diff --git a/backend/src/main/java/ru/micord/ervu/security/SecurityConstants.java b/backend/src/main/java/ru/micord/ervu/security/SecurityConstants.java index 1853716d..12cba8cf 100644 --- a/backend/src/main/java/ru/micord/ervu/security/SecurityConstants.java +++ b/backend/src/main/java/ru/micord/ervu/security/SecurityConstants.java @@ -2,4 +2,7 @@ package ru.micord.ervu.security; public class SecurityConstants { 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"; } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/EsiaAuthInfoStore.java b/backend/src/main/java/ru/micord/ervu/security/esia/EsiaAuthInfoStore.java index 0b2ddf56..f84963e0 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/EsiaAuthInfoStore.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/EsiaAuthInfoStore.java @@ -9,6 +9,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.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 states = PRNS_UUID_STATE_MAP.get(prnsUUID); if (states == null) { - return false; + throw new EsiaException("State invalid. No state found"); } long currentTime = System.currentTimeMillis(); - states.removeIf(expiringState -> expiringState.getExpiryTime() < currentTime); + + StringBuilder statesStringBuilder = new StringBuilder(); for (ExpiringState expiringState : states) { 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) { 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 96809532..509ecae8 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 @@ -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.MessageBundleUtils; +import static ru.micord.ervu.security.SecurityConstants.PRNS_UUID; + /** * @author Eduard Tihomirov */ @@ -73,7 +75,6 @@ public class EsiaAuthService { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor( "messages/common_errors_messages"); - private static final String PRNS_UUID = "prns_uuid"; @Autowired private ObjectMapper objectMapper; @Autowired @@ -102,7 +103,6 @@ public class EsiaAuthService { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); String timestamp = dt.format(formatter); - String state = UUID.randomUUID().toString(); String prnsUUID = UUID.randomUUID().toString(); Cookie oldPrnsCookie = WebUtils.getCookie(request, PRNS_UUID); if (oldPrnsCookie != null) { @@ -119,12 +119,14 @@ public class EsiaAuthService { parameters.put("scope", scope); parameters.put("scope_org", scopeOrg); parameters.put("timestamp", timestamp); - parameters.put("state", state); + parameters.put("state", "%s"); 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()); - ResponseCookie prnsCookie = securityHelper.createCookie(PRNS_UUID, prnsUUID, "/") + ResponseCookie prnsCookie = securityHelper.createAccessCookie(PRNS_UUID, prnsUUID) .maxAge(esiaConfig.getEsiaStateCookieLifeTime()) .build(); securityHelper.addResponseCookie(response, prnsCookie); @@ -184,16 +186,12 @@ public class EsiaAuthService { Long expiresIn = null; boolean hasRole = false; long timeSignSecret = 0, timeRequestAccessToken = 0, timeVerifySecret = 0; - String verifyStateResult = verifyStateFromCookie(request, state, response); - if (verifyStateResult != null) { - throw new EsiaException(verifyStateResult); - } + verifyStateFromCookie(request, state, response); try { String clientId = esiaConfig.getClientId(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); String timestamp = dt.format(formatter); - String newState = UUID.randomUUID().toString(); String redirectUrl = esiaConfig.getRedirectUrl(); String scope = esiaConfig.getEsiaScopes(); String scopeOrg = esiaConfig.getEsiaOrgScopes(); @@ -203,13 +201,15 @@ public class EsiaAuthService { parameters.put("scope", scope); parameters.put("scope_org", scopeOrg); parameters.put("timestamp", timestamp); - parameters.put("state", newState); + parameters.put("state", "%s"); parameters.put("redirect_uri", redirectUrl); parameters.put("code", esiaAuthCode); long startTime = System.currentTimeMillis(); - String clientSecret = signMap(parameters); + SignResponse signResponse = signMap(parameters); timeSignSecret = System.currentTimeMillis() - startTime; + String newState = signResponse.getState(); + String clientSecret = signResponse.getSignature(); String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl(); String postBody = new FormUrlencoded() .setParameter("client_id", clientId) @@ -303,7 +303,6 @@ public class EsiaAuthService { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); String timestamp = dt.format(formatter); - String state = UUID.randomUUID().toString(); String redirectUrl = esiaConfig.getRedirectUrl(); String scope = esiaConfig.getEsiaScopes(); String scopeOrg = esiaConfig.getEsiaOrgScopes(); @@ -313,11 +312,13 @@ public class EsiaAuthService { parameters.put("scope", scope); parameters.put("scope_org", scopeOrg); parameters.put("timestamp", timestamp); - parameters.put("state", state); + parameters.put("state", "%s"); parameters.put("redirect_uri", esiaConfig.getRedirectUrl()); 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 postBody = new FormUrlencoded() .setParameter("client_id", clientId) @@ -372,7 +373,7 @@ public class EsiaAuthService { } } - private String signMap(Map paramsToSign) { + private SignResponse signMap(Map paramsToSign) { try { StringBuilder toSign = new StringBuilder(); for (String s : paramsToSign.values()) { @@ -391,7 +392,7 @@ public class EsiaAuthService { .build() .send(request, HttpResponse.BodyHandlers.ofString()); errorHandler(response); - return response.body(); + return objectMapper.readValue(response.body(), SignResponse.class); } 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); if (cookie == null) { - return "State invalid. Cookie not found"; + throw new EsiaException("State invalid. Cookie not found"); } String prnsUUID = cookie.getValue(); - if (!EsiaAuthInfoStore.containsState(prnsUUID, state)) { - return "State invalid. State from ESIA not equals with state before"; + try { + EsiaAuthInfoStore.validateState(prnsUUID, state); + } + finally { + EsiaAuthInfoStore.removeState(prnsUUID); + securityHelper.clearAccessCookie(response, PRNS_UUID); } - EsiaAuthInfoStore.removeState(prnsUUID); - securityHelper.clearCookie(response, PRNS_UUID, "/"); - return null; } } diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java index 6ce0411e..bc697581 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java @@ -14,8 +14,9 @@ import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; 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.webbpm.jwt.util.SecurityUtil.AUTH_TOKEN; +import static ru.micord.ervu.security.SecurityConstants.AUTH_MARKER; +import static ru.micord.ervu.security.SecurityConstants.AUTH_TOKEN; +import static ru.micord.ervu.security.SecurityConstants.PRNS_UUID; public final class SecurityHelper { @Value("${cookie.path:#{null}}") @@ -36,9 +37,7 @@ public final class SecurityHelper { } public void clearAccessCookies(HttpServletResponse response) { - ResponseCookie emptyAuthToken = createCookie(AUTH_TOKEN, null, accessCookiePath) - .maxAge(0).build(); - addResponseCookie(response, emptyAuthToken); + clearCookie(response, AUTH_TOKEN, accessCookiePath); ResponseCookie emptyAuthMarker = createCookie(AUTH_MARKER, null, "/") .maxAge(0) @@ -46,6 +45,7 @@ public final class SecurityHelper { .httpOnly(false) .build(); addResponseCookie(response, emptyAuthMarker); + clearCookie(response, PRNS_UUID, accessCookiePath); } public void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { @@ -92,4 +92,12 @@ public final class SecurityHelper { .maxAge(0).build(); 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); + } } 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 c5b5222e..65b7e19b 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 @@ -11,10 +11,8 @@ import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; +import static ru.micord.ervu.security.SecurityConstants.AUTH_TOKEN; public final class SecurityUtil { - public static final String AUTH_TOKEN = "auth_token"; - - public static final String AUTH_MARKER = "webbpm.ervu-lkrp-ul"; private SecurityUtil() { //empty diff --git a/frontend/src/ts/modules/security/guard/auth.guard.ts b/frontend/src/ts/modules/security/guard/auth.guard.ts index dc5fa3e7..be72a006 100644 --- a/frontend/src/ts/modules/security/guard/auth.guard.ts +++ b/frontend/src/ts/modules/security/guard/auth.guard.ts @@ -7,7 +7,7 @@ import {AuthenticationService} from "../authentication.service"; import {EsiaErrorDetail} from "../EsiaErrorDetail"; import {AccessChecker} from "../AccessChecker"; -@Injectable({providedIn:'root'}) +@Injectable({providedIn: 'root'}) export abstract class AuthGuard implements CanActivate { private cspTimeout: number; @@ -68,7 +68,7 @@ export abstract class AuthGuard implements CanActivate { responseType: 'text', observe: 'response', headers: { - "Error-intercept-skip":"true" + "Error-intercept-skip": "true" } }) .toPromise() @@ -95,8 +95,7 @@ export abstract class AuthGuard implements CanActivate { console.error(reason); } return false - }) - .finally(() => this.progressIndicationService.hideProgressBar()); + }); } private checkAccess(): boolean {