Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ target/
!**/src/test/**/target/
/config.json
resources/*
run

### STS ###
.apt_generated
Expand Down
20 changes: 20 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,26 @@
<scope>compile</scope>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>

</dependencies>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.imjustdoom.pluginsite.config.custom;

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Key;
import java.time.Duration;

@Getter
@ConfigurationProperties("jwt")
@ConstructorBinding
public class JwtConfig {
private static final Path KEY_PATH = Path.of("./jwt_secret");

private final Duration expiryTime;
private final String domain;
private final boolean secureCookie;
private final Key key;

public JwtConfig(Duration expiryTime, String domain, boolean secureCookie, String key) throws IOException {
this.expiryTime = expiryTime;
this.domain = domain;
this.secureCookie = secureCookie;
byte[] encodedSecret;
if (key == null) {
if (Files.exists(KEY_PATH)) {
encodedSecret = Files.readAllBytes(KEY_PATH);
} else {
encodedSecret = Keys.secretKeyFor(SignatureAlgorithm.HS512).getEncoded();
Files.write(KEY_PATH, encodedSecret);
}
} else {
encodedSecret = key.getBytes(StandardCharsets.UTF_8);
}
this.key = Keys.hmacShaKeyFor(encodedSecret);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.springframework.util.unit.DataUnit;

@Getter
@ConfigurationProperties(prefix = "app")
@ConfigurationProperties("app")
@ConstructorBinding
@AllArgsConstructor
public class SiteConfig {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,74 @@
package com.imjustdoom.pluginsite.config.security;

import com.imjustdoom.pluginsite.config.custom.SiteConfig;
import com.imjustdoom.pluginsite.config.security.jwt.JwtAuthenticationFilter;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.http.HttpStatus;
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.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final DaoAuthenticationProvider authProvider;
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final SiteConfig siteConfig;

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

@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(this.authProvider);
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userDetailsService).passwordEncoder(this.passwordEncoder);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and().csrf()
.ignoringAntMatchers("/auth/**")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

.authorizeRequests()
.antMatchers("/admin", "/admin/roles").hasRole("ADMIN")
.antMatchers("/resources/create", "/account/details").authenticated()
.antMatchers("/register", "/login").not().authenticated()
.and().authorizeRequests().anyRequest().permitAll()

.anyRequest().permitAll()

.and()
.formLogin().loginProcessingUrl("/login");
.and().logout().disable()
.httpBasic().disable()
.formLogin().disable()
.exceptionHandling().authenticationEntryPoint((request, response, authException) -> response.sendError(HttpStatus.NOT_FOUND.value()))

.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and().addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}

// @Bean
// public CorsConfigurationSource corsConfigurationSource() {
// CorsConfiguration configuration = new CorsConfiguration();
// configuration.setAllowedOrigins(Collections.singletonList(this.siteConfig.getDomain()));
// configuration.addAllowedHeader("*");
// configuration.addAllowedMethod("*");
// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// source.registerCorsConfiguration("/**", configuration);
// return source;
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.imjustdoom.pluginsite.config.security.jwt;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = this.getJwtToken(request);
System.out.println("Doing filter A " + jwt);
if (jwt != null && !jwt.isEmpty() && this.jwtProvider.validateToken(jwt)) {
System.out.println("Doing filter B");
String username = this.jwtProvider.getSubjectFromToken(jwt);
System.out.println("Doing filter B " + username);
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
System.out.println("Doing filter C " + userDetails);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
System.out.println("Doing filter D " + authentication);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("Set authentication");
}

filterChain.doFilter(request, response);
}

private String getJwtToken(HttpServletRequest request) {
if (request.getCookies() == null)
return null;
for (Cookie cookie : request.getCookies()) {
if (!cookie.getName().equals(JwtProvider.COOKIE_NAME))
continue;
return cookie.getValue();
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.imjustdoom.pluginsite.config.security.jwt;

import com.imjustdoom.pluginsite.config.custom.JwtConfig;
import com.imjustdoom.pluginsite.model.Account;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.servlet.http.Cookie;
import java.time.Instant;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtProvider {
public static final String COOKIE_NAME = "JWT-TOKEN";
private static final String USER_ID = "userId";

private final JwtConfig jwtConfig;

public Cookie generateTokenCookie(Authentication authentication) {
Cookie cookie = new Cookie(COOKIE_NAME, this.generateToken(authentication));
cookie.setSecure(this.jwtConfig.isSecureCookie());
cookie.setHttpOnly(true);
cookie.setMaxAge((int) this.jwtConfig.getExpiryTime().getSeconds());
cookie.setPath("/");
if (this.jwtConfig.getDomain() != null)
cookie.setDomain(this.jwtConfig.getDomain());

return cookie;
}

public String generateToken(Authentication authentication) {
Account account = (Account) authentication.getPrincipal();

Claims claims = Jwts.claims().setSubject(account.getUsername());
claims.put(USER_ID, account.getId());

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(Instant.now().plus(this.jwtConfig.getExpiryTime())))
.signWith(this.jwtConfig.getKey())
.compact();
}

public String getSubjectFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(this.jwtConfig.getKey())
.build()
.parseClaimsJws(token)
.getBody().getSubject();
}

public boolean validateToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(this.jwtConfig.getKey())
.build()
.parseClaimsJws(token) != null;
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException ex) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.imjustdoom.pluginsite.config.exception.RestErrorCode;
import com.imjustdoom.pluginsite.config.exception.RestException;
import com.imjustdoom.pluginsite.dtos.in.account.CreateAccountRequest;
import com.imjustdoom.pluginsite.dtos.in.account.UpdateAccountRequest;
import com.imjustdoom.pluginsite.dtos.out.account.AccountDto;
import com.imjustdoom.pluginsite.dtos.out.account.SelfAccountDto;
Expand All @@ -14,10 +13,10 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -31,21 +30,14 @@ public class AccountController {
private final AccountService accountService;
private final MessageService messageService;

@PostMapping("/register")
public String signupSubmit(@RequestBody CreateAccountRequest accountRequest) throws RestException {
Account account = this.accountService.register(accountRequest);

return ""; // todo return a registration DTO of some sort, probably with a session token
}

// todo login with JWT

@GetMapping("/details")
@PreAuthorize("isAuthenticated()")
public SelfAccountDto getSelfAccountDetails(Account account) {
return SelfAccountDto.fromAccount(account);
}

@PatchMapping("/details")
@PreAuthorize("isAuthenticated()")
public SelfAccountDto updateAccountDetails(Account account, @RequestBody UpdateAccountRequest request,
@RequestParam(value = "profilePicture", required = false) MultipartFile file) throws RestException {
return SelfAccountDto.fromAccount(this.accountService.updateAccountDetails(account, request, file));
Expand All @@ -57,6 +49,7 @@ public AccountDto getAccountDetails(@PathVariable int id) throws RestException {
}

@GetMapping("/groups")
@PreAuthorize("isAuthenticated()")
public Page<MessageGroupDto> getMessageGroups(Account account,
@PageableDefault(size = 25) Pageable pageable) throws RestException {
if (pageable.getPageSize() > 50) throw new RestException(RestErrorCode.PAGE_SIZE_TOO_LARGE, "Page size is too large (%s > %s)", pageable.getPageSize(), 50);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -19,6 +20,7 @@
@RestController
@RequestMapping("/admin")
@RequiredArgsConstructor
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public class AdminController {
private final ReportService reportService;

Expand Down
Loading