4

Securing an Application with Spring Boot

In the previous chapter, we learned how to query for data using Spring Data JPA. We figured out how to write custom finders, use Query By Example, and even how to directly access the data store custom JPQL and SQL.

In this chapter, we’ll see how to keep our application secure.

Security is a critical issue. I have said, multiple times, that your application isn’t real until it’s secured.

But security isn’t just a switch we flip and we’re done. It’s a complex problem that requires multiple layers. It requires careful respect.

If there is one thing to appreciate as we dive into this chapter, it’s to never attempt to secure things on your own. Don’t roll your own solution. Don’t assume it’s easy. The person who wrote the commercial utility to crack Word documents for users who had lost their password said he introduced a deliberate delay so it didn’t appear instantaneous.

There are security engineers, computer scientists, and industry leaders who have studied application security for years. Adopting tools and practices developed by industry experts is the first step toward ensuring our application’s data and its users are properly protected.

That’s why our first step will be to turn to a well-respected security tool: Spring Security.

Spring Security has been developed in the open from the beginning (2003). This framework isn’t proprietary but instead has had contributions from well-respected security professionals around the globe. Also, it’s actively maintained by a dedicated portion of the Spring team.

In this chapter, we’ll cover the following topics:

  • Adding Spring Security to a Spring Boot application
  • Creating our own users with a custom security policy
  • Swapping hardcoded users with a Spring Data-backed set of users
  • Leveraging Google to authenticate users
  • Securing web routes and HTTP verbs
  • Securing Spring Data methods

Where to find this chapter’s code

The source code for this chapter can be found at https://github.com/PacktPublishing/Learning-Spring-Boot-3.0/tree/main/ch4.

Adding Spring Security to our project

Before we can do anything with Spring Security, we must add it to our project.

To add Spring Security to our already drafted app, we can easily use the same tactic from the previous chapters:

  1. Visit start.spring.io.
  2. Enter the same project artifact details as before.
  3. Click on DEPENDENCIES.
  4. Select Spring Security.
  5. Click on EXPLORE.
  6. Look for the pom.xml file and click on it.
  7. Copy the new bits onto the clipboard. Watch out! Spring Security has both a starter as well as a test module.
  8. Open up our previous project inside our IDE.
  9. Open our pom.xml file and paste the new bits into the right places.

Hit the refresh button in the IDE, and we’re ready to go!

Out of the box, we can run the application we have built up to this point. The exact same web app backed by Spring Data JPA is runnable…and it will be locked down.

Kind of.

When Spring Boot detects Spring Security on the path, it locks everything down with a randomly generated password. This can be a good thing…or a bad thing.

If we were giving a quick rundown to the CTO of a company, showing how the power of Spring Boot allows us to lock down applications, this can be a good thing.

But if we intend to do anything deeper than pitching, we need another approach. The problem with using Spring Boot’s autoconfigured “user” username and random password is that the password changes every time the app restarts.

We could override the username, password, and even the roles using application.properties, but this isn’t scalable. There is another approach that takes about the same amount of effort and sets us up for a more realistic approach.

This is what we’ll tackle next.

Creating our own users with a custom security policy

Spring Security has a highly pluggable architecture, which we shall take full advantage of throughout this chapter.

The key aspects of securing any application are as follows:

  • Defining the source of users
  • Creating access rules for the users
  • Associating various parts of the app with the access rules
  • Applying the policy to all aspects of the application

Let’s start with the first step and create a source of users. Spring Security comes with an interface for just this task: UserDetailsService.

To leverage this, we’ll start by creating a SecurityConfig Java class with the following code:

@Configuration
public class SecurityConfig {
  @Bean
  public UserDetailsService userDetailsService() {
    UserDetailsManager userDetailsManager = 
      new InMemoryUserDetailsManager();
    userDetailsManager.createUser( 
      User.withDefaultPasswordEncoder() 
        .username("user") 
        .password("password") 
        .roles("USER") 
        .build());
    userDetailsManager.createUser( 
      User.withDefaultPasswordEncoder() 
        .username("admin") 
        .password("password") 
        .roles("ADMIN")
        .build());
    return userDetailsManager;
  }
}

The preceding security code can be described as follows:

  • @Configuration is Spring’s annotation to signal that this class is a source of bean definitions rather than actual application code. Spring Boot will detect it through its component scanning and automatically add all its bean definitions to the application context.
  • UserDetailsService is Spring Security’s interface for defining a source of users. This bean definition, marked with @Bean, creates InMemoryUserDetailsManager.
  • Using InMemoryUserDetailsManager, we can then create a couple of users. Each user has a username, a password, and a role. This code fragment also uses the withDefaultPasswordEncoder() method to avoid encoding the password.

What’s important to understand is that when Spring Security gets added to the classpath, Spring Boot’s autoconfiguration will activate Spring Security’s @EnableWebSecurity annotation. This switches on a standard configuration of various filters and other components.

The components are dynamically picked based on whether this is Spring MVC or its reactive variant, Spring WebFlux. One of the beans required is a UserDetailsService bean.

Spring Boot will autoconfigure the single-user instance version we talked about in the preceding section. But, because we defined our own, Spring Boot will back off and let our version take its place.

Tip

In the code so far, we used withDefaultPasswordEncoder() to store passwords in the clear. Do NOT do this in production! Passwords need to be encrypted before being stored. In fact, there is a long and detailed history of the proper storage of passwords that reduces the risk of not just sniffing out a password but guarding against dictionary attacks. See https://springbootlearning.com/password-storage for more details on properly securing passwords when using Spring Security.

Believe it or not, this is enough to run the application we’ve been developing in this book! Go ahead, either right-click on the public static void main() method and run it or use ./mvnw spring-boot:run in the terminal.

Once the app is up, visit localhost:8080 in a new browser tab, and you should be automatically redirected to this page at /login:

Figure 4.1 – Spring Security’s default login form

Figure 4.1 – Spring Security’s default login form

This is Spring Security’s built-in login form. No need to roll our own. If we enter one of the accounts from the userDetailsService bean, it will let us in!

If hard-coded passwords are making you a tad nervous, then check out the next section, where we’ll move the storage of user credentials to an external database.

Swapping hardcoded users with a Spring Data-backed set of users

Creating a hardcoded set of users is great if we’re creating a demo (or writing a book!), but it’s no way to build a real, production-oriented application. Instead, it’s better to outsource user management to an external database.

By having the application reach out and authenticate against an external user source, it makes it possible for another team, such as our security engineering team, to manage the users through a completely different tool that manages that database.

Decoupling user management from user authentication is a great way to improve the security of the system. So, we’ll combine some of the techniques we learned in the previous chapter with the UserDetailsService interface we learned about in the previous section.

Since we already have Spring Data JPA and H2 on the classpath, we can start off by defining a JPA-based UserAcount domain object as follows:

@Entity
public class UserAccount {
  @Id
  @GeneratedValue 
  private Long id;
  private String username;
  private String password;
  @ElementCollection(fetch = FetchType.EAGER) 
  private List<GrantedAuthority> authorities = //
    new ArrayList<>();
}

The preceding code contains some key features:

  • As discussed in Chapter 3, Querying for Data with Spring Boot, @Entity is JPA’s annotation for denoting classes that are mapped onto relational tables.
  • The primary key is marked by the @Id annotation. @GeneratedValue signals the JPA provider to generate unique values for us.
  • This class also has a username, a password, and a list of authorities.
  • Because the authorities are a collection, JPA 2 offers a simple way to handle this using their @ElementCollection annotation. All these authority values will be stored in a separate table.

Before we can ask Spring Security to fetch user data, we should probably load some up. While in production, we would need to build up a separate tool to create and update the tables. For now, we can just pre-load some entries directly.

To do this, let’s create a Spring Data JPA repository definition aimed at the user manager by creating a UserManagementRepository interface as follows:

public interface UserManagementRepository extends 
  JpaRepository<UserAccount, Long> {
}

The preceding repository extends Spring Data JPA’s JpaRepository, providing a whole suite of operations needed by any user-managing tool.

To take advantage of it, we need Spring Boot to run a piece of code when the app starts up. Add the following bean definition to our SecurityConfig class:

@Bean
CommandLineRunner initUsers(UserManagementRepository 
  repository) {
  return args -> {
    repository.save(new UserAccount("user", "password", 
      "ROLE_USER"));
    repository.save(new UserAccount("admin", "password", 
      "ROLE_ADMIN"));
  };
}

The preceding bean defines Spring Boot's CommandLineRunner (through a Java 8 lambda function).

Tip

CommandLineRunner is a single abstract method (SAM), meaning it has just one method that must be defined. This trait lets us instantiate CommandLineRunner using a lambda expression instead of creating an anonymous class from the pre-Java 8 days.

In our example, we have a bean definition that depends upon UserManagementRepository. Inside the lambda expression, this repository is used to save two UserAccount entries: one user and one admin. With these entries in place, we can finally code our JPA-oriented UserDetailsService!

To fetch a UserAccount entry, we need another Spring Data repository definition. Only this time, we need a very simple one. Nothing that involves saving or deleting. So, create an interface called UserRepository as follows:

public interface UserRepository extends 
  Repository<UserAccount, Long> {
        UserAccount findByUsername(String username);
}

The preceding code is different from the previous repository we created (UserManagementRepository) in the following ways:

  • It extends Repository instead of JpaRepository. This means it starts with absolutely nothing. There are no operations defined except what is right here.
  • It has a custom finder, findByUsername, used to fetch a UserAccount entry based on the username. This is exactly what we’ll need to serve Spring Security later in this section.

This is one of the places where Spring Data really shines. We have focused on the domain of UserAccount and written one repository that is involved with storing data while defining another repository focused on fetching a single entry. All without getting dragged down into writing JPQL or SQL.

With all this in place, we can finally create a bean definition that lets us replace the UserDetailsService bean we defined in the previous section by adding this to our SecurityConfig class:

@Bean
UserDetailsService userService(UserRepository repo) {
  return username -> repo.findByUsername(username)
    .asUser();
}

The preceding bean definition calls for UserRepository. We then use it to fashion a lambda expression that forms UserDetailsService. If we peek at that interface, we’ll find it’s another SAM, a single method named loadUserByName, transforming a string-based username field into a UserDetails object. The incoming argument is username, which we can then delegate to the repository.

UserDetails is Spring Security’s representation of a user’s information. This includes username, password, authorities, and some Boolean values representing locked, expired, and enabled.

Let’s slow down and double-check things, because that last bit may have flown by a bit quickly. The userService bean in the preceding code produces a UserDetailsService bean, not the UserDetails object itself. It’s a service meant to retrieve user data.

The lambda expression inside the bean definition gets transformed into UserDetailsService.loadUserName(), a function that takes username for input and produces a UserDetails object as its output. Imagine someone entering their username at a login prompt. This value is what gets fed into this function.

The repository does the key step of fetching a UserAccount from the database based on username. For Spring Security to work with this entity from the database, it must be converted to a Spring Security User object (which implements the UserDetails interface).

So, we need to circle back to UserAccount and add a convenience method, asUser(), to convert it:

public UserDetails asUser() {
  return User.withDefaultPasswordEncoder() //
    .username(getUsername()) //
    .password(getPassword()) //
    .authorities(getAuthorities()) //
    .build();
}

This method simply creates a Spring Security UserDetails object, using its builder and plugging in the attributes from our entity type. Now, we have a complete solution that outsources user management to a database table.

Warning

If you’re worried about encoding passwords to prevent hack attacks, your concern is justified! This needs to be handled by the user management tool that actually stores these passwords, which we alluded to earlier. We also need to cope with the need to update roles. On top of that, we need a secure solution that guards against hash table attacks.

In fact, user management can become a tedious task to manage. Later in this chapter, in the Leveraging Google to authenticate users section, we’ll investigate alternative ways to manage users.

We mentioned at the beginning of this section that we needed to define a source of users. Mission accomplished! The next bullet point to tackle is to define some access roles. So, let’s delve into that in the next section.

Securing web routes and HTTP verbs

Locking down an application and only allowing authorized users to access it is a big step forward. But, it’s seldom enough.

We must actually confine who can do what. So far, the process we’ve applied where people must prove their identity as part of a closed list of users is known as authentication.

But, the next piece of security that must be applied to any real application is what’s called authorization, that is, what a user is allowed to do.

Spring Security makes this super simple to apply. The first step in customizing our security policy is to add one more bean definition to our SecurityConfig class created earlier in this chapter under the Creating our own users with a custom security policy section.

Up to this point, Spring Boot has had an autoconfigured policy in place. In fact, it may be simpler to show what Spring Boot has inside its own SpringBootWebSecurityConfiguration:

@Bean
SecurityFilterChain defaultSecurityFilterChain
  (HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated();
    http.formLogin();
    http.httpBasic();
  return http.build();
}

The preceding code fragment can be described as follows:

  • @Bean signals this method as a bean definition to be picked up and added to the application context.
  • SecurityFilterChain is the bean type needed to define a Spring Security policy.
  • To define such a policy, we ask for a Spring Security HttpSecurity bean. This gives us a handle on defining rules that will govern our application.
  • authorizeRequests defines exactly how we will, you know, authorize requests. In this case, any request is allowed if the user is authenticated and that is the only rule applied.
  • In addition to that, the formLogin and httpBasic directives are switched on, enabling both HTTP Form and HTTP Basic, two standard authentication mechanisms.
  • The HttpSecurity builder, with these settings, is then used to render SecurityFilterChain through which all servlet requests will be routed.

To provide more detail about formLogin and httpBasic, it’s important to understand some facts.

Form authentication involves having a nice HTML form, which can be stylized to match the theme of the web application. Spring Security even provides a default one (which this chapter shall use). Form authentication also supports logging out.

Basic authentication has nothing to do with HTML and form rendering but instead involves a popup baked into every browser. There is no support for customization, and the only way to throw away credentials is to either shut down or restart the browser. Basic authentication also makes it simple to authenticate with a command-line tool, such as curl.

In general, by having both form and basic authentication, the application will defer to form-based authentication in the browser while still allowing basic authentication from command-line tools.

This security policy provided by Spring Boot contains no authorization whatsoever. It essentially grants access to everything as long as the user is authenticated.

An example of a more detailed policy could be as follows:

@Bean
SecurityFilterChain configureSecurity(HttpSecurity http) 
  throws Exception {
    http.authorizeHttpRequests() //
      .requestMatchers("/resources/**", "/about", "/login")
        .permitAll() //
        .requestMatchers(HttpMethod.GET, "/admin/**")
        .hasRole("ADMIN") //
        .requestMatchers("/db/**").access((authentication, 
          object) -> {
            boolean anyMissing = Stream.of("ADMIN", 
                                            "DBA")//
            .map(role -> hasRole(role)
            .check(authentication, object).isGranted()) //
            .filter(granted -> !granted) //
            .findAny() //
            .orElse(false); //
    return new AuthorizationDecision(!anyMissing);
        }) //
        .anyRequest().denyAll() //
        .and() //
        .formLogin() //
        .and() //
        .httpBasic();
    return http.build();
}

The preceding security policy has a lot more detail, so let’s take it apart, clause by clause:

  • The method signature is identical to Spring Boot’s default policy shown previously. The method name may be different, but that doesn’t really matter.
  • This policy uses authorizeHttpRequests, signaling web-based checks.
  • The first rule is a path-based check to see if the URL starts with /resources, /about, or /login. If so, access is immediately granted, regardless of authentication status. In other words, these pages are freely accessible without logging in.
  • The second rule looks for any GET calls to the /admin pages. These calls indicate that the user has the ADMIN role. This is where an HTTP verb can be combined with a path to control access. This can be especially useful to lock down things such as the DELETE operations.
  • The third rule shows a much more powerful and customizable check. If the user is attempting to access anything underneath the /db path, then a special access check is performed. The preceding code has a lambda function where we are handed a copy of the current user’s authentication along with the object that is being checked. The function takes a stream of roles (DBA and ADMIN), checks if the user is granted the role, looks for any roles not granted, and if there are any, denies access. In other words, the user must be both a DBA and ADMIN to access this path.
  • The last rule denies access. This is a generally good pattern. If the user can’t meet any of the earlier rules, they shouldn’t be granted access to anything.
  • After the rules, both form authentication and basic authentication are enabled, just like with Spring Boot’s default policy.

Security is a complex beast. That’s why no matter what rules are provided by Spring Security, we must always have the ability to write custom access checks. The rule governing access to /db/** is a perfect example.

Instead of expecting Spring Security to capture every permutation of possible rules, it’s easier to grant us the ability to write a custom check. In our example, we have chosen to check that someone possesses all roles. (It should be noted that Spring Security has built-in functions to detect whether a user has any one of a given list of roles but cannot check for all roles.)

This custom rule we’ve written up is a perfect example of why many, many test cases should be written! We’ll dig into this in more detail in Chapter 5, Testing with Spring Boot, but the complexity of these rules should serve as a prelude to why it’s critical to test both success and failure paths, ensuring the rules are working!

Note

Roles versus authorities: Spring Security has a fundamental concept known as authorities. Essentially, an authority is a defined permission to access something. However, the concept of having ROLE_ADMIN, ROLE_USER, ROLE_DBA, and others where the prefix of ROLE_ categorizes such authorities is so commonly used that Spring Security has a full suite of APIs to support role checking. In this situation, a user who has the authority of ROLE_ADMIN or simply the role of ADMIN would be able to GET any of the admin pages.

Using what we’ve learned, let’s see if we can fashion a suitable policy for our video-listing site. First, let’s write down some requirements:

  • Everyone must log in to access anything
  • The initial list of videos should only be visible to authenticated users
  • Any search features should be available to authenticated users
  • Only admin users can add new videos
  • Any other forms of access will be disabled
  • These rules should apply to both the HTML web page as well as to command-line interactions

Using these requirements, we should be able to define a SecurityChainFilter bean:

@Bean
SecurityFilterChain configureSecurity(HttpSecurity http) 
  throws Exception {
    http.authorizeHttpRequests() //
      .requestMatchers("/login").permitAll() //
      .requestMatchers("/", "/search").authenticated() //
      .requestMatchers(HttpMethod.GET, "/api/**")
      .authenticated() //
      .requestMatchers(HttpMethod.POST, "/new-video", 
                        "/api/**").hasRole("ADMIN") //
      .anyRequest().denyAll() //
      .and() //
      .formLogin() //
      .and() //
      .httpBasic();
    return http.build();
}

The preceding security policy can be described as follows:

  • @Bean denotes this bean definition as taking an HttpSecurity bean and producing a SecurityChainFilter bean. This is the hallmark of defining a Spring Security policy.
  • Using authorizeHttpRequests, we can see a series of rules. The first one grants everyone access to the /login page, whether or not they are logged in.
  • The second rule grants access to the base URL / as well as the search results to anyone who is authenticated. While we could have a specific role, there is no need to constrain the base page as such.
  • The third rule confines GET access to any /api URL to an authenticated user. This allows command-line access to the site and is the API equivalent of allowing any authenticated user access to the base web page.
  • The fourth rule restricts POST access to both /new-video as well as /api/new-video only to authenticated users who also have the ADMIN role.
  • The fifth rule says that any user that doesn’t match any of the previous rules will be denied access regardless of authentication or authorization.
  • To wrap things up, both form and basic authentication are enabled.

There is one lingering issue we must decide on: Cross-Site Request Forgery (CSRF). We’ll make our choice on what path to take in the next section.

To CSRF or not to CSRF, that is the question

CSRF represents a particular attack vector that Spring Security guards against, by default. (Spring Security guards against many attack methods, but most don’t require a policy decision).

It’s a bit technical, but CSRF involves tricking already-authenticated users into clicking on rogue links. The rogue link asks the user to authorize a request, essentially granting the malicious attacker inside access.

The best way to guard against this is to embed a nonce into secured assets and refuse requests that lack them. A nonce is a semi-random number generated on the server that marks proper resources. The nonce is embedded as a CSRF token and must be embedded in any state-changing bits of HTML, typically forms.

If you use a templating engine that integrates tightly with Spring Boot, such as Thymeleaf, then there’s no need to do anything. Thymeleaf templates will automatically add suitable CSRF-based HTML inputs to any forms rendered on the page.

Mustache, in its lightness, doesn’t have such integration. However, it’s possible to make Spring Security’s CSRF token available by applying this inside application.properties:

spring.mustache.servlet.expose-request-attributes=true

With the preceding setting, a new attribute, _csrf, is made available to the templating engine. This makes it possible for us to update the search form as follows:

<form action="/search" method="post">
  <label for="value">Search:</label>
  <input type="text" name="value">
  <input type="hidden" name="{{_csrf.parameterName}}" 
    value="{{_csrf.token}}">
  <button type="submit">Search</button>
</form>

The preceding version of the search form contains one additional hidden input. The _csrf token will be automatically exposed for template rendering.

Only a valid HTML template rendering on our own server will have the right _csrf values embedded. A rogue web page that an authenticated user gets tricked into visiting will not have these values. Since the _csrf value changes from request to request, there is no way for other sites to either cache or predict the values.

The other form inside index.mustache that allows us to create new videos also needs this change:

<form action="/new-video" method="post">
  <input type="text" name="name">
  <input type="text" name="description">
  <input type="hidden" name="{{_csrf.parameterName}}" 
    value="{{_csrf.token}}">
  <button type="submit">Submit</button>
</form>

The preceding code shows a lightweight change that strengthens things. In fact, it’s the default policy for Spring Security!

But remember how I mentioned this is an issue that requires a choice? That’s because Spring Security’s CSRF filter either kicks in for both our templates and our JSON-based API controllers, or we disable it for both scenarios.

To be clear, CSRF protections make perfect sense when there is a web page that we log in to. But CSRF protections aren’t needed when doing API calls in a stateless scenario.

Our application is serving both scenarios, so a proper architecture would involve breaking up this application into two different applications. The web one could continue applying proper CSRF protections as shown in the preceding code.

The other application could disable CSRF protections with the following tweak to SecurityFilterChain:

@Bean
SecurityFilterChain configureSecurity(HttpSecurity http) 
  throws Exception {
    http.authorizeHttpRequests() //
      .mvcMatchers("/login").permitAll() //
      .mvcMatchers("/", "/search").authenticated() //
      .mvcMatchers(HttpMethod.GET, "/api/**")
        .authenticated() //
        .mvcMatchers(HttpMethod.POST, "/new-video", 
          "/api/**").hasRole("ADMIN") //
        .anyRequest().denyAll() //
        .and() //
        .formLogin() //
        .and() //
        .httpBasic() //
        .and() //
        .csrf().disable();
    return http.build();
}     

The preceding piece of SecurityConfig is almost the same security policy we created at the end of the previous section. The only change is in the next to last line, where we have .and().csrf().disable(). This tiny directive tells Spring Security to completely switch off CSRF protections.

As an overall approach, let’s drop ApiController, which was brought along from the previous chapters, and presume that it exists in a different application. Thus, there is no need to disable CSRF protections. Instead, we can roll with the changes made to index.mustache as shown in the previous two fragments.

With all this, we have a fistful of ways to apply path-based security. But these are not the only ways to secure our application and are not necessarily optimal for certain scenarios.

So far, we have provided a source of user data as well as created some initial access rules for our users. In the following sections, we will round things out by applying even more strategic security checks.

For example, in the next section, we’ll discover method-based security practices.

Securing Spring Data methods

Where to find this section’s code

The source code used for this portion of the chapter can be found at https://github.com/PacktPublishing/Learning-Spring-Boot-3.0/tree/main/ch4-method-security.

So far, we’ve seen tactics to apply various security provisions based on the URL of the request. But Spring Security also comes with method-level security.

While it’s possible to simply apply these techniques to controller methods, service methods, and in fact, any Spring bean’s method calls, this may appear to be trading one solution for another.

Method-level security specializes in providing a finer-grained ability to lock things down.

Updating our model

Before we can delve into this, we need an update to our domain model used earlier in this chapter. As a reminder, we created a VideoEntity class in the previous section that has an id, name, and description field.

To really take advantage of method-level security, we should augment this entity definition with one additional (and common) convention: adding a username field to represent ownership of the data:

@Entity
class VideoEntity {
  private @Id @GeneratedValue Long id;
  private String username;
  private String name;
  private String description;
  protected VideoEntity() {
    this(null, null, null);
  }
  VideoEntity(String username, String name, String 
    description) {
      this.id = null;
      this.username = username;
      this.description = description;
      this.name = name;
    }
  // getters and setters
}

This updated VideoEntity class is pretty much the same as earlier in this chapter except for the additional username field. (The boilerplate getters and setters are excluded for brevity).

If we’re going to dabble with ownership, it makes sense to update our set of users. Earlier in this chapter, we simply had user and admin. Let’s expand the number of users to alice and bob:

@Bean
CommandLineRunner initUsers(UserManagementRepository 
  repository) {
    return args -> {
      repository.save(new UserAccount("alice", "password", 
        "ROLE_USER"));
      repository.save(new UserAccount("bob", "password", 
        "ROLE_USER"));
      repository.save(new UserAccount("admin", "password", 
        "ROLE_ADMIN"));
  };
}

Remember the initUsers code toward the beginning of the chapter? We are replacing that with this set of three users: alice, bob, and admin. Why? So we can proceed to create a security protocol where alice can only delete videos that she uploaded, and bob can only delete videos that he uploaded.

Alice and Bob?

Security scenarios are often described in terms of Alice and Bob, a convention used since the 1978 paper A Method for Obtaining Digital Signatures and Public-key Cryptosystems by Rivest, Shamir, and Adleman, the inventors of RSA security. See https://en.wikipedia.org/wiki/Alice_and_Bob for more details.

Taking ownership of data

If we’re going to assign ownership to VideoEntity objects, then it makes sense to assign it when new entries are created. So, we should revisit the HomeController method that handles the POST requests to create new entries:

@PostMapping("/new-video")
public String newVideo(@ModelAttribute NewVideo newVideo,
  Authentication authentication) {
    videoService.create(newVideo,authentication.getName());
    return "redirect:/";
  }

This newVideo method in HomeController is just like the method earlier in this chapter except it has one extra argument: authentication.

This is a parameter offered by Spring MVC when Spring Security is on the classpath. It automatically extracts the authentication details stored in the servlet context and populates the Authentication instance.

In this situation, we are extracting its name, a field found in the Authentication interface’s parent interface, java.security.Principal, a standard type. Based on the Javadocs of Principal, this is the name of the principal.

Naturally, this requires that we update VideoService.create as follows:

public VideoEntity create(NewVideo newVideo, String 
  username) {
    return repository.saveAndFlush(new VideoEntity
      (username, newVideo.name(), newVideo.description()));
  }

The preceding updated version of create has two key changes:

  • There’s an extra argument: username
  • username is passed along to the VideoEntity constructor we just updated

These changes will make every newly entered video automatically associate with the currently logged-in user.

Adding a delete button

We talked about providing the ability to delete videos, but locking down that feature only to the owner of the video. Let’s tackle this step-by-step, starting with rendering each video along with a DELETE button inside index.mustache as follows:

{{#videos}}
    <li>
        {{name}}
        <form action="/delete/videos/{{id}}" method="post">
            <input type="hidden" 
                name="{{_csrf.parameterName}}" 
                value="{{_csrf.token}}">
            <button type="submit">Delete</button>
        </form>
    </li>
{{/videos}}

The preceding fragment of index.mustache can be described as follows:

  • The {{#videos}} tag tells Mustache to iterate over the array of the videos attribute
  • It will render an HTML line for every instance found in the database
  • {{name}} will render the name field
  • The <form> entry will create an HTML form with the {{id}} field used to build a link to /delete/videos/{{id}}

It’s important to understand that this form has a hidden _csrf input, as we discussed earlier in this chapter. Since this is HTML and not REST, we are using POST and not DELETE as our HTTP verb of choice.

Now, we need to add a method to HomeController that responds to POST /delete/videos/{{id}} calls as follows:

@PostMapping("/delete/videos/{videoId}")
public String deleteVideo(@PathVariable Long videoId) {
  videoService.delete(videoId);
  return "redirect:/";
}

This method can be described as follows:

  • @PostMapping signals that this method will respond to POST on URL /delete/videos/{videoId}
  • The @PathVariable extracts the videoId argument based on name matching
  • Using the videoId field, we pass it along to VideoService
  • If things work out, the method returns "redirect:/", a Spring MVC directive to issue an HTTP 302 Found, a soft redirect that bounces the user back to GET /

Next, we need to create the delete() method inside VideoService as follows:

public void delete(Long videoId) {
  repository.findById(videoId) //
    .map(videoEntity -> {
      repository.delete(videoEntity);
      return true;
    }) //
    .orElseThrow(() -> new RuntimeException("No video at " 
                                              + videoId));
}

The preceding method can be described as follows:

  • videoId is the primary key for the video to be deleted.
  • We first use the repository’s findById method to look up the entity.
  • Spring Data JPA returns Optional, which we can map over to get the VideoEntity object.
  • Using the VideoEntity object, we can then perform the delete(entity) method. Because delete() has a return type of void, we have to return true to comply with Optional.map’s demand for a return value.
  • If Optional turns out to be empty, we will instead throw RuntimeException.

Locking down access to the owner of the data

We’re almost there. So far, in this chapter we’ve written code that transform’s a video’s id field into a delete operation. But this section is about restricting access with Spring Data methods. While Spring Data JPA’s JpaRepository interface has a handful of delete operations, we must extend this definition inside VideoRepository if we wish to apply security controls as follows:

@PreAuthorize("#entity.username == authentication.name")
@Override
void delete(VideoEntity entity);

The preceding change to Spring Data JPA’s built-in delete(VideoEntity) method can be described as follows:

  • @Override: This annotation will ensure that we don’t alter the name of the method or any other aspect of the method signature
  • @PreAuthorize: This is Spring Security’s method-based annotation that allows us to write customized security checks
  • #entity.username: This de-references the entity argument in the first parameter and then looks up the username parameter using Java bean properties
  • authentication.name: A Spring Security argument to access the current security context’s authentication object and look up the principal’s name

By comparing the username field of VideoEntity against the current user’s name, we can confine this method to only working when a user attempts to delete one of their own videos.

Enabling method-level security

Now, none of this will work if we don’t enable method-level security. We must circle back to the SecurityConfig class we created in the first parts of this chapter and add the following annotation:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
  // prior security configuration details
}

In the preceding code, @EnableMethodSecurity is Spring Security’s annotation to activate method-based security.

Note

Perhaps you’ve heard of @EnableGlobalMethodSecurity? If so, be aware that this is being phased out in favor of @EnableMethodSecurity (used in the preceding code). For starters, the newer @EnableMethodSecurity activates Spring Security’s more powerful @PreAuthorize annotation (and its cousins) by default, while leaving the antiquated @Secured annotation disabled as well as the somewhat limited JSR-250 (@RolesAllowed) annotations. In addition to this, @EnableMethodSecurity leverages Spring Security’s simplified AuthorizationManager API instead of the more complex metadata sources, config attributes, decision managers, and voters.

This sets things up!

Displaying user details on the site

Just a little something to top things off, we are now going to learn about displaying user-based security details along with a logout button.

For this, we first must update HomeController as follows:

@GetMapping
public String index(Model model,
  Authentication authentication) {
    model.addAttribute("videos", videoService.getVideos());
    model.addAttribute("authentication", authentication);
    return "index";
  }

The preceding controller method is the same as earlier in this chapter but with a few changes in Authentication. Like the delete() controller method created earlier in this section, we are also tapping into this provided value by storing the details in an extra model attribute.

This mechanism allows us to provide authentication details for the current user to the template. We’ll take advantage of it by updating index.mustache toward the top as follows:

<h3>User Profile</h3>
<ul>
    <li>Username: {{authentication.name}}</li>
    <li>Authorities: {{authentication.authorities}}</li>
</ul>
<form action="/logout" method="post">
    <input type="hidden" name="{{_csrf.parameterName}}" 
      value="{{_csrf.token}}">
    <button type="submit">Logout</button>
</form>

The preceding fragment of HTML can be described as follows:

  • The username is shown by using {{authentication.name}}.
  • The authorities are shown by using {{authentication.authorities}}.
  • A logout button is provided inside an HTML form. The action to log out with Spring Security is to POST against /logout. It’s vital to know that even logging out requires providing the _csrf token if CSRF hasn’t been disabled!

Important

If it’s not clear, every single HTML form in index.mustache (and any other templates you’ve decided to add) must have the _csrf token as a hidden input. Spring Boot doesn’t have the same level of integration with Mustache’s template engine that it has with Thymeleaf. If you use Thymeleaf, while experiencing a steeper learning curve, it will automatically add these hidden inputs, freeing you up from having to remember them!

Earlier in this chapter, we preloaded some VideoEntity data inside the VideoService. Let’s update this based on alice and bob as follows:

@PostConstruct
void initDatabase() {
  repository.save(new VideoEntity("alice", "Need HELP with 
    your SPRING BOOT 3 App?",
    "SPRING BOOT 3 will only speed things up and make it 
      super SIMPLE to serve templates and raw data."));
  repository.save(new VideoEntity("alice", "Don't do THIS 
    to your own CODE!",
    "As a pro developer, never ever EVER do this to your 
      code. Because you'll ultimately be doing it to 
        YOURSELF!"));
  repository.save(new VideoEntity("bob", "SECRETS to fix 
    BROKEN CODE!",
    "Discover ways to not only debug your code, but to 
      regain your confidence and get back in the game as a 
        software developer."));
}

In the preceding method, the bottom of VideoService can be described as follows:

  • @PostConstruct: A standard Jakarta EE annotation that signals for this method to be run after the application has started
  • repository: The VideoRepository field is used to load two videos for alice and one for bob

With all this in place, we can restart the application and take it for a spin!

If we visit localhost:8080, we are instantly bounced over to Spring Security’s pre-built login page:

Figure 4.2 – Logging in as alice

Figure 4.2 – Logging in as alice

If we log in as alice, we are presented with the following message at the top of the page:

Figure 4.3 – The index template rendering user authentication details

Figure 4.3 – The index template rendering user authentication details

The preceding page shows the extra security details. It has both the Username field as well as the user’s assigned Authorities. Finally, there is a Logout button.

Important!

Do NOT put the user’s password on the page! In fact, it’s probably best to not put the list of authorities either. Figure 4.3 only illustrates the amount of information given to the template to allow sophisticated options. While Mustache is a bit limited in its logic-less nature, Thymeleaf actually has Spring Security extensions that let you optionally render sections and perform security checks. Why aren’t we covering this in this book? Because this book is titled Learning Spring Boot 3.0, not Learning Thymeleaf. But it’s only fair to let you know that you have options.

What happens if, as alice, we clicked on DELETE in the last video (owned by Bob)? Alice will see the following 403 response:

Figure 4.4 – When Spring Security denies access, you get served a 403 forbidden page

Figure 4.4 – When Spring Security denies access, you get served a 403 forbidden page

Hit the Back button on the browser and try to delete one of your own videos, and things will work just fine!

Bonus

For a higher-level, more visual perspective on Spring Security, check out this video at https://springbootlearning.com/security.

With the method-based controls we have covered in this section, we are able to implement fine-grained access. We are controlling things not just at the URL level, but also at the object level. But this comes at a price.

Someone has to manage all these users and their roles. Don’t be surprised if you have to set up a security ops team simply to manage the users. User management can be quite tedious.

This is what drives many teams to consider outsourcing user management entirely to alternative tools, such as Facebook, Twitter, GitHub, Google, or Okta.

We’ll explore how to consider using Google as an identity provider in the next section.

Leveraging Google to authenticate users

Do you dread the thought of managing users and their passwords? Many security teams buy large products to deal with all this. Teams even invest in tools to simply push password resets directly to users, to reduce call volume.

Long story short, user management is a major effort not to be taken lightly; hence, many teams turn to OAuth. Described as “an open standard for access delegation” (https://en.wikipedia.org/wiki/OAuth), OAuth provides a way to outsource user management almost entirely.

OAuth arose as social media applications emerged. A user of a third-party Twitter app used to store their password directly in the app. Not only was this inconvenient when users wanted to change their password, but it was a major security risk!

OAuth lets the application move away from this by instead reaching out to the social media site directly. The user logs in with the social media site, and the site hands back a special token to the app containing well-defined privileges known as scopes. This empowers the application to interact on the user’s behalf.

So, it should come as no surprise that literally every social media service has OAuth support. We can reach out to any application, be it Twitter, Facebook, GitHub, Google, and more, and simply let our users log in there. In fact, some apps support ALL these sites, making it possible to easily let users into your application.

This is not our only choice. We can also establish our own OAuth user service. While this may sound like circling back after pitching the reason to not run our own user management service, there are pros and cons to these various choices.

Pros of using OAuth

  • If you use Facebook or Twitter, anyone who already has a presence there can access your app. Given the popularity of these platforms, this can be an easy way to allow your users access.
  • If you choose GitHub, then your user base should be heavily oriented toward developers. Not every developer has a GitHub account, but a large number do. This can be a boon for apps that are tilted in this direction.
  • If you choose Google, another vast collection of users will be available to you.
  • If you choose Okta, a commercial system you can configure and use, then you have 100% control. Your users don’t have to exist on any social media platform and they don’t have to be developer oriented. You have complete control while still being able to outsource the hard parts of user management.

Cons of using OAuth

  • If someone doesn’t have a presence on your preferred social media network, they must either open an account or choose not to access it. You must also support managing your own users, which we’re trying to get away from.
  • If you choose Facebook, Twitter, Google, GitHub, or any other social media site, you are confined to their scopes. You don’t get to define your own. If the goal is to simply access and leverage user information, this is probably fine. However, if you wish to have managers, board officers, admins, DBAs, and other assorted roles, this won’t suffice.
  • If your application doesn’t take full advantage of GitHub (for example, https://gitter.im), then using GitHub probably isn’t the way to go.

If you need full control of scopes, Okta is the way to go. To top it off, Okta’s development team maintains full integration with Spring Security.

Assuming we’ve made our assessment and made a choice, it’s time to configure Spring Security to hook in.

We could pick any of the mentioned services to illustrate this, but for the rest of this book, let’s go with Google.

Creating a Google OAuth 2.0 application

Before we can take any steps toward authenticating with Google, we must first create an application with Google. To be clear, this means we will register some details on their dashboard, extract credentials, and plug these credentials into our Spring Boot application, granting our users access to Google data.

To do this, go through the following steps:

  1. Go to Google Cloud’s Dashboard (https://console.cloud.google.com/home/dashboard).
  2. Click on the drop-down right next to Select a project in the top-left corner. Hit NEW PROJECT on the popup and accept the default values.
  3. Select your new project so it’s showing in the drop-down at the top.
  4. On the left-hand panel on the Google Cloud Dashboard, scroll down and hover over APIs and services. On the pop-up menu, click on Enabled APIs and services.
  5. On the list toward the bottom of the page, look for YouTube Data API v3. Click on it and hit Enable API. This will grant our application access to YouTube’s latest data API.
  6. Back at your newly created application’s dashboard, look on the left-hand panel, and select Credentials.
  7. Click on + CREATE CREDENTIALS. On the pop-up menu, select OAuth Client.
  8. For application type, select Web Application.
  9. In the Name entry, give the application a name.
  10. In the Authorized redirect URIs, enter http://localhost:8080/login/oauth2/code/google.
  11. Once these credentials are created, capture the Client ID and Client secret shown in the top-right corner. We’ll later plug them into our Spring Boot application.
  12. Go back to the left-hand column from earlier (APIs and services at https://console.cloud.google.com/apis/dashboard) and click on the OAuth consent screen.
  13. Underneath test users, create an entry for each email address you wish to log in under later when we build the Spring Boot application.

These steps may seem tedious, but all modern-day OAuth applications on any platform, Google or otherwise, will require some amount of the following:

  • Application definition
  • Approved platform APIs
  • Supported users
  • Callbacks to our own application

It’s really just a matter of digging through the console and finding where the settings are plugged in.

Note

At this stage, our Google application is considered to be in test mode. This means that we are the only ones who can access it. We’ll be able to run the code locally on our machine and shake it out. No one else can access anything unless we publish it.

Adding OAuth Client to a Spring Boot project

Where to find this section’s code

The source code for this portion of the chapter can be found at https://github.com/PacktPublishing/Learning-Spring-Boot-3.0/tree/main/ch4-oauth.

Earlier in this chapter, we took the code from the previous chapter, Querying for Data with Spring Boot, and added Spring Security.

In this section, we actually need to start afresh:

  1. Go and visit Spring Initializr at https://start.spring.io.
  2. Select or enter the following details:
    • Project: Maven Project
    • Language: Java
    • Spring Boot: 3.0.0
    • Group: com.springbootlearning.learningspringboot3
    • Artifact: ch4-oauth
    • Name: Chapter 4 (OAuth)
    • Description: Securing an Application with Spring Boot and OAuth 2.0
    • Package name: com.springbootlearning.learningspringboot3
    • Packaging: Jar
    • Java: 17
  3. Click on ADD DEPENDENCIES. Then, select the following:
    • OAuth 2 Client
    • Spring Web
    • Spring Reactive Web
    • Mustache
  4. Click on the GENERATE button at the bottom of the screen.
  5. Import the code into your favorite IDE.

First of all, what are we doing with both Spring Web and Spring Reactive Web? Spring Web is how we built a servlet-based web application using Spring MVC. But another feature we are going to use is Google’s Oauth API, which leverages WebClient, Spring’s next-generation HTTP client. It’s found in Spring Reactive WebFlux.

When Spring Boot sees both Spring Web and Spring Reactive Web on the classpath, it will default to a standard embedded Apache Tomcat and run a standard servlet container.

With all this in place, we can start building our OAuth web application!

The Spring Initializr creates an application.properties file as part of the generated project. Due to the repetitive nature of some of the properties we must access, we will switch to the YAML-based variant. Simply rename that file as application.yaml.

Inside that file, add the following entry:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            clientId: **your Google Client ID**
            clientSecret: **your Google Client secret**
            scope: openid,profile,email, 
                   https://www.googleapis.com/auth/youtube

In the preceding code, application.yaml lets us enter properties in a hierarchical fashion. This is most helpful when we have to enter multiple properties at the same sublevel. In this case, we have several entries at spring.security.oauth2.client.registration.google.

Warning

OAuth2, due to its incredible flexibility, comes with many settings. This is necessary due to the flow of users coming to our app, being forwarded over to the other platform for authentication, and then flowing back to our application. To ease setup, Spring Security has added CommonOAuth2Provider with pre-baked settings for Google, GitHub, Facebook, and Okta. We only have to plug in clientId and clientSecret. Technically, that’s enough to authenticate with Google. But since we plan to leverage the YouTube Data API, we have added a scope setting, which we’ll discuss later.

The web we are building, which speaks to Google, can be described as an OAuth2-authorized client. This is represented in Spring Security OAuth2 using OAuth2AuthorizedClient. To facilitate the flow between our application and Google, Spring Boot autoconfigures ClientRegistrationRepository as well as OAuth2AuthorizedClientRepository.

These are the classes that parse the clientId and clientSecret properties in the preceding application.yaml fragment. An additional reason we need these repositories is that OAuth2 supports working with more than one OAuth2 provider.

Surely you’ve seen certain websites that support letting you log in using multiple options including Facebook, Twitter, Google, and perhaps even Apple?

Hence, we need some functionality to broker all these requests. To do this, we need to create a SecurityConfig class and add the following bean definition:

@Configuration
public class SecurityConfig {
    @Bean
    public OAuth2AuthorizedClientManager clientManager(
      ClientRegistrationRepository clientRegRepo,
        OAuth2AuthorizedClientRepository authClientRepo) {
      OAuth2AuthorizedClientProvider clientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
          .authorizationCode()
          .refreshToken()
          .clientCredentials()
          .password()
          .build();
      DefaultOAuth2AuthorizedClientManager clientManager =
        new DefaultOAuth2AuthorizedClientManager(
          clientRegRepo, authClientRepo);
      clientManager
        .setAuthorizedClientProvider(clientProvider);
      return clientManager;
    }
}

The preceding clientManager() bean definition will request the two autoconfigured Oauth2 beans mentioned earlier and blend them together into DefaultOAuth2AuthorizedClientManager. This bean will do the legwork of pulling the necessary properties from application.yaml and using them in the context of an incoming servlet request.

Tip

The flow of OAuth2 can seem a bit daunting. This is why you may wish to read more about it at https://oauth.net/2/, the official site for the OAuth 2.0 spec. It’s also important to appreciate that yes, this is a boilerplate, but once completed, we’ll have a valuable resource available for our Spring Boot application.

Believe it or not, we’re close to taking advantage of Google as our OAuth 2 platform of choice. When Spring Security OAuth2 is put on the classpath, Spring Boot has autoconfiguration policies that will automatically lock down our application. But this time, instead of creating a random password for a fixed username, the OAuth 2 beans mentioned earlier are combined with OAuth2AuthorizationClientManager.

But we want to go one step further. We want to actually invoke Google’s YouTube Data API, which we’ll cover in the next section.

Invoking an OAuth2 API remotely

To hook OAuth 2 support into an HTTP remote service invoker such as WebClient, start by creating a class named YouTubeConfig and add the following bean definitions:

@Configuration
public class YouTubeConfig {
  static String YOUTUBE_V3_API = //
    "https://www.googleapis.com/youtube/v3";
  @Bean
  WebClient webClient(OAuth2AuthorizedClientManager 
                        clientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction 
     oauth2 = //
      new 
       ServletOAuth2AuthorizedClientExchangeFilterFunction(
        clientManager);
    oauth2.setDefaultClientRegistrationId("google");
    return WebClient.builder() //
      .baseUrl(YOUTUBE_V3_API) //
      .apply(oauth2.oauth2Configuration()) //
      .build();
  }
}

The preceding bean definition creates a new WebClient, Spring’s more recent HTTP remote service invoker. While the venerable RestTemplate isn’t going anywhere, WebClient offers many new-and-improved ways to interact with remote HTTP services. One of the biggest improvements is its fluent API along with fully realized support for reactive services. While we aren’t using them in this chapter, this is something we’ll touch on later in this book.

In the preceding code, not only is this WebClient pointed at Google’s YouTube v3 API, but it also registers an exchange filter function using our previously created OAuth2AuthorizedClientManager as a way to give it OAuth2 power.

Note

An exchange filter function is a concept not found in servlets and Spring MVC, but instead in Spring WebFlux’s reactive paradigm. It’s very similar to a classic servlet filter in that every request that passes through the WebClient will invoke this function. This will ensure that the current user is logged in to Google and has the right authorization.

Another reason we may wish to use Spring WebFlux’s WebClient, despite having a servlet-based application, is to leverage one of Spring Framework’s most recent additions: HTTP client proxies.

The idea of HTTP client proxies is to capture all the details needed to interact with a remote service in an interface definition and let Spring Framework, under the hood, marshal the request and response.

We can capture one such exchange by creating an interface named YouTube, as follows:

interface YouTube {
    @GetExchange("/search?part=snippet&type=video")
    SearchListResponse channelVideos( //
      @RequestParam String channelId, //
      @RequestParam int maxResults, //
      @RequestParam Sort order);
    enum Sort {
      DATE("date"), //
      VIEW_COUNT("viewCount"), //
      TITLE("title"), //
      RATING("rating");
      private final String type;
      Sort(String type) {
        this.type = type;
      }
    }
}

The preceding interface has a single method: channelVideos. In truth, the name of this method doesn’t matter, because it’s the @GetExchange method that matters. In Chapter 2, Creating a Web Application with Spring Boot, we saw how to use @GetMapping to link HTTP GET operations with Spring MVC controller methods.

For HTTP remoting, the counterpart annotation is @GetExchange. This tells Spring Framework to remotely invoke /search?part=snippet&type=video using an HTTP GET call.

If it’s not obvious, the path in the @GetExchange call is appended to the base URL configured earlier, https://www.googleapis.com/youtube/v3, forming a complete URL to access this API.

In addition to specifying the HTTP verb along with the URL, this method has three inputs: channelId, maxResults, and order. The @RequestParam annotation indicates that these parameters are to be added to the URL as query parameters in the form of ?channelId=<value>&maxResults=<value>&order=<value>. To top things off, the order parameter is constrained to what the API considers acceptable values using a Java enum, Sort.

The names of the query parameters are lifted from the method’s argument names. While it’s possible to override these using the @RequestParam annotation, I find it easier to simply set each argument’s name to match the API.

For this particular API, there is no HTTP request body (as can be seen on the APIs documentation at https://developers.google.com/youtube/v3/docs/search/list). The whole request is contained in the URL.

But if you did need to send over some data to a different API, perhaps one expecting an HTTP POST, then you can use @PostExchange. You would provide the data as another method argument and apply @RequestBody so that Spring Framework knows to ask Jackson to serialize the provided data into JSON.

The response from this API is a JSON document shown in detail further down in the Response section of the Search function in the preceding list, as follows:

{
    "kind": "youtube#searchListResponse",
    "etag": etag,
    "nextPageToken": string,
    "prevPageToken": string,
    "regionCode": string,
    "pageInfo": {
        "totalResults": integer,
        "resultsPerPage": integer
    },
    "items": [
        search Resource
    ]
}

Java 17 records really shine while capturing the preceding API response. We need some barebone Java objects that are mostly data-oriented, and we need them fast. So, create a record called SearchListResponse as follows:

record SearchListResponse(String kind, String etag, String 
  nextPageToken, String prevPageToken, PageInfo pageInfo,
    SearchResult[] items) {
}

We can include all the fields we want and leave out the ones we don’t care about. In the preceding code, most of the fields are plain old Java strings, but the last two, PageInfo and SearchResult, are not.

So, create some more Java records, putting each in its own file:

record PageInfo(Integer totalResults, Integer
  resultsPerPage) {
}
record SearchResult(String kind, String etag, SearchId id, 
  SearchSnippet snippet) {
}

The process to create the preceding types is to simply walk through each nested type on Google’s YouTube API documentation, and capture the fields as shown. They’ll describe them as strings, integers, nested types, or links to other types. For each subtype that has its own section, simply create another record!

Tip

The name of the record’s type doesn’t matter. The critical part is ensuring the name of the field matches the names in the JSON structure being passed back.

With what we’ve captured so far in records, we now need to create SearchId as well as SearchSnippet, again, each in its own file:

record SearchId(String kind, String videoId, String 
  channelId, String playlistId) {
}
record SearchSnippet(String publishedAt, String channelId, 
  String title, String description,
    Map<String, SearchThumbnail> thumbnails, String 
      channelTitle) {
}

These record types are almost complete, given that they are almost all built-in Java types. The only one missing is SearchThumbnail. If we read YouTube’s API reference docs, we can easily wrap things up with this record definition:

record SearchThumbnail(String url, Integer width, Integer 
  height) {
}

This last record type is just a string and a couple of integers, so we’re done!

But not quite. We’ve invested loads of time configuring OAuth 2 and a remoting HTTP service to talk to YouTube. The cherry on top is building the web layer of our app, shown in the next section.

Creating an OAuth2-powered web app

Now, it’s time to create a web controller to start rendering things:

@Controller
public class HomeController {
  private final YouTube youTube;
  public HomeController(YouTube youTube) {
    this.youTube = youTube;
  }
  @GetMapping
  String index(Model model) {
    model.addAttribute("channelVideos", //
      youTube.channelVideos("UCjukbYOd6pjrMpNMFAOKYyw",
         10, YouTube.Sort.VIEW_COUNT));
    return "index";
  }
}

The preceding web controller has some key points:

  • @Controller indicates that this is a template-based web controller. Each web method returns the name of a template to render.
  • We are injecting the YouTube service through constructor injection, a concept touched upon back in Chapter 2, Creating a Web Application with Spring Boot.
  • The index method has a Spring MVC Model object, where we create a channelVideos attribute. It invokes our YouTube service’s channelVideos method with a channel ID, a page size of 10, and uses view counts as the way to sort search results.
  • The name of the template to render is index.

Since we are using Mustache as our templating engine of choice, the name of the template expands to src/main/resources/templates/index.mustache. To define it, we can start coding some pretty simple HTML 5 as follows:

<!doctype html>
<html lang="en">
<head>
    <link href="style.css" rel="stylesheet" 
    type="text/css"/>
</head>
<body>
<h1>Greetings Learning Spring Boot 3.0 fans!</h1>
<p>
   In this section, we are learning how to make
   a web app using Spring Boot 3.0 + OAuth 2.0
</p>
<h2>Your Videos</h2>
<table>
    <thead>
    <tr>
        <td>Id</td>
        <td>Published</td>
        <td>Thumbnail</td>
        <td>Title</td>
        <td>Description</td>
    </tr>
    </thead>
    <tbody>
    {{#channelVideos.items}}
        <tr>
            <td>{{id.videoId}}</td>
            <td>{{snippet.publishedAt}}</td>
            <td>
                <a href="https://www.youtube.com/watch?v=
                {{id.videoId}}" target="_blank">
                <img src="{{snippet.thumbnail.url}}" 
                alt="thumbnail"/>
                </a>
            </td>
            <td>{{snippet.title}}</td>
            <td>{{snippet.shortDescription}}</td>
        </tr>
    {{/channelVideos.items}}
    </tbody>
</table>
</body>

We aren’t going to explore every facet of HTML 5 in the preceding code. But some of the key bits of Mustache are as follows:

  • Mustache directives are wrapped in double curly braces, whether it’s to iterate over an array ({{#channelVideos.items}}) or a single field ({{id.videoId}}).
  • A Mustache directive that starts with a pound sign (#) is a signal to iterate, generating a copy of HTML for every entry. Because the SearchListResponse items fields are an array of SearchResult entries, the HTML inside that tag is repeated for each entry.
  • The thumbnail field inside SearchSnippet actually has multiple entries for each video. As Mustache is a logic-less engine, we need to augment that record definition with some extra methods to support our templating needs.

Adding a way to pick the right thumbnail and also curtailing the description field to less than a hundred characters can be implemented by updating the SearchSnippet record as follows:

record SearchSnippet(String publishedAt, String channelId, 
  String title, String description,
    Map<String, SearchThumbnail> thumbnails, String 
      channelTitle) {
  String shortDescription() {
    if (this.description.length() <= 100) {
      return this.description;
    }
    return this.description.substring(0, 100);
  }
  SearchThumbnail thumbnail() {
    return this.thumbnails.entrySet().stream()
      .filter(entry -> entry.getKey().equals("default"))
      .findFirst() 
      .map(Map.Entry::getValue)
      .orElse(null);
  }
}

From the preceding code, we can see that the following will occur:

  • The shortDescription method will either return the description field directly or a 100-character substring
  • The thumbnail method will iterate over the thumbnail map entries, find the one named default, and return it

As a tasty finish, let’s apply CSS so our table comes out nicely polished. Create src/main/resources/static/style.css:

table {
    table-layout: fixed;
    width: 100%;
    border-collapse: collapse;
    border: 3px solid #039E44;
}
thead th:nth-child(1) {
    width: 30%;
}
thead th:nth-child(2) {
    width: 20%;
}
thead th:nth-child(3) {
    width: 15%;
}
thead th:nth-child(4) {
    width: 35%;
}
th, td {
    padding: 20px;
}

As per the preceding code, Spring MVC will serve up static resources found in src/main/resources/static automatically. With all this in place, let’s launch the application! Visit localhost:8080 and we should automatically get forwarded to Google’s login page:

Figure 4.5 – Google’s login page

Figure 4.5 – Google’s login page

This login page will show any Google accounts you have used (I have several!). What’s important is to pick an account that you registered earlier on the Google Cloud Dashboard with the application. Otherwise, it won’t work!

Now if we had accepted CommonOAuth2Provider’s standard Google scope list, then all we’d be asking of Google is for user account details, such as an email address. Then, we would have been redirected back to our own web app.

But since we customized the scope property in order to tap into the YouTube API, another prompt will pop up, asking us to select a specific YouTube channel (If you don’t have one, you’ll have to declare one, even if you don’t upload anything!).

Figure 4.6 – YouTube’s channel selection page

Figure 4.6 – YouTube’s channel selection page

Pick your channel (Yes, I have several!). From here, we’ll get redirected back to our Spring Boot Mustache template, as follows:

Figure 4.7 – Spring Boot YouTube-flavored template

Figure 4.7 – Spring Boot YouTube-flavored template

Just like that, we have YouTube data on display on our own web page! The thumbnails even have hyperlinks, allowing you to open a new browser tab and watch the videos.

Tip

The preceding code is fetching data from my YouTube channel, showing the most popular videos (Figure 4.7 has been trimmed to fit this book). However, you can plug in any channel ID (not vanity channel URL) and get a readout. For example, check out my friend Dan Vega’s channel, a Spring developer advocate who has made multiple Spring videos, by typing in youTube.channelVideos("UCc98QQw1D-y38wg6mO3w4MQ", 10, YouTube.Sort.VIEW_COUNT).

By using OAuth2, we have successfully built a system where we can offload user management to a third-party service. This greatly reduces our own risk when it comes to user management by offloading it to Google (or whatever OAuth2 service we choose).

In fact, this is a prime reason to leverage a third-party service. There are many start-ups and businesses that offer keen services, requiring little more than a user id and email address to identify users.

Ever notice how some websites even offer to let you log in through multiple external services? That’s because we can actually define entries on multiple platforms, add all their entries to application.yaml, and then coordinate them all.

I’ll leave it to you as an exercise to tinker with your application to support multiple external services.

Bonus

Feel free to join me for a hands-on live stream, where we literally create a Google OAuth2 client, register it with Spring Boot 3, and serve up YouTube data at https://springbootlearning.com/oauth2!

Summary

Over the course of this chapter, we have learned how to secure a Spring MVC application. We plugged in custom users, applied path-based controls, and even added method-level fine-grained controls using Spring Security. We topped things off by outsourcing user management to the lofty Google using Spring Security’s OAuth2 integration. We took advantage of this by grabbing hold of some YouTube data and serving up video links.

This chapter may seem long, but in truth, security is a complex beast. Hopefully, with the various tactics shown in this chapter, you’ll have some solid ideas on what to do when it’s time to secure your own applications.

In the next chapter, Testing with Spring Boot, we’ll explore how to ensure our code is rock solid with various testing mechanisms.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset