feat: facebook login (#820)

* Facebook strategy

* Update user_auth_system.md

* Update user_auth_system.md
This commit is contained in:
Marco Beretta 2023-08-25 02:10:48 +02:00 committed by GitHub
parent a569020312
commit 007d51ede1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 155 additions and 27 deletions

View file

@ -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

View file

@ -63,6 +63,11 @@ const userSchema = mongoose.Schema(
unique: true,
sparse: true,
},
facebookId: {
type: String,
unique: true,
sparse: true,
},
openidId: {
type: String,
unique: true,

View file

@ -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',

View file

@ -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,

View file

@ -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();

View file

@ -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,
);

View file

@ -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() {
</div>
</>
)}
{startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with Facebook"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/facebook`}
>
<FacebookIcon />
<p>{localize('com_auth_facebook_login')}</p>
</a>
</div>
</>
)}
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">

View file

@ -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() {
</div>
</>
)}
{startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with Facebook"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/facebook`}
>
<FacebookIcon />
<p>{localize('com_auth_facebook_login')}</p>
</a>
</div>
</>
)}
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">

View file

@ -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 () => {

View file

@ -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

View file

@ -0,0 +1,28 @@
import React from 'react';
export default function FacebookIcon() {
return (
<svg viewBox="0 0 40 40" width="25" height="25">
<linearGradient
id="a"
x1={-277.375}
x2={-277.375}
y1={406.602}
y2={407.573}
gradientTransform="matrix(40 0 0 -39.7778 11115.001 16212.334)"
gradientUnits="userSpaceOnUse"
>
<stop offset={0} stopColor="#0062e0" />
<stop offset={1} stopColor="#19afff" />
</linearGradient>
<path
fill="url(#a)"
d="M16.7 39.8C7.2 38.1 0 29.9 0 20 0 9 9 0 20 0s20 9 20 20c0 9.9-7.2 18.1-16.7 19.8l-1.1-.9h-4.4l-1.1.9z"
/>
<path
fill="#fff"
d="m27.8 25.6.9-5.6h-5.3v-3.9c0-1.6.6-2.8 3-2.8H29V8.2c-1.4-.2-3-.4-4.4-.4-4.6 0-7.8 2.8-7.8 7.8V20h-5v5.6h5v14.1c1.1.2 2.2.3 3.3.3 1.1 0 2.2-.1 3.3-.3V25.6h4.4z"
/>
</svg>
);
}

View file

@ -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';

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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: '电子邮箱',

View file

@ -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**

3
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -159,6 +159,7 @@ export type TResetPassword = {
export type TStartupConfig = {
appTitle: string;
googleLoginEnabled: boolean;
facebookLoginEnabled: boolean;
openidLoginEnabled: boolean;
githubLoginEnabled: boolean;
openidLabel: string;