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 a pure AngularJS application, communicating nicely with our REST-API.
What is Basic Authentication?
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.
Front-end
1. Send Authorization header with each request in AngularJS
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.
2. Application
app.js
'use strict';
var App = angular.module('myApp',[]);
App.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
}]);
|
3. Service to communicate with REST API on server
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;
}
}]);
|
4. Controller
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
}
}]);
|
5. View
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"
for
=
"uname"
>Name</
label
>
<
div
class
=
"col-md-7"
>
<
input
type
=
"text"
ng-model
=
"ctrl.user.name"
id
=
"uname"
class
=
"username form-control input-sm"
placeholder
=
"Enter your name"
required
ng-minlength
=
"3"
/>
<
div
class
=
"has-error"
ng-show
=
"myForm.$dirty"
>
<
span
ng-show
=
"myForm.uname.$error.required"
>This is a required field</
span
>
<
span
ng-show
=
"myForm.uname.$error.minlength"
>Minimum length required is 3</
span
>
<
span
ng-show
=
"myForm.uname.$invalid"
>This field is invalid </
span
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
class
=
"row"
>
<
div
class
=
"form-group col-md-12"
>
<
label
class
=
"col-md-2 control-lable"
for
=
"age"
>Age</
label
>
<
div
class
=
"col-md-7"
>
<
input
type
=
"number"
ng-model
=
"ctrl.user.age"
id
=
"age"
class
=
"form-control input-sm"
placeholder
=
"Enter your Age"
/>
</
div
>
</
div
>
</
div
>
<
div
class
=
"row"
>
<
div
class
=
"form-group col-md-12"
>
<
label
class
=
"col-md-2 control-lable"
for
=
"salary"
>Salary</
label
>
<
div
class
=
"col-md-7"
>
<
input
type
=
"number"
ng-model
=
"ctrl.user.salary"
id
=
"salary"
class
=
"form-control input-sm"
placeholder
=
"Enter your Salary"
ng-pattern
=
"/^[0-9]+(\.[0-9]{1,2})?$/"
step
=
"0.01"
required/>
<
div
class
=
"has-error"
ng-show
=
"myForm.$dirty"
>
<
span
ng-show
=
"myForm.salary.$error.required"
>This is a required field</
span
>
<
span
ng-show
=
"myForm.salary.$invalid"
>This field is invalid </
span
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
class
=
"row"
>
<
div
class
=
"form-actions floatRight"
>
<
input
type
=
"submit"
value
=
"{{!ctrl.user.id ? 'Add' : 'Update'}}"
class
=
"btn btn-primary btn-sm"
ng-disabled
=
"myForm.$invalid"
>
<
button
type
=
"button"
ng-click
=
"ctrl.reset()"
class
=
"btn btn-warning btn-sm"
ng-disabled
=
"myForm.$pristine"
>Reset Form</
button
>
</
div
>
</
div
>
</
form
>
</
div
>
</
div
>
<
div
class
=
"panel panel-default"
>
<!-- Default panel contents -->
<
div
class
=
"panel-heading"
><
span
class
=
"lead"
>List of Users </
span
></
div
>
<
div
class
=
"tablecontainer"
>
<
table
class
=
"table table-hover"
>
<
thead
>
<
tr
>
<
th
>ID.</
th
>
<
th
>Name</
th
>
<
th
>Age</
th
>
<
th
>Salary</
th
>
<
th
width
=
"100"
>
</
tr
>
</
thead
>
<
tbody
>
<
tr
ng-repeat
=
"u in ctrl.users"
>
<
td
><
span
ng-bind
=
"u.id"
></
span
></
td
>
<
td
><
span
ng-bind
=
"u.name"
></
span
></
td
>
<
td
><
span
ng-bind
=
"u.age"
></
span
></
td
>
<
td
><
span
ng-bind
=
"u.salary"
></
span
></
td
>
<
td
>
<
button
type
=
"button"
ng-click
=
"ctrl.edit(u.id)"
class
=
"btn btn-success custom-width"
>Edit</
button
> <
button
type
=
"button"
ng-click
=
"ctrl.remove(u.id)"
class
=
"btn btn-danger custom-width"
>Remove</
button
>
</
tr
>
</
tbody
>
</
table
>
</
div
>
</
div
>
</
div
>
<
script
src
=
"./js/app.js"
></
script
>
<
script
src
=
"./js/authInterceptor.js"
></
script
>
<
script
src
=
"./js/user_service.js"
></
script
>
<
script
src
=
"./js/user_controller.js"
></
script
>
</
body
>
</
html
>
|
Back-end
1. REST API
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);
}
}
|
2. Enable Basic Authentication in Spring Security
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();
}
}
|
Running the application
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 withweb.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
.
Download Source Code
Front-end Application:
Back-end Application: