How to Secure Your Web Application Part II: Securing Your Web Application With Spring Security

Dustin Noah Young
edataconsulting

--

Welcome to part 2 of this tutorial (if you haven’t yet, go check out part 1). Before we move on, let’s have a look at what we’ve accomplished so far:

We have created a simple web application that functions as a fan page for a space-faring agency. The application consists of a back-end API that uses Maven and Spring Boot for Java, and a front end that uses Node.js and Angular. Currently, a user can access three pages:

  • Home — a page that displays announcements
  • Q&A — a page that displays questions and answers
  • Users — a page that displays users

When a user clicks on any of these pages, the front end will send a request to the respective endpoint in the back-end API in order to retrieve the data that should be displayed.

As of now, all three of these pages are unsecured, and in this article we are going to secure them using Spring Security.

Our goal is to enforce the following rules to our pages:

  • Home — shall remain accessible to anyone, with or without authentication
  • Q&A — shall only be accessible to anyone who is authenticated
  • Users — shall only be accessible to anyone who is authenticated AND authorized to do so, meaning they need the right permissions for this

You can find the code to this tutorial on GitHub (git-checkout the branch ‘app-secured’). With that settled, let’s begin.

Step 1: Adding A Database

In order to properly demonstrate the authentication and authorization functionality of Spring Security, we are going to make some adjustments to the application. We are going to add a simple H2 database and populate it with some registered users. This will make our scenario a little more believable. Additionally, we will add Hibernate to manage the necessary queries to the database and adjust the code accordingly.

Let’s start by adding the H2 and Spring Boot Starter Data JPA (which includes Hibernate as the standard JPA implementation) dependencies to the pom.xml file:

...
<dependencies>
...
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
...
</dependencies>
...

H2 is an in-memory relational database written in Java. It’s perfect for testing and demonstration purposes, hence why we are using it here.

Since it’s designed as an in-memory database its nature is very volatile. When we start our application the database is created, and when we stop it the database is deleted. You can configure your H2 to store the data that you inserted or changed at runtime but we are not going to need that for this example. Nonetheless, we need to configure our app to use the database in the first place, and to do this we need to go to the application.properties file and add the following lines:

spring.datasource.url=jdbc:h2:mem:spacedb // (1)
spring.datasource.driverClassName=org.h2.Driver // (2)
spring.datasource.username=sa // (3)
spring.datasource.password=sa // (3)
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect // (4)
spring.jpa.hibernate.ddl-auto=none // (5)
spring.h2.console.enabled=true // (6)

With these properties we are stating the following:

  1. The connection URL to the database.
  2. The database driver that is needed for the connection.
  3. User credentials to access the database. You can also leave the password empty if you want to.
  4. The SQL dialect that Hibernate should use when communicating with the database.
  5. Here we are stating that we do not want to use Hibernate for the DDL (Data Definition Language) generation process. That means that our table for registered users is not going to be generated using the entity definitions in our code. Instead, we are going to do that via an SQL file.
  6. This simply lets us access the H2 console in a browser which is great for checking if our table has been created and populated with data. I will demonstrate this after we are done adding and configuring Spring Security.

Next, let’s add the SQL files schema.sql and data.sql to our src/main/resources folder for the DDL and DML (Data Manipulation Language), part. In our case, it’s enough to add a table for the registered users and to insert two users, one that is allowed to visit the Users page (an admin), and one that isn’t (a member).

Looking back at the first article, we hardcoded some user objects in the back end that are sent to the front end when the Users page is accessed. In order to avoid confusion, we shall call our database table t_registered_user. This way we can better differentiate between the objects of type User (the data requested by the front end for display purposes) and the objects of type RegisteredUser (the actual user data with which the app can authenticate/authorize requests).

schema.sql

DROP TABLE IF EXISTS t_registered_user;

CREATE TABLE t_registered_user (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
permission VARCHAR(255) NOT NULL
);

data.sql

INSERT INTO t_registered_user (username, password, permission) VALUES ('Member', '$2a$12$nz69C.Q21r95H3PB7YaLvOo/R6J8mHOSMEjTfR8JSXVR5FTGQrOPS', 'Member');

INSERT INTO t_registered_user (username, password, permission) VALUES ('Admin', '$2a$12$5YxjTJbYYjsOe6bDJ4CKg.rqoOrjbFemh5y.t6P/JzqI0tLGVnLLu', 'Admin');

Notice that the passwords are encrypted here, as they should be. They are encrypted with the BCrypt algorithm which is the default for Spring Security. Just so we can test it later, here are the passwords:

username: Member, password: member<3username: Admin, password: admin<3

After that, we need to create two new packages in our project: entity and repository. The project structure should now look like this:

In the entity package, we will add a class for our registered users.

RegisteredUser.java

@Getter
@Setter
@Entity
@Table(name="t_registered_user")
public class RegisteredUser {
@Id
private long id;
private String username;
private String password;
private String permission;

protected RegisteredUser() {}
}

Since we are using Lombok, we can add the @Getter and @Setter annotations here so that we do not need to write the getter and setter methods ourselves.
The @Entity annotation is used to tell Hibernate that this is an entity class, meaning that this class represents a table in the database. We are also using the @Table annotation to tell Hibernate what the table is called in the database. If we omit this, Hibernate will assume that there is a table called ‘RegisteredUser’. Since the names of the fields are equivalent to the column names of our table, we do not need to map them explicitly (although it could be done with the @Column annotation). The @Id annotation is an exception as it’s there to mark the field that holds a row’s unique identifier, and it’s required by JPA.

Now, we can add the repository which will be responsible for getting the registered users from the database to the repository package.

RegisteredUserRepository.java

@Repository
public interface RegisteredUserRepository extends
JpaRepository<RegisteredUser, Long> {

RegisteredUser findByUsername(String username);
}

NOTE: The right thing to do would be to move all the data that the front end can request (announcements, posts for the Q&A page, and users) to the database. After that, we would need to add the corresponding entities and repositories, and adjust the services accordingly. If you are not familiar with the JPA standard and/or ORMs like Hibernate, I can only encourage you to check them out.

Step 2: Adding Spring Security to the Back End

Now, the moment we have all been waiting for: let’s secure our app with Spring Security (if you want an introduction to Spring Security, go check out part 1. I also have some links there for further reading).

To add Spring Security to your Maven app, go to the pom.xml file located in the root directory for the back end and add the following dependency:

...
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId
</dependency>
...
</dependencies>
...

By doing this we have successfully added Spring Security to our app but we also want to configure it so that it behaves like we want it to. To do this, we must add another configuration file to our config/ folder. This configuration file must fulfill the following points:

  • It needs to be annotated with the @EnableWebSecurity annotation
    This enables Spring Security’s web security support for our app
  • It needs to extend the WebSecurityConfigurerAdapter class
    This provides the configuration class with methods that we can override in order to add our own configurations for Spring Security

Now, by overriding the configure() method, we can configure Spring Security and tell it what endpoints to protect and which ones to leave alone.

WebSecurityConfiguration.java

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors() // (1)
.and()
.httpBasic() // (2)
.and()
.authorizeRequests() // (3)
.antMatchers("/user/").hasAuthority("Admin")
.antMatchers("/", "/announcement/", "/login/",
"/h2-console/**").permitAll()
.anyRequest().authenticated()
.and()
.headers().frameOptions().sameOrigin() // (4)
.and()
.csrf().disable(); // (5)
}
@Autowired
@Qualifier("userDetailsService")
UserDetailsService userDetailsService;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {

auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}

Let’s see what we did here:

  1. We’ve previously defined our CORS settings in WebConfiguration.java. To use those settings we need to register them with Spring Security (it will detect the settings automatically for us).
  2. Here we enable Basic Auth (username + password combination).
  3. If someone wants to access the /user/ endpoint they need to have the role ‘Admin’.
    The endpoints /, /announcement/, /login/, and
    /h2-console/** can be freely accessed by anyone, no authentication required.
    Any other request requires the user to be authenticated.
  4. By default the header for X-Frame-Options is set to ‘DENY’ by Spring Security. This disables rendering within an iframe which is done to prevent Clickjacking. These attacks could allow an attacker to use CSS styling to make a user click on something that they were not intending. By setting this configuration we are only enabling it for requests within the same domain. This will allow us to use the H2 console in a browser.
  5. CSRF is an exploit in which an attacker sends a malicious request to a web application on behalf of an authenticated user unbeknownst to them. It’s a serious exploit that must be avoided at all costs. The impact such an attack can have on your application depends on the endpoints you have exposed and if they trigger a state change in your application (usually POST and PUT requests do this).
    In our case, we just want to demonstrate the authentication and authorization processes, and the only POST request we have is the one for user authentication. To allow this POST request to come through, we will disable Spring Security’s CSRF protection for now (perhaps this can be covered by another article in the future).

In the configuration class we can also see a UserDetailsService bean and the configureGlobal() method. We’ll take a look at the former first.

We need to tell Spring Security how to authenticate users sending requests. In general, this part is highly dependent on how the application’s users are stored (e.g. directly in the application’s database, or in an external IdM — Identity Management — system). In our case, we store the users directly in the application’s database so we need to make sure that Spring Security is prepared for that.
To do that, we are going to create a service that implements the UserDetailService interface provided by Spring Security. We will then override the loadUserByUsername() method and add the logic required to query the database for a registered user.

RegisteredUserService.java

@Service("userDetailsService")
public class RegisteredUserService implements UserDetailsService {
@Autowired
private RegisteredUserRepository registeredUserRepository;

@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
RegisteredUser registeredUser =
this.registeredUserRepository.findByUsername(username);

if (registeredUser == null) {
throw new UsernameNotFoundException("The user " + username
+ " is not a registered user.");
}

return registeredUser;
}
}

In the @Service annotation we define an internal name for the service. Spring will use that internal name to map this service directly to the UserDetailsService bean that we annotated with the @Qualifier annotation in the configuration class.
As is evident in the implementation, the loadUserByUsername() method returns an object of type UserDetails. Since UserDetails is also an interface belonging to Spring Security, we are going to implement that too, but wait, we already have an entity for our registered users and it’s already mapped to the corresponding table in the database. Couldn’t we just use that?

Luckily, the answer is ‘yes’ (DRY FTW!).

RegisteredUser.java

@Getter
@Setter
@Entity
@Table(name="t_registered_user")
public class RegisteredUser implements UserDetails {
@Id
private long id;
private String username;
private String password;
private String permission;

protected RegisteredUser() {}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority role = new
SimpleGrantedAuthority(this.permission);
return Collections.singleton(role);
}

//
// The following methods will not be discussed in the article
//

@Override
public boolean isAccountNonExpired() { return true; }

@Override
public boolean isAccountNonLocked() { return true; }

@Override
public boolean isCredentialsNonExpired() { return true; }

@Override
public boolean isEnabled() { return true; }
}

When implementing the UserDetails interface, you must also override some methods. As you can see, there is a method called getAuthorities() and it expects a collection of objects of type GrantedAuthority or subclasses of it. Registered users just have one role in our case, that role is named ‘permission’ and it’s a simple string object. So we can wrap permission in a SimpleGrantAuthority object, which implements the GrantedAuthority interface, and return it in the form of a collection.
We also need to override two more methods than are displayed in the code above: getUsername() and getPassword(). However, we are using Lombok’s @Getter annotation and it takes care of that for us.
The methods below the comment are not relevant for this article so just set their return values to ‘true’.

Now, to the configureGlobal() method in the configuration class:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {

auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}

Here we are basically telling Spring Security to use the UserDetailsService bean we defined in the lines above and we are telling it to use the BCryptPasswordEncoder to encode the encrypted passwords we get from the database once a user tries to access a secured endpoint with an authentication header.

Now, just one more thing is missing for the back end. With Basic Auth, once a user logs in, they get a response back from the server that tells them if the authentication was successful or not. That is still missing on our side, so we will implement that in the form of a controller that leverages our previously defined service for registered users.
Upon successful authentication, we simply send back the user’s credentials, and if the authentication fails we send back a null value.

AuthenticationController.java

@RestController
@RequestMapping("/login")
public class AuthenticationController {
@Autowired
RegisteredUserService registeredUserService;

@PostMapping("/")
public AuthenticationCredentials login(@RequestBody
AuthenticationCredentials authenticationCredentials) {

RegisteredUser registeredUser = (RegisteredUser)
this.registeredUserService
.loadUserByUsername(authenticationCredentials.getUsername());

if (registeredUser == null) {
authenticationCredentials = null;
}

return authenticationCredentials;
}
}

Let’s run our app with this command:

mvn spring-boot:run

In a browser, navigate to the URL http://localhost:8080/rest/api/h2-console/, we can login to the H2 console with the username and password we defined in the application.properties file:

Once we are logged in, we can see that the t_registered_user table is present in the database. And by running a SELECT query we can retrieve the users we defined in the data.sql file:

Now, let’s use Postman to test our secured back-end REST API. In the images below we can see that http://localhost:8080/rest/api/announcement/ does not require any authentication headers to send the data:

The same cannot be said about http://localhost:8080/rest/api/questionAndAnswer/. It requires valid credentials in the Basic Auth header:

While http://localhost:8080/rest/api/user/ only works for users with the ‘Admin’ role:

And finally, we can see that you need valid credentials to perform a successful login (http://localhost:8080/rest/api/login/):

Step 4: Adding The Login And Logout Functionalities to The Front End

Since we are now finished with securing our back end, all we have to do now is adjust the front end accordingly. For this, we need to add a login page and we also need to add some logic to send an authentication request and handle the response.

We can start with the Login page by adding a new component to our component/ folder in the front end. For this, we can just execute the following command in a terminal:

ng generate component login

Now we just need to define the login form in the component’s HTML template and add a login button. Once the button is clicked a submit event is fired and the handler function login(), which is located in the component’s TypeScript file, will be executed.

login.component.html

<div>
<h1 class="header">Login</h1>
<form [formGroup]="credentials" class="contentList"
(submit)="login()">

<div class="form-group row justify-content-center text-center">
<label class="col-2" for="username">Username</label>
<input class="col-3" [formControlName]="'username'"
id="username" type="text"/>
</div>
<br/>
<div class="form-group row justify-content-center text-center">
<label class="col-2" for="password">Password</label>
<input class="col-3" [formControlName]="'password'"
id="password" type="password"/>
</div>
<br/>
<div class="row justify-content-center text-center">
<button class="col-2 button" type="submit">Login</button>
</div>
</form>
<br/>
<div *ngIf="showAuthenticationErrorMessage">
<p class="warningMessage">Warning: Authentication failed. Wrong
username and/or password was used.</p>
</div>
</div>

login.component.ts

@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
credentials: FormGroup;
showAuthenticationErrorMessage: boolean = false;

constructor(private authenticationService: AuthenticationService,
private formBuilder: FormBuilder) { }

ngOnInit(): void {
this.credentials = this.formBuilder.group({
username: '',
password: ''
})
}

login() {
this.authenticationService.login({
username: this.credentials.get('username').value,
password: this.credentials.get('password').value})
.subscribe({
error: error => {
this.showAuthenticationErrorMessage = true;
}
})
}
}

It’s also important to note that we are using reactive forms here. Since reactive forms define a model-driven approach we can define our form model as a form group comprised of two form controls: one for the username and one for the password. We do this using a FormBuilder, which allows us to use a simple syntax for the form definition. The [formGroup] and [formControl] directives in the HTML template allow us to bind our form to the model defined in the TypeScript file.
Once the user submits the form, the login() method is called. There the front-end service for authentication is used to send the username and password to the back end. If the authentication is not successful, an error message is displayed.

authentication.service.ts

@Injectable()
export class AuthenticationService {
private authenticatedUser:
BehaviorSubject<AuthenticationCredentials>; // (1)
public authenticatedUserAsObservable:
Observable<AuthenticationCredentials>; // (1)

constructor(private http: HttpClient,
private router: Router) {
this.authenticatedUser =
new BehaviorSubject<AuthenticationCredentials>
(JSON.parse(localStorage.getItem('user'))); // (1)
this.authenticatedUserAsObservable =
this.authenticatedUser.asObservable(); // (1)
}

public login(credentials: AuthenticationCredentials) { // (2)
return this.http.post<AuthenticationCredentials>
(environment.backEndUrl
.concat(RestEndpointConstants.LOGIN_ENDPOINT),
{username: credentials.username,
password: credentials.password})
.pipe(map(response => {
if (response.username) {
localStorage.setItem('user', JSON.stringify(credentials));
this.authenticatedUser.next(credentials);
this.router.navigateByUrl('');
}
}));
}

public logout() { // (3)
localStorage.removeItem('user');
this.authenticatedUser.next(null);
this.router.navigateByUrl('/login')
}
public getAuthenticatedUser() { // (4)
return this.authenticatedUser.value;
}
}
  1. We have defined a BehaviorSubject called authenticatedUser that can emit a new object of type AuthenticationCredentials. In the constructor, we initialize it with an object called ‘user’ that is located in the local storage. This allows the service to keep track of an authenticated user in case of a browser refresh. If an item called ‘user’ does not exist, localstorage.getitem() returns a null value instead.
    We have also defined an Observable object called authenticatedUserAsObservable, and in the constructor, we initialize it as an observable of authenticatedUser. So every time authenticatedUser emits a new value, this observable will notify every component subscribed to it.
  2. The login() method takes the user’s credentials as input and sends a POST request to the back end. If the response contains the username (authentication was successful), the user’s credentials are stored in the local storage, authenticatedUser emits them, and the user will be redirected to the Home page (the announcements page).
    NOTE: Basic Auth on its own is unsecure because the credentials are sent in an unencrypted fashion. We would have to use it in conjunction with HTTPS to make it more secure. This also applies to the request interceptor we are about to discuss. However, we will not implement this in this article.
  3. The logout() method does not send anything to the back end, instead, it simply removes the user from the local storage, emits a null value to authenticatedUser, and redirects the user back to the login page.
  4. We’ve also defined a getter for the value currently held by authenticatedUser — it will become relevant in the next paragraphs.

This is all fine and good, but as soon as the user has logged in, how do we make sure to send the credentials to the back end every time we go to the Q&A and Users pages? This is necessary so that Spring Security can determine whether the user is allowed to view that data or not.
We can ensure this by writing a request interceptor that implements Angular’s HttpInterceptor interface and adding it to a new folder called src/app/helpers/:

http-request-interceptor.ts

@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService)
{}

intercept(request: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {

const user = this.authenticationService.getAuthenticatedUser();
if (user) {
request = request.clone({
setHeaders: {
Authorization: 'Basic ' + btoa(user.username + ':'
+ user.password)
}
})
}
return next.handle(request);
}
}

Every time the front end sends a request to the back end, it’s intercepted by this class and the intercept() method is executed. We use the getter method mentioned above to see if the user is already authenticated. If so, we add the credentials to the request’s authorization header.

Now, to finally bind everything together, there are still a few things we need to do.

First, we register AuthenticationService and HttpRequestInterceptor in the AppModule. Both are registered in the ‘providers’ section. Note that I have also added the ReactiveFormsModule to the imports section. This is necessary since we are using reactive forms in our login component.

app.module.ts

@NgModule({
declarations: [
AppComponent,
UserComponent,
QuestionAndAnswerComponent,
AnnouncementComponent,
LoginComponent
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
ReactiveFormsModule
],
providers: [
AnnouncementService,
QuestionAndAnswerService,
UserService,
AuthenticationService,
DatePipe,
{ provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }

Next, we add new buttons to the app component’s HTML template for the login and logout functionalities.

app.component.html

<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<div class="navigationBarTitle navbar-brand">{{title}}</div>
<a class="nav-tabs navigationBarLink" routerLink="/">Home</a>
<a class="nav-tabs navigationBarLink"
routerLink="/questionsAndAnswers">Q&A</a>
<a class="nav-tabs navigationBarLink"
routerLink="/users">Users</a>
<div class="authButtons">
<a *ngIf="!authenticatedUser else logoutButton" class="nav-tabs
navigationBarLink" routerLink="/login">Login</a>
<ng-template #logoutButton>
<a class="nav-tabs navigationBarLink" (click)="logout()">
Logout <b>{{authenticatedUser.username}}</b>
</a>
</ng-template>
</div>
</nav>
<div class="componentContent">
<router-outlet></router-outlet>
</div>

And finally, we subscribe to the observable for the authenticated user in the authentication service and add the logout() method to AppComponent’s TypeScript file.

app.component.ts

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title: string = 'Space Travels Fan Page';
authenticatedUser: AuthenticationCredentials;

constructor(private authenticationService: AuthenticationService)
{

this.authenticationService.authenticatedUserAsObservable
.subscribe(authenticatedUser =>
this.authenticatedUser = authenticatedUser
);
}

logout() {
this.authenticationService.logout();
}
}

With the back end still running, we can now run our front end and test if everything works as it should.

We can see that accessing Home does not require us to be logged in. However, when accessing Q&A or Users we get a warning saying that the user must be logged in to view the page:

The Login page also works as expected, and when logged in as the user ‘Member’ we can finally view the Q&A page but not the Users page:

However, if we are logged in as ‘Admin’, we can view the Users page:

Logging in as an unknown user will result in a warning message appearing:

Additional Adjustments

To make the app a little nicer to look at and deal with I added and adjusted some styles and also added a component for the warning messages. Feel free to check the code for those changes if you are interested.

Conclusion

In this article, we managed to take our existing web application and secure the back-end REST API’s endpoints. We did this by adding an H2 database with some registered users, and by adding Spring Security to the back-end project and configuring it to behave as we want it to. We’ve also added a mechanism for the authentication process.
In the front end we added a login page and some logic to send authentication requests to the back end. We’ve also created an HTTP interceptor that adds an authenticated user’s credentials to the requests headers.
By running both projects and testing them together, we have proven that we now indeed have a secured web application.

And that about sums up our journey from creating an unsecured web application with Spring Boot and Angular, and securing it with Spring Security. I really hope these articles help and feel free to follow us on Medium in order to get notifications for more useful articles.

--

--

Dustin Noah Young
edataconsulting

Your friendly traveler, gamer, and nerd. I'm also a software developer. 📍New Zealand