[Fullstack] – Angular 7.2 integration with Spring Boot REST API using Redux, JPA and JWT Authentication

Many times when I present a framework to developers who are not generally familiar with it, the hands-on activity part of the presentation usually becomes the most favorite, as it allows participants to flex some muscles and write code that usually does trivial things. I tend to keep the objective of this challenge simple but fun, and try to cover as many topics as possible, as this project is generally the first end-to-end experience of the developer on the said framework.

In this tutorial I’ll walk you through creating an Angular 7.2 application which integrates with a Spring Boot 2.1 REST API, with JWA authentication and JPA integration. I’m hoping to give a good overview of these frameworks and leave you with a functional app that will allow you to develop it further.

The end goal is to create an application that shows some content, allows the user to login and enables features that are available to logged in users only. For the content part, we’re going to implement a simple RSS feed reader which pulls content from the sites we’re going to add (Note: there are a lot of RSS readers out there, and the purpose of this article is not to compete with those products!). If we had to formalize the requirements a little bit:

  • As a visitor, I want to see content retrieved from 3rd party service when I open the landing page
  • As a visitor, I want to browse predefined content categories in the application
  • As a visitor, I want to be able to login to the application with a username and password
  • As a logged in user, I want to be able to delete or add new feeds from the application

We’re going to start with the backend first, and then work on the UI, and finally integrate them together. This post will cover the backend portion where we setup JWT and JPA and a simple REST endpoint.

Spring Boot is a framework that aims at freeing developers from the need to define boilerplate configuration, allowing to create stand-alone Spring applications with embedded Tomcat and no requirement for XML configuration. Spring Boot has an online initializer that you can use to create our project.

Go ahead and use the initializer to create a project with Web, JPA, H2, Security and Cloud Oauth2 packages.

  • Web: This package contains Tomcat server and Spring MVC. This package will provide the base of our REST API
  • JPA: Java Persistence API which will be used for reading and writing image categories
  • H2: An in-memory database with SQL support
  • Security: Will be used to secure our REST endpoint
  • OAuth2: Will enable the OAuth2 and JSON Web Token pattern in our application

Optionally, extract the package to feedbrowser\api and initialize feedbrowser folder with a Git repo. You can access the source code on my repo.

  • cd imagebrowser
  • git init
  • git add
  • git commit -m "Initial Spring Boot commit
  • Create a new Github repository
  • git remote add origin https://github.com/borabilgin/imagebrowser.git (Change to your git repository URL)
  • git push -u origin master

Now that we have our API in a Git repo, we can open it in our favorite IDE. You have multiple options to choose from, IntelliJ and Spring Tool Suite are two of them.

I’ll be using IntelliJ, so to run the project simply click on Import Project, browse to the imagebrowser\api folder and select Import project from external model. Since I’ve selected Maven on Spring Initializr, that’s what I’ll select here.

To start the project, find the ImagebrowserApplication under src\main\java\com.demo.imagebrowser and hit the Run button

The rest of the article will focus on 3 sections:

  • Setting up Spring Security
  • Setting up a Data Source
  • Creating a protected and unprotected REST endpoints

Setting up Spring Security

Spring Security is provides authentication, authorization and other security features for Spring applications. It has Kerberos, OAuth and SAML support. In our application we’ll be using OAuth2.

Crash intro to OAuth2:

OAuth2 is an authorization framework that enables applications to obtain access to user accounts on an HTTP Service. The user authentication is performed by the service that hosts the user account, and third party applications can be authorized to access the user account. If you had any applications requesting access to your Google or Facebook account features (e.g. your contact list), that’s an example of OAuth2 usage.

There are a few roles in OAuth2:

  • Client (the third party application): The client is the application that is attempting to get access to user’s account. In our case this will be the Angular application.
  • Resource Server (the API): The resource server is the API that hosts the restricted resources. It verifies the access token and allow or deny access to resource. The resource in our case is the REST endpoint that will expose image categories, and the protected resource that’ll require authorization will be the delete endpoint that’ll allow an authorized user to delete a category.
  • Authorization server: This is the server that presents the interface where the user approves or denies the request (e.g. can this application access your contact list). In large scale deployments, this server is generally built as a separate component. In our application, it’ll reside in the same module as the REST API.

Here’s a sequence diagram. To put it simply, End User contacts Authorization Server through the Client Application (Angular app), and retrieves the JSON Web token, and then uses that token to contact Resource Server (Image API).

OAuth2 can use JWT as a token (but is not limited to). In our demo application, we’ll be using JWT as our token for our OAuth2 server.

Crash intro to JWT:

  • JSON Web Token is a JSON Object that provides a safe way to identify user claims between a UI app and a server. The JWT token has a header, payload and signature
  • Once the user authenticates with the system, a JWT is created and given back to the user. The user sends that token with every API call, which allows the server to validate whether the user was previously authenticated, and which claims the user may have
  • The header part contains the token type (JWT) and hashing algorithm
  • The payload contains the claims, usually a user id or a role id
  • The signature is the hashed version of header and payload with a secret key
  • The JSON Web Token is a combination of header, payload and signature. The server verifies the signature by hashing the header and payload again, if they match, the token is verified. If not, the token is rejected.

With this concept in mind, our focus here will be on creating an Authorization Server that can utilize a Token Store to generate and validate JSON Web Tokens.

Let’s start by configuring JWT:

security.oauth2.resource.filter-order=3
security.signing-key=AjYtfrTh532Gh1X
security.encoding-strength=256
security.security-realm=ImageBrowser JWT Realm
security.jwt.client-id=jwtclientid
security.jwt.client-secret=S7h5R3dxgg19S
security.jwt.grant-type=password
security.jwt.scope-read=read
security.jwt.scope-write=write
security.jwt.resource-ids=jwtresourceid

The properties file contain varios JWT related settings. Some important ones:

  • security.oauth2.resource.filter-order: OAuth2 resources are protected by a filter chain with the order specified by security.oauth2.resource.filter-order. The default is, after the filter, protecting the actuator endpoints (so actuator endpoints stay on HTTP Basic unless you change the order).
  • security.encoding-strength: The log rounds to use for the BCrypt password encoder (between 4 and 31)
  • security.security-realm: Arbitrary realm name
  • security.jwt.client-id: (required) the client id.
  • security.jwt.client-secret: (required for trusted clients) the client secret, if any.
  • security.jwt.scope: The scope to which the client is limited. If scope is undefined or empty (the default) the client is not limited by scope.
  • security.jwt.grant-type: Grant types that are authorized for the client to use. Default value is empty.

The SecurityConfig.class class uses the JWT settings to configura a Token Store which would be used to store/retrieve tokens.

package com.demo.imagebrowser.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${security.signing-key}")
private String signingKey;
@Value("${security.encoding-strength}")
private Integer encodingStrength;
@Value("${security.security-realm}")
private String securityRealm;
private UserDetailsService userDetailsService;
public SecurityConfig(@Autowired UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder(encodingStrength));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.realmName(securityRealm)
.and()
.csrf()
.disable();
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
}
  • @EnableWebSecurity: Allows Spring to find and automatically apply the class to the global WebSecurity
  • @EnableGlobalMethodSecurity: Allows us to secure our methods with Java configuration. Provides AOP security on methods, allowing PreAuthorize and PostAuthorize.
  • JwtTokenStore: The JSON Web Token (JWT) version of the store encodes all the data about the grant into the token itself (so no back end store at all which is a significant advantage). The JwtTokenStore is not really a "store" in the sense that it doesn’t persist any data, but it plays the same role of translating betweeen token values and authentication information in the DefaultTokenServices. It has a dependency on JwtAccessTokenConverter
  • JwtAccessTokenConverter: Allows us to customize an access token (e.g. through its additional information map) during the process of creating a new token for use by a client. Note that we used a symmetric key to sign our tokens, which means we’ll need to use the same key for Resource Server.
  • UserDetailsService: In a later commit we will inject a custom implementation of UserDetailsService, this will be used to retrieve user details from the database.

Now we can configure the Authorization server

package com.demo.imagebrowser.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.Arrays;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Value("${security.jwt.client-id}")
private String jwtClientId;
@Value("${security.jwt.client-secret}")
private String jwtClientSecret;
@Value("${security.jwt.grant-type}")
private String jwtGrantType;
@Value("${security.jwt.scope-read}")
private String jwtScopeRead;
@Value("${security.jwt.scope-write}")
private String jwtScopeWrite;
@Value("${security.jwt.resource-ids}")
private String jwtResourceIds;
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(jwtClientId)
.secret(jwtClientSecret)
.authorizedGrantTypes(jwtGrantType)
.scopes(jwtScopeRead, jwtScopeWrite)
.resourceIds(jwtResourceIds);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.authenticationManager(authenticationManager);
}
}
  • @EnableAuthorizationServer: This annotation enables an Authorization Server in the current application context, which must be DispatcherServlet context (as in Spring MVC).
  • AuthorizationServerConfig class extends AuthorizationServerConfigurerAdapter which is an implementation of AuthorizationServerConfigurer. Having this bean tells Spring to use this configuration instead of auto-configuration.
  • This class has 2 configure methods:

    • The first one configures ClientDetailsServiceConfigurer. Here we’re telling Spring to use our JWT configuration details:

    • clientId which should match on the server and on the client

    • clientSecret which is client application’s password (ideally this should be stored in a database),

    • grantType specifies what we’ll be granting to the client application. The most commonly used one is Authorization Code. This grant type is optimized for server side applications, where client secret is confidentially maintained. If you allowed any application to access your Facebook or Gmail features, most likely they used authorization code grant type. In our demo application, we’re using password grant type, which means that we’ll be issuing a token based on successful authentication with resource owner’s user and password. Other options include Client Credentials and Refresh Token.

    • scope specifies the level of access that the application is requesting. Because we’re going to allow adding/deleting categories, we’re giving read and write scope.

    • resourceId specifies the ID of the resource. There can be multiple resources, and this resource ID must match the resource ID that’s used in the ResourceServer

    • AuthenticationManager: On the second configure method, we’ve provided our AuthenticationManager bean so that it can authenticate our user using userDetailsService. If you take a closer look, userDetailsService was provided in SecurityConfig class.

    • TokenEnhancerChain is a composite token enhancer that loops oer its delegate enhancers. This enhancer enables chaining multiple types of claims containing different information.

Setting up the Resource Server

package com.demo.imagebrowser.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private ResourceServerTokenServices tokenServices;
@Value("${security.jwt.resource-ids}")
private String jwtResourceIds;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(jwtResourceIds).tokenServices(tokenServices);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.and()
.authorizeRequests()
.antMatchers("/actuator/**", "/api-docs/**").permitAll()
.antMatchers("/secure/**" ).authenticated();
}
}
  • @EnableResourceServer: This annotation enables a resource server in the current application context. It works by creating a security filter which authenticates requests with an incoming OAuth2 token. The filter order, for some reason is hardcoded to 3, which we had to match in our configuration by adding security.oauth2.resource.filter-order=3.
  • The resource server we configured here can dictate permission for any endpoint in our application. The second configure method does that by adding matchers to http requests and assigning a permission level (permitAll or authenticated). For the endpoints that require authentication, the client’s token will be validated.
  • Remember that generally Resource and Authentication servers are supposed to reside in separate modules. In our demo application, they’re in the same module and sharing the same TokenService.

Setting up a Data Source

package com.demo.imagebrowser.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.demo.imagebrowser.repository")
public class DataSourceConfig {
@Bean
public DataSource datasource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase dataSource = builder
.setType(EmbeddedDatabaseType.H2)
.addScript("db-scripts/001-schema.sql")
.addScript("db-scripts/002-data.sql")
.build();
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("datasource") DataSource ds) {
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(ds);
entityManagerFactory.setPackagesToScan(new String[]{"com.demo.imagebrowser.domain"});
JpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter);
return entityManagerFactory;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
  • While we could ask Spring Boot to automatically configure the H2 in-memory database, we’re doing it explicitly in DataSourceConfig so that you can see the configuration path and also change it to another database to fit into your usecase (I thought about using MongoDB, but maybe for another post)
  • EnableTransactionManagementenables Spring’s annotation driven trancation management capability. With this annotation, @Transactional annotations within your own code are evaluated.
  • EnableJpaRepositories is an annotation to enable JPA Repositories. Will scan the package of the annotated configuration class for Spring Data repositoroes by default, but we’re asking it to scan com.demo.imagebrowser.repositories instead.
  • addScript: Defines the initial SQL scripts (schema and seed data) that we’d like to run on startup.

Setting up Database

36 lines (30 sloc) 939 Bytes | Database Schema
CREATE TABLE FEED_CATEGORY {
ID INT NOT NULL AUTO_INCREMENT,
NAME NVARCHAR(255) NOT NULL
};
CREATE TABLE FEED {
ID INT NOT NULL AUTO_INCREMENT,
NAME NVARCHAR(255) NOT NULL,
ADDRESS NVARCHAR(2048) NOT NULL,
CATEGORY_ID INT NOT NULL,
CONSTRAINT CONSTRAINT_FEED_CATEGORY_FK FOREIGN KEY (CATEGORY_ID) REFERENCES FEED_CATEGORY,
PRIMARY KEY(ID)
};
CREATE TABLE APP_ROLE {
ID INT NOT NULL AUTO_INCREMENT,
DESCRIPTION NVARCHAR(255) NOT NULL,
ROLE_NAME NVARCHAR(255) NOT NULL,
PRIMARY KEY(ID)
};
CREATE TABLE APP_USER {
ID INT NOT NULL AUTO_INCREMENT,
USERNAME NVARCHAR(255) NOT NULL,
PASSWORD NVARCHAR(255) NOT NULL,
PRIMARY KEY (ID)
};
CREATE TABLE USER_ROLE {
USER_ID INT NOT NULL,
ROLE_ID INT NOT NULL,
CONSTRAINT USER_ROLE_USER_FK FOREIGN KEY (USER_ID) REFERENCES APP_USER (ID),
CONSTRAINT USER_ROLE_ROLE_FK FOREIGN KEY (ROLE_ID) REFERENCES APP_ROLE (ID)
};
21 lines (14 sloc) 1.04 KB | Seed Data
INSERT INTO APP_ROLE (ID, DESCRIPTION, ROLE_NAME) VALUES (1, 'Regular User', 'USER');
INSERT INTO APP_ROLE (ID, DESCRIPTION, ROLE_NAME) VALUES (2, 'Admin User', 'ADMIN');
-- Password is 'password'
INSERT INTO APP_USER (IS, USERNAME, PASSWORD) VALUES (1, 'Bora', '$2a$09$5pvrWJ0Bg3ARBzWEp9t1IO6GRASmBqIJf7rPZVJpu0iV8BToIlX9y');
INSERT INTO APP_USER (IS, USERNAME, PASSWORD) VALUES (1, 'Admin', '$2a$09$5pvrWJ0Bg3ARBzWEp9t1IO6GRASmBqIJf7rPZVJpu0iV8BToIlX9y');
INSERT INTO USER_ROLE(USER_ID, ROLE_ID) VALUES (1,1);
INSERT INTO USER_ROLE(USER_ID, ROLE_ID) VALUES (2,1);
INSERT INTO USER_ROLE(USER_ID, ROLE_ID) VALUES (2,2);
INSERT INTO FEED_CATEGORY(ID, NAME) VALUES (1, 'News Stories');
INSERT INTO FEED_CATEGORY(ID, NAME) VALUES (2, 'Tech related');
INSERT INTO FEED_CATEGORY(ID, NAME) VALUES (3, 'Time wasters');
INSERT INTO FEED (ID, NAME, ADDRESS, CATEGORY_ID) VALUES (1, 'Google News', '', 1);
INSERT INTO FEED (ID, NAME, ADDRESS, CATEGORY_ID) VALUES (1, 'Hacker News', '', 2);
INSERT INTO FEED (ID, NAME, ADDRESS, CATEGORY_ID) VALUES (1, 'Reddit', '', 3);
  • Here we simply added 2 SQL files to the application which we’ll use during application startup. The schema file will be used to create tables and relationships between them, and the data file will be used to seed the data with a few initial records.

Setting up Domain Model

package com.demo.imagebrowser.domain;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Objects;
@Entity
@Table(name = "APP_USER")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@NotNull
@Column(name = "USERNAME")
private String username;
@NotNull
@Column(name= "PASSWORD")
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "USER_ROLE",
joinColumns = @JoinColumn(name = "USER_ID",
referencedColumnName = "ID"),
inverseJoinColumns = @JoinColumn(name = "ROLE_ID",
referencedColumnName = "ID"))
private List<Role> roles;
public User() {
}
public User(Long id, String username, String password, List<Role> roles) {
this.id = id;
this.username = username;
this.password = password;
this.roles = roles;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
package com.demo.imagebrowser.domain;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.Objects;
@Entity
@Table(name = "APP_ROLE")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@NotNull
@Column(name = "DESCRIPTION")
private String description;
@NotNull
@Column(name = "ROLE_NAME")
private String roleName;
public Role() {
}
public Role(Long id, String description, String roleName) {
this.id = id;
this.description = description;
this.roleName = roleName;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Role role = (Role) o;
return Objects.equals(id, role.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
package com.demo.imagebrowser.domain;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "FEED")
public class Feed {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@NotNull
@Column(name = "NAME")
private String name;
@NotNull
@Column(name = "ADDRESS")
private String address;
@NotNull
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "CATEGORY_ID", referencedColumnName = "ID")
private FeedCategory category;
public Feed() {
}
public Feed(Long id, String name, String address, FeedCategory category) {
this.id = id;
this.name = name;
this.address = address;
this.category = category;
}
}
package com.demo.imagebrowser.domain;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.Objects;
@Entity
@Table(name = "FEED_CATEGORY")
public class FeedCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@NotNull
@Column(name = "NAME")
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public FeedCategory() {
}
public FeedCategory(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FeedCategory that = (FeedCategory) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
  • Few things to note in our domain model:
    • @ManyToMany: Specifies a many-valued association with many-to-many multiplicity. In our domain model, users can have multiple roles. There are two sides to it: owning side and the non-owning (inverse) side. The join table is specified on the owning side, which in our case is User. If however, the relationship was bidirectional (so User would have Roles, and Roles would have a property called Users), then the other side would use the mappedBy property of @ManyToMany to specify the relationship field.
    • @OneToOne: Specifies a single-valued association with one-to-one multiplicity. In our domain model, a Feed item can have 1 Feed Category. We use @OneToOne annotation to specify this relationship.
    • Eager vs Lazy loading: Simply put, entities in relationship (e.g. roles) will be fetched when the parent (User) is fetched in Eager loading. In Lazy loading, they’ll be loaded when you try to access them.

Adding repositories

package com.demo.imagebrowser.repository;
import com.demo.imagebrowser.domain.User;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}
  • The very cool thing about JPA is that it supports defining a SQL query derived from our method names. In this case, Spring will translate our method findByUsername to JPQL as select u from User u where u.username = ?1 and add the parameter username to the query.

Adding services

package com.demo.imagebrowser.service.impl;
import com.demo.imagebrowser.domain.Feed;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.repository.FeedCategoryRepository;
import com.demo.imagebrowser.repository.FeedRepository;
import com.demo.imagebrowser.service.FeedService;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections;
import java.util.List;
public class FeedServiceImpl implements FeedService {
@Autowired
private FeedRepository feedRepository;
@Autowired
private FeedCategoryRepository feedCategoryRepository;
@Override
public List<Feed> findByCategoryName(String categoryName) {
FeedCategory category = feedCategoryRepository.findByName(categoryName);
if (category != null) {
return feedRepository.findByCategory(category);
} else {
return Collections.emptyList();
}
}
@Override
public void deleteCategoryByName(String categoryName) {
feedCategoryRepository.deleteByName(categoryName);
}
@Override
public FeedCategory addCategory(String categoryName) {
FeedCategory category = new FeedCategory();
category.setName(categoryName);
feedCategoryRepository.save(category);
return category;
}
@Override
public Feed addFeed(Feed feed) {
return feedRepository.save(feed);
}
}
  • There are 2 important services we added in this commit: UserDetailsService and FeedService
  • UserDetailsService implements Spring’s UserDetailsService, specifically the loadUserByUsername method. This method is generally called by an AuthenticationProvider to authenticate a user. In our case, when username and password is submitted, this method will be called to find the password for the user and also load the roles assigned to the user.
  • FeedService provides CRUD methods by using the underlying FeedRepository and FeedCategoryRepository

Creating protected and unprotected REST endpoints

package com.demo.imagebrowser.controller;
import com.demo.imagebrowser.domain.Feed;
import com.demo.imagebrowser.service.FeedService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/feed")
public class FeedController {
@Autowired
private FeedService feedService;
@ResponseBody
@RequestMapping(method = RequestMethod.GET)
public List<Feed> getFeeds() {
return feedService.getAll();
}
}
package com.demo.imagebrowser.controller;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.service.FeedService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/secure/category")
public class FeedCategoryController {
@Autowired
private FeedService feedService;
@RequestMapping(method = RequestMethod.PUT)
public ResponseEntity<FeedCategory> addCategory(@RequestParam() String categoryName) {
FeedCategory category = feedService.addCategory(categoryName);
return new ResponseEntity<>(category, HttpStatus.OK);
}
}
  • FeedController has a getFeeds method that returns all feed entries. The @ResponseBody annotation tells the controller that the returned object should be serialized into JSON (in our case, List of Feed). @RequestMapping annotation allows us to configure the HTTP method we’ll use for the controller method (in our case, GET)
  • FeedCategoryController has an addCategory method that saves the category in the database. We use PUT method here as the expectation is to create the category if it doesn’t exist, and not create another category if it does exist. This method uses @RequestParam to map the categoryName parameter from request body to our method parameter.

Securing addCategory method

package com.demo.imagebrowser.controller;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.service.FeedService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/secure/category")
public class FeedCategoryController {
@Autowired
private FeedService feedService;
@RequestMapping(method = RequestMethod.PUT)
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<FeedCategory> addCategory(@RequestParam() String categoryName) {
FeedCategory category = feedService.addCategory(categoryName);
return new ResponseEntity<>(category, HttpStatus.OK);
}
}
  • By adding the @PreAuthorize annotation, we’re telling Spring that only users who belong to ADMIN group can execute this method. Everyone else will get a 401 Unauthorized error.

Clean up

  • If you’re using Java JDK 9 or above, you need to add JAXB-API to your pom.xml as a dependency. If you’re using JDK 8, you can skip this step.

Testing

Testing Repository layer

  • To ensure our application works as expected, it’s imperative that we add tests. Testing the repositories is straightforward once you understand the setup. Let’s take UserRepositoryIntegrationTest as an example
package com.demo.imagebrowser.repository;
import com.demo.imagebrowser.domain.Role;
import com.demo.imagebrowser.domain.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
public void shouldReturnUser_whenSearchByUsername() {
final String expectedUsername = "bora";
User user = new User(expectedUsername, "abc", Collections.emptyList());
entityManager.persist(user);
entityManager.flush();
User found = userRepository.findByUsername(expectedUsername);
assertEquals("Found user's name should be equal to inserted user's name", found.getUsername(), expectedUsername);
}
@Test
public void shouldIncludeRole_whenSearchByUsername() {
final String expectedUsername = "bora";
Role role = new Role();
role.setRoleName("user");
role.setDescription("test role");
entityManager.persist(role);
entityManager.flush();
User user = new User(expectedUsername, "abc", Arrays.asList(role));
entityManager.persist(user);
entityManager.flush();
User found = userRepository.findByUsername(expectedUsername);
assertTrue("Role size should be 1", found.getRoles().size() == 1);
assertEquals("Found user's role should be equal to inserted user's role", found.getRoles().get(0).getRoleName(), role.getRoleName());
}
}
  • We decorate our test with @RunWith(SpringRunner.class), which loads our Spring ApplicationContext and provides support for having beans @Autowired into our test instance.
  • @DataJpaTest basically sets up H2 in-memory database for testing, sets up Spring Data and DataSource, performs an @EntityScan and turns on SQL logging (you’ll notice this in your test output – every action is logged, and if a test fails you’ll see transaction rollback statements).
  • We @Autowire our repository to the test instance
  • The individual test methods shouldReturnUser_whenSearchByUsername and shouldIncludeRole_whenSearchByUsername are self explanatory, they execute the given when then scenario. In the first one, we create a User entity (given), we save it through the repository (when), and then we expect to find it when we search for it by its username (then).
  • The other integration tests in this commit follow the same pattern.

Testing the service layer

  • We continue to add tests for the service layer to ensure that our application works as expected. One caveat is that, while testing the service layer, we don’t necessarily care how the underlying repository layer is implemented. For this reason, we don’t test the wiring of the repository layer, rather we mock it with @MockBean and tell Spring what it should do when we ask the repository to perform an operation.
  • To ensure our implementation of service interface is returned, we use @TestConfiguration annotation to create the UserDetailsServiceImpl or FeedServiceImpl instances when UserDetailsService or FeedService beans are requested.
  • The setUp method is special as it runs before the tests and this is where we tell Spring to mock the repository methods and return the expected values when we ask for certain operations. This method is executed before each test.
package com.demo.imagebrowser.service.impl;
import com.demo.imagebrowser.domain.Role;
import com.demo.imagebrowser.domain.User;
import com.demo.imagebrowser.repository.UserRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
public class UserDetailsServiceIntegrationTest {
private static String USERNAME = "TEST_USER";
private static String ROLE = "ELEVATED_USER_ROLE";
private List<Role> roles = Arrays.asList(new Role("description", ROLE));
private User testUser = new User(USERNAME, "password", roles);
@TestConfiguration
static class UserDetailsServiceIntegrationTestContextConfiguration {
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService();
}
}
@Autowired
private UserDetailsService userDetailsService;
@MockBean
private UserRepository userRepository;
@Before
public void setUp() throws Exception {
Mockito.when(userRepository.findByUsername(USERNAME)).thenReturn(testUser);
}
@Test
public void shouldReturnUser_whenSearchByUsername(){
assertNotNull("Should find user", userDetailsService.loadUserByUsername(USERNAME));
}
@Test
public void shouldMatchUsername_whenSearchByUsername(){
assertEquals("Should find by username", testUser.getUsername(), userDetailsService.loadUserByUsername(USERNAME).getUsername());
}
@Test
public void shouldMatchRoles_whenSearchByUsername(){
roles.stream().forEach(r -> assertTrue("Role should match", userDetailsService.loadUserByUsername(USERNAME).getAuthorities().stream().anyMatch(a -> ((GrantedAuthority) a).getAuthority().equals(r.getRoleName()))));
}
}
package com.demo.imagebrowser.service.impl;
import com.demo.imagebrowser.domain.Feed;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.repository.FeedCategoryRepository;
import com.demo.imagebrowser.repository.FeedRepository;
import com.demo.imagebrowser.service.FeedService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
public class FeedServiceImplTest {
private static String CATEGORY_NAME = "TEST CATEGORY";
private static String FEED_NAME = "TEST FEED";
private Feed testFeed;
private FeedCategory testFeedCategory;
@TestConfiguration
static class FeedServiceImplTestContextConfiguration {
@Bean
public FeedService feedService() {
return new FeedServiceImpl();
}
}
@Autowired
private FeedService feedService;
@MockBean
private FeedRepository feedRepository;
@MockBean
private FeedCategoryRepository feedCategoryRepository;
@Before
public void setUp() throws Exception {
testFeedCategory = new FeedCategory(CATEGORY_NAME);
testFeed = new Feed(FEED_NAME, "test", testFeedCategory);
Mockito.when(feedCategoryRepository.findByName(CATEGORY_NAME)).thenReturn(testFeedCategory);
Mockito.when(feedRepository.findByCategory(testFeedCategory)).thenReturn(Arrays.asList(testFeed));
Mockito.when(feedCategoryRepository.findByName(CATEGORY_NAME)).thenReturn(testFeedCategory);
Mockito.when(feedRepository.save(testFeed)).thenReturn(testFeed);
}
@Test
public void shouldReturnFeed_whenSearchByCategoryName() {
List<Feed> feeds = feedService.findByCategoryName(CATEGORY_NAME);
assertNotNull("Should return feed", feeds);
assertTrue("Category should have 1 feed", feeds.size() == 1);
assertEquals("Correct feed should have been returned", FEED_NAME, feeds.get(0).getName());
}
@Test
public void shouldReturnTrue_whenSearchByCategoryName() {
assertTrue("Category should exist", feedService.categoryExists(CATEGORY_NAME));
}
@Test
public void shouldSaveFeed_whenGivenFeedEntity() {
assertEquals("Feed should be saved", testFeed, feedService.addFeed(testFeed));
}
}

Testing the REST Controller layer

package com.demo.imagebrowser.controller;
import com.demo.imagebrowser.domain.Feed;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.service.FeedService;
import com.demo.imagebrowser.service.impl.UserDetailsService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest(FeedController.class)
public class FeedControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private FeedService feedService;
@MockBean
private UserDetailsService userDetailsService;
@Test
public void shouldReturnJSON_whenRequestedAllFeeds() throws Exception {
FeedCategory category = new FeedCategory("test");
List<Feed> feeds = Arrays.asList(new Feed("test", "test", category));
given(feedService.getAll()).willReturn(feeds);
mvc.perform(get("/feed")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].name").value(feeds.get(0).getName()));
}
}
package com.demo.imagebrowser.controller;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.service.FeedService;
import com.demo.imagebrowser.service.impl.UserDetailsService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest(FeedCategoryController.class)
public class FeedCategoryControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private FeedService feedService;
@MockBean
private UserDetailsService userDetailsService;
@Test
public void shouldReturnCategory_whenAddingNewCategory() throws Exception {
String categoryName = "test";
FeedCategory category = new FeedCategory(categoryName);
given(feedService.addCategory(categoryName)).willReturn(category);
mvc.perform(put("/secure/category", categoryName)
.contentType(MediaType.APPLICATION_JSON)
.param("categoryName", categoryName))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value(category.getName()));
}
@Test
public void shouldReturnError_whenAddingNewCategoryWithNoNameProvided() throws Exception {
mvc.perform(put("/secure/category")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().is4xxClientError());
}
}
  • To ensure our controllers work well with the service layer, we added a couple test cases. @WebMvcTest annotation auto-configures the Spring MVC to run our tests by disabling full auto-configuration and only configuring relevant parts such as @Controller. We also use @MockBean, which, as the name suggests, provide mock implementations for required dependencies, in this case the service layer.
  • Our test cases focus on happy path, but in FeedCategoryControllerTest we’re testing what should happen in case the user doesn’t provide category name as aa request parameter. I would also recommend reviewing FeedControllerTest and see how we’re able to check the returned data object, verify its size and contents by using the jsonPath helper.
  • Since FeedCategoryController has an addCategory method that expects calling user to be in ADMIN group, we have to turn off security for FeedCategoryController test. In this commit we do that by passing secure=false to the @WebMvcTest annotation.

Integration tests

package com.demo.imagebrowser;
import com.demo.imagebrowser.ImagebrowserApplication;
import com.demo.imagebrowser.domain.Feed;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.repository.FeedCategoryRepository;
import com.demo.imagebrowser.repository.FeedRepository;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.junit.Assert.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = ImagebrowserApplication.class)
@AutoConfigureMockMvc
@TestPropertySource(
locations = "classpath:application-test.properties")
public class ImageBrowserApplicationIntegrationTest {
@Autowired
private MockMvc mvc;
@Autowired
private FeedRepository feedRepository;
@Autowired
private FeedCategoryRepository feedCategoryRepository;
@Before
public void setup() {
feedCategoryRepository.deleteAll();
feedRepository.deleteAll();
FeedCategory category1 = new FeedCategory("category1");
FeedCategory category2 = new FeedCategory("category2");
Feed testFeed1 = new Feed("test", "test", category1);
Feed testFeed2 = new Feed("test", "test", category1);
Feed testFeed3 = new Feed("test", "test", category2);
feedCategoryRepository.save(category1);
feedCategoryRepository.save(category2);
feedRepository.save(testFeed1);
feedRepository.save(testFeed2);
feedRepository.save(testFeed3);
}
@Test
public void shouldReturnAllFeedItems_whenGetFeeds() throws Exception{
mvc.perform(get("/feed")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(3)))
.andExpect(jsonPath("$[0].name").value("test"));
}
@Test
public void shouldThrowError_whenAddingCategoriesUnauthorized() throws Exception{
mvc.perform(get("/secure/category")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized());
}
@Test
public void shouldReturnAddedCategory_whenAddingCategoriesAuthorized() throws Exception{
mvc.perform(get("/secure/category").with(user("Admin"))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized());
}
}
  • The integration test is used to test all application layers without any mocking involved. The @SpringBootTest annotation in this test creates the ApplicationContext for the entire container.
  • This test configures an in-memory h2 database defined in properties file. It could be another database (e.g. MongoDB) if that’s your usecase.
  • The test sets up the database by adding a couple categories and three feeds. It then tests the get method in FeedController and addCategory method in FeedCategoryController. The addCategory method is tested with an unauthorized as well as an authorized user. With unauthorized user, we’re expecting a 401 result, and with the authorized user, we’re expecting a 200 success response.

But wait, how do we test the authentication route?

package com.demo.imagebrowser;
import com.demo.imagebrowser.domain.Feed;
import com.demo.imagebrowser.domain.FeedCategory;
import com.demo.imagebrowser.repository.FeedCategoryRepository;
import com.demo.imagebrowser.repository.FeedRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.json.JacksonJsonParser;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = ImagebrowserApplication.class)
@TestPropertySource(locations = "classpath:application-test.properties")
public class ImageBrowserApplicationIntegrationTest {
private MockMvc mvc;
@Autowired
private FeedRepository feedRepository;
@Autowired
private FeedCategoryRepository feedCategoryRepository;
@Autowired
private WebApplicationContext wac;
@Autowired
private FilterChainProxy springSecurityFilterChain;
@Value("${security.jwt.client-id}")
private String jwtClientId;
@Before
public void setup() {
feedCategoryRepository.deleteAll();
feedRepository.deleteAll();
FeedCategory category1 = new FeedCategory("category1");
FeedCategory category2 = new FeedCategory("category2");
Feed testFeed1 = new Feed("test", "test", category1);
Feed testFeed2 = new Feed("test", "test", category1);
Feed testFeed3 = new Feed("test", "test", category2);
feedCategoryRepository.save(category1);
feedCategoryRepository.save(category2);
feedRepository.save(testFeed1);
feedRepository.save(testFeed2);
feedRepository.save(testFeed3);
this.mvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(springSecurityFilterChain).build();
}
private String getToken(String username, String password) throws Exception {
String jwtSecret = "jwtclientsecret";
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "password");
params.add("client_id", jwtClientId);
params.add("username", username);
params.add("password", password);
ResultActions result
= this.mvc.perform(post("/oauth/token")
.params(params)
.with(httpBasic(jwtClientId, jwtSecret))
.accept("application/json;charset=UTF-8"))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"));
String resultString = result.andReturn().getResponse().getContentAsString();
JacksonJsonParser jsonParser = new JacksonJsonParser();
return jsonParser.parseMap(resultString).get("access_token").toString();
}
@Test
public void shouldReturnAllFeedItems_whenGetFeeds() throws Exception{
mvc.perform(get("/feed")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(3)))
.andExpect(jsonPath("$[0].name").value("test"));
}
@Test
public void shouldThrowError_whenAddingCategoriesUnauthorized() throws Exception{
String categoryName = "test_category";
mvc.perform(put("/secure/category", categoryName)
.contentType(MediaType.APPLICATION_JSON)
.param("categoryName", categoryName))
.andExpect(status().isUnauthorized());
}
@Test
public void shouldReturnAddedCategory_whenAddingCategoriesAuthorized() throws Exception{
String categoryName = "test_category";
String jwtToken = getToken("Admin", "password");
mvc.perform(put("/secure/category", categoryName)
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.param("categoryName", categoryName))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value(categoryName));
}
}
  • The above commit has a test method shouldReturnAddedCategory_whenAddingCategoriesAuthorized, which is supposed to test the FeedCategoryController.addCategory method, though it doesn’t seem to do that. We fix that test by expecting HTTPStatus.OK response instead of HTTPStatus.Unauthorized. But wait – the test is failing now, why?

Seems like we forgot a few things during configuration, and the final integration test helped us catch these issues. This commit has the test working with these changes:

  • UserDetailsService requires a bean that implements PasswordEncoder. We either have to provide this bean, or we update all the passwords in the database and add {bcrypt} (or whatever encoder you prefer to use, including {noop} for demo purposes) prefix to them (e.g. {bcrypt}$2356...). We choose to add the bean instead of modifying the SQL data.
  • We added logging.level.org.springframework.security=DEBUG property to application-test.properties file so that we can see the debug messages from Spring security framework while running our integration test.
  • It turns out the client secret in application.properties file needs to be encrypted with BCrypt as well, otherwise Spring will complain with the error: There is no PasswordEncoder mapped for the id "null". So we update the properties file with the encoded version of the client secret. Note, the clients will still send the secret in plain text format, and Spring will encode it and compare to the encoded version provided in our properties file.
  • We added the getToken method to the integration test file as a helper. This method, when given a valid username and password, will authenticate us by posting to oauth/token endpoint and give us a valid token. We use this token in our request to /secure/category by passing it in the http header.

Closing

Spring framework offers a comprehensive model for building OAUTH enabled REST applications, and in this post we built a simple RSS feed reader using Spring Boot. Full source code is available here. In the next article we’ll look at building an Angular 7 client which would connect to our backend and authenticate using JSON Web Tokens, and show the RSS feed contents.

In the next post, I’ll walk you through building the Angular 7 client application which would connect to this backend and read the feeds.

References

Leave a Comment

Your email address will not be published. Required fields are marked *