This post shows how an AngularJS application can consume a REST API which is secured with Basic authentication using Spring Security.
Post Secure Spring REST API with Basic Authentication shows in great details how to secure a REST API using Basic authentication with Spring Security. That application will serve as a Back-end for this example. Although we will touch the main concepts here, complete code for the back-end will not be repeated here again. Please download, install and start that application locally in order to test the AngularJS app from this post.
We will mainly focus onto Front-end side which is pure AngularJS based application, communicating nicely with our REST-API. Let’s get started.
Traditional authentication approaches like login pages or session identification are good for web based clients involving human interaction but does not really fit well when communicating with [REST] clients which may not even be a web application. Think of an API over a server which tries to communicate with another API on a totally different server, without any human intervention.
Basic Authentication provides a solution for this problem, although not very secure. With Basic Authentication, clients send it’s Base64 encoded credentials with each request, using HTTP [Authorization] header . That means each request is independent of other request and server may/does not maintain any state information for the client, which is good for scalability point of view.
Since Authentication header needs to be sent with Each request, Interceptors are a good choice to handle that instead of manually specifying the header in all $http methods.
authInterceptor.js
angular.module('myApp') .factory('AuthInterceptor', [function() { return { // Send the Authorization header with each request 'request': function(config) { config.headers = config.headers || {}; var encodedString = btoa("bill:abc123"); config.headers.Authorization = 'Basic '+encodedString; return config; } }; }]);
Notice how we have used btoa()
function to get the Base64 encoded string from user-credentials. That’s all we need to enable basic authentication. Rest of the application is typical AngularJS app communicating with REST API on server. Now this interceptor needs to be registered with AngularJS application, as shown below.
app.js
'use strict'; var App = angular.module('myApp',[]); App.config(['$httpProvider', function($httpProvider) { $httpProvider.interceptors.push('AuthInterceptor'); }]);
Below service does not refer to any security related stuff. Thanks to interceptors, it is managed seperately.
UserService.js
'use strict'; angular.module('myApp').factory('UserService', ['$http', '$q', function($http, $q){ var REST_SERVICE_URI = 'http://localhost:8080/SecureRESTApiWithBasicAuthentication/user/'; var factory = { fetchAllUsers: fetchAllUsers, createUser: createUser, updateUser:updateUser, deleteUser:deleteUser }; return factory; function fetchAllUsers() { var deferred = $q.defer(); $http.get(REST_SERVICE_URI) .then( function (response) { deferred.resolve(response.data); }, function(errResponse){ console.error('Error while fetching Users'); deferred.reject(errResponse); } ); return deferred.promise; } function createUser(user) { var deferred = $q.defer(); $http.post(REST_SERVICE_URI, user) .then( function (response) { deferred.resolve(response.data); }, function(errResponse){ console.error('Error while creating User'); deferred.reject(errResponse); } ); return deferred.promise; } function updateUser(user, id) { var deferred = $q.defer(); $http.put(REST_SERVICE_URI+id, user) .then( function (response) { deferred.resolve(response.data); }, function(errResponse){ console.error('Error while updating User'); deferred.reject(errResponse); } ); return deferred.promise; } function deleteUser(id) { var deferred = $q.defer(); $http.delete(REST_SERVICE_URI+id) .then( function (response) { deferred.resolve(response.data); }, function(errResponse){ console.error('Error while deleting User'); deferred.reject(errResponse); } ); return deferred.promise; } }]);
user_controller.js
'use strict'; angular.module('myApp').controller('UserController', ['$scope', 'UserService', function($scope, UserService) { var self = this; self.user={id:null,username:'',address:'',email:''}; self.users=[]; self.submit = submit; self.edit = edit; self.remove = remove; self.reset = reset; fetchAllUsers(); function fetchAllUsers(){ UserService.fetchAllUsers() .then( function(d) { self.users = d; }, function(errResponse){ console.error('Error while fetching Users'); } ); } function createUser(user){ UserService.createUser(user) .then( fetchAllUsers, function(errResponse){ console.error('Error while creating User'); } ); } function updateUser(user, id){ UserService.updateUser(user, id) .then( fetchAllUsers, function(errResponse){ console.error('Error while updating User'); } ); } function deleteUser(id){ UserService.deleteUser(id) .then( fetchAllUsers, function(errResponse){ console.error('Error while deleting User'); } ); } function submit() { if(self.user.id===null){ console.log('Saving New User', self.user); createUser(self.user); }else{ updateUser(self.user, self.user.id); console.log('User updated with id ', self.user.id); } reset(); } function edit(id){ console.log('id to be edited', id); for(var i = 0; i < self.users.length; i++){ if(self.users[i].id === id) { self.user = angular.copy(self.users[i]); break; } } } function remove(id){ console.log('id to be deleted', id); if(self.user.id === id) {//clean form if the user to be deleted is shown there. reset(); } deleteUser(id); } function reset(){ self.user={id:null,username:'',address:'',email:''}; $scope.myForm.$setPristine(); //reset Form } }]);
index.html
<html>
<head>
<title>Form Demo</title>
<style>
.username.ng-valid {
background-color: lightgreen;
}
.username.ng-dirty.ng-invalid-required {
background-color: red;
}
.username.ng-dirty.ng-invalid-minlength {
background-color: yellow;
}
.email.ng-valid {
background-color: lightgreen;
}
.email.ng-dirty.ng-invalid-required {
background-color: red;
}
.email.ng-dirty.ng-invalid-email {
background-color: yellow;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="./css/app.css">
</head>
<body ng-app="myApp">
<div class="generic-container" ng-controller="UserController as ctrl">
<div class="panel panel-default">
<div class="panel-heading"><span class="lead">User Registration Form </span></div>
<div class="formcontainer">
<form ng-submit="ctrl.submit()" name="myForm" class="form-horizontal">
<input type="hidden" ng-model="ctrl.user.id" />
<div class="row">
<div class="form-group col-md-12">
<label class="col-md-2 control-lable" >
Shown below is the REST API our Angular app will be communicating with.
package com.websystique.springmvc.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; import com.websystique.springmvc.model.User; import com.websystique.springmvc.service.UserService; @RestController public class HelloWorldRestController { @Autowired UserService userService; //Service which will do all data retrieval/manipulation work //-------------------Retrieve All Users-------------------------------------------------------- @RequestMapping(value = "/user/", method = RequestMethod.GET) public ResponseEntity<List<User>> listAllUsers() { List<User> users = userService.findAllUsers(); if(users.isEmpty()){ return new ResponseEntity<List<User>>(HttpStatus.NO_CONTENT);//You many decide to return HttpStatus.NOT_FOUND } return new ResponseEntity<List<User>>(users, HttpStatus.OK); } //-------------------Retrieve Single User-------------------------------------------------------- @RequestMapping(value = "/user/{id}", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE}) public ResponseEntity<User> getUser(@PathVariable("id") long id) { System.out.println("Fetching User with id " + id); User user = userService.findById(id); if (user == null) { System.out.println("User with id " + id + " not found"); return new ResponseEntity<User>(HttpStatus.NOT_FOUND); } return new ResponseEntity<User>(user, HttpStatus.OK); } //-------------------Create a User-------------------------------------------------------- @RequestMapping(value = "/user/", method = RequestMethod.POST) public ResponseEntity<Void> createUser(@RequestBody User user, UriComponentsBuilder ucBuilder) { System.out.println("Creating User " + user.getName()); if (userService.isUserExist(user)) { System.out.println("A User with name " + user.getName() + " already exist"); return new ResponseEntity<Void>(HttpStatus.CONFLICT); } userService.saveUser(user); HttpHeaders headers = new HttpHeaders(); headers.setLocation(ucBuilder.path("/user/{id}").buildAndExpand(user.getId()).toUri()); return new ResponseEntity<Void>(headers, HttpStatus.CREATED); } //------------------- Update a User -------------------------------------------------------- @RequestMapping(value = "/user/{id}", method = RequestMethod.PUT) public ResponseEntity<User> updateUser(@PathVariable("id") long id, @RequestBody User user) { System.out.println("Updating User " + id); User currentUser = userService.findById(id); if (currentUser==null) { System.out.println("User with id " + id + " not found"); return new ResponseEntity<User>(HttpStatus.NOT_FOUND); } currentUser.setName(user.getName()); currentUser.setAge(user.getAge()); currentUser.setSalary(user.getSalary()); userService.updateUser(currentUser); return new ResponseEntity<User>(currentUser, HttpStatus.OK); } //------------------- Delete a User -------------------------------------------------------- @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE) public ResponseEntity<User> deleteUser(@PathVariable("id") long id) { System.out.println("Fetching & Deleting User with id " + id); User user = userService.findById(id); if (user == null) { System.out.println("Unable to delete. User with id " + id + " not found"); return new ResponseEntity<User>(HttpStatus.NOT_FOUND); } userService.deleteUserById(id); return new ResponseEntity<User>(HttpStatus.NO_CONTENT); } //------------------- Delete All Users -------------------------------------------------------- @RequestMapping(value = "/user/", method = RequestMethod.DELETE) public ResponseEntity<User> deleteAllUsers() { System.out.println("Deleting All Users"); userService.deleteAllUsers(); return new ResponseEntity<User>(HttpStatus.NO_CONTENT); } }
With two steps, you can enable the Basic Authentication in Spring Security Configuration.
1. Configure httpBasic
: Configures HTTP Basic authentication. [http-basic in XML]
2. Configure authentication entry point with BasicAuthenticationEntryPoint
: In case the Authentication fails [invalid/missing credentials], this entry point will get triggered. It is very important, because we don’t want [Spring Security default behavior] of redirecting to a login page on authentication failure [ We don’t have a login page]. Additionally, we want to remain stateless [no session information needs to be maintained on server]. Shown below is the complete Spring Security configuration with httpBasic and entry point setup.
package com.websystique.springmvc.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; 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; @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private static String REALM="MY_TEST_REALM"; @Autowired public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("bill").password("abc123").roles("ADMIN"); auth.inMemoryAuthentication().withUser("tom").password("abc123").roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/user/**").hasRole("ADMIN") .and().httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint()) .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//We don't need session. } @Bean public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){ return new CustomBasicAuthenticationEntryPoint(); } /* To allow Pre-flight [OPTIONS] request from browser */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**"); } }
package com.websystique.springmvc.security; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; public class CustomBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint { @Override public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName() + ""); PrintWriter writer = response.getWriter(); writer.println("HTTP Status 401 : " + authException.getMessage()); } @Override public void afterPropertiesSet() throws Exception { setRealmName("MY_TEST_REALM"); super.afterPropertiesSet(); } }
Back-end : Build and deploy the Backend from Post Secure Spring REST API with Basic Authentication.
Front-end : Download the AngularJS app from this post, deploy it [Put it in htdocs folder of your Apache server e.g, and start Apache]. You could have run FE, without even putting it behind Apache Server, but as your FE app grows with templates , it becomes a necessity to use a server.
Browse to http://localhost/AngularClientWithBasicAuth/
Open the Developer tools and verify the request, a Basic Authentication header is sent.
You might have notice that there is one more request towards server before the actual GET. It’s a OPTION request fired by browser itself, not under application control.
In order to allow this pre-flight request, we have adopted our Security configuration with web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
.
Front-end Application:
Back-end Application:
If you like tutorials on this site, why not take a step further and connect me on Facebook , Google Plus & Twitter as well? I would love to hear your thoughts on these articles, it will help improve further our learning process.
In this post we will be developing a full-blown CRUD application using Spring Boot, AngularJS, Spring Data, JPA/Hibernate and MySQL,…
Spring Boot complements Spring REST support by providing default dependencies/converters out of the box. Writing RESTful services in Spring Boot…
Being able to start the application as standalone jar is great, but sometimes it might not be possible to run…
Spring framework has taken the software development industry by storm. Dependency Injection, rock solid MVC framework, Transaction management, messaging support,…
Let's secure our Spring REST API using OAuth2 this time, a simple guide showing what is required to secure a…
So You've got the REST API for your application, and now you want to secure it. How to do that?…
View Comments
With the right handbag, a woman is unstoppable.
Webentwickler sind die Visionäre hinter erfolgreichen Online-Präsenzen.
Webentwickler sind die Innovationsmotoren der digitalen Ära.
Thank you for the auspicious writeup. It in fact was a amusement account it. Look advanced to more added agreeable from you! By the way, how could we communicate?
مع مجموعة واسعة من التركيبات ، بما في ذلك الأكواع ، المحملات ، الوصلات ، وأكثر من ذلك ، يقدم مصنع إيليت بايب Elite Pipe حلولًا شاملة لأنظمة وتركيبات الأنابيب المختلفة.
localhost not found in Access-Control-Allow-Origin header.
Error! Someone help me!!
I am new to Angular and Spring, and learning it. I have a doubt that whether authInterceptor.js file will be visible publicly in browser, as JS can be viewed by any one on browser. If yes, than how can spring REST webservice can be secure as anyone can know username and password for the authentication.
Great article. Would be nice to add an update for Angular 4.
Hi Websystique,
Thanks for your great article, I read one of your article angular + spring MVC and implemented to my project. now i am trying to learn Spring Security + angular, when i downloaded your sample code both angular & backend and tried to run it, i am getting error - Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Although i am adding CORSFilter as per your code. Please help me
Hi Atul, sorry i was out for sometime.Regarding your issue,you should not get this error if you have correctly added the CORS filter with Access-Control-Allow-Origin header for all.
Hi websystique, it would be great if you share the spring social tutorials.Thank YOU