mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-05 18:18:51 +01:00
WIP 🔐 feat: PassKey (#5606)
* added PassKey authentication. * fixed issue with test :) * Delete client/src/components/Auth/AuthLayout.tsx * fix: conflicted issue
This commit is contained in:
parent
2a506df443
commit
1cb1c9196d
20 changed files with 569 additions and 12 deletions
4
api/cache/index.js
vendored
4
api/cache/index.js
vendored
|
|
@ -1,5 +1,7 @@
|
|||
const keyvFiles = require('./keyvFiles');
|
||||
const getLogStores = require('./getLogStores');
|
||||
const logViolation = require('./logViolation');
|
||||
const mongoUserStore = require('./mongoUserStore');
|
||||
const mongoChallengeStore = require('./mongoChallengeStore');
|
||||
|
||||
module.exports = { ...keyvFiles, getLogStores, logViolation };
|
||||
module.exports = { ...keyvFiles, getLogStores, logViolation, mongoUserStore, mongoChallengeStore };
|
||||
35
api/cache/mongoChallengeStore.js
vendored
Normal file
35
api/cache/mongoChallengeStore.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const ChallengeStore = require('~/models/ChallengeStore');
|
||||
|
||||
class MongoChallengeStore {
|
||||
async get(userId) {
|
||||
try {
|
||||
const challenge = await ChallengeStore.findOne({ userId }).lean().exec();
|
||||
return challenge ? challenge.challenge : undefined;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error fetching challenge for userId ${userId}:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async save(userId, challenge) {
|
||||
try {
|
||||
await ChallengeStore.findOneAndUpdate(
|
||||
{ userId },
|
||||
{ challenge, createdAt: new Date() },
|
||||
{ upsert: true, new: true, setDefaultsOnInsert: true },
|
||||
).exec();
|
||||
} catch (error) {
|
||||
console.error(`❌ Error saving challenge for userId ${userId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(userId) {
|
||||
try {
|
||||
await ChallengeStore.deleteOne({ userId }).exec();
|
||||
} catch (error) {
|
||||
console.error(`❌ Error deleting challenge for userId ${userId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MongoChallengeStore;
|
||||
55
api/cache/mongoUserStore.js
vendored
Normal file
55
api/cache/mongoUserStore.js
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
const User = require('~/models');
|
||||
|
||||
class MongoUserStore {
|
||||
async get(identifier, byID = false) {
|
||||
let user;
|
||||
if (byID) {
|
||||
user = await User.getUserById(identifier);
|
||||
} else {
|
||||
user = await User.findUser({ email: identifier });
|
||||
}
|
||||
if (user) {
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
email: user.email,
|
||||
passkeys: user.passkeys,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async save(user) {
|
||||
if (!user.id) {
|
||||
const createdUser = await User.createUser(
|
||||
{
|
||||
email: user.email,
|
||||
username: user.email,
|
||||
passkeys: user.passkeys,
|
||||
},
|
||||
/* disableTTL */ true,
|
||||
/* returnUser */ true,
|
||||
);
|
||||
return {
|
||||
id: createdUser._id.toString(),
|
||||
email: createdUser.email,
|
||||
passkeys: createdUser.passkeys,
|
||||
};
|
||||
} else {
|
||||
const updatedUser = await User.updateUser(user.id, {
|
||||
email: user.email,
|
||||
username: user.email,
|
||||
passkeys: user.passkeys,
|
||||
});
|
||||
if (!updatedUser) {
|
||||
throw new Error('Failed to update user');
|
||||
}
|
||||
return {
|
||||
id: updatedUser._id.toString(),
|
||||
email: updatedUser.email,
|
||||
passkeys: updatedUser.passkeys,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MongoUserStore;
|
||||
6
api/models/ChallengeStore.js
Normal file
6
api/models/ChallengeStore.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
const mongoose = require('mongoose');
|
||||
const challengeSchema = require('~/models/schema/challengeSchema');
|
||||
|
||||
const ChallengeStore = mongoose.model('Challenge', challengeSchema);
|
||||
|
||||
module.exports = ChallengeStore;
|
||||
22
api/models/schema/challengeSchema.js
Normal file
22
api/models/schema/challengeSchema.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const challengeSchema = mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
challenge: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
index: {
|
||||
expires: '5m',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = challengeSchema;
|
||||
|
|
@ -39,6 +39,13 @@ const Session = mongoose.Schema({
|
|||
},
|
||||
});
|
||||
|
||||
const passkeySchema = mongoose.Schema({
|
||||
id: { type: String, required: true },
|
||||
publicKey: { type: Buffer, required: true },
|
||||
counter: { type: Number, default: 0 },
|
||||
transports: { type: [String], default: [] },
|
||||
});
|
||||
|
||||
/** @type {MongooseSchema<MongoUser>} */
|
||||
const userSchema = mongoose.Schema(
|
||||
{
|
||||
|
|
@ -117,6 +124,10 @@ const userSchema = mongoose.Schema(
|
|||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
passkeys: {
|
||||
type: [passkeySchema],
|
||||
default: [],
|
||||
},
|
||||
plugins: {
|
||||
type: Array,
|
||||
default: [],
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ const AppService = require('./services/AppService');
|
|||
const staticCache = require('./utils/staticCache');
|
||||
const noIndex = require('./middleware/noIndex');
|
||||
const routes = require('./routes');
|
||||
const { WebAuthnStrategy } = require('passport-simple-webauthn2');
|
||||
const { mongoUserStore, mongoChallengeStore } = require('~/cache');
|
||||
|
||||
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION } = process.env ?? {};
|
||||
|
||||
|
|
@ -77,11 +79,29 @@ const startServer = async () => {
|
|||
passport.use(ldapLogin);
|
||||
}
|
||||
|
||||
/* Passkey (WebAuthn) Strategy */
|
||||
if (process.env.PASSKEY_ENABLED) {
|
||||
|
||||
const userStore = new mongoUserStore();
|
||||
const challengeStore = new mongoChallengeStore();
|
||||
|
||||
passport.use(
|
||||
new WebAuthnStrategy({
|
||||
rpID: process.env.RP_ID || 'localhost',
|
||||
rpName: process.env.APP_TITLE || 'LibreChat',
|
||||
userStore,
|
||||
challengeStore,
|
||||
debug: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (isEnabled(ALLOW_SOCIAL_LOGIN)) {
|
||||
configureSocialLogins(app);
|
||||
}
|
||||
|
||||
app.use('/oauth', routes.oauth);
|
||||
app.use('/webauthn', routes.authWebAuthn);
|
||||
/* API Endpoints */
|
||||
app.use('/api/auth', routes.auth);
|
||||
app.use('/api/actions', routes.actions);
|
||||
|
|
|
|||
44
api/server/routes/authWebAuthn.js
Normal file
44
api/server/routes/authWebAuthn.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/register',
|
||||
passport.authenticate('webauthn', { session: false }),
|
||||
(req, res) => {
|
||||
res.json(req.user);
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/register',
|
||||
passport.authenticate('webauthn', { session: false, failureRedirect: '/login' }),
|
||||
(req, res) => {
|
||||
res.json({ user: req.user });
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/login',
|
||||
passport.authenticate('webauthn', { session: false }),
|
||||
(req, res) => {
|
||||
res.json(req.user);
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
passport.authenticate('webauthn', { session: false, failureRedirect: '/login' }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const token = await setAuthTokens(req.user.id, res);
|
||||
res.status(200).json({ token, user: req.user });
|
||||
} catch (err) {
|
||||
console.error('[WebAuthn Login Callback]', err);
|
||||
res.status(500).json({ message: 'Something went wrong during login' });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -51,6 +51,7 @@ router.get('/', async function (req, res) {
|
|||
!!process.env.APPLE_TEAM_ID &&
|
||||
!!process.env.APPLE_KEY_ID &&
|
||||
!!process.env.APPLE_PRIVATE_KEY_PATH,
|
||||
passkeyLoginEnabled : !!process.env.PASSKEY_ENABLED && !!process.env.RP_ID,
|
||||
openidLoginEnabled:
|
||||
!!process.env.OPENID_CLIENT_ID &&
|
||||
!!process.env.OPENID_CLIENT_SECRET &&
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const authWebAuthn = require('./authWebAuthn');
|
||||
const assistants = require('./assistants');
|
||||
const categories = require('./categories');
|
||||
const tokenizer = require('./tokenizer');
|
||||
|
|
@ -55,5 +56,6 @@ module.exports = {
|
|||
assistants,
|
||||
categories,
|
||||
staticRoute,
|
||||
authWebAuthn,
|
||||
banner,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue