forked from devondragon/SpringUserFramework
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWebAuthnManagementAPI.java
More file actions
170 lines (148 loc) · 5.91 KB
/
Copy pathWebAuthnManagementAPI.java
File metadata and controls
170 lines (148 loc) · 5.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package com.digitalsanctuary.spring.user.api;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.digitalsanctuary.spring.user.dto.WebAuthnCredentialInfo;
import com.digitalsanctuary.spring.user.exceptions.WebAuthnException;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.service.UserService;
import com.digitalsanctuary.spring.user.service.WebAuthnCredentialManagementService;
import com.digitalsanctuary.spring.user.util.GenericResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* REST API for WebAuthn credential management.
*
* <p>
* This controller provides endpoints for managing WebAuthn credentials (passkeys). Authenticated users can list their
* registered passkeys, rename them for easier identification, and delete passkeys they no longer use.
* </p>
*
* <p>
* Endpoints:
* </p>
* <ul>
* <li>GET /user/webauthn/credentials - List all passkeys for the authenticated user</li>
* <li>GET /user/webauthn/has-credentials - Check if user has any passkeys</li>
* <li>PUT /user/webauthn/credentials/{id}/label - Rename a passkey</li>
* <li>DELETE /user/webauthn/credentials/{id} - Delete a passkey</li>
* </ul>
*/
@RestController
@RequestMapping("/user/webauthn")
@RequiredArgsConstructor
@Slf4j
public class WebAuthnManagementAPI {
private final WebAuthnCredentialManagementService credentialManagementService;
private final UserService userService;
/**
* Get user's registered passkeys.
*
* @param userDetails the authenticated user details
* @return ResponseEntity containing list of credential information
*/
@GetMapping("/credentials")
public ResponseEntity<List<WebAuthnCredentialInfo>> getCredentials(@AuthenticationPrincipal UserDetails userDetails) {
User user = userService.findUserByEmail(userDetails.getUsername());
if (user == null) {
log.error("User not found: {}", userDetails.getUsername());
throw new RuntimeException("User not found");
}
List<WebAuthnCredentialInfo> credentials = credentialManagementService.getUserCredentials(user);
return ResponseEntity.ok(credentials);
}
/**
* Check if user has any passkeys.
*
* @param userDetails the authenticated user details
* @return ResponseEntity containing true if user has passkeys, false otherwise
*/
@GetMapping("/has-credentials")
public ResponseEntity<Boolean> hasCredentials(@AuthenticationPrincipal UserDetails userDetails) {
User user = userService.findUserByEmail(userDetails.getUsername());
if (user == null) {
log.error("User not found: {}", userDetails.getUsername());
throw new RuntimeException("User not found");
}
boolean hasCredentials = credentialManagementService.hasCredentials(user);
return ResponseEntity.ok(hasCredentials);
}
/**
* Rename a passkey.
*
* <p>
* Updates the user-friendly label for a passkey. The label helps users identify their passkeys (e.g., "My iPhone", "Work Laptop").
* </p>
*
* <p>
* The label must be non-empty and no more than 255 characters.
* </p>
*
* @param id the credential ID to rename
* @param request the rename request containing the new label
* @param userDetails the authenticated user details
* @return ResponseEntity with success message or error
*/
@PutMapping("/credentials/{id}/label")
public ResponseEntity<GenericResponse> renameCredential(@PathVariable String id, @RequestBody @Valid RenameCredentialRequest request,
@AuthenticationPrincipal UserDetails userDetails) {
try {
User user = userService.findUserByEmail(userDetails.getUsername());
if (user == null) {
throw new WebAuthnException("User not found");
}
credentialManagementService.renameCredential(id, request.label(), user);
return ResponseEntity.ok(new GenericResponse("Passkey renamed successfully"));
} catch (WebAuthnException e) {
log.error("Failed to rename credential: {}", e.getMessage());
return ResponseEntity.badRequest().body(new GenericResponse(e.getMessage()));
}
}
/**
* Delete a passkey.
*
* <p>
* Soft-deletes a passkey by marking it as disabled. Includes last-credential protection to prevent users from being
* locked out of their accounts.
* </p>
*
* <p>
* If this is the user's last passkey and they have no password, the deletion will be blocked with an error message.
* </p>
*
* @param id the credential ID to delete
* @param userDetails the authenticated user details
* @return ResponseEntity with success message or error
*/
@DeleteMapping("/credentials/{id}")
public ResponseEntity<GenericResponse> deleteCredential(@PathVariable String id, @AuthenticationPrincipal UserDetails userDetails) {
try {
User user = userService.findUserByEmail(userDetails.getUsername());
if (user == null) {
throw new WebAuthnException("User not found");
}
credentialManagementService.deleteCredential(id, user);
return ResponseEntity.ok(new GenericResponse("Passkey deleted successfully"));
} catch (WebAuthnException e) {
log.error("Failed to delete credential: {}", e.getMessage());
return ResponseEntity.badRequest().body(new GenericResponse(e.getMessage()));
}
}
/**
* Request DTO for renaming credential.
*
* @param label the new label (must not be blank)
*/
public record RenameCredentialRequest(@NotBlank String label) {
}
}