mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
fix: harden API key authentication
This commit is contained in:
@@ -754,7 +754,7 @@ export async function handleVerifyPassword(request: Request, env: Env, userId: s
|
||||
}
|
||||
|
||||
// POST /api/accounts/api-key
|
||||
export async function handleGetApiKey(request: Request, env: Env, userId: string): Promise<Response>{
|
||||
export async function handleGetApiKey(request: Request, env: Env, userId: string): Promise<Response> {
|
||||
return apiKey(request, env, userId, false);
|
||||
}
|
||||
|
||||
@@ -763,7 +763,7 @@ export async function handleRotateApiKey(request: Request, env: Env, userId: str
|
||||
return apiKey(request, env, userId, true);
|
||||
}
|
||||
|
||||
async function apiKey(request: Request, env: Env, userId: string,rotate: boolean): Promise<Response> {
|
||||
async function apiKey(request: Request, env: Env, userId: string, rotate: boolean): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const auth = new AuthService(env);
|
||||
const user = await storage.getUserById(userId);
|
||||
@@ -786,28 +786,32 @@ async function apiKey(request: Request, env: Env, userId: string,rotate: boolean
|
||||
if (!currentHash) return errorResponse('masterPasswordHash is required', 400);
|
||||
const valid = await auth.verifyPassword(currentHash, user.masterPasswordHash, user.email);
|
||||
if (!valid) return errorResponse('Invalid password', 400);
|
||||
|
||||
|
||||
if (rotate || user.apiKey === null) {
|
||||
// Upstream apikeys are 30-character random alphanumeric strings
|
||||
user.apiKey = randomStringAlphanum(LIMITS.auth.clientSecretLength);
|
||||
if (rotate) {
|
||||
user.securityStamp = generateUUID();
|
||||
await storage.deleteRefreshTokensByUserId(user.id);
|
||||
}
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
return jsonResponse({
|
||||
apiKey: user.apiKey,
|
||||
revisionDate: user.updatedAt,
|
||||
revisionDate: user.updatedAt,
|
||||
object: 'apiKey',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a random alphanumeric string of the given length using crypto.getRandomValues.
|
||||
function randomStringAlphanum(length: number): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
|
||||
let result = "";
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[array[i] % chars.length];
|
||||
}
|
||||
|
||||
@@ -48,6 +48,18 @@ function parseCookieValue(request: Request, name: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function constantTimeEquals(a: string, b: string): boolean {
|
||||
const encA = new TextEncoder().encode(a);
|
||||
const encB = new TextEncoder().encode(b);
|
||||
if (encA.length !== encB.length) return false;
|
||||
|
||||
let diff = 0;
|
||||
for (let i = 0; i < encA.length; i++) {
|
||||
diff |= encA[i] ^ encB[i];
|
||||
}
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
function buildRefreshCookie(request: Request, refreshToken: string, maxAgeSeconds: number): string {
|
||||
const isHttps = new URL(request.url).protocol === 'https:';
|
||||
const parts = [
|
||||
@@ -395,8 +407,12 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
||||
return identityErrorResponse('Account is disabled', 'invalid_grant', 400);
|
||||
}
|
||||
|
||||
if (!user.apiKey || !constantTimeEquals(clientSecret, user.apiKey)) {
|
||||
await rateLimit.recordFailedLogin(loginIdentifier);
|
||||
return identityErrorResponse('ClientId or clientSecret is incorrect. Try again', 'invalid_grant', 400);
|
||||
}
|
||||
|
||||
// Persist device only after successful password + (optional) 2FA verification.
|
||||
// Persist device only after successful client credential verification.
|
||||
const deviceSession =
|
||||
deviceInfo.deviceIdentifier
|
||||
? { identifier: deviceInfo.deviceIdentifier, sessionStamp: generateUUID() }
|
||||
@@ -423,7 +439,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
||||
access_token: accessToken,
|
||||
expires_in: LIMITS.auth.accessTokenTtlSeconds,
|
||||
token_type: 'Bearer',
|
||||
...(shouldUseWebSession(request) ? { web_session: true } : { refresh_token: refreshToken } ),
|
||||
...(shouldUseWebSession(request) ? { web_session: true } : { refresh_token: refreshToken }),
|
||||
Key: user.key,
|
||||
PrivateKey: user.privateKey,
|
||||
AccountKeys: accountKeys,
|
||||
@@ -643,10 +659,10 @@ export async function handleRevocation(request: Request, env: Env): Promise<Resp
|
||||
}
|
||||
|
||||
export function checkClientCredentialsParam(clientId: string, clientSecret: string, scope: string): boolean {
|
||||
if (scope !== "api") {
|
||||
if (scope !== 'api') {
|
||||
return false;
|
||||
}
|
||||
if (!clientId.startsWith("user.")) {
|
||||
if (!clientId.startsWith('user.')) {
|
||||
return false;
|
||||
}
|
||||
if (!clientSecret) {
|
||||
|
||||
@@ -121,11 +121,11 @@ export async function handleAuthenticatedRoute(
|
||||
return handleSetVerifyDevices(request, env, userId);
|
||||
}
|
||||
|
||||
if (path === '/api/accounts/api_key' && method === 'POST') {
|
||||
if ((path === '/api/accounts/api-key' || path === '/api/accounts/api_key') && method === 'POST') {
|
||||
return handleGetApiKey(request, env, userId);
|
||||
}
|
||||
|
||||
if (path === '/api/accounts/rotate_api_key' && method === 'POST') {
|
||||
if ((path === '/api/accounts/rotate-api-key' || path === '/api/accounts/rotate_api_key') && method === 'POST') {
|
||||
return handleRotateApiKey(request, env, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export async function createFirstUser(db: D1Database, safeBind: SafeBind, user:
|
||||
const email = user.email.toLowerCase();
|
||||
const stmt = db.prepare(
|
||||
'INSERT INTO users(id, email, name, master_password_hint, master_password_hash, key, private_key, public_key, kdf_type, kdf_iterations, kdf_memory, kdf_parallelism, security_stamp, role, status, verify_devices, totp_secret, totp_recovery_code, api_key, created_at, updated_at) ' +
|
||||
'SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?' +
|
||||
'SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ' +
|
||||
'WHERE NOT EXISTS (SELECT 1 FROM users LIMIT 1)'
|
||||
);
|
||||
const result = await safeBind(
|
||||
|
||||
Reference in New Issue
Block a user