diff --git a/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java b/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java new file mode 100644 index 0000000..9993b23 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/LogoutSuccessHandler.java @@ -0,0 +1,34 @@ +package ru.micord.ervu.security; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import ru.micord.ervu.security.esia.service.EsiaAuthService; + +public class LogoutSuccessHandler + implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler { + + private final CsrfTokenRepository csrfTokenRepository; + private final EsiaAuthService esiaAuthService; + + public LogoutSuccessHandler(CsrfTokenRepository csrfTokenRepository, + EsiaAuthService esiaAuthService) { + this.csrfTokenRepository = csrfTokenRepository; + this.esiaAuthService = esiaAuthService; + } + + @Override + 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(); + CsrfToken csrfToken = this.csrfTokenRepository.generateToken(request); + this.csrfTokenRepository.saveToken(csrfToken, request, response); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java index c9d8e2b..a5aaa4d 100644 --- a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java @@ -4,59 +4,105 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import ru.micord.ervu.security.webbpm.jwt.filter.JwtAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestHandler; +import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; +import org.springframework.web.filter.RequestContextFilter; +import ru.micord.ervu.security.esia.service.EsiaAuthService; +import ru.micord.ervu.security.filter.FilterChainExceptionHandler; +import ru.micord.ervu.security.webbpm.jwt.JwtAuthenticationProvider; +import ru.micord.ervu.security.webbpm.jwt.JwtMatcher; import ru.micord.ervu.security.webbpm.jwt.UnauthorizedEntryPoint; +import ru.micord.ervu.security.webbpm.jwt.filter.JwtAuthenticationFilter; +import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper; +import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; + +import static ru.micord.ervu.security.SecurityConstants.ESIA_LOGOUT; @Configuration @EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { +public class SecurityConfig { + private static final String[] PERMIT_ALL = new String[] { + "/version", "/esia/url", "/esia/auth", "esia/refresh" + }; + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + @Autowired + private EsiaAuthService esiaAuthService; + @Autowired + private FilterChainExceptionHandler filterChainExceptionHandler; + @Autowired + private JwtAuthenticationProvider jwtAuthenticationProvider; - @Autowired - private JwtAuthenticationFilter jwtAuthenticationFilter; + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) { + auth.authenticationProvider(jwtAuthenticationProvider); + } - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + httpConfigure(http); + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(new RequestContextFilter(), LogoutFilter.class); + http.addFilterAfter(filterChainExceptionHandler, RequestContextFilter.class); + return http.build(); + } - httpConfigure(http); - http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - } + protected void httpConfigure(HttpSecurity httpSecurity) throws Exception { + CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + tokenRepository.setCookiePath("/"); + XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler(); + delegate.setCsrfRequestAttributeName(null); + // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the + // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler + CsrfTokenRequestHandler requestHandler = delegate::handle; + httpSecurity.authorizeHttpRequests( + (authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers(PERMIT_ALL) + .permitAll() + .anyRequest() + .authenticated()) + .csrf((csrf) -> csrf.csrfTokenRepository(tokenRepository) + .csrfTokenRequestHandler(requestHandler)) + .logout((logout) -> logout.logoutUrl(ESIA_LOGOUT) + .logoutSuccessHandler(new LogoutSuccessHandler(tokenRepository, esiaAuthService))) + .exceptionHandling() + .authenticationEntryPoint(entryPoint()) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } - protected void httpConfigure(HttpSecurity httpSecurity) throws Exception { - String[] permitAll = {"/version", "/esia/url", "/esia/auth", "esia/refresh"}; + public AuthenticationEntryPoint entryPoint() { + return new UnauthorizedEntryPoint(); + } - httpSecurity.authorizeRequests() - .antMatchers(permitAll).permitAll() - .antMatchers("/**").authenticated() - .and() - .csrf().disable() - .exceptionHandling().authenticationEntryPoint(entryPoint()) - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS); - } + @Bean + AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } - public AuthenticationEntryPoint entryPoint() { - return new UnauthorizedEntryPoint(); - } + @Bean + public SecurityHelper securityHelper() { + return new SecurityHelper(); + } - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Bean - public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { - JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter("/**", - entryPoint() - ); - jwtAuthenticationFilter.setAuthenticationManager(authenticationManagerBean()); - return jwtAuthenticationFilter; - } + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter(SecurityHelper securityHelper, + AuthenticationManager manager, + JwtTokenService jwtTokenService) { + JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter( + new JwtMatcher("/**", PERMIT_ALL), entryPoint(), securityHelper, jwtTokenService); + jwtAuthenticationFilter.setAuthenticationManager(manager); + return jwtAuthenticationFilter; + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/SecurityConstants.java b/backend/src/main/java/ru/micord/ervu/security/SecurityConstants.java new file mode 100644 index 0000000..1853716 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/SecurityConstants.java @@ -0,0 +1,5 @@ +package ru.micord.ervu.security; + +public class SecurityConstants { + public static final String ESIA_LOGOUT = "/esia/logout"; +} 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 2f4465e..2a3ab21 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 @@ -1,15 +1,22 @@ package ru.micord.ervu.security.esia.controller; -import javax.servlet.http.Cookie; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import ru.micord.ervu.security.esia.model.PersonDataModel; import ru.micord.ervu.security.esia.model.PersonModel; import ru.micord.ervu.security.esia.service.EsiaAuthService; import ru.micord.ervu.security.esia.service.PersonalDataService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; +import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; /** * @author Eduard Tihomirov @@ -23,13 +30,16 @@ public class EsiaController { @Autowired private PersonalDataService personalDataService; + @Autowired + private JwtTokenService jwtTokenService; + @RequestMapping(value = "/esia/url") public String getEsiaUrl() { return esiaAuthService.generateAuthCodeUrl(); } - @RequestMapping(value = "/esia/auth", params = "code", method = RequestMethod.GET) - public boolean esiaAuth(@RequestParam("code") String code, HttpServletRequest request, HttpServletResponse response) { + @GetMapping(value = "/esia/auth", params = "code") + public ResponseEntity esiaAuth(@RequestParam("code") String code, HttpServletRequest request, HttpServletResponse response) { return esiaAuthService.getEsiaTokensByCode(code, request, response); } @@ -40,18 +50,8 @@ public class EsiaController { @RequestMapping(value = "/esia/person") public PersonDataModel getPersonModel(HttpServletRequest request) { - String accessToken = null; - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals("access_token")) { - accessToken = cookie.getValue(); - } - } - } - if (accessToken == null) { - return null; - } + String accessToken = jwtTokenService.getAccessToken(request); + DateFormat df = new SimpleDateFormat("dd.MM.yyyy"); PersonModel personModel = personalDataService.getPersonModel(accessToken); PersonDataModel personDataModel = new PersonDataModel(); personDataModel.birthDate = personModel.getBirthDate(); @@ -69,24 +69,8 @@ public class EsiaController { @RequestMapping(value = "/esia/userfullname") public String getUserFullname(HttpServletRequest request) { - String accessToken = null; - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals("access_token")) { - accessToken = cookie.getValue(); - } - } - } - if (accessToken == null) { - return null; - } + String accessToken = jwtTokenService.getAccessToken(request); PersonModel personModel = personalDataService.getPersonModel(accessToken); return personModel.getLastName() + " " + personModel.getFirstName().charAt(0) + ". " + personModel.getMiddleName().charAt(0) + "."; } - - @RequestMapping(value = "/esia/logout") - public String logout(HttpServletRequest request, HttpServletResponse response) { - return esiaAuthService.logout(request, response); - } } 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 5d07a7d..302eef4 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 @@ -22,10 +22,14 @@ import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContext; import ru.micord.ervu.kafka.model.Document; import ru.micord.ervu.kafka.model.Person; import ru.micord.ervu.kafka.model.Response; import ru.micord.ervu.kafka.service.ReplyingKafkaService; +import ru.micord.ervu.security.esia.token.TokensStore; import ru.micord.ervu.security.esia.config.EsiaConfig; import ru.micord.ervu.security.esia.model.FormUrlencoded; import ru.micord.ervu.security.esia.model.EsiaAccessToken; @@ -36,6 +40,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; +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; @@ -44,10 +50,6 @@ import ru.micord.ervu.security.webbpm.jwt.model.Token; */ @Service public class EsiaAuthService { - - @Value("${cookie.path:#{null}}") - private String path; - @Autowired private ObjectMapper objectMapper; @@ -64,6 +66,9 @@ public class EsiaAuthService { @Autowired private PersonalDataService personalDataService; + @Autowired + private SecurityHelper securityHelper; + @Value("${ervu.kafka.reply.topic}") private String requestReplyTopic; @@ -141,7 +146,7 @@ public class EsiaAuthService { return uriBuilder.toString(); } - public boolean getEsiaTokensByCode(String esiaAuthCode, HttpServletRequest request, HttpServletResponse response) { + public ResponseEntity getEsiaTokensByCode(String esiaAuthCode, HttpServletRequest request, HttpServletResponse response) { try { String clientId = esiaConfig.getClientId(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); @@ -183,50 +188,45 @@ public class EsiaAuthService { .build() .send(postReq, HttpResponse.BodyHandlers.ofString()); String responseString = postResp.body(); - EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString, EsiaTokenResponse.class); - if (tokenResponse != null && tokenResponse.getError() != null) { + EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString, + EsiaTokenResponse.class + ); + + if (tokenResponse == null) { + throw new IllegalStateException("Got empty esia response"); + } + + if (tokenResponse.getError() != null) { throw new RuntimeException(tokenResponse.getError_description()); } - String cookiePath = null; - if (path != null) { - cookiePath = path; - } - else { - cookiePath = request.getContextPath(); - } String accessToken = tokenResponse.getAccess_token(); - Cookie cookie = new Cookie("access_token", accessToken); - cookie.setHttpOnly(true); - cookie.setPath(cookiePath); - response.addCookie(cookie); - String refreshToken = tokenResponse.getRefresh_token(); - Cookie cookieRefresh = new Cookie("refresh_token", refreshToken); - cookieRefresh.setHttpOnly(true); - - cookieRefresh.setPath(cookiePath); - response.addCookie(cookieRefresh); - - byte[] decodedBytes = Base64.getDecoder() - .decode( - accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.'))); - String decodedString = new String(decodedBytes); - EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString, EsiaAccessToken.class); - String ervuId = getErvuId(accessToken); - Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), tokenResponse.getExpires_in(), ervuId); - Cookie authToken = new Cookie("auth_token", token.getValue()); - authToken.setPath(cookiePath); - authToken.setHttpOnly(true); - response.addCookie(authToken); - SecurityContextHolder.getContext() - .setAuthentication( - new UsernamePasswordAuthenticationToken(esiaAccessToken.getSbj_id(), null)); - - Cookie isAuth = new Cookie("webbpm.ervu-lkrp-fl", "true"); - isAuth.setMaxAge(tokenResponse.getExpires_in().intValue()); - isAuth.setPath("/"); - response.addCookie(isAuth); - return true; + EsiaAccessToken esiaAccessToken = personalDataService.readToken(accessToken); + String prnOid = esiaAccessToken.getSbj_id(); + Long expiresIn = tokenResponse.getExpires_in(); + TokensStore.addAccessToken(prnOid, accessToken, expiresIn); + TokensStore.addRefreshToken(prnOid, refreshToken, expiresIn); + Response ervuIdResponse = getErvuIdResponse(accessToken); + Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId()); + int expiry = tokenResponse.getExpires_in().intValue(); + Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry); + response.addCookie(accessCookie); + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + JwtAuthentication authentication = new JwtAuthentication(usernamePasswordAuthenticationToken, + esiaAccessToken.getSbj_id(), token.getValue()); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry); + response.addCookie(authMarkerCookie); + if (ervuIdResponse.getErrorData() != null) { + return new ResponseEntity<>( + "Доступ запрещен. " + ervuIdResponse.getErrorData().getName(), + HttpStatus.FORBIDDEN + ); + } + return ResponseEntity.ok("Authentication successful"); } catch (Exception e) { throw new RuntimeException(e); @@ -235,15 +235,7 @@ public class EsiaAuthService { public void getEsiaTokensByRefreshToken(HttpServletRequest request, HttpServletResponse response) { try { - String refreshToken = null; - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals("refresh_token")) { - refreshToken = cookie.getValue(); - } - } - } + String refreshToken = jwtTokenService.getRefreshToken(request); String clientId = esiaConfig.getClientId(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx"); ZonedDateTime dt = ZonedDateTime.now(); @@ -288,43 +280,26 @@ public class EsiaAuthService { throw new RuntimeException(tokenResponse.getError_description()); } String accessToken = tokenResponse.getAccess_token(); - Cookie cookie = new Cookie("access_token", accessToken); - cookie.setHttpOnly(true); - String cookiePath = null; - if (path != null) { - cookiePath = path; - } - else { - cookiePath = request.getContextPath(); - } - cookie.setPath(cookiePath); - response.addCookie(cookie); - String newRefreshToken = tokenResponse.getRefresh_token(); - Cookie cookieRefresh = new Cookie("refresh_token", newRefreshToken); - cookieRefresh.setHttpOnly(true); - cookieRefresh.setPath(cookiePath); - response.addCookie(cookieRefresh); - - byte[] decodedBytes = Base64.getDecoder() - .decode( - accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.'))); - String decodedString = new String(decodedBytes); - EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString, EsiaAccessToken.class); - String ervuId = getErvuId(accessToken); - Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), tokenResponse.getExpires_in(), ervuId); - Cookie authToken = new Cookie("auth_token", token.getValue()); - authToken.setPath(cookiePath); - authToken.setHttpOnly(true); - response.addCookie(authToken); - SecurityContextHolder.getContext() - .setAuthentication( - new UsernamePasswordAuthenticationToken(esiaAccessToken.getSbj_id(), null)); - - Cookie isAuth = new Cookie("webbpm.ervu-lkrp-fl", "true"); - isAuth.setMaxAge(tokenResponse.getExpires_in().intValue()); - isAuth.setPath("/"); - response.addCookie(isAuth); + EsiaAccessToken esiaAccessToken = personalDataService.readToken(accessToken); + String prnOid = esiaAccessToken.getSbj_id(); + Long expiresIn = tokenResponse.getExpires_in(); + TokensStore.addAccessToken(prnOid, accessToken, expiresIn); + TokensStore.addRefreshToken(prnOid, newRefreshToken, expiresIn); + Response ervuIdResponse = getErvuIdResponse(accessToken); + Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId()); + int expiry = tokenResponse.getExpires_in().intValue(); + Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry); + response.addCookie(accessCookie); + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + JwtAuthentication authentication = new JwtAuthentication(usernamePasswordAuthenticationToken, + esiaAccessToken.getSbj_id(), token.getValue()); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry); + response.addCookie(authMarkerCookie); } catch (Exception e) { throw new RuntimeException(e); @@ -365,23 +340,10 @@ public class EsiaAuthService { public String logout(HttpServletRequest request, HttpServletResponse response) { try { - Cookie[] cookies = request.getCookies(); - if (cookies != null) - for (Cookie cookie : cookies) { - if (cookie.getName().equals("webbpm.ervu-lkrp-fl")) { - cookie.setValue(""); - cookie.setPath("/"); - cookie.setMaxAge(0); - response.addCookie(cookie); - } - else if (cookie.getName().equals("auth_token") || cookie.getName().equals("refresh_token") - || cookie.getName().equals("access_token")) { - cookie.setValue(""); - cookie.setPath(cookie.getPath()); - cookie.setMaxAge(0); - response.addCookie(cookie); - } - } + securityHelper.clearAccessCookies(response); + String userId = jwtTokenService.getUserAccountId(request); + TokensStore.removeAccessToken(userId); + TokensStore.removeRefreshToken(userId); String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl(); String redirectUrl = esiaConfig.getRedirectUrl(); URL url = new URL(logoutUrl); @@ -395,22 +357,14 @@ public class EsiaAuthService { } } - public String getErvuId(String accessToken) { + public Response getErvuIdResponse(String accessToken) { try { PersonModel personModel = personalDataService.getPersonModel(accessToken); Person person = copyToPerson(personModel); String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, requestReplyTopic, objectMapper.writeValueAsString(person) ); - Response response = objectMapper.readValue(kafkaResponse, Response.class); - if (response.getErrorData() != null) { - throw new RuntimeException( - "Error code = " + response.getErrorData().getCode() + ", error name = " - + response.getErrorData().getName()); - } - else { - return response.getErvuId(); - } + return objectMapper.readValue(kafkaResponse, Response.class); } catch (Exception e) { throw new RuntimeException(e); diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java index 79b11ed..f4590c3 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaPersonalDataService.java @@ -31,11 +31,7 @@ public class EsiaPersonalDataService implements PersonalDataService { @Override public PersonModel getPersonModel(String accessToken) { try { - byte[] decodedBytes = Base64.getDecoder() - .decode( - accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.'))); - String decodedString = new String(decodedBytes); - EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString, EsiaAccessToken.class); + EsiaAccessToken esiaAccessToken = readToken(accessToken); String prnsId = esiaAccessToken.getSbj_id(); PersonModel personModel = getPersonData(prnsId, accessToken); personModel.setPassportModel( @@ -88,4 +84,23 @@ public class EsiaPersonalDataService implements PersonalDataService { throw new RuntimeException(e); } } + + @Override + public EsiaAccessToken readToken(String accessToken) { + try { + byte[] decodedBytes = Base64.getDecoder() + .decode( + accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.')) + .replace('-', '+') + .replace('_', '/')); + String decodedString = new String(decodedBytes); + EsiaAccessToken esiaAccessToken = objectMapper.readValue(decodedString, + EsiaAccessToken.class + ); + return esiaAccessToken; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/PersonalDataService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/PersonalDataService.java index 37edafa..303c9fa 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/PersonalDataService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/PersonalDataService.java @@ -1,5 +1,6 @@ package ru.micord.ervu.security.esia.service; +import ru.micord.ervu.security.esia.model.EsiaAccessToken; import ru.micord.ervu.security.esia.model.PersonModel; /** @@ -8,4 +9,5 @@ import ru.micord.ervu.security.esia.model.PersonModel; public interface PersonalDataService { PersonModel getPersonModel(String accessToken); + EsiaAccessToken readToken(String accessToken); } diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/ExpiringToken.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/ExpiringToken.java new file mode 100644 index 0000000..f6a476e --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/esia/token/ExpiringToken.java @@ -0,0 +1,34 @@ +package ru.micord.ervu.security.esia.token; + +/** + * @author Eduard Tihomirov + */ +public class ExpiringToken { + private String accessToken; + private long expiryTime; + + public ExpiringToken(String accessToken, long expiryTime) { + this.accessToken = accessToken; + this.expiryTime = expiryTime; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public long getExpiryTime() { + return expiryTime; + } + + public void setExpiryTime(long expiryTime) { + this.expiryTime = expiryTime; + } + + boolean isExpired() { + return System.currentTimeMillis() > expiryTime; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java new file mode 100644 index 0000000..4665295 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensClearShedulerService.java @@ -0,0 +1,20 @@ +package ru.micord.ervu.security.esia.token; + +import net.javacrumbs.shedlock.core.SchedulerLock; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Eduard Tihomirov + */ +@Service +public class TokensClearShedulerService { + @Scheduled(cron = "${esia.token.clear.cron:0 0 */1 * * *}") + @SchedulerLock(name = "clearToken") + @Transactional + public void load() { + TokensStore.removeExpiredRefreshToken(); + TokensStore.removeExpiredAccessToken(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensStore.java b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensStore.java new file mode 100644 index 0000000..9804b80 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/esia/token/TokensStore.java @@ -0,0 +1,60 @@ +package ru.micord.ervu.security.esia.token; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Eduard Tihomirov + */ +public class TokensStore { + private static final Map accessTokensMap = new ConcurrentHashMap<>(); + private static final Map refreshTokensMap = new ConcurrentHashMap<>(); + + public static void addAccessToken(String prnOid, String token, long expiresIn) { + if (token != null) { + long expiryTime = System.currentTimeMillis() + 1000L * expiresIn; + accessTokensMap.put(prnOid, new ExpiringToken(token, expiryTime)); + } + } + + public static String getAccessToken(String prnOid) { + return accessTokensMap.get(prnOid).getAccessToken(); + } + + public static void removeExpiredAccessToken() { + for (String key : accessTokensMap.keySet()) { + ExpiringToken token = accessTokensMap.get(key); + if (token != null && token.isExpired()) { + accessTokensMap.remove(key); + } + } + } + + public static void removeExpiredRefreshToken() { + for (String key : refreshTokensMap.keySet()) { + ExpiringToken token = refreshTokensMap.get(key); + if (token != null && token.isExpired()) { + refreshTokensMap.remove(key); + } + } + } + + public static void removeAccessToken(String prnOid) { + accessTokensMap.remove(prnOid); + } + + public static void addRefreshToken(String prnOid, String token, long expiresIn) { + if (token != null) { + long expiryTime = System.currentTimeMillis() + 1000L * expiresIn; + refreshTokensMap.put(prnOid, new ExpiringToken(token, expiryTime)); + } + } + + public static String getRefreshToken(String prnOid) { + return refreshTokensMap.get(prnOid).getAccessToken(); + } + + public static void removeRefreshToken(String prnOid) { + refreshTokensMap.remove(prnOid); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/filter/FilterChainExceptionHandler.java b/backend/src/main/java/ru/micord/ervu/security/filter/FilterChainExceptionHandler.java new file mode 100644 index 0000000..7cce4a1 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/filter/FilterChainExceptionHandler.java @@ -0,0 +1,29 @@ +package ru.micord.ervu.security.filter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; + +@Component +public class FilterChainExceptionHandler extends OncePerRequestFilter { + @Autowired + @Qualifier("handlerExceptionResolver") + private HandlerExceptionResolver resolver; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) { + try { + filterChain.doFilter(request, response); + } + catch (Exception e) { + resolver.resolveException(request, response, null, e); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java index 494cf51..f709679 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtAuthenticationProvider.java @@ -1,5 +1,7 @@ package ru.micord.ervu.security.webbpm.jwt; +import java.util.Collections; + import io.jsonwebtoken.ExpiredJwtException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; @@ -44,10 +46,12 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { throw new BadCredentialsException("Auth token is not valid for user " + token.getUserAccountId()); } - UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = - new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); + UsernamePasswordAuthenticationToken pwdToken = + UsernamePasswordAuthenticationToken.authenticated(token.getUserAccountId(), null, + Collections.emptyList() + ); - return new JwtAuthentication(usernamePasswordAuthenticationToken, token.getUserAccountId()); + return new JwtAuthentication(pwdToken, token.getUserAccountId(), token.getValue()); } @Override diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtMatcher.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtMatcher.java new file mode 100644 index 0000000..f0c90ce --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/JwtMatcher.java @@ -0,0 +1,33 @@ +package ru.micord.ervu.security.webbpm.jwt; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.extractAuthToken; + +public final class JwtMatcher implements RequestMatcher { + private final Set excludedPathMatchers; + private final AntPathRequestMatcher securedMatcher; + + public JwtMatcher(String securedPath, String... excludedPaths) { + this.securedMatcher = new AntPathRequestMatcher(securedPath); + this.excludedPathMatchers = Arrays.stream(excludedPaths) + .map(AntPathRequestMatcher::new) + .collect(Collectors.toSet()); + } + + @Override + public boolean matches(HttpServletRequest request) { + if (this.excludedPathMatchers.stream().anyMatch(matcher -> matcher.matches(request))) { + return false; + } + else { + return extractAuthToken(request) != null && this.securedMatcher.matches(request); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java index ef8af20..c4f60f7 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/filter/JwtAuthenticationFilter.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import javax.servlet.FilterChain; import javax.servlet.ServletException; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -16,69 +15,81 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.RequestMatcher; import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; +import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper; +import ru.micord.ervu.security.webbpm.jwt.model.Token; +import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; + +import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.extractAuthToken; + /** * @author Flyur Karimov */ public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter { - private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final Logger LOGGER = LoggerFactory.getLogger( + MethodHandles.lookup().lookupClass()); private final AuthenticationEntryPoint entryPoint; - public JwtAuthenticationFilter(String securityPath, AuthenticationEntryPoint entryPoint) { - super(securityPath); + private final SecurityHelper securityHelper; + + private final JwtTokenService jwtTokenService; + + public JwtAuthenticationFilter(RequestMatcher requestMatcher, + AuthenticationEntryPoint entryPoint, + SecurityHelper securityHelper, + JwtTokenService jwtTokenService) { + super(requestMatcher); this.entryPoint = entryPoint; + this.securityHelper = securityHelper; + this.jwtTokenService = jwtTokenService; } @Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse) throws AuthenticationException { - String token = extractAuthTokenFromRequest(httpServletRequest); + HttpServletResponse httpServletResponse) + throws AuthenticationException { + String tokenStr = extractAuthToken(httpServletRequest); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { - authentication = new JwtAuthentication(null, null, token); + authentication = new JwtAuthentication(null, null, tokenStr); } try { authentication = getAuthenticationManager().authenticate(authentication); + if (!httpServletRequest.getRequestURI().endsWith("esia/logout")) { + Token token = jwtTokenService.getToken(tokenStr); + String[] ids = token.getUserAccountId().split(":"); + if (ids.length != 2) { + throw new CredentialsExpiredException("Invalid token. User has no ervuId"); + } + } } catch (CredentialsExpiredException e) { + securityHelper.clearAccessCookies(httpServletResponse); httpServletResponse.setStatus(401); LOGGER.warn(e.getMessage()); + return null; } return authentication; } - @Override - protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { - return extractAuthTokenFromRequest(request) != null; - } - @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, - FilterChain chain, Authentication authentication) throws IOException, ServletException { + FilterChain chain, Authentication authentication) + throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } @Override - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { + protected void unsuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) + throws IOException, ServletException { LOGGER.error("Jwt unsuccessful authentication exception", exception); SecurityContextHolder.clearContext(); entryPoint.commence(request, response, exception); } - - public String extractAuthTokenFromRequest(HttpServletRequest httpRequest) { - String token = null; - Cookie[] cookies = httpRequest.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals("auth_token")) { - token = cookie.getValue(); - } - } - } - return token; - } } 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 new file mode 100644 index 0000000..0a222c7 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/helper/SecurityHelper.java @@ -0,0 +1,43 @@ +package ru.micord.ervu.security.webbpm.jwt.helper; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Value; +import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; + +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; + + public void clearAccessCookies(HttpServletResponse response) { + Cookie tokenCookie = createCookie(AUTH_TOKEN, null, null); + tokenCookie.setMaxAge(0); + tokenCookie.setPath(accessCookiePath); + tokenCookie.setHttpOnly(true); + response.addCookie(tokenCookie); + + Cookie markerCookie = createCookie(AUTH_MARKER, null, null); + markerCookie.setMaxAge(0); + markerCookie.setPath("/"); + response.addCookie(markerCookie); + } + + public Cookie createAccessCookie(String cookieValue, int expiry) { + Cookie authToken = createCookie(SecurityUtil.AUTH_TOKEN, cookieValue, accessCookiePath); + authToken.setPath(accessCookiePath); + authToken.setMaxAge(expiry); + return authToken; + } + + public Cookie createAuthMarkerCookie(String cookieValue, int expiry) { + Cookie marker = createCookie(AUTH_MARKER, cookieValue, "/"); + marker.setMaxAge(expiry); + marker.setHttpOnly(false); + return marker; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java index 58d06dc..5478da2 100644 --- a/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/service/JwtTokenService.java @@ -1,12 +1,9 @@ package ru.micord.ervu.security.webbpm.jwt.service; import java.lang.invoke.MethodHandles; -import java.util.Arrays; import java.util.Base64; import java.util.Date; -import java.util.Optional; import javax.crypto.SecretKey; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import io.jsonwebtoken.Claims; @@ -17,10 +14,13 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import ru.micord.ervu.security.esia.token.TokensStore; import ru.micord.ervu.security.webbpm.jwt.model.Token; import ru.cg.webbpm.modules.resources.api.ResourceMetadataUtils; +import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.extractAuthToken; + /** * @author Flyur Karimov */ @@ -29,7 +29,7 @@ public class JwtTokenService { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @Value("${webbpm.security.token.issuer:}") + @Value("${webbpm.security.token.issuer:#{null}}") private final String tokenIssuerName = ResourceMetadataUtils.PROJECT_GROUP_ID + "." + ResourceMetadataUtils.PROJECT_ARTIFACT_ID; private final SecretKey SIGNING_KEY; @@ -80,13 +80,27 @@ public class JwtTokenService { } public String getErvuId() { - String authToken = Optional.ofNullable(request.getCookies()) - .map(cookies -> Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals("auth_token")) - .findFirst() - .map(Cookie::getValue) - .orElseThrow(() -> new RuntimeException("Failed to get auth data. User unauthorized."))) - .orElseThrow(() -> new RuntimeException("Failed to get auth data. User unauthorized.")); - return getToken(authToken).getUserAccountId().split(":")[1]; + String extractAuthToken = extractAuthToken(request); + return getToken(extractAuthToken).getUserAccountId().split(":")[1]; + } + + public String getAccessToken(HttpServletRequest request) { + return TokensStore.getAccessToken(getUserAccountId(request)); + } + + public String getRefreshToken(HttpServletRequest request) { + return TokensStore.getRefreshToken(getUserAccountId(request)); + } + + public String getUserAccountId(HttpServletRequest request) { + String authToken = extractAuthToken(request); + + if (authToken != null) { + String[] ids = getToken(authToken).getUserAccountId().split(":"); + return ids[0]; + } + else { + throw new RuntimeException("Failed to get auth data. User unauthorized."); + } } } 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 new file mode 100644 index 0000000..730cca9 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/security/webbpm/jwt/util/SecurityUtil.java @@ -0,0 +1,45 @@ +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"; + + public static final String AUTH_MARKER = "webbpm.ervu-lkrp-fl"; + + private 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/micord.env b/config/micord.env index 17336d4..5061bc0 100644 --- a/config/micord.env +++ b/config/micord.env @@ -30,4 +30,7 @@ ERVU_KAFKA_RECRUIT_HEADER_CLASS=Request@urn://rostelekom.ru/RP-SummonsTR/1.0.5 ERVU_KAFKA_REGISTRY_EXTRACT_REQUEST_TOPIC=ervu.extract.info.request ERVU_KAFKA_REGISTRY_EXTRACT_REPLY_TOPIC=ervu.extract.info.response ERVU_KAFKA_EXTRACT_HEADER_CLASS=Request@urn://rostelekom.ru/ERVU-extractFromRegistryTR/1.0.3 -ERVU_KAFKA_DOC_LOGIN_MODULE=org.apache.kafka.common.security.scram.ScramLoginModule \ No newline at end of file +ERVU_KAFKA_DOC_LOGIN_MODULE=org.apache.kafka.common.security.scram.ScramLoginModule + +ESIA_TOKEN_CLEAR_CRON=0 0 */1 * * * +COOKIE_PATH=/fl \ No newline at end of file diff --git a/config/standalone/dev/standalone.xml b/config/standalone/dev/standalone.xml index 5d00a4b..1428635 100644 --- a/config/standalone/dev/standalone.xml +++ b/config/standalone/dev/standalone.xml @@ -78,6 +78,7 @@ + diff --git a/frontend/src/resources/app-config.json b/frontend/src/resources/app-config.json index fce2327..3fad55f 100644 --- a/frontend/src/resources/app-config.json +++ b/frontend/src/resources/app-config.json @@ -4,7 +4,7 @@ "filter_cleanup_interval_hours": 720, "filter_cleanup_check_period_minutes": 30, "auth_method": "form", - "enable.version.in.url": "%enable.version.in.url%", + "enable.version.in.url": "false", "backend.context": "fl", "guard.confirm_exit": false, "message_service_error_timeout": "", diff --git a/frontend/src/resources/app.version b/frontend/src/resources/app.version index 8b878d3..4df9f77 100644 --- a/frontend/src/resources/app.version +++ b/frontend/src/resources/app.version @@ -1 +1 @@ -%project.version% +1.8.2-SNAPSHOT diff --git a/frontend/src/resources/template/app/component/log_out.html b/frontend/src/resources/template/app/component/log_out.html index ff99c6d..f488796 100644 --- a/frontend/src/resources/template/app/component/log_out.html +++ b/frontend/src/resources/template/app/component/log_out.html @@ -2,4 +2,5 @@
Мои данные -
\ No newline at end of file + + \ No newline at end of file diff --git a/frontend/src/ts/modules/app/app.module.ts b/frontend/src/ts/modules/app/app.module.ts index 4df41c8..eb52cbc 100644 --- a/frontend/src/ts/modules/app/app.module.ts +++ b/frontend/src/ts/modules/app/app.module.ts @@ -1,4 +1,4 @@ -import {forwardRef, NgModule} from "@angular/core"; +import {APP_INITIALIZER, forwardRef, NgModule} from "@angular/core"; import {NgbModule} from "@ng-bootstrap/ng-bootstrap"; import {CommonModule, registerLocaleData} from "@angular/common"; import localeRu from '@angular/common/locales/ru'; @@ -23,6 +23,7 @@ import {TextWithDialogLinks} from "../../ervu/component/textwithdialoglinks/Text import {LogOutComponent} from "./component/logout.component"; import {LoadForm} from "../../ervu/component/container/LoadForm"; import {InMemoryStaticGrid} from "../../ervu/component/grid/InMemoryStaticGrid"; +import {AuthenticationService} from "../security/authentication.service"; registerLocaleData(localeRu); export const DIRECTIVES = [ @@ -37,6 +38,10 @@ export const DIRECTIVES = [ forwardRef(() => InMemoryStaticGrid) ]; +export function checkAuthentication(authService: AuthenticationService): () => Promise { + return () => authService.checkAuthentication(); +} + @NgModule({ imports: [ CommonModule, @@ -57,6 +62,13 @@ export const DIRECTIVES = [ DIRECTIVES ], providers: [ + AuthenticationService, + { + provide: APP_INITIALIZER, + useFactory: checkAuthentication, + deps: [AuthenticationService], + multi: true, + }, { provide: ProgressIndicationService, useClass: AppProgressIndicationService } ], bootstrap: [], diff --git a/frontend/src/ts/modules/app/component/logout.component.ts b/frontend/src/ts/modules/app/component/logout.component.ts index 2b2f765..c6674d4 100644 --- a/frontend/src/ts/modules/app/component/logout.component.ts +++ b/frontend/src/ts/modules/app/component/logout.component.ts @@ -27,10 +27,10 @@ export class LogOutComponent implements OnInit{ } } - public logout(): void { - this.httpClient.get("esia/logout").toPromise().then(url => { + logout(): Promise { + return this.httpClient.post('esia/logout', {}, { responseType: 'text' as 'json' }).toPromise().then(url => { window.open(url, "_self"); - }) + }); } public getUserFullname(): string { diff --git a/frontend/src/ts/modules/security/TokenConstants.ts b/frontend/src/ts/modules/security/TokenConstants.ts new file mode 100644 index 0000000..597fe75 --- /dev/null +++ b/frontend/src/ts/modules/security/TokenConstants.ts @@ -0,0 +1,4 @@ +export class TokenConstants { + public static readonly CSRF_TOKEN_NAME = "XSRF-TOKEN"; + public static readonly CSRF_HEADER_NAME = "X-XSRF-TOKEN"; +} diff --git a/frontend/src/ts/modules/security/authentication.service.ts b/frontend/src/ts/modules/security/authentication.service.ts new file mode 100644 index 0000000..0267da3 --- /dev/null +++ b/frontend/src/ts/modules/security/authentication.service.ts @@ -0,0 +1,27 @@ +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from "rxjs"; +import {CookieService} from "ngx-cookie"; +import {tap} from "rxjs/operators"; +import {AppConfigService} from "@webbpm/base-package"; + +@Injectable({providedIn: 'root'}) +export class AuthenticationService { + + constructor(private http: HttpClient, + private cookieService: CookieService, + private appConfigService: AppConfigService) { + } + + checkAuthentication(): Promise{ + return this.appConfigService.load().then(value => this.http.get("version").toPromise()) + } + + logout(): Promise { + return this.http.post('esia/logout', {}).toPromise(); + } + + public isAuthenticated(): boolean { + return this.cookieService.get('webbpm.ervu-lkrp-fl') != null; + } +} diff --git a/frontend/src/ts/modules/security/guard/auth.guard.ts b/frontend/src/ts/modules/security/guard/auth.guard.ts index 80095f3..c90374e 100644 --- a/frontend/src/ts/modules/security/guard/auth.guard.ts +++ b/frontend/src/ts/modules/security/guard/auth.guard.ts @@ -2,8 +2,8 @@ import {Injectable} from "@angular/core"; import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from "@angular/router"; import {Observable} from "rxjs"; import {HttpClient, HttpParams} from "@angular/common/http"; -import {CookieService} from "ngx-cookie"; import {MessagesService} from "@webbpm/base-package"; +import {AuthenticationService} from "../authentication.service"; @Injectable({providedIn:'root'}) export abstract class AuthGuard implements CanActivate { @@ -11,7 +11,7 @@ export abstract class AuthGuard implements CanActivate { protected constructor( protected router: Router, private httpClient: HttpClient, - private cookieService: CookieService, + private authenticationService: AuthenticationService, private messageService: MessagesService ) { } @@ -36,11 +36,24 @@ export abstract class AuthGuard implements CanActivate { } else if (code) { const params = new HttpParams().set('code', code); - this.httpClient.get("esia/auth", {params: params}).toPromise().then( - () => window.open(url.origin + url.pathname, "_self")) - .catch((reason) => - console.error(reason) - ); + this.httpClient.get("esia/auth", + { + params: params, responseType: 'text', observe: 'response', headers: { + "Error-intercept-skip": "true" + } + }) + .toPromise() + .then( + (response) => { + window.open(url.origin + url.pathname, "_self"); + }) + .catch((reason) => { + let errorMessage = reason.error.messages != null + ? reason.error.messages + : reason.error.replaceAll('\\', ''); + this.messageService.error(errorMessage); + console.error(reason); + }); return false; } else { @@ -55,11 +68,7 @@ export abstract class AuthGuard implements CanActivate { }); } - private checkAccess(): Promise | boolean { - return this.getIsAuth() != null; - }; - - public getIsAuth(): string { - return this.cookieService.get('webbpm.ervu-lkrp-fl'); + private checkAccess(): boolean { + return this.authenticationService.isAuthenticated(); } } diff --git a/frontend/src/ts/modules/webbpm/interceptor/absolute-url-csrf.interceptor.ts b/frontend/src/ts/modules/webbpm/interceptor/absolute-url-csrf.interceptor.ts new file mode 100644 index 0000000..ea23188 --- /dev/null +++ b/frontend/src/ts/modules/webbpm/interceptor/absolute-url-csrf.interceptor.ts @@ -0,0 +1,26 @@ +import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {CookieService} from "ngx-cookie"; +import {TokenConstants} from "../../security/TokenConstants"; + +@Injectable() +export class AbsoluteUrlCsrfInterceptor implements HttpInterceptor { + + constructor(private cookieService: CookieService) { + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + + let requestToForward = req; + let token = this.cookieService.get(TokenConstants.CSRF_TOKEN_NAME) as string; + + if (token != null) { + let headers = {}; + let headerName = TokenConstants.CSRF_HEADER_NAME; + headers[headerName] = token; + requestToForward = req.clone({setHeaders: headers}); + } + return next.handle(requestToForward); + } +} diff --git a/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.prod.ts b/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.prod.ts index 07735d5..6cd9ffe 100644 --- a/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.prod.ts +++ b/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.prod.ts @@ -4,9 +4,11 @@ import { HttpSecurityErrorInterceptor, HttpSecurityInterceptor } from "@webbpm/base-package"; +import {AbsoluteUrlCsrfInterceptor} from "./absolute-url-csrf.interceptor"; export const DEFAULT_HTTP_INTERCEPTOR_PROVIDERS = [ {provide: HTTP_INTERCEPTORS, useClass: HttpSecurityInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: HttpSecurityErrorInterceptor, multi: true}, - {provide: HTTP_INTERCEPTORS, useClass: FormDirtyInterceptor, multi: true} + {provide: HTTP_INTERCEPTORS, useClass: FormDirtyInterceptor, multi: true}, + {provide: HTTP_INTERCEPTORS, useClass: AbsoluteUrlCsrfInterceptor, multi: true} ]; diff --git a/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.ts b/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.ts index ee46e0c..77b3b40 100644 --- a/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.ts +++ b/frontend/src/ts/modules/webbpm/interceptor/default-interceptors.ts @@ -1,9 +1,11 @@ import {HTTP_INTERCEPTORS} from "@angular/common/http"; import {FormDirtyInterceptor, HttpSecurityInterceptor} from "@webbpm/base-package"; import {DevHttpSecurityErrorInterceptor} from "./http-security-error-interceptor.dev"; +import {AbsoluteUrlCsrfInterceptor} from "./absolute-url-csrf.interceptor"; export const DEFAULT_HTTP_INTERCEPTOR_PROVIDERS = [ {provide: HTTP_INTERCEPTORS, useClass: HttpSecurityInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: DevHttpSecurityErrorInterceptor, multi: true}, - {provide: HTTP_INTERCEPTORS, useClass: FormDirtyInterceptor, multi: true} + {provide: HTTP_INTERCEPTORS, useClass: FormDirtyInterceptor, multi: true}, + {provide: HTTP_INTERCEPTORS, useClass: AbsoluteUrlCsrfInterceptor, multi: true}, ]; diff --git a/frontend/src/ts/modules/webbpm/webbpm.module.ts b/frontend/src/ts/modules/webbpm/webbpm.module.ts index c9ef120..3326d6e 100644 --- a/frontend/src/ts/modules/webbpm/webbpm.module.ts +++ b/frontend/src/ts/modules/webbpm/webbpm.module.ts @@ -17,6 +17,8 @@ import { import {AppRoutingModule} from "../app/app-routing.module"; import {GlobalErrorHandler} from "./handler/global-error.handler.prod"; import {DEFAULT_HTTP_INTERCEPTOR_PROVIDERS} from "./interceptor/default-interceptors.prod"; +import {HttpClientModule, HttpClientXsrfModule} from "@angular/common/http"; +import {TokenConstants} from "../security/TokenConstants"; let IMPORTS = [ BrowserAnimationsModule, @@ -30,7 +32,10 @@ let IMPORTS = [ CoreModule, ComponentsModule, AppModule, - WebbpmRoutingModule + WebbpmRoutingModule, + HttpClientModule, + HttpClientXsrfModule.withOptions( + {cookieName: TokenConstants.CSRF_TOKEN_NAME, headerName: TokenConstants.CSRF_HEADER_NAME}) ]; @NgModule({ diff --git a/pom.xml b/pom.xml index eb4314d..232557d 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,27 @@ + + org.springframework.security + spring-security-bom + 5.8.14 + import + pom + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + ru.cg.webbpm platform-bom @@ -163,14 +184,6 @@ ru.cg.webbpm.modules.database database-impl - - net.javacrumbs.shedlock - shedlock-spring - - - net.javacrumbs.shedlock - shedlock-provider-jdbc-template -