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 (
@@ -43,46 +45,26 @@ function Login() {

)} - {startupConfig?.googleLoginEnabled && ( + {startupConfig?.socialLoginEnabled && ( +
+
Or
+
+ )} + {startupConfig?.googleLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
-
Or
-
+
- - - - - - + href={`${startupConfig.serverDomain}/oauth/google`}> +

{localize(lang, 'com_auth_google_login')}

)} - {startupConfig?.openidLoginEnabled && ( + {startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && ( <>
) : ( - - - + )}

{startupConfig.openidLabel}

)} - {startupConfig?.githubLoginEnabled && ( - <> -
-
Or
-
-
- - - - -

Login with GitHub

-
-
- -)} + {startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && ( + <> +
+ + +

Login with Github

+
+
+ + )} + {startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && ( + <> + +
+ + +

Login with Discord

+
+
+ + )}
); -} - +}; export default Login; diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx index 29a9ec353f..83ef456330 100644 --- a/client/src/components/Auth/Registration.tsx +++ b/client/src/components/Auth/Registration.tsx @@ -9,6 +9,7 @@ import { TRegisterUser, useGetStartupConfig } from '@librechat/data-provider'; +import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components' function Registration() { const navigate = useNavigate(); @@ -283,51 +284,28 @@ function Registration() { {localize(lang, 'com_auth_login')}

- {startupConfig?.googleLoginEnabled && ( + {startupConfig?.socialLoginEnabled && ( +
+
Or
+
+ )} + {startupConfig?.googleLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
-
Or
-
- - - - - - + href={`${startupConfig.serverDomain}/oauth/google`}> +

{localize(lang, 'com_auth_google_login')}

)} - {startupConfig?.openidLoginEnabled && ( + {startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
-
Or
-
+
) : ( - - - + )}

{startupConfig.openidLabel}

)} - {startupConfig?.githubLoginEnabled && ( - <> -
-
Or
-
-
- - - - -

Login with GitHub

-
-
- -)} + {startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && ( + <> +
+ + +

Login with Github

+
+
+ + )} + {startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && ( + <> + +
+ + +

Login with Discord

+
+
+ + )} ); diff --git a/client/src/components/Auth/__tests__/Login.spec.tsx b/client/src/components/Auth/__tests__/Login.spec.tsx index 6c6d4c9edd..b142367546 100644 --- a/client/src/components/Auth/__tests__/Login.spec.tsx +++ b/client/src/components/Auth/__tests__/Login.spec.tsx @@ -27,7 +27,9 @@ const setup = ({ openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', githubLoginEnabled: true, + discordLoginEnabled: true, registrationEnabled: true, + socialLoginEnabled: true, serverDomain: 'mock-server' } } diff --git a/client/src/components/Auth/__tests__/Registration.spec.tsx b/client/src/components/Auth/__tests__/Registration.spec.tsx index b720116fd7..f0bc8fa51f 100644 --- a/client/src/components/Auth/__tests__/Registration.spec.tsx +++ b/client/src/components/Auth/__tests__/Registration.spec.tsx @@ -27,7 +27,9 @@ const setup = ({ openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', githubLoginEnabled: true, + discordLoginEnabled: true, registrationEnabled: true, + socialLoginEnabled: true, serverDomain: 'mock-server' } } diff --git a/client/src/components/svg/DiscordIcon.jsx b/client/src/components/svg/DiscordIcon.jsx new file mode 100644 index 0000000000..1c1732f63b --- /dev/null +++ b/client/src/components/svg/DiscordIcon.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export default function DiscordIcon() { + return ( + + + + + ); +} diff --git a/client/src/components/svg/GithubIcon.jsx b/client/src/components/svg/GithubIcon.jsx new file mode 100644 index 0000000000..e3a83cc73f --- /dev/null +++ b/client/src/components/svg/GithubIcon.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export default function GithubIcon() { + return ( + + + + ); +} diff --git a/client/src/components/svg/GoogleIcon.jsx b/client/src/components/svg/GoogleIcon.jsx new file mode 100644 index 0000000000..ef7ec6c8c9 --- /dev/null +++ b/client/src/components/svg/GoogleIcon.jsx @@ -0,0 +1,29 @@ +import React from 'react'; + +export default function GoogleIcon() { + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/client/src/components/svg/OpenIDIcon.jsx b/client/src/components/svg/OpenIDIcon.jsx new file mode 100644 index 0000000000..ee29683eef --- /dev/null +++ b/client/src/components/svg/OpenIDIcon.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function OpenIDIcon() { + return ( + + + + ); +} diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index a5361ff25f..1d6c9ca174 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -4,4 +4,8 @@ export { default as BingIcon } from './BingIcon'; export { default as CogIcon } from './CogIcon'; export { default as Spinner } from './Spinner'; export { default as MessagesSquared } from './MessagesSquared'; -export { default as StopGeneratingIcon } from './StopGeneratingIcon'; \ No newline at end of file +export { default as StopGeneratingIcon } from './StopGeneratingIcon'; +export { default as GoogleIcon } from './GoogleIcon'; +export { default as OpenIDIcon } from './OpenIDIcon'; +export { default as GithubIcon } from './GithubIcon'; +export { default as DiscordIcon } from './DiscordIcon'; \ No newline at end of file diff --git a/docs/install/user_auth_system.md b/docs/install/user_auth_system.md index 12298c188b..5733ec0b01 100644 --- a/docs/install/user_auth_system.md +++ b/docs/install/user_auth_system.md @@ -77,9 +77,29 @@ OPENID_CALLBACK_URL=/oauth/openid/callback 5. Remove the Active checkbox in the Webhook section 6. Save changes and generate a Client Secret 7. In the Permissions & events tab select, open the Account Permissions and set Email addresses to Read-only -8. Put the Client ID and Client Secret in the .env file +8. Put the Client ID and Client Secret in the .env file: +``` +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 +``` 9. Save the .env file +--- +## How to Set Up Discord Authentication + +1. Go to [Discord Developer Portal](https://discord.com/developers) +2. Create a new Application and give it a name +4. In the OAuth2 general settings add a redirect URL and set it as "[Your DOMAIN_CLIENT](https://github.com/danny-avila/LibreChat/blob/main/.env.example#L219)/oauth/discord/callback" (example: http://localhost:3080/oauth/discord/callback) +5. in the Default Authorization Link set applications.commands +6. Save changes and reset the Client Secret +7. Put the Client ID and Client Secret in the .env file: +``` +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 +``` +8. Save the .env file --- ## **Email and Password Reset** diff --git a/package-lock.json b/package-lock.json index 8df0a52c0b..209faa2056 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "dependencies": { "axios": "^1.4.0", "passport": "^0.6.0", + "passport-discord": "^0.1.4", "passport-github2": "^0.1.12" }, "devDependencies": { @@ -20404,6 +20405,14 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-discord": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/passport-discord/-/passport-discord-0.1.4.tgz", + "integrity": "sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==", + "dependencies": { + "passport-oauth2": "^1.5.0" + } + }, "node_modules/passport-facebook": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz", @@ -41105,6 +41114,14 @@ "utils-merge": "^1.0.1" } }, + "passport-discord": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/passport-discord/-/passport-discord-0.1.4.tgz", + "integrity": "sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==", + "requires": { + "passport-oauth2": "^1.5.0" + } + }, "passport-facebook": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz", diff --git a/package.json b/package.json index 947529cc51..fec617b50a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "axios": "^1.4.0", "passport": "^0.6.0", + "passport-discord": "^0.1.4", "passport-github2": "^0.1.12" }, "devDependencies": { diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 7faa1e207a..be2abe9f32 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -252,8 +252,10 @@ export type TStartupConfig = { githubLoginEnabled: boolean; openidLabel: string; openidImageUrl: string; + discordLoginEnabled: boolean; serverDomain: string; registrationEnabled: boolean; + socialLoginEnabled: boolean; } export type TRefreshTokenResponse = {