Skip to content

Commit

Permalink
implemented Apple Auth login.
Browse files Browse the repository at this point in the history
Closes: danny-avila#3438

TODO:
- write config Doc
  • Loading branch information
rubentalstra committed Jan 26, 2025
1 parent 8b31f25 commit 94ea18a
Show file tree
Hide file tree
Showing 20 changed files with 173 additions and 13 deletions.
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,14 @@ GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/oauth/google/callback

# Apple
APPLE_CLIENT_ID=
APPLE_TEAM_ID=
APPLE_KEY_ID=
# /path/to/AuthKey.p8
APPLE_PRIVATE_KEY_PATH=
APPLE_CALLBACK_URL=/oauth/apple/callback

# OpenID
OPENID_CLIENT_ID=
OPENID_CLIENT_SECRET=
Expand Down
6 changes: 6 additions & 0 deletions api/models/schema/userSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const { SystemRoles } = require('librechat-data-provider');
* @property {string} [ldapId] - Optional LDAP ID for the user
* @property {string} [githubId] - Optional GitHub ID for the user
* @property {string} [discordId] - Optional Discord ID for the user
* @property {string} [appleId] - Optional Apple ID for the user
* @property {Array} [plugins=[]] - List of plugins used by the user
* @property {Array.<MongoSession>} [refreshToken] - List of sessions with refresh tokens
* @property {Date} [expiresAt] - Optional expiration date of the file
Expand Down Expand Up @@ -111,6 +112,11 @@ const userSchema = mongoose.Schema(
unique: true,
sparse: true,
},
appleId: {
type: String,
unique: true,
sparse: true,
},
plugins: {
type: Array,
default: [],
Expand Down
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"openai-chat-tokens": "^0.2.8",
"openid-client": "^5.4.2",
"passport": "^0.6.0",
"passport-apple": "^2.0.2",
"passport-custom": "^1.1.1",
"passport-discord": "^0.1.4",
"passport-facebook": "^3.0.0",
Expand Down
5 changes: 5 additions & 0 deletions api/server/routes/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ router.get('/', async function (req, res) {
!!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
githubLoginEnabled: !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET,
googleLoginEnabled: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,
appleLoginEnabled:
!!process.env.APPLE_CLIENT_ID &&
!!process.env.APPLE_TEAM_ID &&
!!process.env.APPLE_KEY_ID &&
!!process.env.APPLE_PRIVATE_KEY_PATH,
openidLoginEnabled:
!!process.env.OPENID_CLIENT_ID &&
!!process.env.OPENID_CLIENT_SECRET &&
Expand Down
35 changes: 35 additions & 0 deletions api/server/routes/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ router.get(
oauthHandler,
);

/**
* Facebook Routes
*/
router.get(
'/facebook',
passport.authenticate('facebook', {
Expand All @@ -77,6 +80,9 @@ router.get(
oauthHandler,
);

/**
* OpenID Routes
*/
router.get(
'/openid',
passport.authenticate('openid', {
Expand All @@ -94,6 +100,9 @@ router.get(
oauthHandler,
);

/**
* GitHub Routes
*/
router.get(
'/github',
passport.authenticate('github', {
Expand All @@ -112,6 +121,10 @@ router.get(
}),
oauthHandler,
);

/**
* Discord Routes
*/
router.get(
'/discord',
passport.authenticate('discord', {
Expand All @@ -131,4 +144,26 @@ router.get(
oauthHandler,
);

/**
* Apple Routes
*/
// Apply body-parser only to Apple routes
router.get(
'/apple',
passport.authenticate('apple', {
session: false,
}),
);

// Apply body-parser middleware only to POST callback route
router.post(
'/apple/callback',
passport.authenticate('apple', {
failureRedirect: `${domains.client}/oauth/error`,
failureMessage: true,
session: false,
}),
oauthHandler,
);

module.exports = router;
5 changes: 5 additions & 0 deletions api/server/socialLogins.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
githubLogin,
discordLogin,
facebookLogin,
appleLogin,
} = require('~/strategies');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
Expand All @@ -30,6 +31,10 @@ const configureSocialLogins = (app) => {
if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) {
passport.use(discordLogin());
}
if (process.env.APPLE_CLIENT_ID && process.env.APPLE_PRIVATE_KEY_PATH) {
passport.use(appleLogin());

}
if (
process.env.OPENID_CLIENT_ID &&
process.env.OPENID_CLIENT_SECRET &&
Expand Down
53 changes: 53 additions & 0 deletions api/strategies/appleStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const socialLogin = require('./socialLogin');
const { Strategy: AppleStrategy } = require('passport-apple');
const { logger } = require('~/config');
const jwt = require('jsonwebtoken');

/**
* Extract profile details from the decoded idToken
* @param {Object} params - Parameters from the verify callback
* @param {string} params.idToken - The ID token received from Apple
* @param {Object} params.profile - The profile object (may contain partial info)
* @returns {Object} - The extracted user profile details
*/
const getProfileDetails = ({ idToken, profile }) => {
if (!idToken) {
logger.error('idToken is missing');
throw new Error('idToken is missing');
}

const decoded = jwt.decode(idToken);

logger.debug(
`Decoded Apple JWT: ${JSON.stringify(decoded, null, 2)}`,
);

return {
email: decoded.email,
id: decoded.sub,
avatarUrl: null, // Apple does not provide an avatar URL
username: decoded.email
? decoded.email.split('@')[0].toLowerCase()
: `user_${decoded.sub}`,
name: decoded.name
? `${decoded.name.firstName} ${decoded.name.lastName}`
: profile.displayName || null,
emailVerified: true, // Apple verifies the email
};
};

// Initialize the social login handler for Apple
const appleLogin = socialLogin('apple', getProfileDetails);

module.exports = () =>
new AppleStrategy(
{
clientID: process.env.APPLE_CLIENT_ID,
teamID: process.env.APPLE_TEAM_ID,
callbackURL: `${process.env.DOMAIN_SERVER}${process.env.APPLE_CALLBACK_URL}`,
keyID: process.env.APPLE_KEY_ID,
privateKeyLocation: process.env.APPLE_PRIVATE_KEY_PATH,
passReqToCallback: false, // Set to true if you need to access the request in the callback
},
appleLogin,
);
2 changes: 1 addition & 1 deletion api/strategies/discordStrategy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { Strategy: DiscordStrategy } = require('passport-discord');
const socialLogin = require('./socialLogin');

const getProfileDetails = (profile) => {
const getProfileDetails = ({ profile }) => {
let avatarUrl;
if (profile.avatar) {
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png';
Expand Down
2 changes: 1 addition & 1 deletion api/strategies/facebookStrategy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const FacebookStrategy = require('passport-facebook').Strategy;
const socialLogin = require('./socialLogin');

const getProfileDetails = (profile) => ({
const getProfileDetails = ({ profile }) => ({
email: profile.emails[0]?.value,
id: profile.id,
avatarUrl: profile.photos[0]?.value,
Expand Down
2 changes: 1 addition & 1 deletion api/strategies/githubStrategy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { Strategy: GitHubStrategy } = require('passport-github2');
const socialLogin = require('./socialLogin');

const getProfileDetails = (profile) => ({
const getProfileDetails = ({ profile }) => ({
email: profile.emails[0].value,
id: profile.id,
avatarUrl: profile.photos[0].value,
Expand Down
2 changes: 1 addition & 1 deletion api/strategies/googleStrategy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
const socialLogin = require('./socialLogin');

const getProfileDetails = (profile) => ({
const getProfileDetails = ({ profile }) => ({
email: profile.emails[0].value,
id: profile.id,
avatarUrl: profile.photos[0].value,
Expand Down
2 changes: 2 additions & 0 deletions api/strategies/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const appleLogin = require('./appleStrategy');
const passportLogin = require('./localStrategy');
const googleLogin = require('./googleStrategy');
const githubLogin = require('./githubStrategy');
Expand All @@ -8,6 +9,7 @@ const jwtLogin = require('./jwtStrategy');
const ldapLogin = require('./ldapStrategy');

module.exports = {
appleLogin,
passportLogin,
googleLogin,
githubLogin,
Expand Down
6 changes: 4 additions & 2 deletions api/strategies/socialLogin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ const { findUser } = require('~/models');
const { logger } = require('~/config');

const socialLogin =
(provider, getProfileDetails) => async (accessToken, refreshToken, profile, cb) => {
(provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => {
try {
const { email, id, avatarUrl, username, name, emailVerified } = getProfileDetails(profile);
const { email, id, avatarUrl, username, name, emailVerified } = getProfileDetails({
idToken, profile,
});

const oldUser = await findUser({ email: email.trim() });
const ALLOW_SOCIAL_REGISTRATION = isEnabled(process.env.ALLOW_SOCIAL_REGISTRATION);
Expand Down
23 changes: 17 additions & 6 deletions client/src/components/Auth/SocialLoginRender.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon, AppleIcon } from '~/components';

import SocialButton from './SocialButton';

Expand All @@ -18,7 +18,7 @@ function SocialLoginRender({
}

const providerComponents = {
discord: startupConfig?.discordLoginEnabled && (
discord: startupConfig.discordLoginEnabled && (
<SocialButton
key="discord"
enabled={startupConfig.discordLoginEnabled}
Expand All @@ -29,7 +29,7 @@ function SocialLoginRender({
id="discord"
/>
),
facebook: startupConfig?.facebookLoginEnabled && (
facebook: startupConfig.facebookLoginEnabled && (
<SocialButton
key="facebook"
enabled={startupConfig.facebookLoginEnabled}
Expand All @@ -40,7 +40,7 @@ function SocialLoginRender({
id="facebook"
/>
),
github: startupConfig?.githubLoginEnabled && (
github: startupConfig.githubLoginEnabled && (
<SocialButton
key="github"
enabled={startupConfig.githubLoginEnabled}
Expand All @@ -51,7 +51,7 @@ function SocialLoginRender({
id="github"
/>
),
google: startupConfig?.googleLoginEnabled && (
google: startupConfig.googleLoginEnabled && (
<SocialButton
key="google"
enabled={startupConfig.googleLoginEnabled}
Expand All @@ -62,7 +62,18 @@ function SocialLoginRender({
id="google"
/>
),
openid: startupConfig?.openidLoginEnabled && (
apple: startupConfig.appleLoginEnabled && (
<SocialButton
key="apple"
enabled={startupConfig.appleLoginEnabled}
serverDomain={startupConfig.serverDomain}
oauthPath="apple"
Icon={AppleIcon}
label={localize('com_auth_apple_login')}
id="apple"
/>
),
openid: startupConfig.openidLoginEnabled && (
<SocialButton
key="openid"
enabled={startupConfig.openidLoginEnabled}
Expand Down
18 changes: 18 additions & 0 deletions client/src/components/svg/AppleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

export default function AppleIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
viewBox="0 0 814 1000"
id="apple"
className="h-6 w-6"
>
<path
d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"
fill="currentColor"
/>
</svg>
);
}
1 change: 1 addition & 0 deletions client/src/components/svg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { default as FacebookIcon } from './FacebookIcon';
export { default as OpenIDIcon } from './OpenIDIcon';
export { default as GithubIcon } from './GithubIcon';
export { default as DiscordIcon } from './DiscordIcon';
export { default as AppleIcon } from './AppleIcon';
export { default as AnthropicIcon } from './AnthropicIcon';
export { default as SendIcon } from './SendIcon';
export { default as LinkIcon } from './LinkIcon';
Expand Down
1 change: 1 addition & 0 deletions client/src/localization/languages/Eng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ export default {
com_auth_facebook_login: 'Continue with Facebook',
com_auth_github_login: 'Continue with Github',
com_auth_discord_login: 'Continue with Discord',
com_auth_apple_login: 'Sign in with Apple',
com_auth_email: 'Email',
com_auth_email_required: 'Email is required',
com_auth_email_min_length: 'Email must be at least 6 characters',
Expand Down
2 changes: 1 addition & 1 deletion librechat.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ interface:

# Example Registration Object Structure (optional)
registration:
socialLogins: ['github', 'google', 'discord', 'openid', 'facebook']
socialLogins: ['github', 'google', 'discord', 'openid', 'facebook', 'apple']
# allowedDomains:
# - "gmail.com"

Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/data-provider/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ export type TStartupConfig = {
githubLoginEnabled: boolean;
googleLoginEnabled: boolean;
openidLoginEnabled: boolean;
appleLoginEnabled: boolean;
openidLabel: string;
openidImageUrl: string;
/** LDAP Auth Configuration */
Expand Down

0 comments on commit 94ea18a

Please sign in to comment.