TODO List Project - Spring Security

previous_step

Implementing Basic Authentication with Spring Boot and Spring Security

1. Overview of Security with Basic Auth and JWT

Next, we will implement a basic approach called basic authentication, there along with every request you’d need to send a user and password. So if I have to agree to this request, I would need to send in a user id and password. We will also discuss the fact that sending a user id and password with every request is not safe becaue if somebody gets the id and password then they can do everything with it. And that’s where we create a temporary token call JWT, i.e. JSON Web Token, and we will discuss about what is the difference between basic security and JWT, and how JWT is more secure. We enhance our services with authentication, and have front end application connect to it.

2. Setting Up Spring Security

The easiest way you can make something secure is adding a very simple dependency, spring-boot-starter-security. It is the spring boot starter for implementing security in web applications, as well as RESTful services.

Now, when visiting http://localhost:8080/*, it is taking us to log in page. There is a form in the login page. That’s why, this is called a form based authentication. The default username is user, and the default password is printed on the console. After inputing username and password, we can execute any number of other requests. So once you enter the user id and password with form based authenticcation, a session cookie is set. So there is a session for you which is created on the server side, and a cookie is now registered in your browser, and that cookie is sent along with every request.

3. Configuring Standard UserId and Password

We can set the username and password in application.properties.

spring.security.user.name=user
spring.security.user.password=password

4. Enhancing Angular Welcome Data Service to use Basic Auth

Now, if we click the button Get Welcome Message, we may encouter an errer. That is Access to XMLHttpRequest at 'http://localhost:8080/hello-world-bean/path-variable/teemo' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. To solve it, we need to add authentication in the header.

// http://localhost:8080/hello-world-bean/path-variable/teemo
executeHelloWorldServiceWithPathVariable(name) {
//console.log("Execute Hello World Bean Service");
  let basicAuthHeaderString = this.createBasicAuthenticationHttpHeader();

  let headers = new HttpHeaders({ Authorization: basicAuthHeaderString })

  return this.http.get<HelloWorldBean>(`http://localhost:8080/hello-world-bean/path-variable/${name}`, {headers: headers});
}

createBasicAuthenticationHttpHeader() {
  let username = 'teemo';
  let password = '233';
  let basicAuthHeaderString = 'Basic' + window.btoa(username + ':' + password);
  return basicAuthHeaderString;
}

Now the error messsage bocomes Access to XMLHttpRequest at 'http://localhost:8080/hello-world-bean/path-variable/teemo' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status. When we check the network, we can see there is a new request which is being sent. Let’s look at the General. There is a new request method which is being used. It is OPTIONS.

What is happending is as soon as we added basic authentication some kind of authentication to our request. Before making the http call, there is something called options check which is done and you can see that this options check is failing. We will fix it in the next step, along with something called CSRM.

5. Configuring Spring Security to Disable CSRF and Enable OPTION Requests

What happens is whenever we add authorization credentials on a GET request, or a POST request, what happens is a preflight request. So before sending the complete request, what happens is a request called options request is sent to check if you have the right permitions.
And what is happening is that options request is being denied right now. Next, we will fix it.
The other think will be working on this specific step, is something called CSRF, i.e. cross-site request format.

  • Find the method configure() in class WebSecurityConfigurerAdapter and override it to solve the problem with options, as well as to disable something calle CSRF.
    • .csrf().disable() : disable CSRF
    • .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() : except for options request to all URLS
    • .anyRequest().authenticated() : authenticate all the requests
    • .httpBasic() : use basic authentication

The code is shown as follows.

package com.example.todoList.basic.auth;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;

@Configuration
@EnableWebSecurity
public class SpringSecurityConfigurationBasicAuth extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    http
	  .csrf().disable()
	  .authorizeRequests()
	  .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
	  .anyRequest().authenticated()
	  .and()//.formLogin().and()
	  .httpBasic();
  }
}

Ensure that this call should be in the same or subclass of the TodoListApplication().

6. Creating Angular HttpInterceptor to Add Basic Auth Header

Now, we can successfully call the HelloWorldBean request. But other requests are not valid. To solve this, one option is adding the above authentication header in every http request. Absolutely this is not efficient.
Then we can use HttpInterceptor. The interceptors would enable us to add any request header to every request.

  • Create a service HttpInterceptor. ng generate service service/http/HttpIntercepterBasicAuth
  • Let HttpIntercepterBasicAuthService implementsHttpInterceptor and override method intercept(request: HttpRequest<any>, next: HttpHandler).
    • clone the request.
    • add authorization header.
    • send the modified request to the next HTTP handler.

The code is shown as follows.

export class HttpIntercepterBasicAuthService implements HttpInterceptor {

  constructor(

  ) { }

  intercept(request: HttpRequest<any>, next: HttpHandler) {
    let username = 'teemo';
    let password = '233';
    let basicAuthHeaderString = 'Basic ' + window.btoa(username + ':' + password);

    request = request.clone({
      setHeaders: {
        Authorization: basicAuthHeaderString
      }
    })

    return next.handle(request);
  }
}

7. Configure HttpInterceptor as Provider in App Module

Next, we configure HttpInterceptor to be used in our module. Where is that module? app.module.ts. In here we have to confinure a provider for HttpInterceptor. We can do it like this,

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: HttpIntercepterBasicAuthService, multi: true }
],

Now, all requests related to Todo Service work.

8. Create Basic Authentication RESTful Service in Spring Boot

The problem is the login page is still using hard-coded authentication. Now we’ll create a service in the backend. The idea behind that is when the user is trying to login, we try to hit the service, and if the service returns validate response, then that means the user is authenticated. Otherwise the user is not really authenticated.

We create a controller AuthenticationController.

@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class AuthenticationController {
  // GET
  @GetMapping(path = "/basicauth")
  public AuthenticationBean authBean() {
      return new AuthenticationBean("You are authenticated.");
  }
}

9. Create Angular Basic Authentication Service

We want to call the above RESTful service in our LoginComponent. First, let’s code basic-authentication.service.ts.

  • .pipe() : if the http request is successful, then do the things in pipe as well. In pipe(), we can define, if there is a proper response coming back, then map it.
  • An important thing about doing stuff when you are working on the observer is to return the data back. Because the observable is executed only when somebody subscribes to it. And they would want to see the data.
CLICK ME
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})

export class BasicAuthenticationService {

  constructor(
    private http: HttpClient
  ) { }

  authenticate(username, password) {
    if (username === 'teemo' && password === '233') {
      sessionStorage.setItem('authenticaterUser', username);
      return true;
    }

    return false;
  }

  executeAuthenticationService(username, password) {
    let basicAuthHeaderString = 'Basic ' + window.btoa(username + ':' + password);

    let headers = new HttpHeaders({
      Authorization: basicAuthHeaderString
    })

    return this.http.get<AuthenticationBean>(
      `http://localhost:8080/basicauth`, 
      {headers: headers}).pipe(
        map(
          data => {
            sessionStorage.setItem('authenticaterUser', username);
            return data;
          }
        )
      );
  }

  isUserLoggedIn() {
    let user = sessionStorage.getItem('authenticaterUser')
    return !(user === null)
  }

  logout() {
    sessionStorage.removeItem('authenticaterUser');
  }

}

export class AuthenticationBean {
  constructor(public message: string) {}
}

10. Connecting Login Page to Basic Authentication Service

In login.component.ts, add method handleBasicAuthLogin().

handleBasicAuthLogin() {
  this.basicAuthenticationService
    .executeAuthenticationService(this.username, this.password)
    .subscribe(
      data => {
        console.log(data);
        this.router.navigate(['welcome', this.username])
        this.invalidLogin = false;
      },
      error => {
        console.log(error)
        this.invalidLogin = true;
      }
    )
}

In the next few steps, we refactor our code in Angular Frontend to eliminate hard-code.

11. Refactoring Angular Basic Authentication Service

CLICK ME
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class BasicAuthenticationService {

  constructor(
    private http: HttpClient
  ) { }

  executeAuthenticationService(username, password) {
    
    let basicAuthHeaderString = 'Basic ' + window.btoa(username + ':' + password);

    let headers = new HttpHeaders({
      Authorization: basicAuthHeaderString
    })

    return this.http.get<AuthenticationBean>(
      `http://localhost:8080/basicauth`, 
      {headers: headers}).pipe(
        map(
          data => {
            sessionStorage.setItem('authenticaterUser', username);
            sessionStorage.setItem('token', basicAuthHeaderString);

            return data;
          }
        )
      );
  }

  getAuthenticatedUser() {
    return sessionStorage.getItem('authenticaterUser');
  }

  getAuthenticatedToken() {
    if (this.getAuthenticatedUser())
      return sessionStorage.getItem('token');
  }

  isUserLoggedIn() {
    let user = sessionStorage.getItem('authenticaterUser')
    return !(user === null)
  }

  logout() {
    sessionStorage.removeItem('authenticaterUser');
    sessionStorage.removeItem('token');
  }

}

export class AuthenticationBean {
  constructor(public message: string) {}
}

12. Refactoring HttpInterceptor to use Basic Authentication Token

Remove the hard-coded part, get the token and username from session.

CLICK ME
constructor(
  private basicAuthenticationService: BasicAuthenticationService
) { }

intercept(request: HttpRequest<any>, next: HttpHandler) {
  // let username = 'teemo';
  // let password = '233';
  //let basicAuthHeaderString = 'Basic ' + window.btoa(username + ':' + password);

  let basicAuthHeaderString = this.basicAuthenticationService.getAuthenticatedToken();
  let username = this.basicAuthenticationService.getAuthenticatedUser();

  if (basicAuthHeaderString && username) {
    request = request.clone({
      setHeaders: {
        Authorization: basicAuthHeaderString
      }
    })
  }

  return next.handle(request);
}

13. Best Practice - Use Constants for URLs and Tokens

Up till new we hard coded the URL of the services, i.e. http://localhost:8080/. One of the important thing is, as we change from one environment to another environment, this stuff can change. That’s the reason why we would actually want to create constants for this stuff.

Next, we will create a file app.contants.ts under folder app.

export const API_URL = "http://localhost:8080"

We can also use const in basic-authentication.service.ts. That is,

export const TOKEN = 'token';
export const AUTHENTICATED_USER = 'authenticaterUser';

Note that the above export const must be added above the @Injectable({ providedIn: 'root'})

So when we find ourselves using the same kind of constants, same kind of values multiple times, we can use a constants to present the value, and use it across the place. And app.constants.ts could be the single most place where we would put all the variables and when you’re building something for a different environment you can replace it with the specific value for a specific use, and be done with it.

next_step

   Reprint policy


《TODO List Project - Spring Security》 by Tong Shi is licensed under a Creative Commons Attribution 4.0 International License
  TOC