This is a typical part of Spring Security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().and().cors().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login", "/api/v1/auth/**").permitAll();
http.authorizeRequests().anyRequest().authenticated();
}
I have a problem with http.authorizeRequests().anyRequest().authenticated().
After adding it, when I call non-existing endpoints, for example: GET: /api/v1/not-existing, I receive 403 instead of expected 404 response.
I want to protect all my resources but I want to get 404 when calling not existing resources.
How can I fix it?
I am okay with this behaviour . If an user is not authenticated , why bother to worry about telling him more information about your system. Just like if an user does not have permission to view your harddisk , why need to let him can discover your harddisk directory tree structure .
If you really want to return 404 , you need to customize AuthenticationEntryPoint and AccessDeniedHandler in the ExceptionTranslationFilter . Both of them will be invoked if an user does not have enough permission to visit an endpoint (i.e. AccessDeniedException happen). The former is for the anonymous user and the latter is for the non-anonymous user (i.e. user that is authenticated successfully but without enough permission)
Both of their default implementation (i.e Http403ForbiddenEntryPoint and AccessDeniedHandlerImpl) simply return 403 now . You have to customize them such that they will first check if there are existing endpoints to serve the current HttpServletRequest and return 404 if no. You can do it by looping through the HandlerMapping inside the DispatcherServlet and check if any of the HandlerMapping can handle the current HttpServletRequest.
First create an object that do this check :
public class HttpRequestEndpointChecker {
private DispatcherServlet servlet;
public HttpRequestEndpointChecker(DispatcherServlet servlet) {
this.servlet = servlet;
}
public boolean isEndpointExist(HttpServletRequest request) {
for (HandlerMapping handlerMapping : servlet.getHandlerMappings()) {
try {
HandlerExecutionChain foundHandler = handlerMapping.getHandler(request);
if (foundHandler != null) {
return true;
}
} catch (Exception e) {
return false;
}
}
return false;
}
}
Then customize AuthenticationEntryPoint and AccessDeniedHandler to use this object for checking :
public class MyAccessDeniedHandler extends AccessDeniedHandlerImpl {
private HttpRequestEndpointChecker endpointChecker;
public MyAccessDeniedHandler(HttpRequestEndpointChecker endpointChecker) {
this.endpointChecker = endpointChecker;
}
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (!endpointChecker.isEndpointExist(request)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource not found");
} else {
super.handle(request, response, accessDeniedException);
}
}
}
public class MyAuthenticationEntryPoint extends Http403ForbiddenEntryPoint {
private HttpRequestEndpointChecker endpointChecker;
public MyAuthenticationEntryPoint(HttpRequestEndpointChecker endpointChecker) {
this.endpointChecker = endpointChecker;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
if (!endpointChecker.isEndpointExist(request)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource not found");
} else {
super.commence(request, response, authException);
}
}
}
And configure them :
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DispatcherServlet dispatcherServlet;
#Autowired
private HttpRequestEndpointChecker endpointChecker;
#Override
protected void configure(HttpSecurity http) throws Exception {
..............
..............
http.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint(endpointChecker))
.accessDeniedHandler(new MyAccessDeniedHandler(endpointChecker));
}
#Bean
public HttpRequestEndpointChecker endpointChecker() {
return new HttpRequestEndpointChecker(dispatcherServlet);
}
}
It seems to me that your only option is the following:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().and().cors().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login", "/api/v1/auth/**").permitAll();
http.authorizeRequests().antMatchers(all-your-endpoints).authenticated();
http.authorizeRequests().anyRequest().permitAll();
}
You need to replace all-your-endpoints with a regex or multiple regex that match all your endpoints. In fact, you can even get rid of http.authorizeRequests().antMatchers("/login", "/api/v1/auth/**").permitAll(); unless you really want to be explicit about it.
Related
I'm trying to make a basic authentication service, for some business logic i need to acceept all basic auth credentials and make them hit another service (and there it will be fail if the credentials are wrong).
So I'm trying to throw an exception when the basic auth is not present, or are empty credentials.
This is my SecurityConfigurer:
#Configuration
#EnableWebSecurity
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Autowired
STGAuthenticationProvider authProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and().httpBasic();
}
}
And this is my CustomAuthProvider:
#Component
public class STGAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
if(!StringUtils.isBlank(username) && !StringUtils.isBlank(password)) {
return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
} else {
throw new STGNoCredentialsException(Constants.Error.NO_CREDENTIALS);
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Actually my app gives me "401 Unauthorized" if i send a request with no auth (I would really like to get my custom Exception you can see at my CustomAuthProvider).
And when i send just 1 credential (username or password), or no one, my service answer me with empty body at POSTMAN. Can you guys help me?
From what I understand, your issue is similar to one I had a few days ago: I needed to return a 401 instead of a 403 whenever an endpoint was called with no authorisation or with auth token expired.
With respect to your code, I would add .exceptionHandling().authenticationEntryPoint(...) to your WebSecurityConfigurerAdapter as follows
#Configuration
#EnableWebSecurity
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
/* other stuff */
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and().httpBasic()
.exceptionHandling().authenticationEntryPoint(/*custom exception*/);
}
}
and then, instead of /*custom exception*/ add something as new MyAuthException(), where MyAuthException looks like the following:
#Component
public class MyAuthException implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) /*throws ...*/ {
response.setStatus(/* your status */);
response.getWriter().write(/*the body of your answer*/);
/* whatever else you want to add to your response */
/* or you could throw an exception, I guess*/
}
}
(I don't remember and right now I can't check whether this class needs to be marked as #Component, I think not).
I need to implement authorization with a specific header (say "sessionId") and secure all uri's except one.
I extended OncePerRequestFilter and implemented custom AuthenticationProvider to check if sessionId is valid (as well as custom Token class etc).
How it works now: for any uri it immediately jumps to AuthSessionAuthenticationProvider's authenticate method right after AuthSessionFilter is applied and returns 403 if header sessionId isn't specified. But I want some uri's to allow access without that header.
It all together:
config:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(permittedUris).permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(new AuthSessionAccessDeniedHandler())
.and().addFilterBefore(new AuthSessionFilter(), BasicAuthenticationFilter.class);
}
Filter:
public class AuthSessionFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication auth = new AuthSessionToken(request.getHeader("sessionId"));
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
}
}
Provider:
public class AuthSessionAuthenticationProvider implements AuthenticationProvider {
//...
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AuthSessionToken token = (AuthSessionToken) authentication;
if (token.getSessionId() == null) {
throw new AccessDeniedException("Missing header sessionId");
}
AuthSessionAuthorities user = authSessionService.getUserAuthoritiesToken(token.getSessionId());
if (user == null) {
throw new AccessDeniedException("Session ID invalid: " + token.getSessionId());
}
token.setAuthenticatedUser(user);
return token;
}
//...
}
I found more elegant solution that was developed exactly for that purpose.
It's a RequestHeaderAuthenticationFilter. And then antMatchers works as expected. The initial configuration looks like this:
#Bean
#SneakyThrows
public RequestHeaderAuthenticationFilter preAuthenticationFilter() {
RequestHeaderAuthenticationFilter preAuthenticationFilter = new RequestHeaderAuthenticationFilter();
preAuthenticationFilter.setPrincipalRequestHeader(SESSION_ID);
preAuthenticationFilter.setCredentialsRequestHeader(SESSION_ID);
preAuthenticationFilter.setExceptionIfHeaderMissing(false);
preAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(true);
preAuthenticationFilter.setAuthenticationManager(authenticationManager());
return preAuthenticationFilter;
}
I am trying to toggle/bypass/disable Spring Security (Authentication and Authorization) for all the requests having particular Request Header.
For example, if a request url is hit with that Request Header, Spring Security should be bypassed, if not it should not be bypassed.
For this, I am using following requestMatchers Spring Security config:
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.GET)
.antMatchers(HttpMethod.OPTIONS)
.requestMatchers(new RequestHeaderRequestMatcher("TEST-HEADER","TEST-VALUE"));
}
My remaining Security Config is :
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity (prePostEnabled = true)
#ConditionalOnProperty (name = "security.enabled", havingValue = "true", matchIfMissing = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private SecurityProps securityProps;
#Autowired
private MyUserDetailsService myUserDetailsService;
#Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
#Autowired
private MyCORSFilter myCORSFilter;
public SecurityConfig() {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.addFilterBefore(myCORSFilter, SessionManagementFilter.class)
.addFilterBefore(requestHeaderFilter(), RequestHeaderAuthenticationFilter.class)
.authenticationProvider(preauthAuthProvider())
.authorizeRequests()
.antMatchers(HttpMethod.GET, securityProps.getNoAuthGetPattern()).permitAll()
.antMatchers(HttpMethod.OPTIONS, securityProps.getNoAuthOptionsPattern()).permitAll()
.requestMatchers(new RequestHeaderRequestMatcher("TEST-HEADER","TEST-VALUE")).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint);
}
#Autowired
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(preauthAuthProvider());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.GET)
.antMatchers(HttpMethod.OPTIONS)
.requestMatchers(new RequestHeaderRequestMatcher("TEST-HEADER","TEST-VALUE"));
}
public RequestHeaderAuthenticationFilter requestHeaderFilter() throws Exception {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
requestHeaderAuthenticationFilter.setPrincipalRequestHeader(MySecurityConstants.LOGIN_HEADER);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager());
requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(false);
requestHeaderAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof MySecurityException) {
myAuthenticationEntryPoint.commenceMySecurityException(request, response, (MySecurityException) exception);
} else if (exception instanceof UsernameNotFoundException) {
myAuthenticationEntryPoint.commenceUsernameNotFoundException(request, response,
(UsernameNotFoundException) exception);
} else if (exception instanceof PreAuthenticatedCredentialsNotFoundException) {
myAuthenticationEntryPoint.commence(request, response, exception);
}
}
});
return requestHeaderAuthenticationFilter;
}
#Bean
public PreAuthenticatedAuthenticationProvider preauthAuthProvider() throws Exception {
PreAuthenticatedAuthenticationProvider authProvider = new PreAuthenticatedAuthenticationProvider();
authProvider.setPreAuthenticatedUserDetailsService(userDetailsServiceWrapper());
return authProvider;
}
#Bean
public UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> userDetailsServiceWrapper()
throws Exception {
UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper =
new UserDetailsByNameServiceWrapper<>();
wrapper.setUserDetailsService(ivyUserDetailsService);
return wrapper;
}
}
With the above settings, I am unable to disable/bypass Spring Security and I am getting the AuthenticationCredentialsNotFoundException exception:
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
Can anyone help me by identifying what am I doing wrong? Is my approach correct or I need to do something else to achieve this?
EDIT :
I am getting this exception in org.springframework.security.access.intercept.AbstractSecurityInterceptor class in beforeInvocation() method where it tries to get the authentication object from SecurityContextHolder. AbstractSecurityInterceptor is invoked by its subclass MethodSecurityInterceptor which is invoked from my Spring Controller which is annotated with #PreAuthorize.
I think your bypass is working fine. Its skipping the check.
The security's authorization check part gets the authenticated object from SecurityContext, which will be set when a request gets through the spring security filter.
So when you skip security filter SecurityContext is not set yet thus the error
You can do something like this to set it manually for your Custom Header Case
try {
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(ctx);
ctx.setAuthentication(event.getAuthentication());
} finally {
SecurityContextHolder.clearContext();
}
Edit 1:
Answering all the queries.
But if thats the case, then I guess all GET call should also have
failed, but my GET calls are working fine.
Since you have added this line All your GET calls are skipped from security check.
.antMatchers(HttpMethod.GET, securityProps.getNoAuthGetPattern()).permitAll()
where can I add the code you have mentioned? Any particular filter or
somewhere else ?
I have done something like this in a Filter.
Refer Here
Look at TokenAuthenticationFilter Class in Answer. Where am manually setting.
Note: Its JWT implementation but good to refer
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenHelper.validateToken(authToken, userDetails)) {
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
What is event in your answer?
I just got that case from Some Answer, cant find its link now. But you can setAuthentication like this or like above
Authentication authentication = new PreAuthenticatedAuthenticationToken("system", null);
authentication.setAuthenticated(true);
context.setAuthentication(authentication);
Why the login isn't prompted with following configuration? When I try to access /public/user, I get error 403 (access denied). However, if I uncomment those commented lines at WebServiceSecurityConfiguration.configure, I got redirected to login page, as desired. Why those lines are needed for from-login being properly configured, as the antMatcher matches different path in the first place. I guess there is some conflict, which misconfigures the AuthenticationEntryPoint, but I don't really have idea how that happens. What I'm trying to achieve is configuring two security chains, one for login path to obtain the JWT token, and another for web services to authenticate against the token. Everything works perfectly with those lines uncommented, but I noticed by accident form-login stopped working without them, and am super confused why is that.
#Configuration
#Profile("javasecurity")
#Order(11)
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private TokenHandler tokenHandler;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").authorities(new SimpleGrantedAuthority("ROLE_USER")).and()
.withUser("admin").password("password").authorities(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("ROLE_ADMIN")).and()
.withUser("guest").password("guest").authorities(new SimpleGrantedAuthority("ROLE_GUEST"));
}
#Override
#Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**")
.permitAll()
.and()
.formLogin()
.successHandler(authenticationSuccessHandler())
.and()
.logout();
}
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
tokenHandler.setToken(response, authentication.getName());
response.getWriter().println("User authenticated and cookie sent");
response.flushBuffer();
}
};
}
#Configuration
#Profile("javasecurity")
#Order(10)
public static class WebServiceSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private TestAuthenticationFilter testAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/secured/**")
.authenticated();
// .and()
// .antMatcher("/secured/**")
// .securityContext().securityContextRepository(new NullSecurityContextRepository())
// .and()
// .addFilterAt(testAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
--
#Component("TestAuthenticationFilter")
public class TestAuthenticationFilter extends GenericFilterBean {
#Autowired
private TokenHandler tokenHandler;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("TestAuthenticationFilter doFitler");
attemptAuthentication((HttpServletRequest) request);
chain.doFilter(request, response);
clearAuthentication();
System.out.println("doFitler end");
}
public void attemptAuthentication(HttpServletRequest request) {
try {
UserDetails user = tokenHandler.loadUserFromToken(request);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, user.getPassword());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
// Do nothing
}
}
public void clearAuthentication() {
SecurityContextHolder.getContext().setAuthentication(null);
}
#Configuration
public static class DisableFilterRegistration {
#Autowired
private TestAuthenticationFilter filter;
#Bean
public FilterRegistrationBean disablerBean() {
FilterRegistrationBean bean = new FilterRegistrationBean(filter);
bean.setEnabled(false);
return bean;
}
}
}
--
#Component("TokenHandler")
public class TokenHandler {
#Autowired(required = false)
private UserDetailsService userDetailsService;
public void setToken(HttpServletResponse response, String username) {
response.addCookie(new Cookie("user", username));
}
public UserDetails loadUserFromToken(HttpServletRequest request) throws BadCredentialsException {
Cookie[] cookies = request.getCookies();
Cookie token = null;
for (Cookie c : cookies) {
if (c.getName().equals("user")) {
token = c;
break;
}
}
if (token == null)
return null;
else
return userDetailsService.loadUserByUsername(token.getValue());
}
}
--
#RestController
#RequestMapping("/public")
public class PublicController {
#GetMapping("/norole")
public String noRole() {
return "no role";
}
#GetMapping("/user")
#PreAuthorize("hasRole('ROLE_USER')")
public String roleUser() {
return "role_user";
}
}
--
#RestController
#RequestMapping("/secured")
public class SecuredController {
#GetMapping("/user")
#PreAuthorize("hasRole('ROLE_USER')")
public String roleUser() {
return "role_user";
}
#GetMapping("/admin")
#PreAuthorize("hasRole('ROLE_ADMIN')")
public String roleAdmin() {
return "role_admin";
}
#GetMapping("/norole")
public String noRole() {
return "no role";
}
}
Login-from got functional again after declaring adding
http.antMatcher("/secured/**")
As the first call in the WebServiceSecurityConfiguration.configure. Does that mean that without it the configuration negates the form-login, which is configured after this particular configuration? Also, it seems like the position of antMatcher can be arbitrary, is this the case? Could someone explain what is actually happening there?
I have set up spring security to authenticate and authorize requests coming into my application. I have set up the configuration as so:
public class OAuth2ServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
// ...set up token store here
resources.authenticationEntryPoint(new AuthenticationEntryPoint() {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//QUESTION
// How do I get the destination controller that this request was going to go to?
// Really, I'd like to get some information about the annotations that were on the destination controller.
response.setStatus(401);
}
});
}
I'd like to grab some information about the destination controller that this request was going to go to. The controller isn't actually going to get hit in this scenario because spring security kicked in and threw out the response before it reached the controller.
Any tips?
Thanks!
Assuming that OAuth2ServerConfiguration is a Spring managed bean, this should work for you.
...
#Autowired
private List<HandlerMapping> handlerMappings;
for (HandlerMapping handlerMapping : handlerMappings) {
HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);
if (handlerExecutionChain != null) {
// handlerExecutionChain.getHandler() is your handler for this request
}
}
If unable to Autowire a List of HandlerMapping, Autowire ApplicationContext and adjust as follows.
for (HandlerMapping handlerMapping : applicationContext.getBeansOfType(HandlerMapping.class).values()) {
HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);
if (handlerExecutionChain != null) {
// handlerExecutionChain.getHandler() is your handler for this request
}
}
You could try this:
#Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// handler is the controller
MyAnnotation annotation = ((HandlerMethod) handler).getMethod().getAnnotation(MyAnnotation.class)
// do stuff with the annotation
}
});
}
}