Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ import org.openapitools.generator.gradle.plugin.tasks.GenerateTask

plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.3'

id 'org.asciidoctor.jvm.convert' version '4.0.5'
id 'org.openapi.generator' version '7.4.0'

id 'jacoco'
id "org.sonarqube" version "7.1.0.6387"
}

group = 'app.esiroi'
Expand Down Expand Up @@ -38,17 +35,6 @@ ext {
set('snippetsDir', file("build/generated-snippets"))
}

jacoco {
toolVersion = "0.8.11"
}

sonar {
properties {
property "sonar.java.source", "21"
property "sonar.java.target", "21"
property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml"
}
}

tasks.register('generateJavaClient', GenerateTask) {
generatorName = "java"
Expand Down Expand Up @@ -98,6 +84,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'app.esiroi:auth-gen:1.0.0'
implementation 'org.flywaydb:flyway-core'
compileOnly 'org.projectlombok:lombok'
Expand All @@ -117,15 +104,8 @@ dependencies {
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
finalizedBy jacocoTestReport
}

jacocoTestReport {
reports {
xml.required = true
html.required = true
}
}

tasks.named('asciidoctor') {
inputs.dir snippetsDir
Expand Down
32 changes: 29 additions & 3 deletions doc/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ servers:
- url: 'http://localhost:8080'

paths:
/v1/ping:
/ping:
get:
tags:
- Health
Expand All @@ -28,7 +28,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Exception'
/v1/login:
/login:
post:
tags:
- Security
Expand All @@ -54,7 +54,33 @@ paths:
schema:
$ref: '#/components/schemas/Exception'

/v1/validateOTP:
/register:
post:
tags:
- Security
summary: Create user
security: []
operationId: createUser
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthUser'
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: Authenticated User
403:
description: Bad credentials
content:
application/json:
schema:
$ref: '#/components/schemas/Exception'

/validateOTP:
post:
tags:
- Security
Expand Down
2 changes: 1 addition & 1 deletion env.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydb
SPRING_DATASOURCE_USERNAME: myuser
SPRING_DATASOURCE_PASSWORD: mypassword
SPRING_JPA_HIBERNATE_DDL_AUTO: none
SPRING_PROFILES_ACTIVE: prod
SPRING_PROFILES_ACTIVE: test
CRYPTO_SALT: a1b2c3d4e5f67890abcdef1234567890
CRYPTO_SECRET_KEY: test
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,87 @@

import app.esiroi.auth.endpoint.mapper.UserRestMapper;
import app.esiroi.auth.endpoint.rest.model.AuthUser;
import app.esiroi.auth.endpoint.rest.model.User;
import app.esiroi.auth.endpoint.security.AuthProvider;
import app.esiroi.auth.service.UserService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/v1")
@Controller
@AllArgsConstructor
public class AuthenticationController {
private final UserService service;
private final UserRestMapper mapper;

@GetMapping("/")
public String index(Model model) {
model.addAttribute("user", new AuthUser());
return "index";
}

@PostMapping("/login")
public User authenticateUser(@RequestBody AuthUser toAuthenticate) {
public String login(@ModelAttribute AuthUser toAuthenticate, HttpServletResponse response) {
var user = service.authenticateUser(toAuthenticate);
return mapper.toRest(user);
var cookie = putTokenInCookie(user.getAccessToken());
response.addCookie(cookie);

return "redirect:/validateOTP";
}

@PostMapping("/logout")
public String logout(HttpServletResponse response) {
var cookie = clearInCookie();
response.addCookie(cookie);
return "redirect:/";
}

@PostMapping("/register")
public String register(@ModelAttribute AuthUser toRegister) {
var toSave = mapper.toDomain(toRegister);
service.saveUser(toSave);
return "redirect:/validateOTP";
}

@GetMapping("/register")
public String registerPage(Model model) {
model.addAttribute("user", new AuthUser());
return "register";
}

@GetMapping("/validateOTP")
public String validateOTP() {
return "otp";
}

@GetMapping("/profile")
public String profile(Model model) {
var email = AuthProvider.getAuthenticatedUserEmail();
model.addAttribute("email", email);
return "profile";
}

@PostMapping("/validateOTP")
public User validateOTP(@RequestBody String otp) {
var persisted = service.validateOTP(otp);
return mapper.toRest(persisted);
public String validate(@RequestParam("otp") String otp) {
service.validateOTP(otp);
return "redirect:/profile";
}

private Cookie putTokenInCookie(String token) {
Cookie cookie = new Cookie("AUTH-TOKEN", token);
cookie.setHttpOnly(true);
cookie.setPath("/");

return cookie;
}

private Cookie clearInCookie() {
Cookie cookie = new Cookie("AUTH-TOKEN", null);
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(0);
return cookie;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package app.esiroi.auth.endpoint.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1")
public class HealthController {

@GetMapping("/ping")
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/app/esiroi/auth/endpoint/mapper/UserRestMapper.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package app.esiroi.auth.endpoint.mapper;

import static java.util.UUID.randomUUID;

import app.esiroi.auth.endpoint.rest.model.AuthUser;
import app.esiroi.auth.endpoint.rest.model.User;
import app.esiroi.auth.endpoint.security.Encryptor;
import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@AllArgsConstructor
public class UserRestMapper {
private final PasswordEncoder passwordEncoder;
private final Encryptor encryptor;

public User toRest(app.esiroi.auth.model.User user) {
return new User()
.id(user.getId())
Expand All @@ -13,4 +23,15 @@ public User toRest(app.esiroi.auth.model.User user) {
.otpValidationRequired(user.isOtpValidationRequired())
.accessToken(user.getAccessToken());
}

public app.esiroi.auth.model.User toDomain(AuthUser user) {
var hashedPass = passwordEncoder.encode(user.getPassword());
var encryptedSecret = encryptor.getInstance().encrypt("test".getBytes());
return app.esiroi.auth.model.User.builder()
.id(randomUUID().toString())
.email(user.getEmail())
.passwordHash(hashedPass)
.otpSecret(encryptedSecret)
.build();
}
}
15 changes: 11 additions & 4 deletions src/main/java/app/esiroi/auth/endpoint/security/JWTAuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -22,10 +24,15 @@ public class JWTAuthFilter extends OncePerRequestFilter {
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");

if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
var cookies = request.getCookies();
if (cookies != null) {
String token =
Arrays.stream(request.getCookies())
.filter(cookie -> "AUTH-TOKEN".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
;

if (jwtConf.validateToken(token)) {
String email = jwtConf.extractEmail(token);
Expand Down
41 changes: 36 additions & 5 deletions src/main/java/app/esiroi/auth/endpoint/security/SecurityConf.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import static org.springframework.web.cors.CorsConfiguration.ALL;

import java.util.List;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -19,32 +19,63 @@
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.HandlerExceptionResolver;

@Configuration
@Slf4j
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConf {
private final JWTAuthFilter authFilter;

private final HandlerExceptionResolver handlerExceptionResolver;

public SecurityConf(
JWTAuthFilter authFilter,
@Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver) {
this.authFilter = authFilter;
this.handlerExceptionResolver = handlerExceptionResolver;
}

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.cors(corsConf -> corsConf.configurationSource(corsConfigurationSource()))
.exceptionHandling(
exceptions ->
exceptions.authenticationEntryPoint(
(request, response, authException) -> {
handlerExceptionResolver.resolveException(
request, response, null, authException);
}))
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers("/css/**", "/js/**", "/images/**")
.permitAll()
.requestMatchers(OPTIONS, "/**")
.permitAll()
.requestMatchers(GET, "/v1/ping")
.requestMatchers(GET, "/ping")
.permitAll()
.requestMatchers(GET, "/")
.permitAll()
.requestMatchers(POST, "/v1/login")
.requestMatchers(POST, "/login")
.permitAll()
.requestMatchers(POST, "/v1/validateOTP")
.requestMatchers(POST, "/logout")
.permitAll()
.requestMatchers(GET, "/register")
.permitAll()
.requestMatchers(POST, "/register")
.permitAll()
.requestMatchers(GET, "/validateOTP")
.authenticated()
.requestMatchers(POST, "/validateOTP")
.authenticated()
.requestMatchers(GET, "/profile")
.authenticated()
.anyRequest()
.denyAll())
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/app/esiroi/auth/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public User authenticateUser(AuthUser toAuthenticate) {
throw new ForbiddenException("Bad credentials");
}

public User saveUser(User user) {
return repository.save(user);
}

public User validateOTP(String otp) {
var email = AuthProvider.getAuthenticatedUserEmail();
var authUser = getUserByEmail(email);
Expand Down
Loading