fix: proxy passkey bind through Management API
Logto's /api/my-account/ endpoints reject the opaque access token with 401 even though /api/verifications/ accepts it. The bind step now goes through the SaaS backend which calls the Management API instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,17 @@ public class AccountController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- WebAuthn bind (proxied via Management API since Account API /my-account/ returns 401) ---
|
||||||
|
|
||||||
|
record WebAuthnBindRequest(String verificationRecordId, String identityVerificationId) {}
|
||||||
|
|
||||||
|
@PostMapping("/mfa/webauthn/bind")
|
||||||
|
public ResponseEntity<Void> bindWebAuthn(@AuthenticationPrincipal Jwt jwt,
|
||||||
|
@RequestBody WebAuthnBindRequest request) {
|
||||||
|
accountService.bindWebAuthnPasskey(jwt.getSubject(), request.verificationRecordId(), request.identityVerificationId());
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
// --- MFA Preference ---
|
// --- MFA Preference ---
|
||||||
|
|
||||||
@PostMapping("/mfa/method-preference")
|
@PostMapping("/mfa/method-preference")
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ public class AccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void bindWebAuthnPasskey(String userId, String verificationRecordId, String identityVerificationId) {
|
||||||
|
logtoClient.bindWebAuthnMfa(userId, verificationRecordId, identityVerificationId);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Passkeys ---
|
// --- Passkeys ---
|
||||||
|
|
||||||
public List<PasskeyCredential> listPasskeys(String userId) {
|
public List<PasskeyCredential> listPasskeys(String userId) {
|
||||||
|
|||||||
@@ -653,6 +653,18 @@ public class LogtoManagementClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bind a WebAuthn passkey to a user via the Management API. */
|
||||||
|
public void bindWebAuthnMfa(String userId, String verificationRecordId, String identityVerificationId) {
|
||||||
|
if (!isAvailable()) throw new IllegalStateException("Logto not configured");
|
||||||
|
restClient.post()
|
||||||
|
.uri(config.getLogtoEndpoint() + "/api/users/" + userId + "/mfa-verifications")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(Map.of("type", "WebAuthn", "verificationRecordId", verificationRecordId))
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity();
|
||||||
|
}
|
||||||
|
|
||||||
/** Enable the Account Center API for MFA management (WebAuthn passkey registration). */
|
/** Enable the Account Center API for MFA management (WebAuthn passkey registration). */
|
||||||
public void enableAccountCenter() {
|
public void enableAccountCenter() {
|
||||||
if (!isAvailable()) return;
|
if (!isAvailable()) return;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
|
import { api } from './client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logto Account API client for WebAuthn passkey registration.
|
* Logto Account API client for WebAuthn passkey registration.
|
||||||
* Calls Logto's Account API endpoints directly (same domain).
|
* Calls Logto's Account API endpoints directly (same domain).
|
||||||
|
* The bind step goes through the SaaS backend (Management API).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function accountApi(
|
async function accountApi(
|
||||||
@@ -72,19 +74,10 @@ export async function registerPasskey(
|
|||||||
const verifyData = await verifyRes.json();
|
const verifyData = await verifyRes.json();
|
||||||
const verifiedRecordId = verifyData.verificationRecordId;
|
const verifiedRecordId = verifyData.verificationRecordId;
|
||||||
|
|
||||||
// Step 5: Bind the passkey — requires logto-verification-id header for sensitive op
|
// Step 5: Bind via SaaS backend (Management API) — Logto's /api/my-account/
|
||||||
const bindRes = await accountApi(
|
// rejects the opaque token, so we proxy through our backend.
|
||||||
'POST',
|
await api.post('/account/mfa/webauthn/bind', {
|
||||||
'/my-account/mfa-verifications',
|
verificationRecordId: verifiedRecordId,
|
||||||
token,
|
identityVerificationId,
|
||||||
{
|
});
|
||||||
type: 'WebAuthn',
|
|
||||||
newIdentifierVerificationRecordId: verifiedRecordId,
|
|
||||||
},
|
|
||||||
{ 'logto-verification-id': identityVerificationId },
|
|
||||||
);
|
|
||||||
if (!bindRes.ok) {
|
|
||||||
const err = await bindRes.json().catch(() => ({}));
|
|
||||||
throw new Error(err.message || `Failed to bind passkey (${bindRes.status})`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user