Search code examples
angularjscorsjerseybasic-authentication

AngularJS $http, CORS and Basic authentication with Jersey


I am learning on AngularJS. I have created the sample project with CORS with Basic Authentication. My server side code is Jersey Rest. But Still I am getting 403 Forbidden Error. I don't understand. Am I missing something. My code is given below. Please help me to solve it.


ServerCode--

/*
 * This is the controller level Code to get all UserlogInHistoryAngular
 * information
 */
@GET
@Path("/getall")
@Produces({
  MediaType.APPLICATION_XML,
  MediaType.APPLICATION_JSON
})
public Response getAllUserLogInHistoryAngular() {
  log.info("Controller layer for Get All UserlogInHistory");
  UserLoginHistoryService userLogInHistoryService = new UserLoginHistoryService();

  GenericEntity < List < UserLogInHistory >> userLoginHistory = new GenericEntity < List < UserLogInHistory >> (
    userLogInHistoryService.getAllUserlogInHisotry()) {};

  return Response
    .status(200)
    .entity(userLoginHistory)
    .header("Access-Control-Allow-Credentials", true)
    .header("Access-Control-Allow-Origin", "*")
    .header("Access-Control-Allow-Methods",
      "GET, POST, DELETE, PUT, OPTIONS")
    .header("Access-Control-Allow-Headers",
      "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-CSRF-Token, Accept-Version, Content-Length, Content-MD5,  Date, X-Api-Version, X-File-Name")
    .allow("OPTIONS").build();    
}    
}

AngularJS Code--

var myapp = angular
  .module('myapp', ['angularUtils.directives.dirPagination']);

myapp.config(function($httpProvider) {
  //Enable cross domain calls
  $httpProvider.defaults.withCredentials = true;
  $httpProvider.defaults.useXDomain = true;
});


myapp.controller('customerController', function($scope, $http) {
      var encodingstring = window.btoa("numery" + ":" + "password");

      console.log(encodingstring);

      $http({
        withCredentials: true,
        headers: {
          'Authorization': 'Basic ' + encodingstring,
          'Content-Type': 'application/json; charset=utf-8'
        },
        method: "GET",
        url: "http://localhost:8080/userloginhistoryapi/rest/secured/userloginhistory/getall"
      }).then(function(response) {
        $scope.lstUser = response.data;
        //$log.info(response);

        console.log(response.data);
        console.log($scope.lstUser);
      })

      $scope.sortColumn = "name";
      $scope.reverseSort = false;

      $scope.sortData = function(column) {
        $scope.reverseSort = ($scope.sortColumn == column) ? !$scope.reverseSort : false;
        $scope.sortColumn = column;
      };

      $scope.getSortColumn = function(column) {

        if ($scope.sortColumn == column) {
          return $scope.reverseSort ? 'arrow-down' : 'arrow-up';
        }
        return '';
      };

      function getSelectedIndex(id) {
        for (var i = 0; i < $scope.listCustomers.length; i++)
          if ($scope.listCustomers[i].id == id)
            return i
        return -1;
      }; 

Error--

angular.js:13018 OPTIONS http://localhost:8080/userloginhistoryapi/rest/secured/userloginhistory/getall 403 (Forbidden)
(anonymous) @ angular.js:13018
sendReq @ angular.js:12744
serverRequest @ angular.js:12485
processQueue @ angular.js:17396
(anonymous) @ angular.js:17444
$digest @ angular.js:18557
$apply @ angular.js:18945
bootstrapApply @ angular.js:1939
invoke @ angular.js:5108
doBootstrap @ angular.js:1937
bootstrap @ angular.js:1957
angularInit @ angular.js:1842
(anonymous) @ angular.js:35431
trigger @ angular.js:3491
(index):1 Failed to load http://localhost:8080/userloginhistoryapi/rest/secured/userloginhistory/getall: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8081' is therefore not allowed access. The response had HTTP status code 403.
angular.js:15018 Possibly unhandled rejection: {"data":null,"status":-1,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","withCredentials":true,"headers":{"Authorization":"Basic bnVtZXJ5OnBhc3N3b3Jk","Accept":"application/json, text/plain, */*"},"url":"http://localhost:8080/userloginhistoryapi/rest/secured/userloginhistory/getall"},"statusText":"","xhrStatus":"error"}
(anonymous) @ angular.js:15018
(anonymous) @ angular.js:11302
processChecks @ angular.js:17428
$digest @ angular.js:18557
$apply @ angular.js:18945
done @ angular.js:12799
completeRequest @ angular.js:13056
requestError @ angular.js:12972
error (async)
(anonymous) @ angular.js:12985
sendReq @ angular.js:12744
serverRequest @ angular.js:12485
processQueue @ angular.js:17396
(anonymous) @ angular.js:17444
$digest @ angular.js:18557
$apply @ angular.js:18945
bootstrapApply @ angular.js:1939
invoke @ angular.js:5108
doBootstrap @ angular.js:1937
bootstrap @ angular.js:1957
angularInit @ angular.js:1842
(anonymous) @ angular.js:35431
trigger @ angular.js:3491
VM2032:185 [CodeLive] HTTP detected: Connecting using WS
VM2032:109 [CodeLive] Connected to CodeLive at ws://127.0.0.1:42529
bundle.js:10 license url https://www.genuitec.com/go/webclipse-buy-now

Solution

  • So here's what's going on. The CORS Preflight request is an OPTIONS request. This request happens prior to the real request. It checks with the server to see if the request is allowed. In response to the preflight request, the server should send back the Access-Control-X-X headers like you are in your getAllUserLogInHistoryAngular method.

    So what is happening in your case during the preflight, is that it hits the Basic Auth filter and automatically gets rejected without adding the CORS response headers. Your putting the CORS response headers in the resource method does nothing and is useless. Generally the CORS should be handled in a filter so it handles all requests prior to hitting the resource methods.

    Here's what you should do. Just like you did for Basic Auth, use a ContainerRequestFilter to handle the CORS. You want this filter to be called before the Auth filter because the CORS preflight doesn't care about authentication and it will not send the authentication credentials with the request. In this filter, you should check to see if it is a CORS preflight request. Generally this can be done by checking for an OPTIONS method and he presence of the Origin header. If it is a preflight, then abort the request and add the CORS headers in a ContainerResponseFilter. It might look something like

    @Provider
    @PreMatching
    public class CorsFilter implements ContainerRequestFilter, ContainerResponseFilter {
    
        @Override
        public void filter(ContainerRequestContext request) throws IOException {
            if (isPreflightRequest(request)) {
                request.abortWith(Response.ok().build());
                return;
            }
        }
    
        private static boolean isPreflightRequest(ContainerRequestContext request) {
            return request.getHeaderString("Origin") != null
                    && request.getMethod().equalsIgnoreCase("OPTIONS");
        }
    
        @Override
        public void filter(ContainerRequestContext request, ContainerResponseContext response)
                throws IOException {
            if (request.getHeaderString("Origin") == null) {
                return;
            }
            if (isPreflightRequest(request)) {
                response.getHeaders().add("Access-Control-Allow-Credentials", "true");
                response.getHeaders().add("Access-Control-Allow-Methods",
                    "GET, POST, PUT, DELETE, OPTIONS, HEAD");
                response.getHeaders().add("Access-Control-Allow-Headers",
                    "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-CSRF-Token, " +
                    "Accept-Version, Content-Length, Content-MD5,  Date, X-Api-Version, X-File-Name");
            }
            response.getHeaders().add("Access-Control-Allow-Origin", "*");
        }
    }
    

    Notice it's a @PreMatching filter, which will cause it to be called before your Auth filter (assuming that filter is not also a pre-matching filter, in which case you should also use the @Priority annotation).

    What this filter does is check to see if the request is a preflight request, and if it is, it aborts the entire request with a 200, which causes the request to skip the resource method, skip all other request filters after it, and jump to the response filter. In the response filter, we check to see if it is preflight by checking a property we set earlier. If it is, then we set the CORS response headers. If you want learn more about CORS and more about this specific filter implementation, check out this post.

    So no here's the flow of what will happen

    Client                    Browser                    Server
    ------                    -------                    ------
    
    Request endpoint -------> Remembers request -------> Sends back CORS
                              But first sends            response headers
                              CORS preflight             from CorsFilter
                                                                 |      
                              Grabs original    <----------------+
                              request, sends it
                                      |
                                      +----------------> Skips CorsFilter,
                                                         Goes to Auth filter,
                                                         Goes to resource
                                                         Sends resource response
                                                                 |
    Handle response  <------- Receives response <----------------+
                              Gives it to client