fix(auth): keep known accounts on logout, pass transports in scoped auth
Logout no longer removes the account from the picker — users see "Sign in as [username]" on next visit. fetchScopedCredentials now returns full PublicKeyCredentialDescriptor with transports so the browser can locate the right authenticator without showing a picker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b625913eba
commit
3ead9b4ca0
|
|
@ -8,6 +8,7 @@
|
|||
import {
|
||||
registerPasskey,
|
||||
authenticatePasskey,
|
||||
base64urlToBuffer,
|
||||
detectCapabilities,
|
||||
startConditionalUI,
|
||||
WebAuthnCapabilities,
|
||||
|
|
@ -578,10 +579,10 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch scoped credential IDs from the auth server for a given username.
|
||||
* Returns the credential ID array, or undefined to fall back to unscoped.
|
||||
* Fetch scoped credentials from the auth server for a given username.
|
||||
* Returns PublicKeyCredentialDescriptor[] with transports, or undefined to fall back to unscoped.
|
||||
*/
|
||||
private async fetchScopedCredentials(username: string): Promise<string[] | undefined> {
|
||||
private async fetchScopedCredentials(username: string): Promise<PublicKeyCredentialDescriptor[] | undefined> {
|
||||
try {
|
||||
const res = await fetch(`${ENCRYPTID_AUTH}/api/auth/start`, {
|
||||
method: 'POST',
|
||||
|
|
@ -591,7 +592,11 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
if (!res.ok) return undefined;
|
||||
const { options, userFound } = await res.json();
|
||||
if (!userFound || !options.allowCredentials?.length) return undefined;
|
||||
return options.allowCredentials.map((c: any) => c.id);
|
||||
return options.allowCredentials.map((c: any) => ({
|
||||
type: 'public-key' as const,
|
||||
id: base64urlToBuffer(c.id),
|
||||
...(c.transports?.length ? { transports: c.transports } : {}),
|
||||
}));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -605,14 +610,14 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
|
||||
try {
|
||||
// If a username was selected, scope the passkey prompt to that user's credentials
|
||||
let credentialIds: string[] | undefined;
|
||||
let scopedCredentials: PublicKeyCredentialDescriptor[] | undefined;
|
||||
if (username) {
|
||||
credentialIds = await this.fetchScopedCredentials(username);
|
||||
scopedCredentials = await this.fetchScopedCredentials(username);
|
||||
// If user not found on server, fall back to unscoped (still let them try)
|
||||
}
|
||||
|
||||
// Authenticate — scoped if we have credential IDs, unscoped otherwise
|
||||
const result = await authenticatePasskey(credentialIds);
|
||||
// Authenticate — scoped if we have credentials, unscoped otherwise
|
||||
const result = await authenticatePasskey(scopedCredentials);
|
||||
|
||||
// Initialize key manager with PRF output
|
||||
const keyManager = getKeyManager();
|
||||
|
|
@ -722,13 +727,7 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
|
||||
private handleLogout() {
|
||||
const sessionManager = getSessionManager();
|
||||
|
||||
// Remove this account from known accounts list
|
||||
const session = sessionManager.getSession();
|
||||
if (session?.claims.username) {
|
||||
removeKnownAccount(session.claims.username);
|
||||
}
|
||||
|
||||
// Keep known account in localStorage so the picker shows on next login
|
||||
sessionManager.clearSession();
|
||||
|
||||
const keyManager = getKeyManager();
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ export async function registerPasskey(
|
|||
* (if the authenticator supports PRF).
|
||||
*/
|
||||
export async function authenticatePasskey(
|
||||
credentialIds?: string | string[], // Optional: one or more credential IDs to scope, or let user choose
|
||||
credentials?: string | string[] | PublicKeyCredentialDescriptor[],
|
||||
config: Partial<EncryptIDConfig> = {}
|
||||
): Promise<AuthenticationResult> {
|
||||
// Abort any pending conditional UI to prevent "request already pending" error
|
||||
|
|
@ -273,12 +273,18 @@ export async function authenticatePasskey(
|
|||
|
||||
// Build allowed credentials list
|
||||
let allowCredentials: PublicKeyCredentialDescriptor[] | undefined;
|
||||
if (credentialIds) {
|
||||
const ids = Array.isArray(credentialIds) ? credentialIds : [credentialIds];
|
||||
allowCredentials = ids.map(id => ({
|
||||
type: 'public-key' as const,
|
||||
id: new Uint8Array(base64urlToBuffer(id)),
|
||||
}));
|
||||
if (credentials) {
|
||||
if (typeof credentials === 'string') {
|
||||
allowCredentials = [{ type: 'public-key', id: new Uint8Array(base64urlToBuffer(credentials)) }];
|
||||
} else if (credentials.length > 0 && typeof credentials[0] === 'string') {
|
||||
allowCredentials = (credentials as string[]).map(id => ({
|
||||
type: 'public-key' as const,
|
||||
id: new Uint8Array(base64urlToBuffer(id)),
|
||||
}));
|
||||
} else {
|
||||
// Already PublicKeyCredentialDescriptor[]
|
||||
allowCredentials = credentials as PublicKeyCredentialDescriptor[];
|
||||
}
|
||||
}
|
||||
|
||||
// Build authentication options
|
||||
|
|
|
|||
Loading…
Reference in New Issue