12/27/16

RESTfull Authorization Service with LDAP - Using Spring Security and OAuth 2


In my previous blog, I had some sample code to explain the implementation of a RESTfull Authorization Service, using Spring Security and OAuth2. In this blog I will explain how to use your LDAP service to authenticate user login. For easier understanding, I will use the same code, but will high-light in RED, the portions need to be touched/modified for LDAP Integration.


Please note that, the LDAP properties configuration may be different, specific to your organization.

Here are my Gradle build dependency entries. If you are using Maven you can choose the jar artifact versions accordingly.
project.ext {
    
springversion = '4.0.3.RELEASE'
   
 jerseyversion = '1.8'
    
}

dependencies {

    

providedCompile "javax.servlet:servlet-api:2.5"
    
compile "com.google.code.gson:gson:1.7.2"

    

//Jersey
    
compile group: 'com.sun.jersey', name: 'jersey-server', version: jerseyversion
    
compile group: 'com.sun.jersey', name: 'jersey-json', version: jerseyversion

    compile("com.sun.jersey.contribs:jersey-spring:1.8") {
        
    exclude module: 'spring-beans'
        
    exclude module: 'spring-core'
        
    exclude module: 'spring-web'
        
    exclude module: 'spring-context'
    }

    

//Spring
    
compile group: 'org.springframework', name: 'spring-web', version: springversion
    
compile group: 'org.springframework', name: 'spring-context', version: springversion
    
compile group: 'org.springframework', name: 'spring-core', version: springversion
    
compile group: 'org.springframework', name: 'spring-webmvc', version: springversion
    
compile group: 'org.springframework', name: 'spring-beans', version: springversion

    

//OAuth2  and Spring Security
    
compile group: 'org.springframework.security.oauth', name: 'spring-security-oauth2', version:'2.0.3.RELEASE'
    
compile group: 'org.springframework.security', name: 'spring-security-core', version:'3.2.4.RELEASE'
    
compile group: 'org.springframework.security', name: 'spring-security-ldap', version:'3.2.4.RELEASE'


}

Web.xml

<web-app id="WebApp_ID" version="2.4"
         
xmlns="http://java.sun.com/xml/ns/j2ee" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    

<display-name>OAUTH2 Authorization Services</display-name>

    

<context-param>
        
<param-name>contextConfigLocation</param-name>
        
<param-value>/WEB-INF/spring-servlet.xml</param-value>
    
</context-param>

    

<listener>
        
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    

<filter>
        
<filter-name>springSecurityFilterChain</filter-name>
        
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        
<init-param>
            
<param-name>contextAttribute</param-name>
            
<param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.spring</param-value>
        
</init-param>
    
</filter>
    

<filter-mapping>
        
<filter-name>springSecurityFilterChain</filter-name>
        
<url-pattern>/*</url-pattern>
    
</filter-mapping>

    

<servlet>
        
<servlet-name>spring</servlet-name>
        
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        
<load-on-startup>1</load-on-startup>
    
</servlet>
    

<servlet-mapping>
        
<servlet-name>spring</servlet-name>
        
<url-pattern>/</url-pattern>
    
</servlet-mapping>



</web-app>

spring-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>


<beans 
xmlns="http://www.springframework.org/schema/beans"
       
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
       xmlns:sec="http://www.springframework.org/schema/security" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"

       xsi:schemaLocation="http://www.springframework.org/schema/beans
                      http://www.springframework.org/schema/beans/spring-beans.xsd
                      http://www.springframework.org/schema/security/oauth2
                      http://www.springframework.org/schema/security/spring-security-oauth2.xsd
                      http://www.springframework.org/schema/security
                      
http://www.springframework.org/schema/security/spring-security.xsd
                      http://www.springframework.org/schema/context
                      
http://www.springframework.org/schema/context/spring-context.xsd
                      http://www.springframework.org/schema/mvc
                            
http://www.springframework.org/schema/mvc/spring-mvc.xsd">


    

<context:property-placeholder location="classpath:spring-context.properties"/>

<context:component-scan base-package="com.test” />

  

<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
    <property name="url" value="${ldap.url}"/>
    
<property name="base" value="${ldap.base}"/>
    
<property name="userDn" value="${ldap.userdn}"/>
    
<property name="password" value="${ldap.password}"/>
    
<property name="pooled" value="${ldap.pooled}"/>

</bean>


<bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
    
<constructor-arg ref="contextSource"/>

</bean>


<bean id="authManager" class="com.test.security.AuthenticationManager">
    
<property name="ldapTemplate" ref="ldapTemplate"/>

</bean> 

<!-- For requesting and accessing token -->
<http pattern="/oauth/token" create-session="stateless"
          
authentication-manager-ref="authenticationManager"
          
xmlns="http://www.springframework.org/schema/security" >

<intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY" />
        
<anonymous enabled="false" />
        
<http-basic entry-point-ref="clientAuthenticationEntryPoint" />
        
<custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER" />
        <access-denied-handler ref="oauthAccessDeniedHandler" />
    
</http>

    

<!-- For accessing protected resources after receiving token -->
<http pattern="/api/**" create-session="never"
          
entry-point-ref="oauthAuthenticationEntryPoint"
          
xmlns="http://www.springframework.org/schema/security">
        
<anonymous enabled="false" />
        
<intercept-url pattern="/api/**" method="GET" />
        
<custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
        
<access-denied-handler ref="oauthAccessDeniedHandler" />
    
</http>

    

<!-- For logout --> 

<http pattern="/logout" create-session="never"
          
entry-point-ref="oauthAuthenticationEntryPoint"
          
xmlns="http://www.springframework.org/schema/security">
        
<anonymous enabled="false" />
        
<intercept-url pattern="/logout" method="GET" />
        
<sec:logout invalidate-session="true" logout-url="/logout" success-handler-ref="logoutSuccessHandler"   />
        
<custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
        
<access-denied-handler ref="oauthAccessDeniedHandler" />
    
</http>

    

<bean id="logoutSuccessHandler" class="com.lb.security.service.LogoutImpl" >
        
<property name="tokenstore" ref="tokenStore">
</property>
    
</bean>

    

<bean id="oauthAuthenticationEntryPoint"
          class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    </bean>

    

<bean id="clientAuthenticationEntryPoint"
          class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
        <property name="realmName" value="springsec/client" />
        
<property name="typeName" value="Basic" />
    
</bean>

    

<bean id="oauthAccessDeniedHandler"
          class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler">
    </bean>

    

<bean id="clientCredentialsTokenEndpointFilter"
          class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
        <property name="authenticationManager" ref="authenticationManager" />
    
</bean>

    

<authentication-manager alias="authenticationManager"
                            xmlns="http://www.springframework.org/schema/security">
        
<authentication-provider user-service-ref="clientDetailsUserService" />
    
</authentication-manager>

    

<bean id="clientDetailsUserService"
          class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
        <constructor-arg ref="clientDetails" />
    
</bean>

    

<bean id="clientDetails" class="com.test.security.service.ClientDetailsServiceImpl"/>

    

<authentication-manager id="userAuthenticationManager"
                            xmlns="http://www.springframework.org/schema/security">
        
<authentication-provider  ref="customUserAuthenticationProvider">
        
</authentication-provider>
    
</authentication-manager>

    

<bean id="customUserAuthenticationProvider"
          class="com.test.security.service.CustomUserAuthenticationProvider">
    
</bean>

    

<oauth:authorization-server
            
client-details-service-ref="clientDetails" 
token-services-ref="tokenServices">
        
<oauth:authorization-code />
        
<oauth:implicit/>
        
<oauth:refresh-token/>
        
<oauth:client-credentials />
        
<oauth:password authentication-manager-ref="userAuthenticationManager"/>
    </oauth:authorization-server>

    

<oauth:resource-server id="resourceServerFilter"
                          
 resource-id="springsec" token-services-ref="tokenServices" />

    

<bean id="tokenStore"
          class="org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore" />

    

<bean id="tokenServices"
          class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
        
<property name="tokenStore" ref="tokenStore" />
        
<property name="supportRefreshToken" value="true" />
        
<property name="accessTokenValiditySeconds" value="1500"></property>
        
<property name="clientDetailsService" ref="clientDetails" />
    
</bean>


    

<mvc:annotation-driven />  
  

    
<mvc:default-servlet-handler />




</beans>


spring-context.properties


 # LDAP context source properties

ldap.url=ldap://<your-ldap-uri-here>:389/



#The ldap.base may be specific to your organization. Please confirm to your Organization Settings
#please check for additional entries required for you (should be used as a comma separated string)
ldap.base=DC=com



ldap.userdn=<your-ldap—domain>\\username

ldap.password=password
ldap.pooled=true

ClientDetailsServiceImpl.java

package com.test.security.service;



import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;

import org.springframework.security.oauth2.provider.ClientDetails;

import org.springframework.security.oauth2.provider.ClientDetailsService;

import org.springframework.security.oauth2.provider.NoSuchClientException;

import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import org.springframework.stereotype.Service;

import java.util.ArrayList;

import java.util.List;




@Service

public class ClientDetailsServiceImpl implements ClientDetailsService {

    

public ClientDetails loadClientByClientId(String clientId)
            throws OAuth2Exception {
        
if (clientId.equals(“myClient”)) {

            
List<String> authorizedGrantTypes=new ArrayList();
            authorizedGrantTypes.add("password");
            
authorizedGrantTypes.add("refresh_token");
            authorizedGrantTypes.add("client_credentials");

            

BaseClientDetails clientDetails = new BaseClientDetails();
            clientDetails.setClientId("myClient");
            
clientDetails.setClientSecret("secret1”);
            clientDetails.setAuthorizedGrantTypes(authorizedGrantTypes);

            

List<String> scopes = new ArrayList<>();
           
 scopes.add("read");
            
 scopes.add("write");
            
 clientDetails.setScope(scopes);

            

return clientDetails;

        

} else{
            
throw new NoSuchClientException("No client FOUND ... with requested id: "
                    + clientId);
        }
    

}

}


CustomUserAuthenticationProvider.java


package com.test.security.service;



import com.test.models.UserDetails;

import com.test.security.AuthenticationManager;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.core.GrantedAuthority;

import javax.naming.NamingException;

import java.util.ArrayList;
import java.util.List;



public class CustomUserAuthenticationProvider implements AuthenticationProvider {


    

@Autowired

private AuthenticationManager authManager;

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

       
        

         if(isValidLADPUser(authentication.getPrincipal(), authentication.getCredentials()))
        {
 
           List<GrantedAuthority> grantedAuthorities = new ArrayList();
   
           UsernamePasswordAuthenticationToken auth=new 
                      UsernamePasswordAuthenticationToken(authentication.getPrincipal(), 
                          authentication.getCredentials(),grantedAuthorities);

            
           return auth;
        
          }else{
            
             throw new BadCredentialsException("Bad User Credentials PROVIDED....");
        
          }
    
}


    

public boolean supports(Class<?> arg0) {
        
// TODO Auto-generated method stub
        
return true;
    
}


//LDAP Authentication goes here

public boolean isValidLADPUser(Object username, Object password){

    

UserDetails userDetails = null;
    
if(null != username && null != password){

        
  try {
            
        String fullusername = username.toString();
            
        userDetails = authManager.login(!fullusername.contains("\\") ? 
                              "<your-ldap-domain>\\".concat(fullusername):
                              fullusername, password.toString());
        
        } catch (NamingException e) {
            
            return false;
        
        }

    
}
    
return (null != userDetails);

}

}

AuthenticationManager.java

package com.test.security;



import com.test.models.UserDetails;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.ldap.core.LdapTemplate;

import org.springframework.ldap.filter.AndFilter;

import org.springframework.ldap.filter.EqualsFilter;

import org.springframework.ldap.support.LdapUtils;


import javax.naming.NamingEnumeration;

import javax.naming.NamingException;

import javax.naming.directory.DirContext;

import javax.naming.directory.SearchControls;

import javax.naming.directory.SearchResult;

import java.util.ArrayList;
import java.util.List;



public class AuthenticationManager {

    

private String authenticationErrorMessage;
    
@Autowired
    
private LdapTemplate ldapTemplate;
    
@Autowired
    
private UserDetails userDetails;

    
public UserDetails login(String userName, String password) throws NamingException {

        
// If User is not a part of user groups this message is displayed.
        
authenticationErrorMessage = “Invalid User Group”;



        
List<?> lstUser = getUsers(userName, password);
        
if (lstUser != null && lstUser.size() == 1) {
            
userDetails = (UserDetails) lstUser.get(0);
           
 return userDetails;
        
}
        
return null;
    
}

    


public List<UserDetails> getUsers(String userName, String password) throws NamingException {
        
DirContext ctx;
        
try {
            ctx = ldapTemplate.getContextSource().getContext(userName, password);
        } catch (org.springframework.ldap.NamingException ne) {
            

authenticationErrorMessage = “Invalid User”;
            
return null;
        
}
        

List<UserDetails> list = new ArrayList<UserDetails>();
        
NamingEnumeration<SearchResult> results = null;
       
 try {
             
     SearchControls controls = new SearchControls();
     
         controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            
      results = ctx.search("", buildFilter(userName), controls);

            
     while (results.hasMoreElements()) {
                
         SearchResult search = results.next();
             
       list.add(BeanUtility.convertToContact(search.getAttributes(),
        
                search.getNameInNamespace()));
            
     }
        
} finally {
            
    LdapUtils.closeContext(ctx);
            
    if (results != null) {
                
       results.close();
            
    }
        
}
return list;
    

}

    



private String buildFilter(String userid) {
        
  AndFilter _andFilter = new AndFilter();
        
 _andFilter.and(new EqualsFilter("objectclass", "user"));
        
 _andFilter.and(new EqualsFilter("sAMAccountName", userid
                .substring(userid.indexOf("\\") + 1)));
        return _andFilter.encode();
    

}

    


public UserDetails getUserDetails() {
        
return userDetails;
    
}

    

public void setUserDetails(UserDetails userDetails) {
        
 this.userDetails = userDetails;
    
}

    

public LdapTemplate getLdapTemplate() {
        
return ldapTemplate;
    
}

    

public void setLdapTemplate(LdapTemplate ldapTemplate) {
        
this.ldapTemplate = ldapTemplate;
    
}

    

public String getAuthenticationErrorMessage() {
       
 return authenticationErrorMessage;
    
}


}

UserDetails.java


package com.test.models;


import org.springframework.stereotype.Component;


import java.io.Serializable;



@Component

public class UserDetails implements Serializable {
    

private String fName;
    
private String lName;

    

public String getfName() {
        
return fName;
    
}

    
public void setfName(String fName) {
        
this.fName = fName;
    
}

    
public String getlName() {
        
return lName;
    
}

    
public void setlName(String lName) {
        
this.lName = lName;
    
}

    

@Override
    
public String toString() {
        
return "UserDetails{" +
                "fName='" + fName + '\'' +
                ", lName='" + lName + '\'' +
                
                '}';
   
 }


}


BeanUtility.java


package com.test.utils;



import com.test.models.UserDetails;

import javax.naming.NamingException;

import javax.naming.directory.Attributes;

public class BeanUtility {

    

public static UserDetails convertToContact(Attributes attributes, String fullRDN) throws NamingException {
        
UserDetails userDetails = new UserDetails();
        
try {
            
userDetails.setfName((String) attributes.get("givenname").get());
            userDetails.setlName((String) attributes.get("sn").get());
       
 } catch (Exception e) {
            
        
}
        

return userDetails;
    
}

}

LogoutImpl.java

package com.test.security.service;



import org.springframework.security.core.Authentication;

import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;

import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;




public class LogoutImpl implements LogoutSuccessHandler  {


    

InMemoryTokenStore  tokenstore;

    

public InMemoryTokenStore getTokenstore() {
        
return tokenstore;
    
}

    

public void setTokenstore(InMemoryTokenStore tokenstore) {
        
this.tokenstore = tokenstore;
    
}

    

@Override
    
public void onLogoutSuccess(HttpServletRequest paramHttpServletRequest,
            HttpServletResponse paramHttpServletResponse,
            Authentication paramAuthentication) throws IOException,
            ServletException {
        

removeaccess(paramHttpServletRequest);
        paramHttpServletResponse.getOutputStream().write("\n\tLogged Out successfully.".getBytes());

    
}


   
  
public void removeaccess(HttpServletRequest req){

        
String tokens=req.getHeader("Authorization");
        
System.out.println(tokens);
        
String value=tokens.substring(tokens.indexOf(" ")).trim();
        
DefaultOAuth2AccessToken token= new DefaultOAuth2AccessToken(value);
        System.out.println("This token: "+token);
        
tokenstore.removeAccessToken(value);
        
System.out.println("\n\tAccess Token got Removed Successfully!!");

    
}



}

MyResources.java


package com.test.resources;



import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseBody;



@Controller

@RequestMapping("/api")

public class MyResources {


    

@RequestMapping(value = “/myInfo", method = RequestMethod.GET)
    
@ResponseBody
    
public String createInfo(){
        
   return "\n\n\tProtected Resource “myInfo” Accessed !\n";

    
}

    

@RequestMapping(value = "/auth/signedin", method = RequestMethod.GET)
    
@ResponseBody
    
public String signedin(){
        
  return "{\"Login\": \"valid\" }";

    
}



}

Request/Response

Assuming my project (root-context) name is oauth2poc, from a Mac Terminal try following . . .

Resource Owner Password Credentials Grant

The resource owner password credentials authorization grant method works by giving the client application access to the resource owners credentials.

Request (for token):

curl -X POST localhost:9004/oauth2poc/oauth/token -d "grant_type=password" -d "client_id=myClient" -d "client_secret=secret1" -d "username=your_username" -d "password=your_password"

Response:

{
    "access_token": "9uiodea3-ce51-4bab-a9cf-c6f66900opp00",
    "expires_in": 1499,
    "scope": "read,write",
    "token_type": "bearer"
}



Request (for protected resources):

curl localhost:9004/oauth2poc/api/auth/signedin -H "Authorization: Bearer  9uiodea3-ce51-4bab-a9cf-c6f66900opp00"

Response:

{ "Login" : "valid"} 

Request (for protected resources):

curl localhost:9004/oauth2poc/api/myInfo -H "Authorization: Bearer  7eeedea3-ce51-4bab-a9cf-c6f66900d22b"

Response:

Protected Resource “myInfo” Accessed !

Client Credentials Grant

Client credential authorization is for the situations where the client application needs to access resources or call functions in the resource server, which are not related to a specific resource owner.

Request (for token):

curl -X POST localhost:9004/oauth2poc/oauth/token -d "grant_type=client_credentials" -d "client_id=myClient" -d "client_secret=secret1"

Response:

{
    "access_token": "Zxaedea3-ce51-4bab-a9cf-c6f66900d23d",
    "expires_in": 1499,
    "scope": "read,write",
    "token_type": "bearer"
}


Please note: if you are trying from a REST Client tool, such as Postman in Chrome browser, while requesting/accessing token, set in header, Content-type as x-www-form-urlencoded. And while accessing protected resources after receiving token bearer, set the Authorization: Bearer <access-token-here> in the header, while sending the request.

12/22/16

Angular 2 + Spring Boot application with maven

I'm using typescript .ts files in my Angular 2 + Spring Boot application with maven. I run npm install for dependencies and npm run tsc for converting .ts files to .js by exec-maven-plugin.
Below is the plugin portion from my pom.xml. In my application, pacakge.json, tsconfig.json and typings.json all under src/main/resources path, so run npm tasks under the path
pom.xml

     org.springframework.boot
spring-boot-starter-parent 1.3.5.RELEASE war org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 org.springframework.boot spring-boot-maven-plugin org.codehaus.mojo exec-maven-plugin exec-npm-install generate-sources ${project.basedir}/src/main/resources npm install exec exec-npm-run-tsc generate-sources ${project.basedir}/src/main/resources npm run tsc exec
My Angular2 + Spring Boot application folder structure is like below
src/main/resources
                  /app          - .ts and converted .js
                  /css
                  /images
                  /js           - systemjs.config.js is also placed here
                  /node_modules - generated by npm install and will include in war
                  /typings
                  application.properties
                  package.json
                  tsconfig.json
                  typings.json

src/main/webapp
               /WEB-INF
                       /jsp     - all .jsp files
On .jsp file head section, include the systemjs.config.js

<script type="text/javascript" src="webjars/zone.js/0.6.12/dist/zone.js"></script> <script type="text/javascript" src="webjars/reflect-metadata/0.1.3/Reflect.js"></script> <script type="text/javascript" src="webjars/systemjs/0.19.27/dist/system.js"></script> <script type="text/javascript" src="js/systemjs.config.js"></script>
<script type="text/javascript" src="webjars/zone.js/0.6.12/dist/zone.js"></script>
<script type="text/javascript" src="webjars/reflect-metadata/0.1.3/Reflect.js"></script>
<script type="text/javascript" src="webjars/systemjs/0.19.27/dist/system.js"></script>
<script type="text/javascript" src="js/systemjs.config.js"></script>
Also here is my WebMvcConfigurerAdapter code to mapping path
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.my.controller")
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if (!registry.hasMappingForPattern("/webjars/**")) {
            registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
        if (!registry.hasMappingForPattern("/images/**")) {
            registry.addResourceHandler("/images/**").addResourceLocations("classpath:/images/");
        }
        if (!registry.hasMappingForPattern("/css/**")) {
            registry.addResourceHandler("/css/**").addResourceLocations("classpath:/css/");
        }
        if (!registry.hasMappingForPattern("/js/**")) {
            registry.addResourceHandler("/js/**").addResourceLocations("classpath:/js/");
        }
        if (!registry.hasMappingForPattern("/app/**")) {
            registry.addResourceHandler("/app/**").addResourceLocations("classpath:/app/");
        }
        if (!registry.hasMappingForPattern("/node_modules/**")) {
            registry.addResourceHandler("/node_modules/**").addResourceLocations("classpath:/node_modules/");
        }
    }

    @Bean
    public InternalResourceViewResolver internalViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        viewResolver.setOrder(1);
        return viewResolver;
    }
}
One thing I want to mention is there some hack on running the exec-maven-plugin on eclipse if the os is Windows or Mac. Linux(Ubuntu) looks no issue at all. When run the build from eclipse on Windows or Mac, the problem is it doesn't understand npm command and try to find such file, even though the maven build is totally fine on Terminal or Command Window.
To solve such issue, I did some tweak. For Mac, making symbolic link for node and npm under/usr/bin path like below. However modifying /usr/bin is not allowed, so I done after rebooting by recovery disk
lrwxr-xr-x     1 root   wheel        17 May 22 03:01 node -> ../local/bin/node
lrwxr-xr-x     1 root   wheel        44 May 22 02:50 npm -> ../local/lib/node_modules/npm/bin/npm-cli.js
For Windows, I made node.bat and npm.bat file under system path like below After doing this, the maven build totally fine from eclipse and command window both on Windows 10.
npm.bat
@echo off
set arg1=%1
set arg2=%2
C:\Progra~1\nodejs\npm.cmd %arg1% %arg2%