Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add User authentification #42

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
8 changes: 4 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ jobs:
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '11'
java-version: '17'

- name: Run tests
run: mvn test
run: ./gradlew test

- name: Build with Maven
run: mvn clean package
- name: Build with Gradle
run: ./gradlew clean build

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand Down
4 changes: 1 addition & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
FROM openjdk:11-jre
RUN rm -rf /usr/local/tomcat/webapps/*
RUN mkdir -p /logs
COPY ./target/API-Gateway-0.0.1-SNAPSHOT.jar /usr/local/tomcat/webapps/API-Gateway-0.0.1-SNAPSHOT.jar
COPY ./build/libs/api-gateway-1.0-SNAPSHOT.jar /usr/local/tomcat/webapps/API-Gateway-0.0.1-SNAPSHOT.jar
EXPOSE 8080
CMD ["sh","-c", "java -jar /usr/local/tomcat/webapps/API-Gateway-0.0.1-SNAPSHOT.jar"]


5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ dependencies {
// Swagger UI
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0'

// Jwt authentification
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// Tests
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'com.h2database:h2'
Expand Down
33 changes: 29 additions & 4 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
version: '3.8'

version: '3.9'
services:
api-gateway-backend:
build:
Expand All @@ -8,11 +7,37 @@ services:
container_name: api-gateway
ports:
- "8080:8080"
restart: always
profiles:
- all
depends_on:
- postgres
environment:
ONTOPORTAL_APIKEY: "put here APIKEY"
SPRING_DATASOURCE_URL: jdbc:postgresql://0.0.0.0:5432/db
POSTGRES_PASSWORD: password
POSTGRES_USER: backend
SPRING_JPA_HIBERNATE_DDL_AUTO: update

postgres:
image: postgres
restart: always
environment:
- POSTGRES_DB=db
- POSTGRES_PASSWORD=password
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to extract this to .env

- POSTGRES_USER=developer
ports:
- "5432:5432"
networks:
- api-gateway
- network

adminer:
image: adminer
restart: always
ports:
- 8081:8080
networks:
- network
networks:
api-gateway:
network:
driver: bridge
71 changes: 71 additions & 0 deletions src/main/java/org/semantics/apigateway/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.semantics.apigateway;

import jakarta.servlet.http.HttpServletResponse;
import org.semantics.apigateway.service.auth.AuthService;
import org.semantics.apigateway.service.auth.JwtAuthenticationFilter;
import org.semantics.apigateway.service.auth.TokenBlacklist;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final AuthService userDetailsService;
private final TokenBlacklist tokenBlacklist;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityConfig(AuthService userDetailsService, TokenBlacklist tokenBlacklist, JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userDetailsService = userDetailsService;
this.tokenBlacklist = tokenBlacklist;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutSuccessHandler((request, response, authentication) -> {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
tokenBlacklist.blacklistToken(token);
}
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write("You have been logged out successfully.");
})
);

return http.build();
}

@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.semantics.apigateway.controller;

import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.semantics.apigateway.model.responses.SuccessResponse;
import org.semantics.apigateway.model.user.*;
import org.semantics.apigateway.service.auth.JwtUtil;
import org.semantics.apigateway.service.auth.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;
import java.util.Date;

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

private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil,
UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}

@PostMapping("/register")
@Operation(tags = {"Users"})
@ResponseStatus(HttpStatus.CREATED)
public SuccessResponse registerUser(@Valid @RequestBody RegisterRequest user) {
User newUser = new User();
newUser.setUsername(user.getUsername());
newUser.setPassword(passwordEncoder.encode(user.getPassword()));
newUser.setRoles(Collections.singleton(Role.USER));
userRepository.save(newUser);

return new SuccessResponse(
"User created successfully",
"success");
}

@PostMapping("/login")
@Operation(tags = {"Users"})
public AuthResponse loginUser(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
String token = jwtUtil.generateToken(authentication.getName());
UserDetails userDetails = (UserDetails) authentication.getPrincipal();

Date expiration = jwtUtil.extractExpiration(token);
GrantedAuthority role = userDetails.getAuthorities().stream().findFirst().orElse(null);
return new AuthResponse(token, loginRequest.getUsername(), role != null ? role.getAuthority() : "", expiration);

}

@GetMapping("/logout")
@Operation(tags = {"Users"})
public SuccessResponse logout(HttpServletRequest request) {
SecurityContextHolder.clearContext();
return new SuccessResponse("Logged out successfully.", "success");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.semantics.apigateway.controller;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import org.semantics.apigateway.model.responses.ErrorResponse;
import org.semantics.apigateway.model.user.InvalidJwtException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.security.SignatureException;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(InvalidJwtException.class)
public ResponseEntity<ErrorResponse> handleJwtException(InvalidJwtException ex) {
ErrorResponse errorResponse = new ErrorResponse("JWT_ERROR", ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
}

@ExceptionHandler({ExpiredJwtException.class, MalformedJwtException.class, SignatureException.class})
public ResponseEntity<ErrorResponse> handleJwtProcessingException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse("JWT_ERROR", "Invalid or expired token: " + ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
}

@ExceptionHandler({BadCredentialsException.class})
public ResponseEntity<ErrorResponse> handleBadCredentialsException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse("BAD_CREDENTIALS", ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.semantics.apigateway.model.responses;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class ErrorResponse {
private String error;
private String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.semantics.apigateway.model.responses;


import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class SuccessResponse {
private String message;
private String status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.semantics.apigateway.model.user;

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Setter
@Getter
public class AuthResponse {

private String token;
private String username;
private String role;
private Date expiration;

public AuthResponse(String token, String username, String role, Date expiration) {
this.token = token;
this.username = username;
this.role = role;
this.expiration = expiration;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.semantics.apigateway.model.user;

import lombok.Getter;

@Getter
public class InvalidJwtException extends RuntimeException {
public InvalidJwtException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.semantics.apigateway.model.user;

import lombok.Getter;
import lombok.Setter;
import jakarta.validation.constraints.NotNull;

@Getter
@Setter
public class LoginRequest {

@NotNull
private String username;

@NotNull
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.semantics.apigateway.model.user;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RegisterRequest {
String username;
String password;
}
5 changes: 5 additions & 0 deletions src/main/java/org/semantics/apigateway/model/user/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.semantics.apigateway.model.user;

public enum Role {
ADMIN, USER
}
Loading
Loading