From 007d51ede1f9648458e93ebc7acdeefed59f9602 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date: Fri, 25 Aug 2023 02:10:48 +0200
Subject: [PATCH] feat: facebook login (#820)
* Facebook strategy
* Update user_auth_system.md
* Update user_auth_system.md
---
.env.example | 7 +++++
api/models/User.js | 5 ++++
api/server/routes/__tests__/config.spec.js | 5 ++++
api/server/routes/config.js | 3 ++
api/server/routes/oauth.js | 6 ++--
api/strategies/facebookStrategy.js | 30 ++++++-------------
client/src/components/Auth/Login.tsx | 16 +++++++++-
client/src/components/Auth/Registration.tsx | 16 +++++++++-
.../components/Auth/__tests__/Login.spec.tsx | 16 ++++++++++
.../Auth/__tests__/Registration.spec.tsx | 16 ++++++++++
client/src/components/svg/FacebookIcon.tsx | 28 +++++++++++++++++
client/src/components/svg/index.ts | 1 +
client/src/localization/languages/Br.tsx | 1 +
client/src/localization/languages/De.tsx | 1 +
client/src/localization/languages/Eng.tsx | 1 +
client/src/localization/languages/Es.tsx | 1 +
client/src/localization/languages/Fr.tsx | 1 +
client/src/localization/languages/It.tsx | 1 +
client/src/localization/languages/Zh.tsx | 1 +
docs/install/user_auth_system.md | 19 ++++++++++++
package-lock.json | 3 +-
package.json | 3 +-
packages/data-provider/src/types.ts | 1 +
23 files changed, 155 insertions(+), 27 deletions(-)
create mode 100644 client/src/components/svg/FacebookIcon.tsx
diff --git a/.env.example b/.env.example
index ee50e58d6c..64c73b7690 100644
--- a/.env.example
+++ b/.env.example
@@ -229,6 +229,13 @@ GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/oauth/google/callback
+# Facebook:
+# Add your Facebook Client ID and Secret here, you must register an app with Facebook to get these values
+# https://developers.facebook.com/
+FACEBOOK_CLIENT_ID=
+FACEBOOK_CLIENT_SECRET=
+FACEBOOK_CALLBACK_URL=/oauth/facebook/callback
+
# OpenID:
# See OpenID provider to get the below values
# Create random string for OPENID_SESSION_SECRET
diff --git a/api/models/User.js b/api/models/User.js
index bcbc141f1e..5e1d035def 100644
--- a/api/models/User.js
+++ b/api/models/User.js
@@ -63,6 +63,11 @@ const userSchema = mongoose.Schema(
unique: true,
sparse: true,
},
+ facebookId: {
+ type: String,
+ unique: true,
+ sparse: true,
+ },
openidId: {
type: String,
unique: true,
diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js
index 87ce05af01..9194d458f0 100644
--- a/api/server/routes/__tests__/config.spec.js
+++ b/api/server/routes/__tests__/config.spec.js
@@ -8,6 +8,8 @@ afterEach(() => {
delete process.env.APP_TITLE;
delete process.env.GOOGLE_CLIENT_ID;
delete process.env.GOOGLE_CLIENT_SECRET;
+ delete process.env.FACEBOOK_CLIENT_ID;
+ delete process.env.FACEBOOK_CLIENT_SECRET;
delete process.env.OPENID_CLIENT_ID;
delete process.env.OPENID_CLIENT_SECRET;
delete process.env.OPENID_ISSUER;
@@ -31,6 +33,8 @@ describe.skip('GET /', () => {
process.env.APP_TITLE = 'Test Title';
process.env.GOOGLE_CLIENT_ID = 'Test Google Client Id';
process.env.GOOGLE_CLIENT_SECRET = 'Test Google Client Secret';
+ process.env.FACEBOOK_CLIENT_ID = 'Test Facebook Client Id';
+ process.env.FACEBOOK_CLIENT_SECRET = 'Test Facebook Client Secret';
process.env.OPENID_CLIENT_ID = 'Test OpenID Id';
process.env.OPENID_CLIENT_SECRET = 'Test OpenID Secret';
process.env.OPENID_ISSUER = 'Test OpenID Issuer';
@@ -51,6 +55,7 @@ describe.skip('GET /', () => {
expect(response.body).toEqual({
appTitle: 'Test Title',
googleLoginEnabled: true,
+ facebookLoginEnabled: true,
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
diff --git a/api/server/routes/config.js b/api/server/routes/config.js
index f86abb1778..2d3433af72 100644
--- a/api/server/routes/config.js
+++ b/api/server/routes/config.js
@@ -5,6 +5,8 @@ router.get('/', async function (req, res) {
try {
const appTitle = process.env.APP_TITLE || 'LibreChat';
const googleLoginEnabled = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
+ const facebookLoginEnabled =
+ !!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET;
const openidLoginEnabled =
!!process.env.OPENID_CLIENT_ID &&
!!process.env.OPENID_CLIENT_SECRET &&
@@ -27,6 +29,7 @@ router.get('/', async function (req, res) {
return res.status(200).send({
appTitle,
googleLoginEnabled,
+ facebookLoginEnabled,
openidLoginEnabled,
openidLabel,
openidImageUrl,
diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js
index bd82f4cb4e..ea5837427e 100644
--- a/api/server/routes/oauth.js
+++ b/api/server/routes/oauth.js
@@ -38,7 +38,8 @@ router.get(
router.get(
'/facebook',
passport.authenticate('facebook', {
- scope: ['public_profile', 'email'],
+ scope: ['public_profile'],
+ profileFields: ['id', 'email', 'name'],
session: false,
}),
);
@@ -49,7 +50,8 @@ router.get(
failureRedirect: `${domains.client}/login`,
failureMessage: true,
session: false,
- scope: ['public_profile', 'email'],
+ scope: ['public_profile'],
+ profileFields: ['id', 'email', 'name'],
}),
(req, res) => {
const token = req.user.generateToken();
diff --git a/api/strategies/facebookStrategy.js b/api/strategies/facebookStrategy.js
index 92ad853645..41d30754bd 100644
--- a/api/strategies/facebookStrategy.js
+++ b/api/strategies/facebookStrategy.js
@@ -5,8 +5,7 @@ const domains = config.domains;
const facebookLogin = async (accessToken, refreshToken, profile, cb) => {
try {
- console.log('facebookLogin => profile', profile);
- const email = profile.emails[0].value;
+ const email = profile.emails[0]?.value;
const facebookId = profile.id;
const oldUser = await User.findOne({
email,
@@ -15,17 +14,17 @@ const facebookLogin = async (accessToken, refreshToken, profile, cb) => {
process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true';
if (oldUser) {
- oldUser.avatar = profile.photos[0].value;
+ oldUser.avatar = profile.photo;
await oldUser.save();
return cb(null, oldUser);
} else if (ALLOW_SOCIAL_REGISTRATION) {
const newUser = await new User({
provider: 'facebook',
facebookId,
- username: profile.name.givenName + profile.name.familyName,
+ username: profile.displayName,
email,
- name: profile.displayName,
- avatar: profile.photos[0].value,
+ name: profile.name?.givenName + ' ' + profile.name?.familyName,
+ avatar: profile.photos[0]?.value,
}).save();
return cb(null, newUser);
@@ -43,23 +42,12 @@ const facebookLogin = async (accessToken, refreshToken, profile, cb) => {
module.exports = () =>
new FacebookStrategy(
{
- clientID: process.env.FACEBOOK_APP_ID,
- clientSecret: process.env.FACEBOOK_SECRET,
+ clientID: process.env.FACEBOOK_CLIENT_ID,
+ clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
callbackURL: `${domains.server}${process.env.FACEBOOK_CALLBACK_URL}`,
proxy: true,
- // profileFields: [
- // 'id',
- // 'email',
- // 'gender',
- // 'profileUrl',
- // 'displayName',
- // 'locale',
- // 'name',
- // 'timezone',
- // 'updated_time',
- // 'verified',
- // 'picture.type(large)'
- // ]
+ scope: ['public_profile'],
+ profileFields: ['id', 'email', 'name'],
},
facebookLogin,
);
diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx
index 65a119de45..025051da5c 100644
--- a/client/src/components/Auth/Login.tsx
+++ b/client/src/components/Auth/Login.tsx
@@ -4,7 +4,7 @@ import { useAuthContext } from '~/hooks/AuthContext';
import { useNavigate } from 'react-router-dom';
import { useLocalize } from '~/hooks';
import { useGetStartupConfig } from 'librechat-data-provider';
-import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
+import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
function Login() {
const { login, error, isAuthenticated } = useAuthContext();
@@ -65,6 +65,20 @@ function Login() {
>
)}
+ {startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
+ <>
+
+ >
+ )}
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx
index cd4a4399e1..397e769f49 100644
--- a/client/src/components/Auth/Registration.tsx
+++ b/client/src/components/Auth/Registration.tsx
@@ -7,7 +7,7 @@ import {
TRegisterUser,
useGetStartupConfig,
} from 'librechat-data-provider';
-import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
+import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
function Registration() {
const navigate = useNavigate();
@@ -308,6 +308,20 @@ function Registration() {
>
)}
+ {startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
+ <>
+
+ >
+ )}
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
diff --git a/client/src/components/Auth/__tests__/Login.spec.tsx b/client/src/components/Auth/__tests__/Login.spec.tsx
index 0be0f47a06..6b8b66f1ef 100644
--- a/client/src/components/Auth/__tests__/Login.spec.tsx
+++ b/client/src/components/Auth/__tests__/Login.spec.tsx
@@ -23,6 +23,7 @@ const setup = ({
isError: false,
data: {
googleLoginEnabled: true,
+ facebookLoginEnabled: true,
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
@@ -67,6 +68,21 @@ test('renders login form', () => {
'href',
'mock-server/oauth/google',
);
+ expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
+ expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
+ 'href',
+ 'mock-server/oauth/facebook',
+ );
+ expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
+ expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
+ 'href',
+ 'mock-server/oauth/github',
+ );
+ expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
+ expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
+ 'href',
+ 'mock-server/oauth/discord',
+ );
});
test('calls loginUser.mutate on login', async () => {
diff --git a/client/src/components/Auth/__tests__/Registration.spec.tsx b/client/src/components/Auth/__tests__/Registration.spec.tsx
index 7c7bbd230d..0c40b29c0c 100644
--- a/client/src/components/Auth/__tests__/Registration.spec.tsx
+++ b/client/src/components/Auth/__tests__/Registration.spec.tsx
@@ -24,6 +24,7 @@ const setup = ({
isError: false,
data: {
googleLoginEnabled: true,
+ facebookLoginEnabled: true,
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
@@ -75,6 +76,21 @@ test('renders registration form', () => {
'href',
'mock-server/oauth/google',
);
+ expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
+ expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
+ 'href',
+ 'mock-server/oauth/facebook',
+ );
+ expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
+ expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
+ 'href',
+ 'mock-server/oauth/github',
+ );
+ expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
+ expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
+ 'href',
+ 'mock-server/oauth/discord',
+ );
});
// eslint-disable-next-line jest/no-commented-out-tests
diff --git a/client/src/components/svg/FacebookIcon.tsx b/client/src/components/svg/FacebookIcon.tsx
new file mode 100644
index 0000000000..131c751ea0
--- /dev/null
+++ b/client/src/components/svg/FacebookIcon.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+export default function FacebookIcon() {
+ return (
+
+ );
+}
diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts
index bcf9e04383..2f4c2289c6 100644
--- a/client/src/components/svg/index.ts
+++ b/client/src/components/svg/index.ts
@@ -13,6 +13,7 @@ export { default as StopGeneratingIcon } from './StopGeneratingIcon';
export { default as RegenerateIcon } from './RegenerateIcon';
export { default as ContinueIcon } from './ContinueIcon';
export { default as GoogleIcon } from './GoogleIcon';
+export { default as FacebookIcon } from './FacebookIcon';
export { default as OpenIDIcon } from './OpenIDIcon';
export { default as GithubIcon } from './GithubIcon';
export { default as DiscordIcon } from './DiscordIcon';
diff --git a/client/src/localization/languages/Br.tsx b/client/src/localization/languages/Br.tsx
index 386672afce..63131461ea 100644
--- a/client/src/localization/languages/Br.tsx
+++ b/client/src/localization/languages/Br.tsx
@@ -34,6 +34,7 @@ export default {
com_auth_sign_up: 'Cadastre-se',
com_auth_sign_in: 'Entrar',
com_auth_google_login: 'Entrar com o Google',
+ com_auth_facebook_login: 'Entrar com o Facebook',
com_auth_github_login: 'Entrar com o Github',
com_auth_discord_login: 'Entrar com o Discord',
com_auth_email: 'Email',
diff --git a/client/src/localization/languages/De.tsx b/client/src/localization/languages/De.tsx
index 96f563d6e5..6f13beeb72 100644
--- a/client/src/localization/languages/De.tsx
+++ b/client/src/localization/languages/De.tsx
@@ -34,6 +34,7 @@ export default {
com_auth_sign_up: 'Registrieren',
com_auth_sign_in: 'Anmelden',
com_auth_google_login: 'Anmelden mit Google',
+ com_auth_facebook_login: 'Anmelden mit Facebook',
com_auth_github_login: 'Anmelden mit Github',
com_auth_discord_login: 'Anmelden mit Discord',
com_auth_email: 'E-Mail',
diff --git a/client/src/localization/languages/Eng.tsx b/client/src/localization/languages/Eng.tsx
index 1288824028..89886d9d63 100644
--- a/client/src/localization/languages/Eng.tsx
+++ b/client/src/localization/languages/Eng.tsx
@@ -34,6 +34,7 @@ export default {
com_auth_sign_up: 'Sign up',
com_auth_sign_in: 'Sign in',
com_auth_google_login: 'Login with Google',
+ com_auth_facebook_login: 'Login with Facebook',
com_auth_github_login: 'Login with Github',
com_auth_discord_login: 'Login with Discord',
com_auth_email: 'Email',
diff --git a/client/src/localization/languages/Es.tsx b/client/src/localization/languages/Es.tsx
index 58911b3100..9442502a13 100644
--- a/client/src/localization/languages/Es.tsx
+++ b/client/src/localization/languages/Es.tsx
@@ -35,6 +35,7 @@ export default {
com_auth_sign_up: 'Registrarse',
com_auth_sign_in: 'Iniciar sesión',
com_auth_google_login: 'Iniciar sesión con Google',
+ com_auth_facebook_login: 'Iniciar sesión con Facebook',
com_auth_github_login: 'Iniciar sesión con GitHub',
com_auth_discord_login: 'Iniciar sesión con Discord',
com_auth_email: 'Email',
diff --git a/client/src/localization/languages/Fr.tsx b/client/src/localization/languages/Fr.tsx
index 0a4c0c9313..eb7c95da17 100644
--- a/client/src/localization/languages/Fr.tsx
+++ b/client/src/localization/languages/Fr.tsx
@@ -35,6 +35,7 @@ export default {
com_auth_sign_up: 'S\'inscrire',
com_auth_sign_in: 'Se connecter',
com_auth_google_login: 'Se connecter avec Google',
+ com_auth_facebook_login: 'Se connecter avec Facebook',
com_auth_github_login: 'Se connecter avec Github',
com_auth_discord_login: 'Se connecter avec Discord',
com_auth_email: 'Courriel',
diff --git a/client/src/localization/languages/It.tsx b/client/src/localization/languages/It.tsx
index b37868851f..e9f4ec8b08 100644
--- a/client/src/localization/languages/It.tsx
+++ b/client/src/localization/languages/It.tsx
@@ -35,6 +35,7 @@ export default {
com_auth_sign_up: 'Registrati',
com_auth_sign_in: 'Accedi',
com_auth_google_login: 'Accedi con Google',
+ com_auth_facebook_login: 'Accedi con Facebook',
com_auth_github_login: 'Accedi con Github',
com_auth_discord_login: 'Accedi con Discord',
com_auth_email: 'Email',
diff --git a/client/src/localization/languages/Zh.tsx b/client/src/localization/languages/Zh.tsx
index 6a8c2460e0..46911e8561 100644
--- a/client/src/localization/languages/Zh.tsx
+++ b/client/src/localization/languages/Zh.tsx
@@ -31,6 +31,7 @@ export default {
com_auth_sign_up: '注册',
com_auth_sign_in: '登录',
com_auth_google_login: '谷歌登录',
+ com_auth_facebook_login: 'Facebook登录',
com_auth_github_login: 'Github登录',
com_auth_discord_login: 'Discord登录',
com_auth_email: '电子邮箱',
diff --git a/docs/install/user_auth_system.md b/docs/install/user_auth_system.md
index b5f7075390..3afe44b078 100644
--- a/docs/install/user_auth_system.md
+++ b/docs/install/user_auth_system.md
@@ -45,6 +45,24 @@ To enable Google login, you must create an application in the [Google Cloud Cons
---
+## Facebook Authentication
+### (It only works with a domain, not with localhost)
+
+1. Go to [Facebook Developer Portal](https://developers.facebook.com/)
+2. Create a new Application and give it a name
+4. In the Dashboard tab select product and select "Facebook login", then tap on "Configure" and "Settings". Male sure "OAuth client access", "Web OAuth access", "Apply HTTPS" and "Use limited mode for redirect URIs" are **enabled**
+5. In the Valid OAuth Redirect URIs add "your-domain/oauth/facebook/callback" (example: http://example.com/oauth/facebook/callback)
+6. Save changes and in the "settings" tab, reset the Client Secret
+7. Put the Client ID and Client Secret in the .env file:
+```bash
+FACEBOOK_CLIENT_ID=your_client_id
+FACEBOOK_CLIENT_SECRET=your_client_secret
+FACEBOOK_CALLBACK_URL=/oauth/facebook/callback # this should be the same for everyone
+```
+8. Save the .env file
+
+---
+
## OpenID Authentication with Azure AD
1. Go to the [Azure Portal](https://portal.azure.com/) and sign in with your account.
@@ -132,6 +150,7 @@ DISCORD_CLIENT_SECRET=your_client_secret
DISCORD_CALLBACK_URL=/oauth/discord/callback # this should be the same for everyone
```
8. Save the .env file
+
---
## **Email and Password Reset**
diff --git a/package-lock.json b/package-lock.json
index 5760a96a59..568fc29d71 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,8 @@
"packages/*"
],
"dependencies": {
- "axios": "^1.4.0"
+ "axios": "^1.4.0",
+ "passport-facebook": "^3.0.0"
},
"devDependencies": {
"@playwright/test": "^1.32.1",
diff --git a/package.json b/package.json
index 17453da225..e8313b10b9 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,8 @@
},
"homepage": "https://github.com/danny-avila/LibreChat#readme",
"dependencies": {
- "axios": "^1.4.0"
+ "axios": "^1.4.0",
+ "passport-facebook": "^3.0.0"
},
"devDependencies": {
"@playwright/test": "^1.32.1",
diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts
index ecf8adbd88..f3b9286e1a 100644
--- a/packages/data-provider/src/types.ts
+++ b/packages/data-provider/src/types.ts
@@ -159,6 +159,7 @@ export type TResetPassword = {
export type TStartupConfig = {
appTitle: string;
googleLoginEnabled: boolean;
+ facebookLoginEnabled: boolean;
openidLoginEnabled: boolean;
githubLoginEnabled: boolean;
openidLabel: string;