mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
feat: Remote Auth
This commit is contained in:
committed by
Aditya Chandel
parent
cb552b5ee2
commit
4c01561f1b
@@ -10,25 +10,22 @@ COPY ./booklore-ui /angular-app/
|
||||
RUN npm run build --configuration=production
|
||||
|
||||
# Stage 2: Build the Spring Boot app with Gradle
|
||||
FROM gradle:jdk21-alpine AS springboot-build
|
||||
FROM gradle:8-jdk21-alpine AS springboot-build
|
||||
|
||||
WORKDIR /springboot-app
|
||||
|
||||
COPY ./booklore-api/gradlew ./booklore-api/gradle/ /springboot-app/
|
||||
COPY ./booklore-api/build.gradle ./booklore-api/settings.gradle /springboot-app/
|
||||
COPY ./booklore-api/gradle /springboot-app/gradle
|
||||
COPY ./booklore-api/src /springboot-app/src
|
||||
COPY ./booklore-api/src/main/resources/application.yaml /springboot-app/src/main/resources/application.yaml
|
||||
|
||||
# Inject version into application.yaml using yq
|
||||
ARG APP_VERSION
|
||||
RUN apk add --no-cache yq && \
|
||||
yq eval '.app.version = strenv(APP_VERSION)' -i /springboot-app/src/main/resources/application.yaml
|
||||
|
||||
RUN ./gradlew clean build
|
||||
RUN gradle clean build
|
||||
|
||||
# Stage 3: Final image
|
||||
FROM eclipse-temurin:21.0.5_11-jre-alpine
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
|
||||
RUN apk update && apk add nginx
|
||||
|
||||
|
||||
@@ -11,4 +11,19 @@ import org.springframework.stereotype.Component;
|
||||
@Setter
|
||||
public class AppProperties {
|
||||
private String pathConfig;
|
||||
private String version;
|
||||
|
||||
private RemoteAuth remoteAuth;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class RemoteAuth {
|
||||
private boolean enabled;
|
||||
private boolean createNewUsers;
|
||||
private String headerName;
|
||||
private String headerUser;
|
||||
private String headerEmail;
|
||||
private String headerGroups;
|
||||
private String adminGroup;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.adityachandel.booklore.config.security;
|
||||
|
||||
import com.adityachandel.booklore.config.AppProperties;
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.model.dto.BookLoreUser;
|
||||
import com.adityachandel.booklore.model.dto.request.UserLoginRequest;
|
||||
import com.adityachandel.booklore.model.entity.BookLoreUserEntity;
|
||||
import com.adityachandel.booklore.repository.UserRepository;
|
||||
import com.adityachandel.booklore.service.user.UserCreatorService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -13,12 +15,15 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
public class AuthenticationService {
|
||||
|
||||
private final AppProperties appProperties;
|
||||
private final UserRepository userRepository;
|
||||
private final UserCreatorService userCreatorService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
@@ -34,15 +39,34 @@ public class AuthenticationService {
|
||||
throw ApiError.INVALID_CREDENTIALS.createException();
|
||||
}
|
||||
|
||||
return loginUser(user);
|
||||
}
|
||||
|
||||
public ResponseEntity<Map<String, String>> loginRemote(String name, String username, String email, String groups) {
|
||||
if (username == null || username.isEmpty()) {
|
||||
throw ApiError.BAD_REQUEST.createException("Remote-User header is missing");
|
||||
}
|
||||
|
||||
Optional<BookLoreUserEntity> user = userRepository.findByUsername(username);
|
||||
if (user.isEmpty() && appProperties.getRemoteAuth().isCreateNewUsers()) {
|
||||
user = Optional.of(userCreatorService.createRemoteUser(name, username, email, groups));
|
||||
}
|
||||
|
||||
if (user.isEmpty()) {
|
||||
throw ApiError.INTERNAL_SERVER_ERROR.createException("User not found and remote user creation is disabled");
|
||||
}
|
||||
|
||||
return loginUser(user.get());
|
||||
}
|
||||
|
||||
public ResponseEntity<Map<String, String>> loginUser(BookLoreUserEntity user) {
|
||||
String accessToken = jwtUtils.generateAccessToken(user);
|
||||
String refreshToken = jwtUtils.generateRefreshToken(user);
|
||||
|
||||
boolean isDefaultPassword = user.isDefaultPassword();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"accessToken", accessToken,
|
||||
"refreshToken", refreshToken,
|
||||
"isDefaultPassword", String.valueOf(isDefaultPassword)
|
||||
"isDefaultPassword", String.valueOf(user.isDefaultPassword())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public class SecurityConfig {
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/refresh").permitAll()
|
||||
.requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/refresh", "/api/v1/auth/remote").permitAll()
|
||||
.requestMatchers("/ws/**").permitAll()
|
||||
.requestMatchers("/api/v1/books/*/cover").permitAll()
|
||||
.requestMatchers(
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
package com.adityachandel.booklore.controller;
|
||||
|
||||
import com.adityachandel.booklore.config.AppProperties;
|
||||
import com.adityachandel.booklore.config.security.AuthenticationService;
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.model.dto.UserCreateRequest;
|
||||
import com.adityachandel.booklore.model.dto.request.RefreshTokenRequest;
|
||||
import com.adityachandel.booklore.model.dto.request.UserLoginRequest;
|
||||
import com.adityachandel.booklore.service.user.UserCreatorService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
public class AuthenticationController {
|
||||
|
||||
private final AppProperties appProperties;
|
||||
private final UserCreatorService userCreatorService;
|
||||
private final AuthenticationService authenticationService;
|
||||
|
||||
@@ -40,4 +43,20 @@ public class AuthenticationController {
|
||||
public ResponseEntity<Map<String, String>> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
|
||||
return authenticationService.refreshToken(request.getRefreshToken());
|
||||
}
|
||||
|
||||
@GetMapping("/remote")
|
||||
public ResponseEntity<Map<String, String>> loginRemote(@RequestHeader Map<String, String> headers) {
|
||||
if (!appProperties.getRemoteAuth().isEnabled()) {
|
||||
throw ApiError.REMOTE_AUTH_DISABLED.createException();
|
||||
}
|
||||
|
||||
String name = headers.get(appProperties.getRemoteAuth().getHeaderName().toLowerCase(Locale.ROOT));
|
||||
String username = headers.get(appProperties.getRemoteAuth().getHeaderUser().toLowerCase(Locale.ROOT));
|
||||
String email = headers.get(appProperties.getRemoteAuth().getHeaderEmail().toLowerCase(Locale.ROOT));
|
||||
String groups = headers.get(appProperties.getRemoteAuth().getHeaderGroups().toLowerCase(Locale.ROOT));
|
||||
log.debug("Remote-Auth: retrieved values from headers: name: {}, username: {}, email: {}, groups: {}", name, username, email, groups);
|
||||
log.debug("Remote-Auth: remote auth settings: {}", appProperties.getRemoteAuth());
|
||||
|
||||
return authenticationService.loginRemote(name, username, email, groups);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ public enum ApiError {
|
||||
PASSWORD_INCORRECT(HttpStatus.BAD_REQUEST, "Incorrect current password"),
|
||||
PASSWORD_TOO_SHORT(HttpStatus.BAD_REQUEST, "Password must be at least 6 characters long"),
|
||||
PASSWORD_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "New password cannot be the same as the current password"),
|
||||
INVALID_CREDENTIALS(HttpStatus.BAD_REQUEST, "Invalid credentials");
|
||||
INVALID_CREDENTIALS(HttpStatus.BAD_REQUEST, "Invalid credentials"),
|
||||
REMOTE_AUTH_DISABLED(HttpStatus.NON_AUTHORITATIVE_INFORMATION, "Remote login is disabled");
|
||||
|
||||
private final HttpStatus status;
|
||||
private final String message;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.adityachandel.booklore.service.user;
|
||||
|
||||
import com.adityachandel.booklore.config.AppProperties;
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.model.dto.settings.BookPreferences;
|
||||
import com.adityachandel.booklore.model.dto.UserCreateRequest;
|
||||
@@ -13,10 +14,12 @@ import com.adityachandel.booklore.repository.UserRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -25,6 +28,7 @@ import java.util.Optional;
|
||||
@AllArgsConstructor
|
||||
public class UserCreatorService {
|
||||
|
||||
private final AppProperties appProperties;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserRepository userRepository;
|
||||
private final LibraryRepository libraryRepository;
|
||||
@@ -59,15 +63,48 @@ public class UserCreatorService {
|
||||
user.setLibraries(new ArrayList<>(libraries));
|
||||
}
|
||||
|
||||
ShelfEntity shelfEntity = ShelfEntity.builder()
|
||||
.user(user)
|
||||
.name("Favorites")
|
||||
.icon("heart")
|
||||
.build();
|
||||
userRepository.save(user);
|
||||
shelfRepository.save(shelfEntity);
|
||||
createUser(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BookLoreUserEntity createRemoteUser(String name, String username, String email, String groups) {
|
||||
boolean isAdmin = false;
|
||||
if (groups != null && appProperties.getRemoteAuth().getAdminGroup() != null) {
|
||||
String groupsContent = groups.trim();
|
||||
if (groupsContent.startsWith("[") && groupsContent.endsWith("]")) {
|
||||
groupsContent = groupsContent.substring(1, groupsContent.length() - 1);
|
||||
}
|
||||
List<String> groupsList = Arrays.asList(groupsContent.split("\\s+"));
|
||||
isAdmin = groupsList.contains(appProperties.getRemoteAuth().getAdminGroup());
|
||||
log.debug("Remote-Auth: user {} will be admin: {}", username, isAdmin);
|
||||
}
|
||||
|
||||
BookLoreUserEntity user = new BookLoreUserEntity();
|
||||
user.setUsername(username);
|
||||
user.setName(name != null ? name : username);
|
||||
user.setEmail(email);
|
||||
user.setDefaultPassword(false);
|
||||
user.setPasswordHash(passwordEncoder.encode(RandomStringUtils.secure().nextAlphanumeric(32)));
|
||||
|
||||
UserPermissionsEntity permissions = new UserPermissionsEntity();
|
||||
permissions.setUser(user);
|
||||
permissions.setPermissionUpload(true);
|
||||
permissions.setPermissionDownload(true);
|
||||
permissions.setPermissionEditMetadata(true);
|
||||
permissions.setPermissionEmailBook(true);
|
||||
permissions.setPermissionAdmin(isAdmin);
|
||||
user.setPermissions(permissions);
|
||||
|
||||
if (isAdmin) {
|
||||
List<LibraryEntity> libraries = libraryRepository.findAll();
|
||||
user.setLibraries(new ArrayList<>(libraries));
|
||||
}
|
||||
|
||||
user.setBookPreferences(buildDefaultBookPreferences());
|
||||
return createUser(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createAdminUser() {
|
||||
BookLoreUserEntity user = new BookLoreUserEntity();
|
||||
user.setUsername("admin");
|
||||
@@ -88,16 +125,20 @@ public class UserCreatorService {
|
||||
user.setPermissions(permissions);
|
||||
user.setBookPreferences(buildDefaultBookPreferences());
|
||||
|
||||
createUser(user);
|
||||
log.info("Created admin user {}", user.getUsername());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
BookLoreUserEntity createUser(BookLoreUserEntity user) {
|
||||
ShelfEntity shelfEntity = ShelfEntity.builder()
|
||||
.user(user)
|
||||
.name("Favorites")
|
||||
.icon("heart")
|
||||
.build();
|
||||
|
||||
userRepository.save(user);
|
||||
user = userRepository.save(user);
|
||||
shelfRepository.save(shelfEntity);
|
||||
|
||||
log.info("Created admin user {}", user.getUsername());
|
||||
return user;
|
||||
}
|
||||
|
||||
public boolean doesAdminUserExist() {
|
||||
|
||||
@@ -2,6 +2,15 @@ app:
|
||||
path-config: '/app/data'
|
||||
version: 'v0.0.40'
|
||||
|
||||
remote-auth:
|
||||
enabled: ${REMOTE_AUTH_ENABLED:false}
|
||||
create-new-users: ${REMOTE_AUTH_CREATE_NEW_USERS:true}
|
||||
header-name: ${REMOTE_AUTH_HEADER_NAME:Remote-Name}
|
||||
header-user: ${REMOTE_AUTH_HEADER_USER:Remote-User}
|
||||
header-email: ${REMOTE_AUTH_HEADER_EMAIL:Remote-Email}
|
||||
header-groups: ${REMOTE_AUTH_HEADER_GROUPS:Remote-Groups}
|
||||
admin-group: ${REMOTE_AUTH_ADMIN_GROUP}
|
||||
|
||||
spring:
|
||||
servlet:
|
||||
multipart:
|
||||
@@ -39,6 +48,7 @@ spring:
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
root: ${ROOT_LOG_LEVEL:INFO}
|
||||
com.adityachandel.booklore: ${LOG_LEVEL:INFO}
|
||||
org.apache.fontbox: ERROR
|
||||
org.apache.pdfbox: ERROR
|
||||
@@ -31,6 +31,11 @@ export class LoginComponent {
|
||||
errorMessage = '';
|
||||
|
||||
constructor(private authService: AuthService, private router: Router) {
|
||||
this.authService.remoteLogin().subscribe({
|
||||
next: () => {
|
||||
this.router.navigate(['/dashboard']);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
login(): void {
|
||||
|
||||
@@ -26,6 +26,17 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
remoteLogin(): Observable<any> {
|
||||
return this.http.get<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/remote`).pipe(
|
||||
tap((response) => {
|
||||
if (response.accessToken && response.refreshToken) {
|
||||
this.saveTokens(response.accessToken, response.refreshToken);
|
||||
this.getRxStompService().activate();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
refreshToken(): Observable<{ accessToken: string; refreshToken: string }> {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/refresh`, {refreshToken}).pipe(
|
||||
|
||||
Reference in New Issue
Block a user