diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java b/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java index b240cbe..42dd494 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/config/EsiaConfig.java @@ -53,6 +53,9 @@ public class EsiaConfig { @Value("${esia.issuer.url}") private String esiaIssuerUrl; + @Value("${esia.marker.ver}") + private String esiaMarkerVer; + public String getEsiaScopes() { String[] scopeItems = esiaScopes.split(","); return String.join(" ", Arrays.stream(scopeItems).map(String::trim).toArray(String[]::new)); @@ -107,4 +110,8 @@ public class EsiaConfig { public String getEsiaIssuerUrl() { return esiaIssuerUrl; } + + public String getEsiaMarkerVer() { + return esiaMarkerVer; + } } 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 624a505..b6172d0 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 @@ -34,14 +34,15 @@ public class EsiaController { private JwtTokenService jwtTokenService; @RequestMapping(value = "/esia/url") - public String getEsiaUrl() { - return esiaAuthService.generateAuthCodeUrl(); + public String getEsiaUrl(HttpServletResponse response) { + return esiaAuthService.generateAuthCodeUrl(response); } @GetMapping(value = "/esia/auth") - public void esiaAuth(@RequestParam(value = "code", required = false) String code, - HttpServletResponse response) { - esiaAuthService.authEsiaTokensByCode(code, response); + public void esiaAuth(@RequestParam(value = "code") String code, + @RequestParam(value = "state") String state, + HttpServletResponse response, HttpServletRequest request) { + esiaAuthService.authEsiaTokensByCode(code, state, response, request); } @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 007d63a..09f8494 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 @@ -8,13 +8,13 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.time.ZonedDateTime; +import java.time.*; 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.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -25,7 +25,9 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.util.WebUtils; import ru.micord.ervu.kafka.model.Document; import ru.micord.ervu.kafka.model.Person; import ru.micord.ervu.kafka.model.Response; @@ -59,6 +61,7 @@ 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; @@ -84,13 +87,14 @@ public class EsiaAuthService { @Value("${ervu.kafka.request.topic}") private String requestTopic; - public String generateAuthCodeUrl() { + public String generateAuthCodeUrl(HttpServletResponse 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 state = UUID.randomUUID().toString(); + String prnsUUID = UUID.randomUUID().toString(); String redirectUrl = esiaConfig.getRedirectUrl(); String redirectUrlEncoded = redirectUrl.replaceAll(":", "%3A") .replaceAll("/", "%2F"); @@ -104,6 +108,9 @@ public class EsiaAuthService { parameters.put("redirect_uri", esiaConfig.getRedirectUrl()); String clientSecret = signMap(parameters); + EsiaTokensStore.addState(prnsUUID, state); + ResponseCookie prnsCookie = securityHelper.createCookie(PRNS_UUID, prnsUUID, "/").build(); + securityHelper.addResponseCookie(response, prnsCookie); String responseType = "code"; @@ -152,17 +159,21 @@ public class EsiaAuthService { return uriBuilder.toString(); } - public void authEsiaTokensByCode(String esiaAuthCode, HttpServletResponse response) { + public void authEsiaTokensByCode(String esiaAuthCode, String state, HttpServletResponse response, HttpServletRequest request) { String esiaAccessTokenStr = null; String prnOid = null; Long expiresIn = null; long signSecret = 0, requestAccessToken = 0, verifySecret = 0; + String verifyStateResult = verifyStateFromCookie(request, state); + if (verifyStateResult != null) { + throw new EsiaException(verifyStateResult); + } 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 state = UUID.randomUUID().toString(); + String newState = UUID.randomUUID().toString(); String redirectUrl = esiaConfig.getRedirectUrl(); String scope = esiaConfig.getEsiaScopes(); @@ -170,7 +181,7 @@ public class EsiaAuthService { parameters.put("client_id", clientId); parameters.put("scope", scope); parameters.put("timestamp", timestamp); - parameters.put("state", state); + parameters.put("state", newState); parameters.put("redirect_uri", redirectUrl); parameters.put("code", esiaAuthCode); @@ -183,7 +194,7 @@ public class EsiaAuthService { .setParameter("code", esiaAuthCode) .setParameter("grant_type", "authorization_code") .setParameter("client_secret", clientSecret) - .setParameter("state", state) + .setParameter("state", newState) .setParameter("redirect_uri", redirectUrl) .setParameter("scope", scope) .setParameter("timestamp", timestamp) @@ -211,6 +222,9 @@ public class EsiaAuthService { tokenResponse != null ? tokenResponse.getError_description() : "response is empty"; throw new IllegalStateException("Esia response error. " + errMsg); } + if (!tokenResponse.getState().equals(newState)) { + throw new EsiaException("Token invalid. State from request not equals with state from response."); + } esiaAccessTokenStr = tokenResponse.getAccess_token(); startTime = System.currentTimeMillis(); String verifyResult = verifyToken(esiaAccessTokenStr); @@ -416,6 +430,9 @@ public class EsiaAuthService { if (!esiaHeader.getSbt().equals("access")) { return "Token invalid. Token sbt: " + esiaHeader.getSbt() + " invalid"; } + if (!esiaHeader.getVer().equals(esiaConfig.getEsiaMarkerVer())) { + return "Token invalid. Token ver: " + esiaHeader.getVer() + " invalid"; + } if (!esiaHeader.getTyp().equals("JWT")) { return "Token invalid. Token type: " + esiaHeader.getTyp() + " invalid"; } @@ -425,17 +442,16 @@ public class EsiaAuthService { if (!esiaAccessToken.getIss().equals(esiaConfig.getEsiaIssuerUrl())) { return "Token invalid. Token issuer:" + esiaAccessToken.getIss() + " invalid"; } - //TODO SUPPORT-8750 -// LocalDateTime iatTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getIat()), -// ZoneId.systemDefault() -// ); -// LocalDateTime expTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getExp()), -// ZoneId.systemDefault() -// ); -// LocalDateTime currentTime = LocalDateTime.now(); -// if (!currentTime.isAfter(iatTime) || !expTime.isAfter(iatTime)) { -// return "Token invalid. Token expired"; -// } + LocalDateTime iatTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getIat()), + ZoneId.systemDefault() + ); + LocalDateTime expTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getExp()), + ZoneId.systemDefault() + ); + LocalDateTime currentTime = LocalDateTime.now(); + if (!currentTime.isAfter(iatTime) || !expTime.isAfter(iatTime)) { + return "Token invalid. Token expired"; + } HttpResponse response = signVerify(accessToken); if (response.statusCode() != 200) { if (response.statusCode() == 401) { @@ -463,4 +479,18 @@ public class EsiaAuthService { throw new EsiaException(e); } } + + private String verifyStateFromCookie(HttpServletRequest request, String state) { + Cookie cookie = WebUtils.getCookie(request, PRNS_UUID); + if (cookie == null) { + return "State invalid. Cookie not found"; + } + String prnsUUID = cookie.getValue(); + String oldState = EsiaTokensStore.getState(prnsUUID); + if (oldState == null || !oldState.equals(state)) { + return "State invalid. State from ESIA not equals with state before"; + } + EsiaTokensStore.removeState(prnsUUID); + return null; + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java index 7f56cc4..9dc37f6 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/token/EsiaTokensStore.java @@ -14,6 +14,7 @@ public class EsiaTokensStore { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final Map accessTokensMap = new ConcurrentHashMap<>(); private static final Map refreshTokensMap = new ConcurrentHashMap<>(); + private static final Map prnsUUIDStateMap = new ConcurrentHashMap<>(); public static void addAccessToken(String prnOid, String token, long expiresIn) { if (token != null) { @@ -75,4 +76,16 @@ public class EsiaTokensStore { public static void removeRefreshToken(String prnOid) { refreshTokensMap.remove(prnOid); } + + public static void addState(String prnsUUID, String state) { + prnsUUIDStateMap.put(prnsUUID, state); + } + + public static String getState(String prnsUUID) { + return prnsUUIDStateMap.get(prnsUUID); + } + + public static String removeState(String prnsUUID) { + return prnsUUIDStateMap.remove(prnsUUID); + } } 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 9c966f6..f04b0ad 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 @@ -48,7 +48,7 @@ public final class SecurityHelper { addResponseCookie(response, emptyAuthMarker); } - private void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { + public void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } diff --git a/frontend/src/ts/modules/security/guard/auth.guard.ts b/frontend/src/ts/modules/security/guard/auth.guard.ts index e6a94c5..a5954a2 100644 --- a/frontend/src/ts/modules/security/guard/auth.guard.ts +++ b/frontend/src/ts/modules/security/guard/auth.guard.ts @@ -25,6 +25,7 @@ export abstract class AuthGuard implements CanActivate { let url = new URL(window.location.href); let params = new URLSearchParams(url.search); let code = params.get('code'); + let state = params.get('state'); let error = params.get('error'); let errorDescription = params.get('error_description'); if (isAccess) { @@ -41,8 +42,8 @@ export abstract class AuthGuard implements CanActivate { this.messageService.error(errorMessage); console.error(consoleError); } - if (code) { - const params = new HttpParams().set('code', code); + if (code && state) { + const params = new HttpParams().set('code', code).set('state', state); this.httpClient.get("esia/auth", { params: params,