diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/config/OAuth2SecurityConfig.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/config/OAuth2SecurityConfig.java index a39b690..bcc4a03 100644 --- a/src/main/java/com/heaerie/server/auth201/Auth201Server/config/OAuth2SecurityConfig.java +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/config/OAuth2SecurityConfig.java @@ -87,7 +87,7 @@ ); // H2 Console configuration (development only) - http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**")); + http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**", "/oauth/register")); http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())); return http.build(); @@ -96,7 +96,7 @@ /** * Registered Client Repository - OAuth 2.1 compliant clients */ - @Bean + @Bean("oauthRegisteredClientRepository") public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) { // Public Client (SPAs, Mobile Apps) - OAuth 2.1 RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString()) diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/ClientRegistrationController.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/ClientRegistrationController.java index a88544f..a14d557 100644 --- a/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/ClientRegistrationController.java +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/ClientRegistrationController.java @@ -2,21 +2,20 @@ import com.heaerie.server.auth201.Auth201Server.dto.ClientRegistrationRequest; import com.heaerie.server.auth201.Auth201Server.dto.ClientRegistrationResponse; +import com.heaerie.server.auth201.Auth201Server.service.ClientRegistrationService; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import java.util.UUID; - @Controller @RequestMapping("/oauth") public class ClientRegistrationController { - private final PasswordEncoder passwordEncoder; + private final ClientRegistrationService clientRegistrationService; - public ClientRegistrationController(PasswordEncoder passwordEncoder) { - this.passwordEncoder = passwordEncoder; + public ClientRegistrationController(ClientRegistrationService clientRegistrationService) { + this.clientRegistrationService = clientRegistrationService; } @GetMapping("/register") @@ -26,39 +25,15 @@ @PostMapping("/register") @ResponseBody - public ResponseEntity registerClient(@RequestBody ClientRegistrationRequest request) { - // Generate client ID - String clientId = generateClientId(request.getClientName()); - - // Generate client secret for confidential clients - String clientSecret = null; - if ("confidential".equals(request.getClientType())) { - clientSecret = generateClientSecret(); + public ResponseEntity registerClient(@RequestBody ClientRegistrationRequest request) { + try { + ClientRegistrationResponse response = clientRegistrationService.registerClient(request); + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Registration failed: " + e.getMessage()); } - - ClientRegistrationResponse response = new ClientRegistrationResponse(); - response.setClientId(clientId); - response.setClientSecret(clientSecret); - response.setClientType(request.getClientType()); - response.setMessage("Client registered successfully"); - - return ResponseEntity.ok(response); - } - - private String generateClientId(String clientName) { - // Create a client ID based on the name and a UUID - String sanitized = clientName.toLowerCase() - .replaceAll("[^a-z0-9-]", "-") - .replaceAll("-+", "-") - .replaceAll("^-|-$", ""); - - String shortUuid = UUID.randomUUID().toString().substring(0, 8); - return sanitized + "-" + shortUuid; - } - - private String generateClientSecret() { - // Generate a secure random secret - return UUID.randomUUID().toString().replace("-", "") + - UUID.randomUUID().toString().replace("-", ""); } } diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/dto/ClientRegistrationRequest.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/dto/ClientRegistrationRequest.java index e93b83f..37d4b62 100644 --- a/src/main/java/com/heaerie/server/auth201/Auth201Server/dto/ClientRegistrationRequest.java +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/dto/ClientRegistrationRequest.java @@ -12,6 +12,9 @@ private List scopes; private boolean requireConsent; private boolean requirePkce; + private Integer accessTokenValidity; + private Integer refreshTokenValidity; + private boolean reuseRefreshTokens; // Getters and Setters public String getClientName() { @@ -85,4 +88,28 @@ public void setRequirePkce(boolean requirePkce) { this.requirePkce = requirePkce; } + + public Integer getAccessTokenValidity() { + return accessTokenValidity; + } + + public void setAccessTokenValidity(Integer accessTokenValidity) { + this.accessTokenValidity = accessTokenValidity; + } + + public Integer getRefreshTokenValidity() { + return refreshTokenValidity; + } + + public void setRefreshTokenValidity(Integer refreshTokenValidity) { + this.refreshTokenValidity = refreshTokenValidity; + } + + public boolean isReuseRefreshTokens() { + return reuseRefreshTokens; + } + + public void setReuseRefreshTokens(boolean reuseRefreshTokens) { + this.reuseRefreshTokens = reuseRefreshTokens; + } } diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/entity/RegisteredClientEntity.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/entity/RegisteredClientEntity.java new file mode 100644 index 0000000..be74e25 --- /dev/null +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/entity/RegisteredClientEntity.java @@ -0,0 +1,209 @@ +package com.heaerie.server.auth201.Auth201Server.entity; + +import jakarta.persistence.*; +import java.time.Instant; + +@Entity +@Table(name = "registered_clients") +public class RegisteredClientEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String clientId; + + @Column + private String clientSecret; + + @Column(nullable = false) + private String clientName; + + @Column + private String clientType; + + @Column(length = 1000) + private String description; + + @Column(length = 4000) + private String redirectUris; + + @Column(length = 4000) + private String logoutRedirectUris; + + @Column(length = 1000) + private String grantTypes; + + @Column(length = 1000) + private String scopes; + + @Column + private boolean requireConsent; + + @Column + private boolean requirePkce; + + @Column + private Integer accessTokenValidity; + + @Column + private Integer refreshTokenValidity; + + @Column + private boolean reuseRefreshTokens; + + @Column + private Instant createdAt; + + @Column + private Instant updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = Instant.now(); + updatedAt = Instant.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = Instant.now(); + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getClientType() { + return clientType; + } + + public void setClientType(String clientType) { + this.clientType = clientType; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getRedirectUris() { + return redirectUris; + } + + public void setRedirectUris(String redirectUris) { + this.redirectUris = redirectUris; + } + + public String getLogoutRedirectUris() { + return logoutRedirectUris; + } + + public void setLogoutRedirectUris(String logoutRedirectUris) { + this.logoutRedirectUris = logoutRedirectUris; + } + + public String getGrantTypes() { + return grantTypes; + } + + public void setGrantTypes(String grantTypes) { + this.grantTypes = grantTypes; + } + + public String getScopes() { + return scopes; + } + + public void setScopes(String scopes) { + this.scopes = scopes; + } + + public boolean isRequireConsent() { + return requireConsent; + } + + public void setRequireConsent(boolean requireConsent) { + this.requireConsent = requireConsent; + } + + public boolean isRequirePkce() { + return requirePkce; + } + + public void setRequirePkce(boolean requirePkce) { + this.requirePkce = requirePkce; + } + + public Integer getAccessTokenValidity() { + return accessTokenValidity; + } + + public void setAccessTokenValidity(Integer accessTokenValidity) { + this.accessTokenValidity = accessTokenValidity; + } + + public Integer getRefreshTokenValidity() { + return refreshTokenValidity; + } + + public void setRefreshTokenValidity(Integer refreshTokenValidity) { + this.refreshTokenValidity = refreshTokenValidity; + } + + public boolean isReuseRefreshTokens() { + return reuseRefreshTokens; + } + + public void setReuseRefreshTokens(boolean reuseRefreshTokens) { + this.reuseRefreshTokens = reuseRefreshTokens; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/repository/RegisteredClientRepository.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/repository/RegisteredClientRepository.java new file mode 100644 index 0000000..576c63c --- /dev/null +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/repository/RegisteredClientRepository.java @@ -0,0 +1,13 @@ +package com.heaerie.server.auth201.Auth201Server.repository; + +import com.heaerie.server.auth201.Auth201Server.entity.RegisteredClientEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RegisteredClientRepository extends JpaRepository { + Optional findByClientId(String clientId); + boolean existsByClientId(String clientId); +} diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/service/ClientRegistrationService.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/service/ClientRegistrationService.java new file mode 100644 index 0000000..43edf0f --- /dev/null +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/service/ClientRegistrationService.java @@ -0,0 +1,98 @@ +package com.heaerie.server.auth201.Auth201Server.service; + +import com.heaerie.server.auth201.Auth201Server.dto.ClientRegistrationRequest; +import com.heaerie.server.auth201.Auth201Server.dto.ClientRegistrationResponse; +import com.heaerie.server.auth201.Auth201Server.entity.RegisteredClientEntity; +import com.heaerie.server.auth201.Auth201Server.repository.RegisteredClientRepository; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Service +public class ClientRegistrationService { + + private final RegisteredClientRepository clientRepository; + private final PasswordEncoder passwordEncoder; + + public ClientRegistrationService(RegisteredClientRepository clientRepository, + PasswordEncoder passwordEncoder) { + this.clientRepository = clientRepository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public ClientRegistrationResponse registerClient(ClientRegistrationRequest request) { + // Validate request + if (request.getClientName() == null || request.getClientName().trim().isEmpty()) { + throw new IllegalArgumentException("Client name is required"); + } + + if (request.getRedirectUris() == null || request.getRedirectUris().isEmpty()) { + throw new IllegalArgumentException("At least one redirect URI is required"); + } + + // Generate unique client ID + String clientId; + do { + clientId = generateClientId(request.getClientName()); + } while (clientRepository.existsByClientId(clientId)); + + // Generate client secret for confidential clients + String clientSecret = null; + String encodedSecret = null; + if ("confidential".equals(request.getClientType())) { + clientSecret = generateClientSecret(); + encodedSecret = passwordEncoder.encode(clientSecret); + } + + // Create entity + RegisteredClientEntity entity = new RegisteredClientEntity(); + entity.setClientId(clientId); + entity.setClientSecret(encodedSecret); + entity.setClientName(request.getClientName()); + entity.setClientType(request.getClientType()); + entity.setDescription(request.getDescription()); + entity.setRedirectUris(String.join(",", request.getRedirectUris())); + + if (request.getLogoutRedirectUris() != null && !request.getLogoutRedirectUris().isEmpty()) { + entity.setLogoutRedirectUris(String.join(",", request.getLogoutRedirectUris())); + } + + entity.setGrantTypes(String.join(",", request.getGrantTypes())); + entity.setScopes(String.join(",", request.getScopes())); + entity.setRequireConsent(request.isRequireConsent()); + entity.setRequirePkce(request.isRequirePkce()); + entity.setAccessTokenValidity(request.getAccessTokenValidity()); + entity.setRefreshTokenValidity(request.getRefreshTokenValidity()); + entity.setReuseRefreshTokens(request.isReuseRefreshTokens()); + + // Save to database + clientRepository.save(entity); + + // Build response + ClientRegistrationResponse response = new ClientRegistrationResponse(); + response.setClientId(clientId); + response.setClientSecret(clientSecret); // Return plain text secret (only shown once) + response.setClientType(request.getClientType()); + response.setMessage("Client registered successfully"); + + return response; + } + + private String generateClientId(String clientName) { + String sanitized = clientName.toLowerCase() + .replaceAll("[^a-z0-9-]", "-") + .replaceAll("-+", "-") + .replaceAll("^-|-$", ""); + + String shortUuid = UUID.randomUUID().toString().substring(0, 8); + return sanitized + "-" + shortUuid; + } + + private String generateClientSecret() { + return UUID.randomUUID().toString().replace("-", "") + + UUID.randomUUID().toString().replace("-", ""); + } +}