diff --git a/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java b/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java index 9993b234..3867b41a 100644 --- a/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java +++ b/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java @@ -25,9 +25,7 @@ public class LogoutSuccessHandler public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String url = esiaAuthService.logout(request, response); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write(url); - response.getWriter().flush(); + response.sendRedirect(url); CsrfToken csrfToken = this.csrfTokenRepository.generateToken(request); this.csrfTokenRepository.saveToken(csrfToken, request, response); } 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 b35991d5..2207f686 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 @@ -29,6 +29,9 @@ public class EsiaConfig { @Value("${esia.redirect.url}") private String redirectUrl; + @Value("${esia.logout.redirect.url}") + private String logoutRedirectUrl; + @Value("${sign.url}") private String signUrl; @@ -101,6 +104,10 @@ public class EsiaConfig { return esiaTokenUrl; } + public String getLogoutRedirectUrl() { + return logoutRedirectUrl; + } + public String getEsiaUploadDataRole() { return esiaUploadDataRole; } 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 55aff54f..de323ab4 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 @@ -15,7 +15,6 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -120,7 +119,7 @@ public class EsiaAuthService { "obj_type", "B L F A", "client_certificate_hash", esiaConfig.getClientCertHash()); - return makeRequest(url, params); + return buildUrl(url, params); } catch (Exception e) { throw new RuntimeException(e); @@ -143,12 +142,13 @@ public class EsiaAuthService { .replace("+", "%20"); } - private static String makeRequest(URL url, Map params) { + private static String buildUrl(URL url, Map params) { StringBuilder uriBuilder = new StringBuilder(url.toString()); uriBuilder.append('?'); for (Map.Entry node : params.entrySet()) { uriBuilder.append(node.getKey()).append('=').append(node.getValue()).append("&"); } + uriBuilder.deleteCharAt(uriBuilder.length() - 1); return uriBuilder.toString(); } @@ -218,8 +218,7 @@ public class EsiaAuthService { EsiaTokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn); Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuId, hasRole); int expiry = tokenResponse.getExpires_in().intValue(); - Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry); - response.addCookie(accessCookie); + securityHelper.addAccessCookies(response,token.getValue(), expiry); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); SecurityContext context = SecurityContextHolder.createEmptyContext(); @@ -228,8 +227,6 @@ public class EsiaAuthService { authenticationManager.authenticate(jwtAuthentication); context.setAuthentication(jwtAuthentication); SecurityContextHolder.setContext(context); - Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry); - response.addCookie(authMarkerCookie); if (!hasRole) { LOGGER.error("The user with id = " + prnOid + " does not have the required role"); return new ResponseEntity<>( @@ -307,8 +304,7 @@ public class EsiaAuthService { String ervuId = getErvuId(esiaAccessTokenStr, prnOid); Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuId, true); int expiry = tokenResponse.getExpires_in().intValue(); - Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry); - response.addCookie(accessCookie); + securityHelper.addAccessCookies(response, token.getValue(), expiry); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); SecurityContext context = SecurityContextHolder.createEmptyContext(); @@ -317,8 +313,6 @@ public class EsiaAuthService { authenticationManager.authenticate(jwtAuthentication); context.setAuthentication(jwtAuthentication); SecurityContextHolder.setContext(context); - Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry); - response.addCookie(authMarkerCookie); } catch (Exception e) { throw new RuntimeException(e); @@ -364,12 +358,12 @@ public class EsiaAuthService { EsiaTokensStore.removeAccessToken(userId); EsiaTokensStore.removeRefreshToken(userId); String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl(); - String redirectUrl = esiaConfig.getRedirectUrl(); + String redirectUrl = esiaConfig.getLogoutRedirectUrl(); URL url = new URL(logoutUrl); Map params = mapOf( "client_id", esiaConfig.getClientId(), "redirect_url", redirectUrl); - return makeRequest(url, params); + return buildUrl(url, params); } catch (Exception e) { throw new RuntimeException(e); 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 0a222c79..9c966f60 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 @@ -1,43 +1,89 @@ package ru.micord.ervu.security.webbpm.jwt.helper; -import javax.servlet.http.Cookie; +import java.net.IDN; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; -import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +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.webbpm.jwt.util.SecurityUtil.createCookie; public final class SecurityHelper { @Value("${cookie.path:#{null}}") private String accessCookiePath; + @Value("${cookie.domain:#{null}}") + private String accessCookieDomain; + @Value("${cookie.secure:false}") + private boolean accessCookieSecure; + @Value("${cookie.same.site:Lax}") + private String accessCookieSameSite; + + @PostConstruct + private void init() { + + if (accessCookieDomain != null) { + accessCookieDomain = IDN.toASCII(accessCookieDomain); + } + } public void clearAccessCookies(HttpServletResponse response) { - Cookie tokenCookie = createCookie(AUTH_TOKEN, null, null); - tokenCookie.setMaxAge(0); - tokenCookie.setPath(accessCookiePath); - tokenCookie.setHttpOnly(true); - response.addCookie(tokenCookie); + ResponseCookie emptyAuthToken = createCookie(AUTH_TOKEN, null, accessCookiePath) + .maxAge(0).build(); + addResponseCookie(response, emptyAuthToken); - Cookie markerCookie = createCookie(AUTH_MARKER, null, null); - markerCookie.setMaxAge(0); - markerCookie.setPath("/"); - response.addCookie(markerCookie); + ResponseCookie emptyAuthMarker = createCookie(AUTH_MARKER, null, "/") + .maxAge(0) + .secure(false) + .httpOnly(false) + .build(); + addResponseCookie(response, emptyAuthMarker); } - public Cookie createAccessCookie(String cookieValue, int expiry) { - Cookie authToken = createCookie(SecurityUtil.AUTH_TOKEN, cookieValue, accessCookiePath); - authToken.setPath(accessCookiePath); - authToken.setMaxAge(expiry); - return authToken; + private void addResponseCookie(HttpServletResponse response, ResponseCookie cookie) { + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } - public Cookie createAuthMarkerCookie(String cookieValue, int expiry) { - Cookie marker = createCookie(AUTH_MARKER, cookieValue, "/"); - marker.setMaxAge(expiry); - marker.setHttpOnly(false); - return marker; + public void addAccessCookies(HttpServletResponse response, String cookieValue, int expiry) { + ResponseCookie authTokenCookie = createCookie(AUTH_TOKEN, cookieValue, accessCookiePath) + .maxAge(expiry) + .build(); + addResponseCookie(response, authTokenCookie); + + ResponseCookie authMarker = createCookie(AUTH_MARKER, "true", "/") + .maxAge(expiry) + .secure(false) + .httpOnly(false) + .build(); + addResponseCookie(response, authMarker); + } + + public ResponseCookie.ResponseCookieBuilder createCookie(String name, String value, String path) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + + if (requestAttributes == null) { + throw new IllegalStateException("Must be called only in request context"); + } + HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( + REFERENCE_REQUEST); + + if (request == null) { + throw new IllegalStateException("Must be called only in request context"); + } + String cookieValue = value != null ? URLEncoder.encode(value, StandardCharsets.UTF_8) : ""; + return ResponseCookie.from(name, cookieValue) + .path(path != null ? path : request.getContextPath()) + .httpOnly(true) + .domain(accessCookieDomain) + .secure(accessCookieSecure) + .sameSite(accessCookieSameSite); } } 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 b2314e37..a190a852 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 @@ -1,16 +1,10 @@ package ru.micord.ervu.security.webbpm.jwt.util; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.util.WebUtils; -import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST; - public final class SecurityUtil { public static final String AUTH_TOKEN = "auth_token"; @@ -20,24 +14,6 @@ public final class SecurityUtil { //empty } - public static Cookie createCookie(String name, String value, String path) { - String cookieValue = value != null ? URLEncoder.encode(value, StandardCharsets.UTF_8) : null; - Cookie cookie = new Cookie(name, cookieValue); - RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); - HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( - REFERENCE_REQUEST); - - if (path != null) { - cookie.setPath(path); - } - else { - cookie.setPath(request.getContextPath()); - } - cookie.setHttpOnly(true); - - return cookie; - } - public static String extractAuthToken(HttpServletRequest httpRequest) { Cookie cookie = WebUtils.getCookie(httpRequest, AUTH_TOKEN); return cookie != null ? cookie.getValue() : null; diff --git a/config.md b/config.md index 850d2b34..ac6fc555 100644 --- a/config.md +++ b/config.md @@ -768,9 +768,13 @@ JBPM использует 3 корневых категории логирова - `ESIA_CLIENT_ID` - – идентификатор системы-клиента (мнемоника системы в ЕСИА указанная прописными буквами) - `ESIA_REDIRECT_URL` - ссылка, по которой должен быть направлен пользователь после того, как ЕСИА даст разрешение на доступ к ресурсу. -Важно: ESIA_REDIRECT_URL должна содержать полный адрес вплоть до последнего слэша +Важно: `ESIA_REDIRECT_URL` должна содержать полный адрес вплоть до последнего слэша - https://lkul.ervu.loc/ - правильное значение параметра - https://lkul.ervu.loc - неправильное значение параметра +- `ESIA_LOGOUT_REDIRECT_URL` - ссылка, по которой должен быть направлен пользователь после logout-a + Важно: `ESIA_LOGOUT_REDIRECT_URL` должна содержать полный адрес вплоть до последнего слэша: +> - https://lkul.ervu.loc/home.html - правильное значение параметра +> - https://lkul.ervu.loc - неправильное значение параметра - `ESIA_UPLOAD_DATA_ROLE` - мнемоника группы, для роли "Сотрудник, ответственный за военно-учетную работу". - `SIGN_URL` - url для подписания с помощью КриптоПро секрета клиента, необходимого для аутентификации через ЕСИА. @@ -813,4 +817,4 @@ JBPM использует 3 корневых категории логирова - `ERVU_KAFKA_JOURNAL_REPLY_TOPIC` - топик для чтения данных по журналу взаимодействия - `ERVU_KAFKA_EXCERPT_REQUEST_TOPIC` - топик для записи запроса для получения выписки по журналу взаимодействия - `ERVU_KAFKA_EXCERPT_REPLY_TOPIC` - топик для чтения выписки по журналу взаимодействия. Содержит ссылку на S3 с файлом выписки -- `DB.JOURNAL.EXCLUDED.STATUSES` - статусы файла, которые необходимо исключить при получении данных по журналу взаимодействия из базы данных приложения \ No newline at end of file +- `DB.JOURNAL.EXCLUDED.STATUSES` - статусы файла, которые необходимо исключить при получении данных по журналу взаимодействия из базы данных приложения diff --git a/config/micord.env b/config/micord.env index 2a3c14e9..d1d0200d 100644 --- a/config/micord.env +++ b/config/micord.env @@ -24,6 +24,7 @@ ESIA_ORG_SCOPE_URL=http://esia.gosuslugi.ru/ ESIA_BASE_URI=https://esia-portal1.test.gosuslugi.ru/ ESIA_CLIENT_ID=MNSV89 ESIA_REDIRECT_URL=https://lkrp-dev.micord.ru/ul/ +ESIA_LOGOUT_REDIRECT_URL=https://lkrp-dev.micord.ru/ul/home.html ESIA_UPLOAD_DATA_ROLE=MNSV89_UPLOAD_DATA SIGN_URL=https://ervu-sign-dev.k8s.micord.ru/sign ESIA_CLIENT_CERT_HASH=04508B4B0B58776A954A0E15F574B4E58799D74C61EE020B3330716C203E3BDD diff --git a/frontend/index.webpack.html b/frontend/index.webpack.html index 54801a3c..7b846a88 100644 --- a/frontend/index.webpack.html +++ b/frontend/index.webpack.html @@ -4,6 +4,9 @@ Личный кабинет юр.лица + + diff --git a/frontend/src/resources/template/app/component/log_out.html b/frontend/src/resources/template/app/component/log_out.html index 996c245f..12503171 100644 --- a/frontend/src/resources/template/app/component/log_out.html +++ b/frontend/src/resources/template/app/component/log_out.html @@ -1,7 +1,10 @@ +
{{getOrgUnitName()}}
Данные организации - +
- + diff --git a/frontend/src/ts/modules/app/component/logout.component.ts b/frontend/src/ts/modules/app/component/logout.component.ts index 0ac178ae..deee5d42 100644 --- a/frontend/src/ts/modules/app/component/logout.component.ts +++ b/frontend/src/ts/modules/app/component/logout.component.ts @@ -1,23 +1,46 @@ -import {ChangeDetectorRef, Component, OnInit} from "@angular/core"; -import {Router} from "@angular/router"; -import {HttpClient} from "@angular/common/http"; +import {ChangeDetectorRef, Component, DoCheck, OnInit} from "@angular/core"; +import {HttpClient, HttpXsrfTokenExtractor} from "@angular/common/http"; import {AuthenticationService} from "../../security/authentication.service"; +import {AppConfigService} from "@webbpm/base-package"; @Component({ moduleId: module.id, selector: "[log-out]", templateUrl: "../../../../../src/resources/template/app/component/log_out.html" }) -export class LogOutComponent implements OnInit{ +export class LogOutComponent implements OnInit, DoCheck{ + private static readonly BACKEND_URL: string = "backend.url"; + private static readonly BACKEND_CONTEXT: string = "backend.context"; + private static readonly LOGOUT_URL_POSTFIX: string = "/esia/logout"; private userFullname: string; private orgUnitName: string; + csrfValue: any; + formAction: any; - constructor(private router: Router, private httpClient: HttpClient, - private authenticationService: AuthenticationService, private cd: ChangeDetectorRef) { + constructor(private httpClient: HttpClient, + private authenticationService: AuthenticationService, + private appConfigService: AppConfigService, + private tokenExtractor: HttpXsrfTokenExtractor, + private cd: ChangeDetectorRef) { + let backendUrl = this.appConfigService.getParamValue(LogOutComponent.BACKEND_URL); + let backendContext = this.appConfigService.getParamValue( + LogOutComponent.BACKEND_CONTEXT); + + if (backendUrl) { + this.formAction = `${backendUrl}${LogOutComponent.LOGOUT_URL_POSTFIX}`; + } + else if (backendContext) { + this.formAction = `/${backendContext}${LogOutComponent.LOGOUT_URL_POSTFIX}`; + } + } + + ngDoCheck(): void { + this.csrfValue = this.tokenExtractor.getToken(); } ngOnInit(): void { + this.csrfValue = this.tokenExtractor.getToken(); let isAuth = this.authenticationService.isAuthenticated(); if (isAuth) { Promise.all([ @@ -31,10 +54,6 @@ export class LogOutComponent implements OnInit{ } } - public logout(): void { - this.authenticationService.logout(); - } - public getUserFullname(): string { return this.userFullname; }