feat: verify Plex server access during auth for existing users with Plex IDs (#2458)

* feat: if local sign-in disabled, verify Plex server access during auth for existing users

* fix: disable local/password login by default

* fix: set localLogin to disabled in getInitialProps

* fix: verify Plex server access on local logins as well
pull/2524/head^2
TheCatLady 3 years ago committed by GitHub
parent aa79dc1c42
commit 85bb30e252
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,8 +15,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
if (!req.user) { if (!req.user) {
return res.status(500).json({ return res.status(500).json({
status: 500, status: 500,
error: error: 'Please sign in.',
'Requested user endpoint without valid authenticated user in session',
}); });
} }
const user = await userRepository.findOneOrFail({ const user = await userRepository.findOneOrFail({
@ -32,10 +31,13 @@ authRoutes.post('/plex', async (req, res, next) => {
const body = req.body as { authToken?: string }; const body = req.body as { authToken?: string };
if (!body.authToken) { if (!body.authToken) {
return res.status(500).json({ error: 'You must provide an auth token' }); return next({
status: 500,
message: 'Authentication token required.',
});
} }
try { try {
// First we need to use this auth token to get the users email from plex.tv // First we need to use this auth token to get the user's email from plex.tv
const plextv = new PlexTvAPI(body.authToken); const plextv = new PlexTvAPI(body.authToken);
const account = await plextv.getUser(); const account = await plextv.getUser();
@ -48,30 +50,7 @@ authRoutes.post('/plex', async (req, res, next) => {
}) })
.getOne(); .getOne();
if (user) { if (!user && !(await userRepository.count())) {
// Let's check if their Plex token is up-to-date
if (user.plexToken !== body.authToken) {
user.plexToken = body.authToken;
}
// Update the user's avatar with their Plex thumbnail, in case it changed
user.avatar = account.thumb;
user.email = account.email;
user.plexUsername = account.username;
// In case the user was previously a local account
if (user.userType === UserType.LOCAL) {
user.userType = UserType.PLEX;
user.plexId = account.id;
}
await userRepository.save(user);
} else {
// Here we check if it's the first user. If it is, we create the user with no check
// and give them admin permissions
const totalUsers = await userRepository.count();
if (totalUsers === 0) {
user = new User({ user = new User({
email: account.email, email: account.email,
plexUsername: account.username, plexUsername: account.username,
@ -81,38 +60,68 @@ authRoutes.post('/plex', async (req, res, next) => {
avatar: account.thumb, avatar: account.thumb,
userType: UserType.PLEX, userType: UserType.PLEX,
}); });
await userRepository.save(user); await userRepository.save(user);
} } else {
const mainUser = await userRepository.findOneOrFail({
select: ['id', 'plexToken', 'plexId'],
order: { id: 'ASC' },
});
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
// Double check that we didn't create the first admin user before running this if (
if (!user) { account.id === mainUser.plexId ||
if (!settings.main.newPlexLogin) { (await mainPlexTv.checkUserAccess(account.id))
) {
if (user) {
if (!user.plexId) {
logger.info( logger.info(
'Failed sign-in attempt from user who has not been imported to Overseerr.', 'Found matching Plex user; updating user with Plex data',
{ {
label: 'Auth', label: 'API',
account: { ip: req.ip,
...account, email: user.email,
authentication_token: '__REDACTED__', userId: user.id,
authToken: '__REDACTED__', plexId: account.id,
}, plexUsername: account.username,
}
);
}
user.plexToken = body.authToken;
user.plexId = account.id;
user.avatar = account.thumb;
user.email = account.email;
user.plexUsername = account.username;
user.userType = UserType.PLEX;
await userRepository.save(user);
} else if (!settings.main.newPlexLogin) {
logger.warn(
'Failed sign-in attempt by unimported Plex user with access to the media server',
{
label: 'API',
ip: req.ip,
email: account.email,
plexId: account.id,
plexUsername: account.username,
} }
); );
return next({ return next({
status: 403, status: 403,
message: 'Access denied.', message: 'Access denied.',
}); });
} else {
logger.info(
'Sign-in attempt from Plex user with access to the media server; creating new Overseerr user',
{
label: 'API',
ip: req.ip,
email: account.email,
plexId: account.id,
plexUsername: account.username,
} }
);
// If we get to this point, the user does not already exist so we need to create the
// user _assuming_ they have access to the Plex server
const mainUser = await userRepository.findOneOrFail({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
});
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
if (await mainPlexTv.checkUserAccess(account.id)) {
user = new User({ user = new User({
email: account.email, email: account.email,
plexUsername: account.username, plexUsername: account.username,
@ -122,17 +131,18 @@ authRoutes.post('/plex', async (req, res, next) => {
avatar: account.thumb, avatar: account.thumb,
userType: UserType.PLEX, userType: UserType.PLEX,
}); });
await userRepository.save(user); await userRepository.save(user);
}
} else { } else {
logger.info( logger.warn(
'Failed sign-in attempt from user without access to the Plex server.', 'Failed sign-in attempt by Plex user without access to the media server',
{ {
label: 'Auth', label: 'API',
account: { ip: req.ip,
...account, email: account.email,
authentication_token: '__REDACTED__', plexId: account.id,
authToken: '__REDACTED__', plexUsername: account.username,
},
} }
); );
return next({ return next({
@ -141,7 +151,6 @@ authRoutes.post('/plex', async (req, res, next) => {
}); });
} }
} }
}
// Set logged in session // Set logged in session
if (req.session) { if (req.session) {
@ -150,10 +159,14 @@ authRoutes.post('/plex', async (req, res, next) => {
return res.status(200).json(user?.filter() ?? {}); return res.status(200).json(user?.filter() ?? {});
} catch (e) { } catch (e) {
logger.error(e.message, { label: 'Auth' }); logger.error('Something went wrong authenticating with Plex account', {
label: 'API',
errorMessage: e.message,
ip: req.ip,
});
return next({ return next({
status: 500, status: 500,
message: 'Something went wrong.', message: 'Unable to authenticate.',
}); });
} }
}); });
@ -164,7 +177,7 @@ authRoutes.post('/local', async (req, res, next) => {
const body = req.body as { email?: string; password?: string }; const body = req.body as { email?: string; password?: string };
if (!settings.main.localLogin) { if (!settings.main.localLogin) {
return res.status(500).json({ error: 'Local user sign-in is disabled.' }); return res.status(500).json({ error: 'Password sign-in is disabled.' });
} else if (!body.email || !body.password) { } else if (!body.email || !body.password) {
return res.status(500).json({ return res.status(500).json({
error: 'You must provide both an email address and a password.', error: 'You must provide both an email address and a password.',
@ -173,28 +186,77 @@ authRoutes.post('/local', async (req, res, next) => {
try { try {
const user = await userRepository const user = await userRepository
.createQueryBuilder('user') .createQueryBuilder('user')
.select(['user.id', 'user.password']) .select(['user.id', 'user.email', 'user.password', 'user.plexId'])
.where('user.email = :email', { email: body.email.toLowerCase() }) .where('user.email = :email', { email: body.email.toLowerCase() })
.getOne(); .getOne();
const isCorrectCredentials = await user?.passwordMatch(body.password); if (!user || !(await user.passwordMatch(body.password))) {
logger.warn('Failed sign-in attempt using invalid Overseerr password', {
label: 'API',
ip: req.ip,
email: body.email,
userId: user?.id,
});
return next({
status: 403,
message: 'Access denied.',
});
}
// User doesn't exist or credentials are incorrect const mainUser = await userRepository.findOneOrFail({
if (!isCorrectCredentials) { select: ['id', 'plexToken', 'plexId'],
logger.info( order: { id: 'ASC' },
'Failed sign-in attempt from user with incorrect credentials.', });
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
if (!user.plexId) {
const plexUsersResponse = await mainPlexTv.getUsers();
const account = plexUsersResponse.MediaContainer.User.find(
(account) =>
account.$.email &&
account.$.email.toLowerCase() === user.email.toLowerCase()
)?.$;
if (account) {
logger.info('Found matching Plex user; updating user with Plex data', {
label: 'API',
ip: req.ip,
email: body.email,
userId: user.id,
plexId: account.id,
plexUsername: account.username,
});
user.plexId = parseInt(account.id);
user.avatar = account.thumb;
user.email = account.email;
user.plexUsername = account.username;
user.userType = UserType.PLEX;
await userRepository.save(user);
}
}
if (
user.plexId &&
user.plexId !== mainUser.plexId &&
!(await mainPlexTv.checkUserAccess(user.plexId))
) {
logger.warn(
'Failed sign-in attempt from Plex user without access to the media server',
{ {
label: 'Auth', label: 'API',
account: { account: {
ip: req.ip, ip: req.ip,
email: body.email, email: body.email,
password: '__REDACTED__', userId: user.id,
plexId: user.plexId,
}, },
} }
); );
return next({ return next({
status: 403, status: 403,
message: 'Your sign-in credentials are incorrect.', message: 'Access denied.',
}); });
} }
@ -205,13 +267,18 @@ authRoutes.post('/local', async (req, res, next) => {
return res.status(200).json(user?.filter() ?? {}); return res.status(200).json(user?.filter() ?? {});
} catch (e) { } catch (e) {
logger.error('Something went wrong while attempting to authenticate.', { logger.error(
label: 'Auth', 'Something went wrong authenticating with Overseerr password',
error: e.message, {
}); label: 'API',
errorMessage: e.message,
ip: req.ip,
email: body.email,
}
);
return next({ return next({
status: 500, status: 500,
message: 'Something went wrong.', message: 'Unable to authenticate.',
}); });
} }
}); });
@ -221,7 +288,7 @@ authRoutes.post('/logout', (req, res, next) => {
if (err) { if (err) {
return next({ return next({
status: 500, status: 500,
message: 'Something went wrong while attempting to sign out.', message: 'Something went wrong.',
}); });
} }
@ -229,14 +296,15 @@ authRoutes.post('/logout', (req, res, next) => {
}); });
}); });
authRoutes.post('/reset-password', async (req, res) => { authRoutes.post('/reset-password', async (req, res, next) => {
const userRepository = getRepository(User); const userRepository = getRepository(User);
const body = req.body as { email?: string }; const body = req.body as { email?: string };
if (!body.email) { if (!body.email) {
return res return next({
.status(500) status: 500,
.json({ error: 'You must provide an email address.' }); message: 'Email address required.',
});
} }
const user = await userRepository const user = await userRepository
@ -247,14 +315,16 @@ authRoutes.post('/reset-password', async (req, res) => {
if (user) { if (user) {
await user.resetPassword(); await user.resetPassword();
userRepository.save(user); userRepository.save(user);
logger.info('Successful request made for recovery link.', { logger.info('Successfully sent password reset link', {
label: 'User Management', label: 'API',
context: { ip: req.ip, email: body.email }, ip: req.ip,
email: body.email,
}); });
} else { } else {
logger.info('Failed request made to reset a password.', { logger.error('Something went wrong sending password reset link', {
label: 'User Management', label: 'API',
context: { ip: req.ip, email: body.email }, ip: req.ip,
email: body.email,
}); });
} }
@ -264,15 +334,16 @@ authRoutes.post('/reset-password', async (req, res) => {
authRoutes.post('/reset-password/:guid', async (req, res, next) => { authRoutes.post('/reset-password/:guid', async (req, res, next) => {
const userRepository = getRepository(User); const userRepository = getRepository(User);
try {
if (!req.body.password || req.body.password?.length < 8) { if (!req.body.password || req.body.password?.length < 8) {
const message = logger.warn('Failed password reset attempt using invalid new password', {
'Failed to reset password. Password must be at least 8 characters long.'; label: 'API',
logger.info(message, { ip: req.ip,
label: 'User Management', guid: req.params.guid,
context: { ip: req.ip, guid: req.params.guid }, });
return next({
status: 500,
message: 'Password must be at least 8 characters long.',
}); });
return next({ status: 500, message: message });
} }
const user = await userRepository.findOne({ const user = await userRepository.findOne({
@ -280,32 +351,44 @@ authRoutes.post('/reset-password/:guid', async (req, res, next) => {
}); });
if (!user) { if (!user) {
throw new Error('Guid invalid.'); logger.warn('Failed password reset attempt using invalid recovery link', {
label: 'API',
ip: req.ip,
guid: req.params.guid,
});
return next({
status: 500,
message: 'Invalid password reset link.',
});
} }
if ( if (
!user.recoveryLinkExpirationDate || !user.recoveryLinkExpirationDate ||
user.recoveryLinkExpirationDate <= new Date() user.recoveryLinkExpirationDate <= new Date()
) { ) {
throw new Error('Recovery link expired.'); logger.warn('Failed password reset attempt using expired recovery link', {
label: 'API',
ip: req.ip,
guid: req.params.guid,
email: user.email,
});
return next({
status: 500,
message: 'Invalid password reset link.',
});
} }
await user.setPassword(req.body.password); await user.setPassword(req.body.password);
user.recoveryLinkExpirationDate = null; user.recoveryLinkExpirationDate = null;
userRepository.save(user); userRepository.save(user);
logger.info(`Successfully reset password`, { logger.info('Successfully reset password', {
label: 'User Management', label: 'API',
context: { ip: req.ip, guid: req.params.guid, email: user.email }, ip: req.ip,
guid: req.params.guid,
email: user.email,
}); });
return res.status(200).json({ status: 'ok' }); return res.status(200).json({ status: 'ok' });
} catch (e) {
logger.info(`Failed to reset password. ${e.message}`, {
label: 'User Management',
context: { ip: req.ip, guid: req.params.guid },
});
return res.status(200).json({ status: 'ok' });
}
}); });
export default authRoutes; export default authRoutes;

Loading…
Cancel
Save