From 747e087cf580ac45ce5ae5040d6de14a68e6cbc3 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Tue, 11 Jul 2023 23:17:58 +0200 Subject: [PATCH] Discord Login (#615) * Add files via upload * Create linode-setup.md * Create cloudflare-setup.md * Update cloudflare-setup.md * Delete 4-linode.png * Delete 3-linode.png * Add files via upload * Add files via upload * Update cloudflare-setup.md * Update linode-setup.md * Rename cloudflare-setup.md to cloudflare.md * Rename linode-setup.md to linode.md * Update mkdocs.yml * Update cloudflare.md * Update linode.md * Update README.md * Update README.md * Update linode.md sentence in Italian * v1 The frontend has been completed, along with the .env variables. However, there is an issue of infinite loading thereafter. * Fix email and remove deprecated GitHub passport * Update user_auth_system.md add How to Set Up a Github Authentication * Update .env.example Improved the comment above the GitHub client ID and secret. * Update user_auth_system.md * Update package.json * Remove unnecessary passport GitHub package * fixed conflicts fixed conflicts between Berry-13:main and danny-avila:main in api/server/index.js 45:54 * Delete e -i HEAD~2 * (WIP) Discord Login * Fix duplicate githubLoginEnabled * .env.example restore * Update user_auth_system.md Discord Login * Fix and new Feature 1. Added Discord login to .env.example. 2. Created Google, Github, and Discord icons in client\src\components\svg. 3. Added the social login option in the .env file; it fixes the ---or---. Check Discord for more information. * fix Login.tsx and Registration.tsx * Update user_auth_system.md * Update .env.example * Added OpenID Icon * quick discord icon fix * discord strategy fix * remove comment --- .env.example | 19 ++- api/models/User.js | 5 + api/server/index.js | 3 + api/server/routes/__tests__/config.spec.js | 8 ++ api/server/routes/config.js | 16 ++- api/server/routes/oauth.js | 28 +++++ api/strategies/discordStrategy.js | 57 +++++++++ api/strategies/githubStrategy.js | 2 +- client/src/components/Auth/Login.tsx | 110 +++++++----------- client/src/components/Auth/Registration.tsx | 108 +++++++---------- .../components/Auth/__tests__/Login.spec.tsx | 2 + .../Auth/__tests__/Registration.spec.tsx | 2 + client/src/components/svg/DiscordIcon.jsx | 12 ++ client/src/components/svg/GithubIcon.jsx | 12 ++ client/src/components/svg/GoogleIcon.jsx | 29 +++++ client/src/components/svg/OpenIDIcon.jsx | 11 ++ client/src/components/svg/index.ts | 6 +- docs/install/user_auth_system.md | 22 +++- package-lock.json | 17 +++ package.json | 1 + packages/data-provider/src/types.ts | 2 + 21 files changed, 328 insertions(+), 144 deletions(-) create mode 100644 api/strategies/discordStrategy.js create mode 100644 client/src/components/svg/DiscordIcon.jsx create mode 100644 client/src/components/svg/GithubIcon.jsx create mode 100644 client/src/components/svg/GoogleIcon.jsx create mode 100644 client/src/components/svg/OpenIDIcon.jsx diff --git a/.env.example b/.env.example index e1d5a57a7d..c43cf05837 100644 --- a/.env.example +++ b/.env.example @@ -187,6 +187,9 @@ MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt # Allow Public Registration ALLOW_REGISTRATION=true +# Allow Social Registration +ALLOW_SOCIAL_LOGIN=false + # JWT Secrets JWT_SECRET=secret JWT_REFRESH_SECRET=secret @@ -219,12 +222,20 @@ OPENID_IMAGE_URL= SESSION_EXPIRY=(1000 * 60 * 60 * 24) * 7 # Github: -# Get the Client ID and Secret from your Github Application +# Get the Client ID and Secret from your Discord Application +# Add your Discord Client ID and Client Secret here: + +GITHUB_CLIENT_ID=your_client_id +GITHUB_CLIENT_SECRET=your_client_secret +GITHUB_CALLBACK_URL=/oauth/github/callback # this should be the same for everyone + +# Discord: +# Get the Client ID and Secret from your Discord Application # Add your Github Client ID and Client Secret here: -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GITHUB_CALLBACK_URL=/oauth/github/callback +DISCORD_CLIENT_ID=your_client_id +DISCORD_CLIENT_SECRET=your_client_secret +DISCORD_CALLBACK_URL=/oauth/discord/callback # this should be the same for everyone ########################### # Application Domains diff --git a/api/models/User.js b/api/models/User.js index 011291c346..25703b421c 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -75,6 +75,11 @@ const userSchema = mongoose.Schema( unique: true, sparse: true }, + discordId: { + type: String, + unique: true, + sparse: true + }, plugins: { type: Array, default: [] diff --git a/api/server/index.js b/api/server/index.js index 947edfdb86..eeb762dd9b 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -45,6 +45,9 @@ config.validate(); // Validate the config if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { require('../strategies/githubStrategy'); } + if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) { + require('../strategies/discordStrategy'); + } if (process.env.OPENID_CLIENT_ID && process.env.OPENID_CLIENT_SECRET && process.env.OPENID_ISSUER && process.env.OPENID_SCOPE && process.env.OPENID_SESSION_SECRET) { diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js index 928fc2208a..c3f0ba7782 100644 --- a/api/server/routes/__tests__/config.spec.js +++ b/api/server/routes/__tests__/config.spec.js @@ -16,8 +16,11 @@ afterEach(() => { delete process.env.OPENID_AUTH_URL; delete process.env.GITHUB_CLIENT_ID; delete process.env.GITHUB_CLIENT_SECRET; + delete process.env.DISCORD_CLIENT_ID; + delete process.env.DISCORD_CLIENT_SECRET; delete process.env.DOMAIN_SERVER; delete process.env.ALLOW_REGISTRATION; + delete process.env.ALLOW_SOCIAL_LOGIN; }); //TODO: This works/passes locally but http request tests fail with 404 in CI. Need to figure out why. @@ -36,8 +39,11 @@ describe.skip('GET /', () => { process.env.OPENID_AUTH_URL= 'http://test-server.com'; process.env.GITHUB_CLIENT_ID = 'Test Github client Id'; process.env.GITHUB_CLIENT_SECRET= 'Test Github client Secret'; + process.env.DISCORD_CLIENT_ID = 'Test Discord client Id'; + process.env.DISCORD_CLIENT_SECRET= 'Test Discord client Secret'; process.env.DOMAIN_SERVER = 'http://test-server.com'; process.env.ALLOW_REGISTRATION = 'true'; + process.env.ALLOW_SOCIAL_LOGIN = 'true'; const response = await request(app).get('/'); @@ -49,8 +55,10 @@ describe.skip('GET /', () => { openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', githubLoginEnabled: true, + discordLoginEnabled: true, serverDomain: 'http://test-server.com', registrationEnabled: 'true', + socialLoginEnabled: 'true', }); }); }); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 115fffa359..bbb5906035 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -12,10 +12,24 @@ router.get('/', async function (req, res) { const openidLabel = process.env.OPENID_BUTTON_LABEL || 'Login with OpenID'; const openidImageUrl = process.env.OPENID_IMAGE_URL; const githubLoginEnabled = !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET; + const discordLoginEnabled = !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET; const serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080'; const registrationEnabled = process.env.ALLOW_REGISTRATION === 'true'; + const socialLoginEnabled = process.env.ALLOW_SOCIAL_LOGIN === 'true'; + + return res.status(200).send({ + appTitle, + googleLoginEnabled, + openidLoginEnabled, + openidLabel, + openidImageUrl, + githubLoginEnabled, + discordLoginEnabled, + serverDomain, + registrationEnabled, + socialLoginEnabled + }); - return res.status(200).send({appTitle, googleLoginEnabled, openidLoginEnabled, openidLabel, openidImageUrl, githubLoginEnabled, serverDomain, registrationEnabled}); } catch (err) { console.error(err); return res.status(500).send({error: err.message}); diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js index 8289232e26..d57ceb0516 100644 --- a/api/server/routes/oauth.js +++ b/api/server/routes/oauth.js @@ -115,4 +115,32 @@ router.get( } ); + +router.get( + '/discord', + passport.authenticate('discord', { + scope: ['identify', 'email'], + session: false + }) +); + +router.get( + '/discord/callback', + passport.authenticate('discord', { + failureRedirect: `${domains.client}/login`, + failureMessage: true, + session: false, + scope: ['identify', 'email'] + }), + (req, res) => { + const token = req.user.generateToken(); + res.cookie('token', token, { + expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)), + httpOnly: false, + secure: isProduction + }); + res.redirect(domains.client); + } +); + module.exports = router; diff --git a/api/strategies/discordStrategy.js b/api/strategies/discordStrategy.js new file mode 100644 index 0000000000..428279ebfb --- /dev/null +++ b/api/strategies/discordStrategy.js @@ -0,0 +1,57 @@ +const passport = require('passport'); +const { Strategy: DiscordStrategy } = require('passport-discord'); +const User = require('../models/User'); +const config = require('../../config/loader'); +const domains = config.domains; + +const discordLogin = new DiscordStrategy( + { + clientID: process.env.DISCORD_CLIENT_ID, + clientSecret: process.env.DISCORD_CLIENT_SECRET, + callbackURL: `${domains.server}${process.env.DISCORD_CALLBACK_URL}`, + scope: ['identify', 'email'] // Request scopes + }, + async (accessToken, refreshToken, profile, cb) => { + try { + const discordId = profile.id; + const email = profile.email; + + const existingUser = await User.findOne({ discordId }); + if (existingUser) { + return cb(null, existingUser); + } + + const userWithEmail = await User.findOne({ email }); + if (userWithEmail) { + userWithEmail.discordId = discordId; + await userWithEmail.save(); + return cb(null, userWithEmail); + } + + let avatarURL; + if (profile.avatar) { + const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'; + avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; + } else { + const defaultAvatarNum = Number(profile.discriminator) % 5; + avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; + } + + const newUser = await User.create({ + provider: 'discord', + discordId, + username: profile.username, + email, + name: profile.global_name, + avatar: avatarURL + }); + + cb(null, newUser); + } catch (err) { + console.error(err); + cb(err); + } + } +); + +passport.use(discordLogin); diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js index d377b61833..57578a26bb 100644 --- a/api/strategies/githubStrategy.js +++ b/api/strategies/githubStrategy.js @@ -33,7 +33,7 @@ const githubLogin = new GitHubStrategy( email, emailVerified: profile.emails[0].verified, name: profile.displayName, - avatar: profile.photos[0].value + avatar: profile.photos[0].value }).save(); cb(null, newUser); diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx index 95a08c7e00..2999c6a220 100644 --- a/client/src/components/Auth/Login.tsx +++ b/client/src/components/Auth/Login.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import LoginForm from './LoginForm'; import { useAuthContext } from '~/hooks/AuthContext'; import { useNavigate } from 'react-router-dom'; @@ -7,6 +7,7 @@ import { useRecoilValue } from 'recoil'; import store from '~/store'; import { localize } from '~/localization/Translation'; import { useGetStartupConfig } from '@librechat/data-provider'; +import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components' function Login() { const { login, error, isAuthenticated } = useAuthContext(); @@ -21,6 +22,7 @@ function Login() { navigate('/chat/new'); } }, [isAuthenticated, navigate]); + return (
{localize(lang, 'com_auth_google_login')}
{localize(lang, 'com_auth_google_login')}