Full Stack Reddit Clone - Spring Boot, React, Electron App - Part 4
Tags: react, java, typescript, webdev
Published: 2020-08-02 16:27:42

# Full Stack Reddit Clone - Spring Boot, React, Electron App - Part 4

## Introduction

Welcome to Part 4 of creating a Reddit clone using Spring Boot, and React.

What are we building in this part?

- User Information Service Implementation
- Update Security Configuration
- Authentication Response
- Login Request DTO
- Update Auth Service
- Creation of Java Key Store
- Creation of JWT
- Login Logic
- Login Endpoint

In [Part 3](https://dev.to/maxicb/full-stack-reddit-clone-spring-boot-react-electron-app-part-3-4a4n) we created the registration and account verification logic!

## Important Links

- Backend Source: https://github.com/MaxiCB/vox-nobis/tree/master/backend
- Frontend Source: https://github.com/MaxiCB/vox-nobis/tree/master/client
- Live URL: In Progress

## Part 1: User Information Service Implementation 😀

Let's cover the user service implementation class we will need. Inside **com.your-name.backend** create a new package called **service**, and add the following classes.

- UserInformationServiceImpl: Is our interface that fetches user information from our PostgreSQL database.

```
package com.maxicb.backend.service;

import com.maxicb.backend.model.User;
import com.maxicb.backend.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collection;
import java.util.Collections;
import java.util.Optional;

@Service
@AllArgsConstructor
public class UserInformationServiceImpl implements UserDetailsService {
    UserRepository userRepository;

    private Collection<? extends GrantedAuthority> fetchAuths (String role) {
        return Collections.singletonList(new SimpleGrantedAuthority(role));
    }

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Optional<User> optionalUser = userRepository.findByUsername(s);
        User user = optionalUser.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + s));

        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                user.getPassword(),
                user.isAccountStatus(),
                true,
                true,
                true,
                fetchAuths("USER"));
    }
}
```

## Part 2: Updated Security Configuration 🎟

Let's cover the security config class we will need to update. Inside **com.your-name.backend.config** update the following classes.

- Security: Handles the security configuration for the whole application, handles encoding the password before storing it into the database, and fetching user information.

```
package com.maxicb.backend.config;

import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
@AllArgsConstructor
public class Security extends WebSecurityConfigurerAdapter {

    UserDetailsService userDetailsService;

    @Autowired
    public void configureGlobalConfig(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/auth/**")
                .permitAll()
                .anyRequest()
                .authenticated();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
```

## Part 3: Authentication Response DTO

Let's cover the Authentication Response DTO class we will need. Inside **com.your-name.backend.dto** create the following class,

- AuthResponse: Defines the data that our backend will send to the client for an authentication response.

```
package com.maxicb.backend.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
        private String authenticationToken;
        private String username;
}
```

## Part 4: Login Request DTO

Let's cover the Login Request DTO class we will need. Inside **com.your-name.backend.dto** create the following class,

- LoginRequest: Defines the data that our backend will recieve from the client during a login request.

```
package com.maxicb.backend.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class LoginRequest {
    private String username;
    private String password;
}
```

## Part 5: Create Java Key Store

Let's cover the java keystore we will need. Inside **resources** place the keystore you will create after you are done.

- Java Keystore:
  You can refer to the oracle docs for creating a keystore [Here](https://docs.oracle.com/cd/E35976_01/server.740/es_admin/src/tadm_ssl_jetty_keystore.html)
  - Key store generation command
  ```
  keytool -genkey -alias alias -keyalg RSA -keystore keystore.jks -keysize 2048
  ```
  Ensure you are in a easily acessible directory when you run this command as it will create the keystore there, and you will need to place it inside your project.
  Ensure you keep track of the password, and alias that you use as you will need it later within the code base.

## Part 6: Create JWT

Let's cover the JWT class we will need. Inside **com.your-name.backend** create a new package called **security**, and add the following class.

- JWTProvider: Handles all of the logic for loading the keystore, and generating JWT based on that.

```
package com.maxicb.backend.security;

import com.maxicb.backend.exception.ActivationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;

import io.jsonwebtoken.Jwts;

@Service
public class JWTProvider {
    private KeyStore keystore;

    @PostConstruct
    public void init() {
        try {
            keystore = KeyStore.getInstance("JKS");
            InputStream resourceStream = getClass().getResourceAsStream("/keystore.jks");
            keystore.load(resourceStream, "password".toCharArray());
        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
            throw new ActivationException("Exception occured while loading keystore");
        }
    }

    public String generateToken(Authentication authentication) {
        org.springframework.security.core.userdetails.User princ = (User) authentication.getPrincipal();
        return Jwts.builder().setSubject(princ.getUsername()).signWith(getPrivKey()).compact();
    }

    private PrivateKey getPrivKey () {
        try {
            return (PrivateKey) keystore.getKey("alias", "password".toCharArray());
        } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
            throw new ActivationException("Exception occurred while retrieving public key");
        }
    }
}
```

## Part 7: Update Auth Service

Let's update out Authentication Service class to add login functionality. Inside **com.your-name.backend.service** update the following class.

- AuthService: We are adding the login logic to our authentication service.

```
package com.maxicb.backend.service;

import com.maxicb.backend.dto.AuthResponse;
import com.maxicb.backend.dto.LoginRequest;
import com.maxicb.backend.dto.RegisterRequest;
import com.maxicb.backend.exception.ActivationException;
import com.maxicb.backend.model.AccountVerificationToken;
import com.maxicb.backend.model.NotificationEmail;
import com.maxicb.backend.model.User;
import com.maxicb.backend.repository.TokenRepository;
import com.maxicb.backend.repository.UserRepository;
import com.maxicb.backend.security.JWTProvider;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

import static com.maxicb.backend.config.Constants.EMAIL_ACTIVATION;

@Service
@AllArgsConstructor

public class AuthService {

    UserRepository userRepository;
    PasswordEncoder passwordEncoder;
    TokenRepository tokenRepository;
    MailService mailService;
    MailBuilder mailBuilder;
    AuthenticationManager authenticationManager;
    JWTProvider jwtProvider;

    @Transactional
    public void register(RegisterRequest registerRequest) {
        User user = new User();
        user.setUsername(registerRequest.getUsername());
        user.setEmail(registerRequest.getEmail());
        user.setPassword(encodePassword(registerRequest.getPassword()));
        user.setCreationDate(Instant.now());
        user.setAccountStatus(false);

        userRepository.save(user);

        String token = generateToken(user);
        String message = mailBuilder.build("Welcome to React-Spring-Reddit Clone. " +
                "Please visit the link below to activate you account : " + EMAIL_ACTIVATION + "/" + token);
        mailService.sendEmail(new NotificationEmail("Please Activate Your Account", user.getEmail(), message));
    }

    public AuthResponse login (LoginRequest loginRequest) {
        Authentication authenticate = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(), loginRequest.getPassword()));
        SecurityContextHolder.getContext().setAuthentication(authenticate);
        String authToken = jwtProvider.generateToken(authenticate);
        return new AuthResponse(authToken, loginRequest.getUsername());
    }

    private String encodePassword(String password) {
        return passwordEncoder.encode(password);
    }

    private String generateToken(User user) {
        String token = UUID.randomUUID().toString();
        AccountVerificationToken verificationToken = new AccountVerificationToken();
        verificationToken.setToken(token);
        verificationToken.setUser(user);
        tokenRepository.save(verificationToken);
        return token;
    }

    public void verifyToken(String token) {
        Optional<AccountVerificationToken> verificationToken = tokenRepository.findByToken(token);
        verificationToken.orElseThrow(() -> new ActivationException("Invalid Activation Token"));
        enableAccount(verificationToken.get());
    }

    public void enableAccount(AccountVerificationToken token) {
        String username = token.getUser().getUsername();
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new ActivationException("User not found with username: " + username));
        user.setAccountStatus(true);
        userRepository.save(user);
    }
}
```

## Part 8: Login Endpoint

Let's update our Auth Controller class to add login endpoint. Inside **com.your-name.backend.controller** update the following class.

- AuthController: Defines the different endpoints for registering, activating, and logging in a user.

```
package com.maxicb.backend.controller;

import com.maxicb.backend.dto.AuthResponse;
import com.maxicb.backend.dto.LoginRequest;
import com.maxicb.backend.dto.RegisterRequest;
import com.maxicb.backend.service.AuthService;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
@AllArgsConstructor
public class AuthController {

    AuthService authService;

    @PostMapping("/register")
    public ResponseEntity register(@RequestBody RegisterRequest registerRequest) {
        authService.register(registerRequest);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @GetMapping("/verify/{token}")
    public ResponseEntity verify(@PathVariable String token) {
        authService.verifyToken(token);
        return new ResponseEntity<>("Account Activated", HttpStatus.OK);
    }

    @PostMapping("/login")
    public AuthResponse register(@RequestBody LoginRequest loginRequest) {
        return authService.login(loginRequest);
    }
}
```

## Conclusion 🔍

- To ensure everything is configured correctly you can run the application, and ensure there are no error in the console. Towards the bottom of the console you should see output similar to below

  ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mcnzflm8vjwnvjcx40sh.PNG "Successful Run")

- If there are no error's in the console you can test you registration logic by sending a post request to **http://localhost:8080/api/auth/register** with the following data

```
{
    "username": "test",
    "email": "test1@test.com",
    "password": "test12345"
}
```

- Once you recieve a 200 OK status back you can check you mailtrap.io inbox to find the activation email that was sent. The link should look similar to http://localhost:8080/api/auth/verify/{token}, **be sure to omit the &lt** from the end of the link. Navigation to the link will activate the account, and you should see "Account Activated" displayed as a response.

- After activating your account you can test you login logic by sending a post request to **http://localhost:8080/api/auth/login** with the following data

```
{
    "username": "test",
    "password": "test12345"
}
```

- After logging in you should see a response similar to below

```
{
    "authenticationToken": {real_long_token},
    "username": "test"
}
```

- In this article we added our JWT token generation, login logic, and login endpoint.

## Next [Part 5](https://dev.to/maxicb/full-stack-reddit-clone-spring-boot-react-electron-app-part-5-2h8j)