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 {
|
import {
|
||||||
registerPasskey,
|
registerPasskey,
|
||||||
authenticatePasskey,
|
authenticatePasskey,
|
||||||
|
base64urlToBuffer,
|
||||||
detectCapabilities,
|
detectCapabilities,
|
||||||
startConditionalUI,
|
startConditionalUI,
|
||||||
WebAuthnCapabilities,
|
WebAuthnCapabilities,
|
||||||
|
|
@ -578,10 +579,10 @@ export class EncryptIDLoginButton extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch scoped credential IDs from the auth server for a given username.
|
* Fetch scoped credentials from the auth server for a given username.
|
||||||
* Returns the credential ID array, or undefined to fall back to unscoped.
|
* 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 {
|
try {
|
||||||
const res = await fetch(`${ENCRYPTID_AUTH}/api/auth/start`, {
|
const res = await fetch(`${ENCRYPTID_AUTH}/api/auth/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -591,7 +592,11 @@ export class EncryptIDLoginButton extends HTMLElement {
|
||||||
if (!res.ok) return undefined;
|
if (!res.ok) return undefined;
|
||||||
const { options, userFound } = await res.json();
|
const { options, userFound } = await res.json();
|
||||||
if (!userFound || !options.allowCredentials?.length) return undefined;
|
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 {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -605,14 +610,14 @@ export class EncryptIDLoginButton extends HTMLElement {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If a username was selected, scope the passkey prompt to that user's credentials
|
// If a username was selected, scope the passkey prompt to that user's credentials
|
||||||
let credentialIds: string[] | undefined;
|
let scopedCredentials: PublicKeyCredentialDescriptor[] | undefined;
|
||||||
if (username) {
|
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)
|
// If user not found on server, fall back to unscoped (still let them try)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate — scoped if we have credential IDs, unscoped otherwise
|
// Authenticate — scoped if we have credentials, unscoped otherwise
|
||||||
const result = await authenticatePasskey(credentialIds);
|
const result = await authenticatePasskey(scopedCredentials);
|
||||||
|
|
||||||
// Initialize key manager with PRF output
|
// Initialize key manager with PRF output
|
||||||
const keyManager = getKeyManager();
|
const keyManager = getKeyManager();
|
||||||
|
|
@ -722,13 +727,7 @@ export class EncryptIDLoginButton extends HTMLElement {
|
||||||
|
|
||||||
private handleLogout() {
|
private handleLogout() {
|
||||||
const sessionManager = getSessionManager();
|
const sessionManager = getSessionManager();
|
||||||
|
// Keep known account in localStorage so the picker shows on next login
|
||||||
// Remove this account from known accounts list
|
|
||||||
const session = sessionManager.getSession();
|
|
||||||
if (session?.claims.username) {
|
|
||||||
removeKnownAccount(session.claims.username);
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionManager.clearSession();
|
sessionManager.clearSession();
|
||||||
|
|
||||||
const keyManager = getKeyManager();
|
const keyManager = getKeyManager();
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ export async function registerPasskey(
|
||||||
* (if the authenticator supports PRF).
|
* (if the authenticator supports PRF).
|
||||||
*/
|
*/
|
||||||
export async function authenticatePasskey(
|
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> = {}
|
config: Partial<EncryptIDConfig> = {}
|
||||||
): Promise<AuthenticationResult> {
|
): Promise<AuthenticationResult> {
|
||||||
// Abort any pending conditional UI to prevent "request already pending" error
|
// Abort any pending conditional UI to prevent "request already pending" error
|
||||||
|
|
@ -273,12 +273,18 @@ export async function authenticatePasskey(
|
||||||
|
|
||||||
// Build allowed credentials list
|
// Build allowed credentials list
|
||||||
let allowCredentials: PublicKeyCredentialDescriptor[] | undefined;
|
let allowCredentials: PublicKeyCredentialDescriptor[] | undefined;
|
||||||
if (credentialIds) {
|
if (credentials) {
|
||||||
const ids = Array.isArray(credentialIds) ? credentialIds : [credentialIds];
|
if (typeof credentials === 'string') {
|
||||||
allowCredentials = ids.map(id => ({
|
allowCredentials = [{ type: 'public-key', id: new Uint8Array(base64urlToBuffer(credentials)) }];
|
||||||
type: 'public-key' as const,
|
} else if (credentials.length > 0 && typeof credentials[0] === 'string') {
|
||||||
id: new Uint8Array(base64urlToBuffer(id)),
|
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
|
// Build authentication options
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue