GIthub Login (#578)

* 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
This commit is contained in:
Marco Beretta 2023-07-04 21:23:42 +02:00 committed by GitHub
parent 8819e83d2c
commit d0078d478d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 199 additions and 4 deletions

View file

@ -218,6 +218,14 @@ OPENID_IMAGE_URL=
# Delay is in millisecond e.g. 7 days is 1000*60*60*24*7
SESSION_EXPIRY=(1000 * 60 * 60 * 24) * 7
# Github:
# Get the Client ID and Secret from your Github Application
# Add your Github Client ID and Client Secret here:
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=/oauth/github/callback
###########################
# Application Domains
###########################

View file

@ -70,6 +70,11 @@ const userSchema = mongoose.Schema(
unique: true,
sparse: true
},
githubId: {
type: String,
unique: true,
sparse: true
},
plugins: {
type: Array,
default: []

View file

@ -42,6 +42,9 @@ config.validate(); // Validate the config
if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) {
require('../strategies/facebookStrategy');
}
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
require('../strategies/githubStrategy');
}
if (process.env.OPENID_CLIENT_ID && process.env.OPENID_CLIENT_SECRET &&
process.env.OPENID_ISSUER && process.env.OPENID_SCOPE &&
process.env.OPENID_SESSION_SECRET) {

View file

@ -14,6 +14,8 @@ afterEach(() => {
delete process.env.OPENID_SESSION_SECRET;
delete process.env.OPENID_BUTTON_LABEL;
delete process.env.OPENID_AUTH_URL;
delete process.env.GITHUB_CLIENT_ID;
delete process.env.GITHUB_CLIENT_SECRET;
delete process.env.DOMAIN_SERVER;
delete process.env.ALLOW_REGISTRATION;
});
@ -32,6 +34,8 @@ describe.skip('GET /', () => {
process.env.OPENID_SESSION_SECRET= 'Test Secret';
process.env.OPENID_BUTTON_LABEL= 'Test OpenID';
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.DOMAIN_SERVER = 'http://test-server.com';
process.env.ALLOW_REGISTRATION = 'true';
@ -44,6 +48,7 @@ describe.skip('GET /', () => {
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
githubLoginEnabled: true,
serverDomain: 'http://test-server.com',
registrationEnabled: 'true',
});

View file

@ -11,10 +11,11 @@ router.get('/', async function (req, res) {
&& !!process.env.OPENID_SESSION_SECRET;
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 serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080';
const registrationEnabled = process.env.ALLOW_REGISTRATION === 'true';
return res.status(200).send({appTitle, googleLoginEnabled, openidLoginEnabled, openidLabel, openidImageUrl, serverDomain, registrationEnabled});
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});

View file

@ -87,4 +87,32 @@ router.get(
}
);
router.get(
'/github',
passport.authenticate('github', {
scope: ['user:email', 'read:user'],
session: false
})
);
router.get(
'/github/callback',
passport.authenticate('github', {
failureRedirect: `${domains.client}/login`,
failureMessage: true,
session: false,
scope: ['user:email', 'read:user']
}),
(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;

View file

@ -0,0 +1,47 @@
const passport = require('passport');
const { Strategy: GitHubStrategy } = require('passport-github2');
const config = require('../../config/loader');
const domains = config.domains;
const User = require('../models/User');
// GitHub strategy
const githubLogin = new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: `${domains.server}${process.env.GITHUB_CALLBACK_URL}`,
proxy: false,
scope: ['user:email'] // Request email scope
},
async (accessToken, refreshToken, profile, cb) => {
try {
let email;
if (profile.emails && profile.emails.length > 0) {
email = profile.emails[0].value;
}
const oldUser = await User.findOne({ email });
if (oldUser) {
return cb(null, oldUser);
}
const newUser = await new User({
provider: 'github',
githubId: profile.id,
username: profile.username,
email,
emailVerified: profile.emails[0].verified,
name: profile.displayName,
avatar: profile.photos[0].value
}).save();
cb(null, newUser);
} catch (err) {
console.error(err);
cb(err);
}
}
);
passport.use(githubLogin);

View file

@ -102,9 +102,38 @@ function Login() {
</div>
</>
)}
{startupConfig?.githubLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<div className="absolute bg-white px-3 text-xs">Or</div>
</div>
<div className="mt-4 flex gap-x-2">
<a
aria-label="Login with GitHub"
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/github`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
id="github"
className="h-5 w-5"
>
<path
fill="currentColor"
d="M12 0a12 12 0 0 0-3.84 23.399c.608.112.832-.256.832-.576v-2.015c-3.395.736-4.115-1.632-4.115-1.632a3.241 3.241 0 0 0-1.359-1.792c-1.104-.736.064-.736.064-.736a2.566 2.566 0 0 1 1.824 1.216a2.638 2.638 0 0 0 3.616 1.024a2.607 2.607 0 0 1 .768-1.6c-2.688-.32-5.504-1.344-5.504-5.984a4.677 4.677 0 0 1 1.216-3.168a4.383 4.383 0 0 1 .128-3.136s1.024-.32 3.36 1.216a11.66 11.66 0 0 1 6.112 0c2.336-1.536 3.36-1.216 3.36-1.216a4.354 4.354 0 0 1 .128 3.136a4.628 4.628 0 0 1 1.216 3.168c0 4.672-2.848 5.664-5.536 5.952a2.881 2.881 0 0 1 .832 2.24v3.36c0 .32.224.672.832.576A12 12 0 0 0 12 0z"
/>
</svg>
<p>Login with GitHub</p>
</a>
</div>
</>
)}
</div>
</div>
);
}
export default Login;

View file

@ -340,6 +340,34 @@ function Registration() {
</div>
</>
)}
{startupConfig?.githubLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<div className="absolute bg-white px-3 text-xs">Or</div>
</div>
<div className="mt-4 flex gap-x-2">
<a
aria-label="Login with GitHub"
href={`${startupConfig.serverDomain}/oauth/github`}
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"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
id="github"
className="h-5 w-5"
>
<path
fill="currentColor"
d="M12 0a12 12 0 0 0-3.84 23.399c.608.112.832-.256.832-.576v-2.015c-3.395.736-4.115-1.632-4.115-1.632a3.241 3.241 0 0 0-1.359-1.792c-1.104-.736.064-.736.064-.736a2.566 2.566 0 0 1 1.824 1.216a2.638 2.638 0 0 0 3.616 1.024a2.607 2.607 0 0 1 .768-1.6c-2.688-.32-5.504-1.344-5.504-5.984a4.677 4.677 0 0 1 1.216-3.168a4.383 4.383 0 0 1 .128-3.136s1.024-.32 3.36 1.216a11.66 11.66 0 0 1 6.112 0c2.336-1.536 3.36-1.216 3.36-1.216a4.354 4.354 0 0 1 .128 3.136a4.628 4.628 0 0 1 1.216 3.168c0 4.672-2.848 5.664-5.536 5.952a2.881 2.881 0 0 1 .832 2.24v3.36c0 .32.224.672.832.576A12 12 0 0 0 12 0z"
/>
</svg>
<p>Login with GitHub</p>
</a>
</div>
</>
)}
</div>
</div>
);

View file

@ -26,6 +26,7 @@ const setup = ({
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
githubLoginEnabled: true,
registrationEnabled: true,
serverDomain: 'mock-server'
}

View file

@ -26,6 +26,7 @@ const setup = ({
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
githubLoginEnabled: true,
registrationEnabled: true,
serverDomain: 'mock-server'
}

View file

@ -243,6 +243,7 @@ export type TStartupConfig = {
openidLoginEnabled: boolean;
openidLabel: string;
openidImageUrl: string;
githubLoginEnabled: boolean;
serverDomain: string;
registrationEnabled: boolean;
}

View file

@ -68,6 +68,19 @@ OPENID_CALLBACK_URL=/oauth/openid/callback
---
## How to Set Up Github Authentication
1. Go to your [Github Developer settings](https://github.com/settings/apps)
2. Create a new Github app
3. Give it a GitHub App name and set in the Homepage URL your [DOMAIN_CLIENT](https://github.com/danny-avila/LibreChat/blob/main/.env.example#L219) (example: http://localhost:3080)
4. Add a callback URL and set it as "[Your DOMAIN_CLIENT](https://github.com/danny-avila/LibreChat/blob/main/.env.example#L219)/oauth/github/callback" (example: http://localhost:3080/oauth/github/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
9. Save the .env file
---
## **Email and Password Reset**
Most of the code is in place for sending password reset emails, but is not yet feature-complete as I have not setup an email server to test it. Currently, submitting a password reset request will then display a link with the one-time reset token that can then be used to reset the password. Understanding that this is a considerable security hazard, email integration will be included in the next release.
@ -83,4 +96,4 @@ If you previously implemented your own user system using the original scaffoldin
### For user updating from an older version of the app:
When the first account is registered, the application will automatically migrate any conversations and presets that you created before the user system was implemented to that account.
if you use login for the first time with a social login account (eg. Google, facebook, etc.), the conversations and presets that you created before the user system was implemented will NOT be migrated to that account.
if you use login for the first time with a social login account (eg. Google, facebook, etc.), the conversations and presets that you created before the user system was implemented will NOT be migrated to that account.

24
package-lock.json generated
View file

@ -14,7 +14,10 @@
"client"
],
"dependencies": {
"langchain": "^0.0.91"
"axios": "^1.4.0",
"langchain": "^0.0.91",
"passport": "^0.6.0",
"passport-github2": "^0.1.12"
},
"devDependencies": {
"@playwright/test": "^1.32.1",
@ -20320,6 +20323,17 @@
"node": ">= 0.4.0"
}
},
"node_modules/passport-github2": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
"integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@ -40406,6 +40420,14 @@
"passport-oauth2": "1.x.x"
}
},
"passport-github2": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
"integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
"requires": {
"passport-oauth2": "1.x.x"
}
},
"passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",

View file

@ -36,7 +36,10 @@
},
"homepage": "https://github.com/danny-avila/LibreChat#readme",
"dependencies": {
"langchain": "^0.0.91"
"axios": "^1.4.0",
"langchain": "^0.0.91",
"passport": "^0.6.0",
"passport-github2": "^0.1.12"
},
"devDependencies": {
"@playwright/test": "^1.32.1",