mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
feat: facebook login (#820)
* Facebook strategy * Update user_auth_system.md * Update user_auth_system.md
This commit is contained in:
parent
a569020312
commit
007d51ede1
23 changed files with 155 additions and 27 deletions
|
@ -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
|
||||
|
|
|
@ -63,6 +63,11 @@ const userSchema = mongoose.Schema(
|
|||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
facebookId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
openidId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
28
client/src/components/svg/FacebookIcon.tsx
Normal file
28
client/src/components/svg/FacebookIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: '电子邮箱',
|
||||
|
|
|
@ -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
3
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -159,6 +159,7 @@ export type TResetPassword = {
|
|||
export type TStartupConfig = {
|
||||
appTitle: string;
|
||||
googleLoginEnabled: boolean;
|
||||
facebookLoginEnabled: boolean;
|
||||
openidLoginEnabled: boolean;
|
||||
githubLoginEnabled: boolean;
|
||||
openidLabel: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue