mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-03 17:18:51 +01:00
fix: disallow literal string
This commit is contained in:
parent
05a4f6cc45
commit
a0c4ddaf9e
3 changed files with 137 additions and 63 deletions
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { TranslationKeys, useLocalize } from '~/hooks';
|
||||
|
||||
type PasskeyAuthProps = {
|
||||
mode: 'login' | 'register';
|
||||
onBack?: () => void; // Optional callback to return to normal login/register view
|
||||
onBack?: () => void;
|
||||
};
|
||||
|
||||
const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
||||
|
|
@ -11,11 +11,85 @@ const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
|||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Utility for showing errors using localized keys
|
||||
const alertError = (key: TranslationKeys, error: any) => {
|
||||
console.error(`${localize(key)} error:`, error);
|
||||
alert(
|
||||
`${localize(key)}: ${error.message}. ${localize('com_auth_passkey_try_again')}`
|
||||
);
|
||||
};
|
||||
|
||||
// Convert login challenge options from the server
|
||||
const processLoginOptions = (options: any) => {
|
||||
options.challenge = base64URLToArrayBuffer(options.challenge);
|
||||
if (options.allowCredentials) {
|
||||
options.allowCredentials = options.allowCredentials.map((cred: any) => ({
|
||||
...cred,
|
||||
id: base64URLToArrayBuffer(cred.id),
|
||||
}));
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
// Convert registration challenge options from the server
|
||||
const processRegistrationOptions = (options: any) => {
|
||||
options.challenge = base64URLToArrayBuffer(options.challenge);
|
||||
options.user.id = base64URLToArrayBuffer(options.user.id);
|
||||
if (options.excludeCredentials) {
|
||||
options.excludeCredentials = options.excludeCredentials.map((cred: any) => ({
|
||||
...cred,
|
||||
id: base64URLToArrayBuffer(cred.id),
|
||||
}));
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
// Format the authentication response from navigator.credentials.get()
|
||||
const getAuthenticationResponse = (credential: PublicKeyCredential) => ({
|
||||
id: credential.id,
|
||||
rawId: arrayBufferToBase64URL(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
authenticatorData: arrayBufferToBase64URL(
|
||||
(credential.response as any).authenticatorData
|
||||
),
|
||||
clientDataJSON: arrayBufferToBase64URL(
|
||||
(credential.response as any).clientDataJSON
|
||||
),
|
||||
signature: arrayBufferToBase64URL(
|
||||
(credential.response as any).signature
|
||||
),
|
||||
userHandle: (credential.response as any).userHandle
|
||||
? arrayBufferToBase64URL((credential.response as any).userHandle)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Format the registration response from navigator.credentials.create()
|
||||
const getRegistrationResponse = (credential: PublicKeyCredential) => ({
|
||||
id: credential.id,
|
||||
rawId: arrayBufferToBase64URL(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
clientDataJSON: arrayBufferToBase64URL(
|
||||
(credential.response as any).clientDataJSON
|
||||
),
|
||||
attestationObject: arrayBufferToBase64URL(
|
||||
(credential.response as any).attestationObject
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
// --- PASSKEY LOGIN FLOW ---
|
||||
async function handlePasskeyLogin() {
|
||||
if (!email) {
|
||||
// (You may wish to replace this literal with a localized string if available.)
|
||||
return alert('Email is required for login.');
|
||||
}
|
||||
if (typeof PublicKeyCredential === 'undefined') {
|
||||
alert(localize('com_auth_passkey_not_supported'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const challengeResponse = await fetch(
|
||||
|
|
@ -23,37 +97,25 @@ const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
|||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!challengeResponse.ok) {
|
||||
const errorData = await challengeResponse.json();
|
||||
throw new Error(errorData.error || 'Failed to get challenge');
|
||||
throw new Error(
|
||||
errorData.error || localize('com_auth_passkey_error')
|
||||
);
|
||||
}
|
||||
const options = await challengeResponse.json();
|
||||
options.challenge = base64URLToArrayBuffer(options.challenge);
|
||||
if (options.allowCredentials) {
|
||||
options.allowCredentials = options.allowCredentials.map((cred: any) => ({
|
||||
...cred,
|
||||
id: base64URLToArrayBuffer(cred.id),
|
||||
}));
|
||||
}
|
||||
const credential = await navigator.credentials.get({ publicKey: options });
|
||||
let options = await challengeResponse.json();
|
||||
options = processLoginOptions(options);
|
||||
|
||||
const credential = (await navigator.credentials.get({
|
||||
publicKey: options,
|
||||
})) as PublicKeyCredential;
|
||||
if (!credential) {
|
||||
throw new Error('Failed to obtain credential');
|
||||
throw new Error(localize('com_auth_passkey_no_credentials'));
|
||||
}
|
||||
const authenticationResponse = {
|
||||
id: credential.id,
|
||||
rawId: arrayBufferToBase64URL(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
authenticatorData: arrayBufferToBase64URL((credential.response as any).authenticatorData),
|
||||
clientDataJSON: arrayBufferToBase64URL((credential.response as any).clientDataJSON),
|
||||
signature: arrayBufferToBase64URL((credential.response as any).signature),
|
||||
userHandle: (credential.response as any).userHandle
|
||||
? arrayBufferToBase64URL((credential.response as any).userHandle)
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
const authenticationResponse = getAuthenticationResponse(credential);
|
||||
const loginCallbackResponse = await fetch('/webauthn/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -61,13 +123,15 @@ const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
|||
});
|
||||
const result = await loginCallbackResponse.json();
|
||||
if (result.user) {
|
||||
alert(localize('com_auth_passkey_login_success'));
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
throw new Error(result.error || 'Authentication failed');
|
||||
throw new Error(
|
||||
result.error || localize('com_auth_passkey_error')
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Passkey login error:', error);
|
||||
alert('Authentication failed: ' + error.message);
|
||||
alertError('com_auth_passkey_failed', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -76,8 +140,13 @@ const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
|||
// --- PASSKEY REGISTRATION FLOW ---
|
||||
async function handlePasskeyRegister() {
|
||||
if (!email) {
|
||||
// (You may wish to replace this literal with a localized string if available.)
|
||||
return alert('Email is required for registration.');
|
||||
}
|
||||
if (typeof PublicKeyCredential === 'undefined') {
|
||||
alert(localize('com_auth_passkey_not_supported'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const challengeResponse = await fetch(
|
||||
|
|
@ -85,34 +154,25 @@ const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
|||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!challengeResponse.ok) {
|
||||
const errorData = await challengeResponse.json();
|
||||
throw new Error(errorData.error || 'Failed to get challenge');
|
||||
throw new Error(
|
||||
errorData.error || localize('com_auth_passkey_error')
|
||||
);
|
||||
}
|
||||
const options = await challengeResponse.json();
|
||||
options.challenge = base64URLToArrayBuffer(options.challenge);
|
||||
options.user.id = base64URLToArrayBuffer(options.user.id);
|
||||
if (options.excludeCredentials) {
|
||||
options.excludeCredentials = options.excludeCredentials.map((cred: any) => ({
|
||||
...cred,
|
||||
id: base64URLToArrayBuffer(cred.id),
|
||||
}));
|
||||
}
|
||||
const credential = await navigator.credentials.create({ publicKey: options });
|
||||
let options = await challengeResponse.json();
|
||||
options = processRegistrationOptions(options);
|
||||
|
||||
const credential = (await navigator.credentials.create({
|
||||
publicKey: options,
|
||||
})) as PublicKeyCredential;
|
||||
if (!credential) {
|
||||
throw new Error('Failed to create credential');
|
||||
throw new Error(localize('com_auth_passkey_create_error'));
|
||||
}
|
||||
const registrationResponse = {
|
||||
id: credential.id,
|
||||
rawId: arrayBufferToBase64URL(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
clientDataJSON: arrayBufferToBase64URL((credential.response as any).clientDataJSON),
|
||||
attestationObject: arrayBufferToBase64URL((credential.response as any).attestationObject),
|
||||
},
|
||||
};
|
||||
|
||||
const registrationResponse = getRegistrationResponse(credential);
|
||||
const registerCallbackResponse = await fetch('/webauthn/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -120,13 +180,15 @@ const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
|||
});
|
||||
const result = await registerCallbackResponse.json();
|
||||
if (result.user) {
|
||||
alert(localize('com_auth_passkey_register_success'));
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
throw new Error(result.error || 'Registration failed');
|
||||
throw new Error(
|
||||
result.error || localize('com_auth_passkey_error')
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Passkey registration error:', error);
|
||||
alert('Registration failed: ' + error.message);
|
||||
alertError('com_auth_passkey_registration_failed', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -180,8 +242,8 @@ const PasskeyAuth: React.FC<PasskeyAuthProps> = ({ mode, onBack }) => {
|
|||
? localize('com_auth_loading')
|
||||
: localize(
|
||||
mode === 'login'
|
||||
? 'com_auth_passkey_login'
|
||||
: 'com_auth_passkey_register',
|
||||
? 'com_auth_passkey_login_success'
|
||||
: 'com_auth_passkey_register_success'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -41,16 +41,16 @@ export default function PassKeys() {
|
|||
{user.passkeys.map((passkey: TPasskey) => (
|
||||
<div key={passkey.id} className="rounded-lg border p-3 bg-gray-50 dark:bg-gray-800">
|
||||
<p className="text-sm">
|
||||
<strong>ID:</strong> {passkey.id}
|
||||
<strong>{localize('com_nav_settings_passkey_label_id')}</strong> {passkey.id}
|
||||
</p>
|
||||
<p className="text-sm break-all">
|
||||
<strong>Public Key:</strong> {Buffer.from(passkey.publicKey).toString('base64')}
|
||||
<strong>{localize('com_nav_settings_passkey_label_public_key')}</strong> {Buffer.from(passkey.publicKey).toString('base64')}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<strong>Usage Counter:</strong> {passkey.counter}
|
||||
<strong>{localize('com_nav_settings_passkey_label_usage_counter')}</strong> {passkey.counter}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<strong>Transports:</strong> {passkey.transports.length > 0 ? passkey.transports.join(', ') : 'None'}
|
||||
<strong>{localize('com_nav_settings_passkey_label_transports')}</strong> {passkey.transports.length > 0 ? passkey.transports.join(', ') : localize('com_nav_settings_passkey_none')}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -779,12 +779,24 @@
|
|||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "You",
|
||||
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint.",
|
||||
"com_auth_passkey_login": "Login with Passkey",
|
||||
"com_auth_passkey_register": "Register with Passkey",
|
||||
"com_auth_loading": "Loading...",
|
||||
"com_auth_back_to_register": "Back to registration",
|
||||
"com_nav_passkeys": "Passkeys",
|
||||
"com_nav_view_passkeys": "View Passkeys",
|
||||
"com_auth_passkey_failed": "Authentication failed",
|
||||
"com_auth_passkey_registration_failed": "Registration failed",
|
||||
"com_auth_passkey_error": "Passkey authentication error",
|
||||
"com_auth_passkey_create_error": "Failed to create passkey",
|
||||
"com_auth_passkey_not_supported": "Passkeys are not supported on this device",
|
||||
"com_auth_passkey_no_credentials": "No passkey credentials found",
|
||||
"com_auth_passkey_register_success": "Successfully registered with passkey",
|
||||
"com_auth_passkey_login_success": "Successfully logged in with passkey",
|
||||
"com_auth_passkey_try_again": "Please try again",
|
||||
"com_nav_settings_passkey_label_id": "ID:",
|
||||
"com_nav_settings_passkey_label_public_key": "Public Key:",
|
||||
"com_nav_settings_passkey_label_usage_counter": "Usage Counter:",
|
||||
"com_nav_settings_passkey_label_transports": "Transports:",
|
||||
"com_nav_settings_passkey_none": "None",
|
||||
"com_ui_no_data": "something needs to go here. was empty",
|
||||
"com_files_table": "something needs to go here. was empty",
|
||||
"com_ui_global_group": "something needs to go here. was empty",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue