Skip to content
Merged
Empty file modified mvnw
100644 → 100755
Empty file.
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-webauthn</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/backendlab/team4you/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package backendlab.team4you.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.webauthn.management.JdbcPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.JdbcUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;

@Configuration
public class SecurityConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

return http
.authorizeHttpRequests(
authorizeHttp -> authorizeHttp
// Public endpoints
.requestMatchers( "/login", "/signup").permitAll()
.anyRequest().authenticated()

// Add elevated permissions

)
.webAuthn( passkeys -> passkeys
.rpId("localhost") //identity of the website
.allowedOrigins("http://localhost:8080")
.rpName("Passkey team4you")
)
.formLogin(form -> form.loginPage("/login"))
.logout(logout -> logout.logoutSuccessUrl("/").permitAll())
.build();
}

//todo: add jte called add-passkey but in thymelife

@Bean
PublicKeyCredentialUserEntityRepository jdbcPublicKeyCredentialRepository(JdbcOperations jdbc) {
return new JdbcPublicKeyCredentialUserEntityRepository(jdbc);
}

@Bean
UserCredentialRepository userCredentialRepository(JdbcOperations jdbc) {
return new JdbcUserCredentialRepository(jdbc);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Bean
public UserDetailsService userDetailsService(){
return username -> User.builder()
.username(username)
.password("{noop}!LOCKED!") // Non-empty impossible-to-match password
.roles("USER")
.accountLocked(true) // Prevent password-based login
.build();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
114 changes: 57 additions & 57 deletions src/main/java/backendlab/team4you/user/AppUser.java
Original file line number Diff line number Diff line change
@@ -1,57 +1,57 @@
package backendlab.team4you.user;

import jakarta.persistence.*;

import java.time.LocalDateTime;

@Entity
@Table(name = "app_user")
public class AppUser {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String email;

@Column(name = "password_hash", nullable = false)
private String passwordHash;

@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

public AppUser() {
}

public Long getId() {
return id;
}

public String getEmail() {
return email;
}

public String getPasswordHash() {
return passwordHash;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public void setEmail(String email) {
this.email = email;
}

public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}

@PrePersist
void onCreate() {
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
}
}
//package backendlab.team4you.user;
//
//import jakarta.persistence.*;
//
//import java.time.LocalDateTime;
//
//@Entity
//@Table(name = "app_user")
//public class AppUser {
//
// @Id
// @GeneratedValue(strategy = GenerationType.IDENTITY)
// private Long id;
//
// @Column(nullable = false, unique = true)
// private String email;
//
// @Column(name = "password_hash", nullable = false)
// private String passwordHash;
//
// @Column(name = "created_at", nullable = false)
// private LocalDateTime createdAt;
//
// public AppUser() {
// }
//
// public Long getId() {
// return id;
// }
//
// public String getEmail() {
// return email;
// }
//
// public String getPasswordHash() {
// return passwordHash;
// }
//
// public LocalDateTime getCreatedAt() {
// return createdAt;
// }
//
// public void setEmail(String email) {
// this.email = email;
// }
//
// public void setPasswordHash(String passwordHash) {
// this.passwordHash = passwordHash;
// }
//
// @PrePersist
// void onCreate() {
// if (this.createdAt == null) {
// this.createdAt = LocalDateTime.now();
// }
// }
//}
15 changes: 15 additions & 0 deletions src/main/java/backendlab/team4you/webauthn/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package backendlab.team4you.webauthn;

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {
@GetMapping("/login")
public String login(CsrfToken token, Model model){
model.addAttribute("csrfToken", token.getToken());
return "login";
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
98 changes: 98 additions & 0 deletions src/main/java/backendlab/team4you/webauthn/SignupController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package backendlab.team4you.webauthn;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.server.ResponseStatusException;

import java.security.SecureRandom;
import java.util.List;

@Controller
public class SignupController {

private final PublicKeyCredentialUserEntityRepository users;
private final SecureRandom random = new SecureRandom();

public SignupController(PublicKeyCredentialUserEntityRepository users) {
this.users = users;
}

@GetMapping("/signup")
String signup(org.springframework.security.web.csrf.CsrfToken token, Model model) {
model.addAttribute("csrfToken", token.getToken());
return "signup";
}

@PostMapping("/signup")
@ResponseBody
public void signup(@RequestBody SignupRequest req, HttpServletRequest request, HttpServletResponse response) {

if (req.username == null || req.username.isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username is required");
}

if (users.findByUsername(req.username) != null) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Username already exists");
}

byte[] idBytes = new byte[32];
random.nextBytes(idBytes);

UserEntity userEntity = new UserEntity(
new Bytes(idBytes),
req.username,
req.displayName
);

users.save(userEntity);

Authentication auth = new UsernamePasswordAuthenticationToken(
userEntity.getName(), null, List.of(new SimpleGrantedAuthority("ROLE_USER")));

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(auth);

SecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.saveContext(context, request, response);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public static class SignupRequest {
private String username;
private String displayName;

public SignupRequest() {
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getDisplayName() {
return displayName;
}

public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}
}
80 changes: 80 additions & 0 deletions src/main/java/backendlab/team4you/webauthn/UserEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package backendlab.team4you.webauthn;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;

import java.time.LocalDateTime;

@Entity
@Table(name = "app_user")
public class UserEntity implements PublicKeyCredentialUserEntity {
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Id
@Column(name = "id", length = 255)
private String id;

@Column(unique = true, nullable = false)
private String name;

private String displayName;

@Column(name = "password_hash", nullable = true)
private String passwordHash;

@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

public UserEntity() {
}

public UserEntity(Bytes id, String name, String displayName) {
this.id = id != null ? id.toBase64UrlString() : null;
this.name = name;
this.displayName = displayName;
this.createdAt = LocalDateTime.now();
}

@Override
public Bytes getId() {
return id != null ? Bytes.fromBase64(id) : null;
}

public void setId(Bytes id) {
this.id = id != null ? id.toBase64UrlString() : null;
}

@Override
public String getName() {
return name;
}

@Override
public @Nullable String getDisplayName() {
return displayName;
}

public void setDisplayName(String displayName) {
this.displayName = displayName;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}

public String getPasswordHash() {
return passwordHash;
}

public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
}
Loading