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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.bugzkit.api.auth;

public record AuthTokens(String accessToken, String refreshToken) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.List;
import org.bugzkit.api.auth.AuthTokens;
import org.bugzkit.api.auth.jwt.util.JwtUtil;
import org.bugzkit.api.auth.payload.dto.DeviceDTO;
import org.bugzkit.api.auth.payload.request.AuthTokensRequest;
import org.bugzkit.api.auth.payload.request.ForgotPasswordRequest;
import org.bugzkit.api.auth.payload.request.RegisterUserRequest;
import org.bugzkit.api.auth.payload.request.ResetPasswordRequest;
import org.bugzkit.api.auth.payload.request.VerificationEmailRequest;
import org.bugzkit.api.auth.payload.request.VerifyEmailRequest;
import org.bugzkit.api.auth.service.AuthService;
import org.bugzkit.api.auth.service.DeviceService;
import org.bugzkit.api.auth.util.AuthUtil;
import org.bugzkit.api.shared.constants.Path;
import org.bugzkit.api.user.payload.dto.UserDTO;
Expand All @@ -17,6 +22,8 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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;
Expand All @@ -26,6 +33,7 @@
@RequestMapping(Path.AUTH)
public class AuthController {
private final AuthService authService;
private final DeviceService deviceService;

@Value("${domain.name}")
private String domain;
Expand All @@ -36,8 +44,9 @@ public class AuthController {
@Value("${jwt.refresh-token.duration}")
private int refreshTokenDuration;

public AuthController(AuthService authService) {
public AuthController(AuthService authService, DeviceService deviceService) {
this.authService = authService;
this.deviceService = deviceService;
}

@PostMapping("/register")
Expand All @@ -49,59 +58,44 @@ public ResponseEntity<UserDTO> register(
@PostMapping("/tokens")
public ResponseEntity<Void> authenticate(
@Valid @RequestBody AuthTokensRequest authTokensRequest, HttpServletRequest request) {
final var ipAddress = AuthUtil.getUserIpAddress(request);
final var authTokensDTO = authService.authenticate(authTokensRequest, ipAddress);
final var accessTokenCookie =
AuthUtil.createCookie(
"accessToken", authTokensDTO.accessToken(), domain, accessTokenDuration);
final var refreshTokenCookie =
AuthUtil.createCookie(
"refreshToken", authTokensDTO.refreshToken(), domain, refreshTokenDuration);
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.build();
final var deviceId = AuthUtil.generateDeviceId();
final var userAgent = request.getHeader("User-Agent");
final var authTokens = authService.authenticate(authTokensRequest, deviceId, userAgent);
return setAuthCookies(authTokens);
}

@DeleteMapping("/tokens")
public ResponseEntity<Void> deleteTokens(HttpServletRequest request) {
final var accessToken = AuthUtil.getValueFromCookie("accessToken", request);
final var ipAddress = AuthUtil.getUserIpAddress(request);
authService.deleteTokens(accessToken, ipAddress);
final var accessTokenCookie = AuthUtil.createCookie("accessToken", "", domain, 0);
final var refreshTokenCookie = AuthUtil.createCookie("refreshToken", "", domain, 0);
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.build();
authService.deleteTokens(accessToken);
return removeAuthCookies();
}

@GetMapping("/tokens/devices")
public ResponseEntity<List<DeviceDTO>> findAllDevices(HttpServletRequest request) {
final var accessToken = AuthUtil.getValueFromCookie("accessToken", request);
final var deviceId = JwtUtil.getDeviceId(accessToken);
return ResponseEntity.ok(deviceService.findAll(deviceId));
}

@DeleteMapping("/tokens/devices")
public ResponseEntity<Void> deleteTokensOnAllDevices() {
authService.deleteTokensOnAllDevices();
final var accessTokenCookie = AuthUtil.createCookie("accessToken", "", domain, 0);
final var refreshTokenCookie = AuthUtil.createCookie("refreshToken", "", domain, 0);
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.build();
return removeAuthCookies();
}

@DeleteMapping("/tokens/devices/{deviceId}")
public ResponseEntity<Void> revokeDevice(@PathVariable String deviceId) {
deviceService.revoke(deviceId);
return ResponseEntity.noContent().build();
}

@PostMapping("/tokens/refresh")
public ResponseEntity<Void> refreshTokens(HttpServletRequest request) {
final var refreshToken = AuthUtil.getValueFromCookie("refreshToken", request);
final var ipAddress = AuthUtil.getUserIpAddress(request);
final var authTokensDTO = authService.refreshTokens(refreshToken, ipAddress);
final var accessTokenCookie =
AuthUtil.createCookie(
"accessToken", authTokensDTO.accessToken(), domain, accessTokenDuration);
final var refreshTokenCookie =
AuthUtil.createCookie(
"refreshToken", authTokensDTO.refreshToken(), domain, refreshTokenDuration);
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.build();
final var userAgent = request.getHeader("User-Agent");
final var authTokens = authService.refreshTokens(refreshToken, userAgent);
return setAuthCookies(authTokens);
}

@PostMapping("/password/forgot")
Expand Down Expand Up @@ -131,4 +125,25 @@ public ResponseEntity<Void> verifyEmail(
authService.verifyEmail(verifyEmailRequest);
return ResponseEntity.noContent().build();
}

private ResponseEntity<Void> setAuthCookies(AuthTokens authTokens) {
final var accessTokenCookie =
AuthUtil.createCookie("accessToken", authTokens.accessToken(), domain, accessTokenDuration);
final var refreshTokenCookie =
AuthUtil.createCookie(
"refreshToken", authTokens.refreshToken(), domain, refreshTokenDuration);
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.build();
}

private ResponseEntity<Void> removeAuthCookies() {
final var accessTokenCookie = AuthUtil.createCookie("accessToken", "", domain, 0);
final var refreshTokenCookie = AuthUtil.createCookie("refreshToken", "", domain, 0);
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class RefreshTokenStore implements Serializable {

@Indexed private Long userId;

@Indexed private String ipAddress;
@Indexed private String deviceId;

@TimeToLive private long timeToLive;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.springframework.data.repository.CrudRepository;

public interface RefreshTokenStoreRepository extends CrudRepository<RefreshTokenStore, String> {
Optional<RefreshTokenStore> findByUserIdAndIpAddress(Long userId, String ipAddress);
Optional<RefreshTokenStore> findByUserIdAndDeviceId(Long userId, String deviceId);

List<RefreshTokenStore> findAllByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.bugzkit.api.user.payload.dto.RoleDTO;

public interface AccessTokenService {
String create(Long userId, Set<RoleDTO> roleDTOs);
String create(Long userId, Set<RoleDTO> roleDTOs, String deviceId);

void check(String token);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package org.bugzkit.api.auth.jwt.service;

import java.util.Optional;
import java.util.Set;
import org.bugzkit.api.user.payload.dto.RoleDTO;

public interface RefreshTokenService {
String create(Long userId, Set<RoleDTO> roleDTOs, String ipAddress);
String create(Long userId, Set<RoleDTO> roleDTOs, String deviceId);

void check(String token);

Optional<String> findByUserIdAndIpAddress(Long userId, String ipAddress);

void delete(String token);

void deleteByUserIdAndIpAddress(Long userId, String ipAddress);
void deleteByUserIdAndDeviceId(Long userId, String deviceId);

void deleteAllByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ public AccessTokenServiceImpl(
}

@Override
public String create(Long userId, Set<RoleDTO> roleDTOs) {
public String create(Long userId, Set<RoleDTO> roleDTOs, String deviceId) {
return JWT.create()
.withIssuer(userId.toString())
.withClaim("roles", roleDTOs.stream().map(RoleDTO::name).toList())
.withClaim("purpose", PURPOSE.name())
.withClaim("deviceId", deviceId)
.withIssuedAt(Instant.now())
.withExpiresAt(Instant.now().plusSeconds(tokenDuration))
.sign(JwtUtil.getAlgorithm(secret));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.auth0.jwt.JWT;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import org.bugzkit.api.auth.jwt.redis.model.RefreshTokenStore;
import org.bugzkit.api.auth.jwt.redis.repository.RefreshTokenStoreRepository;
Expand Down Expand Up @@ -30,17 +29,17 @@ public RefreshTokenServiceImpl(RefreshTokenStoreRepository refreshTokenStoreRepo
}

@Override
public String create(Long userId, Set<RoleDTO> roleDTOs, String ipAddress) {
public String create(Long userId, Set<RoleDTO> roleDTOs, String deviceId) {
final var token =
JWT.create()
.withIssuer(userId.toString())
.withClaim("roles", roleDTOs.stream().map(RoleDTO::name).toList())
.withClaim("purpose", PURPOSE.name())
.withClaim("deviceId", deviceId)
.withIssuedAt(Instant.now())
.withExpiresAt(Instant.now().plusSeconds(tokenDuration))
.sign(JwtUtil.getAlgorithm(secret));
refreshTokenStoreRepository.save(
new RefreshTokenStore(token, userId, ipAddress, tokenDuration));
refreshTokenStoreRepository.save(new RefreshTokenStore(token, userId, deviceId, tokenDuration));
return token;
}

Expand All @@ -63,22 +62,15 @@ private void isInRefreshTokenStore(String token) {
throw new BadRequestException("auth.tokenInvalid");
}

@Override
public Optional<String> findByUserIdAndIpAddress(Long userId, String ipAddress) {
return refreshTokenStoreRepository
.findByUserIdAndIpAddress(userId, ipAddress)
.map(RefreshTokenStore::getRefreshToken);
}

@Override
public void delete(String token) {
refreshTokenStoreRepository.deleteById(token);
}

@Override
public void deleteByUserIdAndIpAddress(Long userId, String ipAddress) {
public void deleteByUserIdAndDeviceId(Long userId, String deviceId) {
refreshTokenStoreRepository
.findByUserIdAndIpAddress(userId, ipAddress)
.findByUserIdAndDeviceId(userId, deviceId)
.ifPresent(refreshTokenStoreRepository::delete);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public static Set<RoleDTO> getRoleDTOs(String token) {
return roles.stream().map(RoleDTO::new).collect(Collectors.toSet());
}

public static String getDeviceId(String token) {
return JWT.decode(token).getClaim("deviceId").asString();
}

public static Instant getIssuedAt(String token) {
return JWT.decode(token).getIssuedAtAsInstant();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.bugzkit.api.auth.mapper;

import org.bugzkit.api.auth.model.Device;
import org.bugzkit.api.auth.payload.dto.DeviceDTO;
import org.mapstruct.Context;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper(injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface AuthMapper {
AuthMapper INSTANCE = Mappers.getMapper(AuthMapper.class);

@Mapping(target = "current", expression = "java(device.getDeviceId().equals(currentDeviceId))")
DeviceDTO deviceToDeviceDTO(Device device, @Context String currentDeviceId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.bugzkit.api.auth.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.bugzkit.api.user.model.User;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(
name = "devices",
indexes = {
@Index(name = "idx_device_user_id", columnList = "user_id"),
@Index(name = "idx_device_user_device", columnList = "user_id, deviceId", unique = true)
})
public class Device {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@Column(nullable = false)
private String deviceId;

private String userAgent;

@Builder.Default
@Column(nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();

@Builder.Default
@Column(nullable = false)
private LocalDateTime lastActiveAt = LocalDateTime.now();
}
Loading