Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't set Authentication Header with Angular and Spring Security

I'm having trouble getting Angular, CORS, SpringSecurity and Basic Authentication to play nicely. I have the following Angular Ajax call where I'm trying to set the header to include a Basic Authorization header in the request.

var headerObj = {headers: {'Authorization': 'Basic am9lOnNlY3JldA=='}};
return $http.get('http://localhost:8080/mysite/login', headerObj).then(function (response) {
    return response.data;
});

I am running my angular app on localhost:8888 and my backend server on localhost:8080. So I had to add a CORS filter to my server side which is Spring MVC. So I created a filter and added it to my web.xml.

web.xml

<filter>
    <filter-name>corsFilter</filter-name>
    <filter-class>com.abcinsights.controller.SimpleCorsFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>corsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

CorsFilter

public class SimpleCorsFilter extends OncePerRequestFilter {

@Override
public void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
    Enumeration<String> headerNames = req.getHeaderNames();
    while(headerNames.hasMoreElements()){
        String headerName = headerNames.nextElement();
        System.out.println("headerName " + headerName);
        System.out.println("headerVal " + req.getHeader(headerName));
    }               
    HttpServletResponse response = (HttpServletResponse) res;
    response.setHeader("Access-Control-Allow-Origin", "*"); 
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
    response.setHeader("Access-Control-Max-Age", "3600");
    response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Range, Content-Disposition, Content-Type, Authorization");
    chain.doFilter(req, res);
}   
}

This all works fine and when I look at the header names/values I see my Authorization header with the Basic value and the hard coded Base64 string.

So then I tried to add SpringSecurity which would use the Authorization header to do basic authentication. Here's my web.xml and spring security config with Spring Security added in.

updated web.xml

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

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/config/security-config.xml</param-value>
</context-param>

<filter>
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

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

security-config.xml

<http auto-config="true" use-expressions="false">
    <intercept-url pattern="/**" access="ROLE_USER" />
    <http-basic/>
    <custom-filter ref="corsHandler" before="SECURITY_CONTEXT_FILTER"/>
</http>

<beans:bean id="corsHandler" class="com.abcinsights.controller.SimpleCorsFilter"/>

<authentication-manager>
    <authentication-provider>
        <jdbc-user-service data-source-ref="dataSource"/>
    </authentication-provider>
</authentication-manager>

As you can see, I had to add my CORS filter to the front of the spring security filter chain (I also removed it from web.xml). This seems to be working in the sense that the CORS filter still gets called when I make a request. But the request coming from my AJAX call no longer has the Authorization header added in. When I remove the SpringSecurity filter chain and just put my CORS filter back into web.xml the Authorization header comes back. I'm at a loss but am wondering if it has something to do with preflight communication working one way when my CORS filter is stand alone in web.xml and another way when it is in the Spring Security filter chain? Any help would be appreciated.

Thanks

like image 629
Joe Rice Avatar asked Jun 19 '15 10:06

Joe Rice


1 Answers

The problem was that angular was sending an HTTP OPTIONS request without the HTTP Authorization header. Spring security was then processing that request and sending back a 401 Unauthorized response. So my angular app was getting a 401 response from spring on the OPTIONS request rather than a 200 response telling angular to go ahead and send the POST request that did have the Authorization header in it. So the fix was to

a) Put my CORSFilter and my SpringSecurity filter in web.xml (rather than having my CORS Filter called from within Spring). Not 100% sure this made a difference but that's what my final web.xml looked like.

b) Added the code from the below link to my CORS Filter so that it doesn't delegate to SpringSecurity for OPTIONS request types.

This post was what got me sorted out: Standalone Spring OAuth2 JWT Authorization Server + CORS

Here's my code:

web.xml

<display-name>ABC Insights</display-name>
<servlet>
    <servlet-name>abcInsightsServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/config/servlet-config.xml</param-value>
    </init-param> 
</servlet>
<servlet-mapping>
    <servlet-name>abcInsightsServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

<!--  Stand Alone Cross Origin Resource Sharing Authorization Configuration -->
<filter>
    <filter-name>corsFilter</filter-name>
    <filter-class>com.abcinsights.controller.SimpleCorsFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>corsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>       
<!--  Spring Security Configuration -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/config/security-config.xml</param-value>
</context-param>    
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Here is my CORS Filter:

public class SimpleCorsFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;       
        response.setHeader("Access-Control-Allow-Origin", "*"); 
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Range, Content-Disposition, Content-Type, Authorization, X-CSRF-TOKEN");  

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }           
    }
}

Here is my Angular code:

var login = function (credentials) {
    console.log("credentials.email: " + credentials.email);
    console.log("credentials.password: " + credentials.password);

    var base64Credentials = Base64.encode(credentials.email + ':' + credentials.password);

    var req = {
        method: 'GET',
        url: 'http://localhost:8080/abcinsights/loginPage',
        headers: {
            'Authorization': 'Basic ' + base64Credentials
        }
    }

    return $http(req).then(function (response) {
        return response.data;
    });        
};

Hope that helps.

like image 66
Joe Rice Avatar answered Sep 21 '22 20:09

Joe Rice