diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 667784f..ab6c3f8 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -28,8 +28,4 @@ jobs: distribution: 'temurin' cache: maven - name: Verify with Maven - run: mvn -B -DskipTests verify --file pom.xml - - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + run: mvn -B -DskipTests verify --file pom.xml \ No newline at end of file diff --git a/docs/openapi/components/schemas/SignInStartResponse.yaml b/docs/openapi/components/schemas/SignInStartResponse.yaml index 12b9384..81862ff 100644 --- a/docs/openapi/components/schemas/SignInStartResponse.yaml +++ b/docs/openapi/components/schemas/SignInStartResponse.yaml @@ -9,3 +9,4 @@ properties: options: type: string description: Options for assertion to pass to `navigator.credentials.create()` + x-field-extra-annotation: '@com.fasterxml.jackson.annotation.JsonRawValue' diff --git a/docs/openapi/components/schemas/SignUpStartResponse.yaml b/docs/openapi/components/schemas/SignUpStartResponse.yaml index b71f9e9..e9144c7 100644 --- a/docs/openapi/components/schemas/SignUpStartResponse.yaml +++ b/docs/openapi/components/schemas/SignUpStartResponse.yaml @@ -9,3 +9,4 @@ properties: options: type: string description: Options to pass to `navigator.credentials.create()` + x-field-extra-annotation: '@com.fasterxml.jackson.annotation.JsonRawValue' diff --git a/docs/openapi/paths/v1_credentials_add_start.yaml b/docs/openapi/paths/v1_credentials_add_start.yaml index 2750bc8..584e9d7 100644 --- a/docs/openapi/paths/v1_credentials_add_start.yaml +++ b/docs/openapi/paths/v1_credentials_add_start.yaml @@ -8,7 +8,7 @@ post: content: application/json: schema: - type: string + $ref: ../components/schemas/SignUpStartRequest.yaml required: true responses: '200': diff --git a/pom.xml b/pom.xml index a425fee..3b51aea 100644 --- a/pom.xml +++ b/pom.xml @@ -199,6 +199,8 @@ true false true + true + true com.helioauth.passkeys.api.generated.models com.helioauth.passkeys.api.generated.api diff --git a/src/main/java/com/helioauth/passkeys/api/controller/ClientApplicationController.java b/src/main/java/com/helioauth/passkeys/api/controller/ClientApplicationController.java index ea28be1..41565bc 100644 --- a/src/main/java/com/helioauth/passkeys/api/controller/ClientApplicationController.java +++ b/src/main/java/com/helioauth/passkeys/api/controller/ClientApplicationController.java @@ -20,18 +20,12 @@ import com.helioauth.passkeys.api.generated.models.AddApplicationRequest; import com.helioauth.passkeys.api.generated.models.Application; import com.helioauth.passkeys.api.generated.models.ApplicationApiKey; -import com.helioauth.passkeys.api.mapper.ClientApplicationMapper; import com.helioauth.passkeys.api.service.ClientApplicationService; import lombok.RequiredArgsConstructor; import lombok.val; 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.net.URI; @@ -42,38 +36,27 @@ * @author Viktor Stanchev */ @RestController -@RequestMapping("/admin/v1/apps") @RequiredArgsConstructor public class ClientApplicationController implements ApplicationsApi { private final ClientApplicationService clientApplicationService; - private final ClientApplicationMapper clientApplicationMapper; - - @GetMapping - @Override public ResponseEntity> listAll() { return ResponseEntity.ok(clientApplicationService.listAll()); } - @GetMapping("/{id}") - @Override public ResponseEntity get(@PathVariable UUID id) { return clientApplicationService.get(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } - @GetMapping("/{id}/api-key") - @Override public ResponseEntity getApiKey(@PathVariable UUID id) { return clientApplicationService.getApiKey(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } - @PostMapping - @Override public ResponseEntity add(@RequestBody AddApplicationRequest request) { Application created = clientApplicationService.add(request.getName()); @@ -81,10 +64,7 @@ public ResponseEntity add(@RequestBody AddApplicationRequest reques .body(created); } - @PutMapping("/{id}") - @Override public ResponseEntity edit(@PathVariable UUID id, @RequestBody String name) { - val updated = clientApplicationService.edit(id, name); return updated @@ -92,8 +72,6 @@ public ResponseEntity edit(@PathVariable UUID id, @RequestBody Stri .orElseGet(() -> ResponseEntity.notFound().build()); } - @DeleteMapping("/{id}") - @Override public ResponseEntity delete(@PathVariable UUID id) { boolean deleted = clientApplicationService.delete(id); diff --git a/src/main/java/com/helioauth/passkeys/api/controller/CredentialsController.java b/src/main/java/com/helioauth/passkeys/api/controller/CredentialsController.java index 4cd97f6..edbf8f7 100644 --- a/src/main/java/com/helioauth/passkeys/api/controller/CredentialsController.java +++ b/src/main/java/com/helioauth/passkeys/api/controller/CredentialsController.java @@ -17,17 +17,26 @@ package com.helioauth.passkeys.api.controller; import com.fasterxml.jackson.core.JsonProcessingException; -import com.helioauth.passkeys.api.contract.*; -import com.helioauth.passkeys.api.service.UserCredentialManager; +import com.helioauth.passkeys.api.contract.SignInFinishRequest; +import com.helioauth.passkeys.api.contract.SignInFinishResponse; +import com.helioauth.passkeys.api.contract.SignInStartRequest; +import com.helioauth.passkeys.api.contract.SignInStartResponse; +import com.helioauth.passkeys.api.contract.SignUpFinishRequest; +import com.helioauth.passkeys.api.contract.SignUpFinishResponse; +import com.helioauth.passkeys.api.contract.SignUpStartRequest; +import com.helioauth.passkeys.api.contract.SignUpStartResponse; import com.helioauth.passkeys.api.service.UserSignInService; import com.helioauth.passkeys.api.service.UserSignupService; - import com.helioauth.passkeys.api.service.exception.SignInFailedException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +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.server.ResponseStatusException; import jakarta.validation.Valid; @@ -35,16 +44,15 @@ /** * @author Viktor Stanchev */ +@Slf4j @RestController @RequestMapping("/v1") @CrossOrigin(origins = "*") -@Slf4j @RequiredArgsConstructor public class CredentialsController { private final UserSignInService userSignInService; private final UserSignupService userSignupService; - private final UserCredentialManager userCredentialManager; @PostMapping(value = "/signup/start", produces = MediaType.APPLICATION_JSON_VALUE) public SignUpStartResponse postSignupStart(@RequestBody @Valid SignUpStartRequest request) { @@ -76,14 +84,4 @@ public SignInFinishResponse finishSignInCredential(@RequestBody SignInFinishRequ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Sign in failed"); } } - - @PostMapping(value = "/credentials/add/start", produces = MediaType.APPLICATION_JSON_VALUE) - public SignUpStartResponse credentialsAddStart(@RequestBody String username) { - return userCredentialManager.createCredential(username); - } - - @PostMapping(value = "/credentials/add/finish", produces = MediaType.APPLICATION_JSON_VALUE) - public SignUpFinishResponse credentialsAddFinish(@RequestBody SignUpFinishRequest request) { - return userCredentialManager.finishCreateCredential(request); - } } \ No newline at end of file diff --git a/src/main/java/com/helioauth/passkeys/api/controller/UsersController.java b/src/main/java/com/helioauth/passkeys/api/controller/UsersController.java index 548cd1b..30b2ada 100644 --- a/src/main/java/com/helioauth/passkeys/api/controller/UsersController.java +++ b/src/main/java/com/helioauth/passkeys/api/controller/UsersController.java @@ -16,15 +16,20 @@ package com.helioauth.passkeys.api.controller; +import com.helioauth.passkeys.api.generated.api.UsersApi; +import com.helioauth.passkeys.api.generated.models.ListPasskeysResponse; +import com.helioauth.passkeys.api.generated.models.SignUpFinishRequest; +import com.helioauth.passkeys.api.generated.models.SignUpFinishResponse; +import com.helioauth.passkeys.api.generated.models.SignUpStartRequest; +import com.helioauth.passkeys.api.generated.models.SignUpStartResponse; import com.helioauth.passkeys.api.service.UserAccountManager; import com.helioauth.passkeys.api.service.UserCredentialManager; -import com.helioauth.passkeys.api.service.dto.ListPasskeysResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @@ -32,21 +37,29 @@ /** * @author Viktor Stanchev */ -@RestController -@RequestMapping("/v1/users") @Slf4j +@RestController @RequiredArgsConstructor -public class UsersController { +public class UsersController implements UsersApi { private final UserCredentialManager userCredentialManager; private final UserAccountManager userAccountManager; - @GetMapping("/{uuid}/credentials") - public ListPasskeysResponse getUserCredentials(@PathVariable UUID uuid) { - return userCredentialManager.getUserCredentials(uuid); + public ResponseEntity getUserCredentials(@PathVariable UUID uuid) { + return ResponseEntity.ok(userCredentialManager.getUserCredentials(uuid)); } - @DeleteMapping("/{uuid}") - public void deleteUser(@PathVariable UUID uuid) { + public ResponseEntity deleteUser(@PathVariable UUID uuid) { userAccountManager.deleteUser(uuid); + return ResponseEntity.noContent().build(); + } + + @CrossOrigin(origins = "*") + public ResponseEntity credentialsAddStart(@RequestBody SignUpStartRequest request) { + return ResponseEntity.ok(userCredentialManager.createCredential(request.getName())); + } + + @CrossOrigin(origins = "*") + public ResponseEntity credentialsAddFinish(@RequestBody SignUpFinishRequest request) { + return ResponseEntity.ok(userCredentialManager.finishCreateCredential(request)); } } diff --git a/src/main/java/com/helioauth/passkeys/api/mapper/UserCredentialMapper.java b/src/main/java/com/helioauth/passkeys/api/mapper/UserCredentialMapper.java index 20a34af..42e8936 100644 --- a/src/main/java/com/helioauth/passkeys/api/mapper/UserCredentialMapper.java +++ b/src/main/java/com/helioauth/passkeys/api/mapper/UserCredentialMapper.java @@ -17,8 +17,9 @@ package com.helioauth.passkeys.api.mapper; import com.helioauth.passkeys.api.domain.UserCredential; +import com.helioauth.passkeys.api.generated.models.PasskeyCredential; +import com.helioauth.passkeys.api.generated.models.SignUpStartResponse; import com.helioauth.passkeys.api.service.dto.CredentialRegistrationResult; -import com.helioauth.passkeys.api.service.dto.PasskeyItem; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingConstants; @@ -30,11 +31,13 @@ */ @Mapper(componentModel = MappingConstants.ComponentModel.SPRING) public interface UserCredentialMapper { - List toDto(List userCredentialList); + List toDto(List userCredentialList); @Mapping(target = "createdAt", ignore = true) @Mapping(target = "lastUsedAt", ignore = true) @Mapping(target = "user", ignore = true) @Mapping(target = "id", ignore = true) UserCredential fromCredentialRegistrationResult(CredentialRegistrationResult registrationResultDto); + + com.helioauth.passkeys.api.contract.SignUpStartResponse toLegacySignUpStartResponse(SignUpStartResponse response); } \ No newline at end of file diff --git a/src/main/java/com/helioauth/passkeys/api/service/UserCredentialManager.java b/src/main/java/com/helioauth/passkeys/api/service/UserCredentialManager.java index 4de3b04..e8a9d8a 100644 --- a/src/main/java/com/helioauth/passkeys/api/service/UserCredentialManager.java +++ b/src/main/java/com/helioauth/passkeys/api/service/UserCredentialManager.java @@ -17,17 +17,16 @@ package com.helioauth.passkeys.api.service; import com.fasterxml.jackson.core.JsonProcessingException; -import com.helioauth.passkeys.api.contract.SignUpFinishRequest; -import com.helioauth.passkeys.api.contract.SignUpFinishResponse; -import com.helioauth.passkeys.api.contract.SignUpStartResponse; import com.helioauth.passkeys.api.domain.User; import com.helioauth.passkeys.api.domain.UserCredential; import com.helioauth.passkeys.api.domain.UserCredentialRepository; import com.helioauth.passkeys.api.domain.UserRepository; +import com.helioauth.passkeys.api.generated.models.ListPasskeysResponse; +import com.helioauth.passkeys.api.generated.models.SignUpFinishRequest; +import com.helioauth.passkeys.api.generated.models.SignUpFinishResponse; +import com.helioauth.passkeys.api.generated.models.SignUpStartResponse; import com.helioauth.passkeys.api.mapper.UserCredentialMapper; import com.helioauth.passkeys.api.service.dto.CredentialRegistrationResult; -import com.helioauth.passkeys.api.service.dto.ListPasskeysResponse; -import com.helioauth.passkeys.api.service.dto.PasskeyItem; import com.helioauth.passkeys.api.service.exception.CreateCredentialFailedException; import com.helioauth.passkeys.api.service.exception.SignUpFailedException; import lombok.RequiredArgsConstructor; @@ -62,8 +61,8 @@ public SignUpStartResponse createCredential(String name) { public SignUpFinishResponse finishCreateCredential(SignUpFinishRequest request) { try { CredentialRegistrationResult result = webAuthnAuthenticator.finishRegistration( - request.requestId(), - request.publicKeyCredential() + request.getRequestId(), + request.getPublicKeyCredential() ); User user = userRepository.findByName(result.name()).orElseThrow(CreateCredentialFailedException::new); @@ -72,7 +71,7 @@ public SignUpFinishResponse finishCreateCredential(SignUpFinishRequest request) userCredential.setUser(user); userCredentialRepository.save(userCredential); - return new SignUpFinishResponse(request.requestId(), user.getId()); + return new SignUpFinishResponse(request.getRequestId(), user.getId()); } catch (IOException e) { log.error("Register Credential failed", e); throw new SignUpFailedException(); @@ -81,8 +80,7 @@ public SignUpFinishResponse finishCreateCredential(SignUpFinishRequest request) public ListPasskeysResponse getUserCredentials(UUID userUuid) { List userCredentials = userCredentialRepository.findAllByUserId(userUuid); - List passkeyItems = userCredentialMapper.toDto(userCredentials); - return new ListPasskeysResponse(passkeyItems); + return new ListPasskeysResponse(userCredentialMapper.toDto(userCredentials)); } } diff --git a/src/main/java/com/helioauth/passkeys/api/service/UserSignupService.java b/src/main/java/com/helioauth/passkeys/api/service/UserSignupService.java index 42e4992..c5420d2 100644 --- a/src/main/java/com/helioauth/passkeys/api/service/UserSignupService.java +++ b/src/main/java/com/helioauth/passkeys/api/service/UserSignupService.java @@ -53,7 +53,7 @@ public SignUpStartResponse startRegistration(String name) { }); try { - return webAuthnAuthenticator.startRegistration(name); + return usercredentialMapper.toLegacySignUpStartResponse(webAuthnAuthenticator.startRegistration(name)); } catch (JsonProcessingException e) { log.error("Register Credential failed", e); throw new SignUpFailedException(); diff --git a/src/main/java/com/helioauth/passkeys/api/service/WebAuthnAuthenticator.java b/src/main/java/com/helioauth/passkeys/api/service/WebAuthnAuthenticator.java index 3aab0e1..a1a6b3e 100644 --- a/src/main/java/com/helioauth/passkeys/api/service/WebAuthnAuthenticator.java +++ b/src/main/java/com/helioauth/passkeys/api/service/WebAuthnAuthenticator.java @@ -21,7 +21,7 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.helioauth.passkeys.api.config.properties.WebAuthnRelyingPartyProperties; import com.helioauth.passkeys.api.contract.SignInStartResponse; -import com.helioauth.passkeys.api.contract.SignUpStartResponse; +import com.helioauth.passkeys.api.generated.models.SignUpStartResponse; import com.helioauth.passkeys.api.mapper.CredentialRegistrationResultMapper; import com.helioauth.passkeys.api.service.dto.CredentialAssertionResult; import com.helioauth.passkeys.api.service.dto.CredentialRegistrationResult; diff --git a/src/test/java/com/helioauth/passkeys/api/service/ClientApplicationServiceTest.java b/src/test/java/com/helioauth/passkeys/api/service/ClientApplicationServiceTest.java index bccc604..d22336b 100644 --- a/src/test/java/com/helioauth/passkeys/api/service/ClientApplicationServiceTest.java +++ b/src/test/java/com/helioauth/passkeys/api/service/ClientApplicationServiceTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.helioauth.passkeys.api.service; import com.helioauth.passkeys.api.domain.ClientApplication; diff --git a/src/test/java/com/helioauth/passkeys/api/service/UserCredentialManagerTest.java b/src/test/java/com/helioauth/passkeys/api/service/UserCredentialManagerTest.java new file mode 100644 index 0000000..e0e5da7 --- /dev/null +++ b/src/test/java/com/helioauth/passkeys/api/service/UserCredentialManagerTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.helioauth.passkeys.api.service; + +import com.helioauth.passkeys.api.domain.UserCredential; +import com.helioauth.passkeys.api.domain.UserCredentialRepository; +import com.helioauth.passkeys.api.domain.UserRepository; +import com.helioauth.passkeys.api.generated.models.ListPasskeysResponse; +import com.helioauth.passkeys.api.mapper.UserCredentialMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Viktor Stanchev + */ +@ExtendWith(MockitoExtension.class) +class UserCredentialManagerTest { + + @Mock + private UserCredentialRepository userCredentialRepository; + + @Mock + private WebAuthnAuthenticator authenticator; + + @Mock + private UserRepository userRepository; + + @Spy + private UserCredentialMapper userCredentialMapper = Mappers.getMapper(UserCredentialMapper.class); + + @InjectMocks + private UserCredentialManager userCredentialManager; + + @Test + void getUserCredentials_returnsEmpty_whenResultEmpty() { + // Arrange + when(userCredentialRepository.findAllByUserId(any())).thenReturn(Collections.emptyList()); + + // Act + ListPasskeysResponse result = userCredentialManager.getUserCredentials(UUID.randomUUID()); + + // Assert + assertTrue(result.getPasskeys().isEmpty(), "Expected no user credentials"); + } + + @Test + void getUserCredentials_returnsResult_whenResultNotEmpty() { + // Arrange + UUID userUuid = UUID.randomUUID(); + + UserCredential credential = UserCredential.builder() + .id(1L) + .credentialId(UUID.randomUUID().toString()) + .displayName("Credential Name") + .createdAt(Instant.now()) + .lastUsedAt(Instant.now()) + .build(); + + List credentialList = Collections.singletonList(credential); + + when(userCredentialRepository.findAllByUserId(userUuid)).thenReturn(credentialList); + + // Act + ListPasskeysResponse response = userCredentialManager.getUserCredentials(userUuid); + + // Assert + assertNotNull(response); + assertFalse(response.getPasskeys().isEmpty(), "The response should not be empty when credentials exist."); + assertEquals(1, response.getPasskeys().size()); + assertEquals("Credential Name", response.getPasskeys().get(0).getDisplayName()); + } +} \ No newline at end of file