Add Plex OAuth to login page

This commit is contained in:
JonnyWong16 2018-07-01 22:55:13 -07:00
parent b49e500221
commit 3bd1b03faf
4 changed files with 273 additions and 63 deletions

View file

@ -3470,6 +3470,9 @@ a.no-highlight:hover {
max-width: 1170px; max-width: 1170px;
} }
} }
.login-body-container {
margin: 50px 0;
}
.login-container { .login-container {
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
@ -3512,6 +3515,30 @@ a.no-highlight:hover {
font-weight: 400; font-weight: 400;
cursor: pointer; cursor: pointer;
} }
.login-divider {
text-align: center;
border-bottom: 1px solid #555;
line-height: 0.1em;
margin: 50px auto;
max-width: 400px;
text-transform: uppercase;
}
.login-divider span {
background: #1f1f1f;
padding: 0 15px;
color: #999;
}
.login-button-plex {
text-align: center;
}
.login-button-plex button#sign-in-plex {
float: none;
}
.login-alert {
text-align: center;
padding: 8px;
display: none;
}
#admin-login-modal .form-group label { #admin-login-modal .form-group label {
font-weight: 400; font-weight: 400;
color: #999; color: #999;

View file

@ -32,20 +32,22 @@
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5"> <meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
</head> </head>
<body> <body style="margin: 0; overflow: auto;">
<div class="body-container"> <div class="login-body-container">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="login-container"> <div class="login-container">
<div class="login-logo"> <div class="login-logo">
<img src="${http_root}images/logo-tautulli-100.png" height="100" alt="PlexPy"> <img src="${http_root}images/logo-tautulli-100.png" height="100" alt="PlexPy">
</div> </div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div id="sign-in-alert" class="alert alert-danger login-alert"></div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-sm-offset-3"> <div class="col-sm-6 col-sm-offset-3">
<form id="login-form"> <form id="login-form">
<div id="incorrect-login" class="alert alert-danger" style="text-align: center; padding: 8px; display: none;">
Incorrect username or password.
</div>
<div class="form-group"> <div class="form-group">
<label for="username" class="control-label"> <label for="username" class="control-label">
Username Username
@ -69,6 +71,19 @@
</form> </form>
</div> </div>
</div> </div>
<div class="row">
<div class="login-divider"><span>or</span></div>
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div id="sign-in-plex-alert" class="alert alert-danger login-alert"></div>
</div>
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3 login-button-plex">
<button id="sign-in-plex" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In with Plex</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -78,25 +93,91 @@
<script> <script>
$('#login-form').submit(function(event) { $('#login-form').submit(function(event) {
event.preventDefault(); event.preventDefault();
$('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In'); signIn(false);
});
function signIn(plex, token) {
$('.login-container button').prop('disabled', true);
if (plex) {
$('#sign-in-plex').html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In with Plex');
} else {
$('#sign-in').html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In');
}
const username = plex ? null : $('#username').val();
const password = plex ? null : $('#password').val();
const remember_me = $('#remember_me').is(':checked') ? '1' : '0';
$.ajax({ $.ajax({
url: '${http_root}auth/signin', url: '${http_root}auth/signin',
type: 'POST', type: 'POST',
data: $(this).serialize(), data: {
username: username,
password: password,
token: token,
remember_me: remember_me
},
dataType: 'json', dataType: 'json',
statusCode: { statusCode: {
200: function() { 200: function() {
window.location = "${redirect_uri or http_root}"; window.location = "${redirect_uri or http_root}";
}, },
401: function() { 401: function() {
$('#incorrect-login').show(); if (plex) {
$('#username').focus(); $('#sign-in-plex-alert').text('Invalid Plex Login.').show();
} else {
$('#sign-in-alert').text('Incorrect username or password.').show();
$('#username').focus();
}
} }
}, },
complete: function() { complete: function() {
$('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i>&nbsp; Sign In'); $('.login-container button').prop('disabled', false);
if (plex) {
$('#sign-in-plex').html('<i class="fa fa-sign-in"></i>&nbsp; Sign In with Plex');
} else {
$('#sign-in').html('<i class="fa fa-sign-in"></i>&nbsp; Sign In');
}
} }
}); });
}
getPlexOAuthURL = function () {
var deferred = $.Deferred();
$.get('get_plex_oauth_url').then(function (data) {
deferred.resolve(data);
});
return deferred;
};
$('#sign-in-plex').click(function() {
getPlexOAuthURL().then(function (data) {
var url = data.url;
var pin = data.pin;
var polling = true;
window.open(url);
(function poll() {
setTimeout(function () {
$.ajax({
url: 'pin/' + pin,
success: function (data) {
if (data.result === 'success'){
polling = false;
signIn(true, data.token)
}
},
complete: function () {
if (polling){
poll();
}
},
timeout: 1000
});
}, 1000);
})();
});
}); });
</script> </script>
</body> </body>

View file

@ -226,6 +226,45 @@ class PlexTV(object):
return server_token return server_token
def get_plextv_pin(self, pin='', output_format=''):
if pin:
uri = '/api/v2/pins/' + pin
request = self.request_handler.make_request(uri=uri,
request_type='GET',
output_format=output_format,
no_token=True)
else:
uri = '/api/v2/pins?strong=true'
request = self.request_handler.make_request(uri=uri,
request_type='POST',
output_format=output_format,
no_token=True)
return request
def get_pin(self, pin=''):
plextv_response = self.get_plextv_pin(pin=pin,
output_format='xml')
if plextv_response:
try:
xml_head = plextv_response.getElementsByTagName('pin')
if xml_head:
pin = {'id': xml_head[0].getAttribute('id'),
'code': xml_head[0].getAttribute('code'),
'token': xml_head[0].getAttribute('authToken')
}
return pin
else:
logger.warn(u"Tautulli PlexTV :: Could not get Plex authentication pin.")
return None
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_pin: %s." % e)
return None
else:
return None
def get_plextv_user_data(self): def get_plextv_user_data(self):
plextv_response = self.get_plex_auth(output_format='dict') plextv_response = self.get_plex_auth(output_format='dict')
@ -819,3 +858,28 @@ class PlexTV(object):
return True return True
else: else:
return False return False
def get_plex_account_details(self):
account_data = self.get_plextv_user_details(output_format='xml')
try:
xml_head = account_data.getElementsByTagName('user')
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_plex_account_details: %s." % e)
return None
for a in xml_head:
account_details = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'username'),
"thumb": helpers.get_xml_attr(a, 'thumb'),
"email": helpers.get_xml_attr(a, 'email'),
"is_home_user": helpers.get_xml_attr(a, 'home'),
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos'),
"user_token": helpers.get_xml_attr(a, 'authToken')
}
return account_details

View file

@ -19,7 +19,6 @@
# Session tool to be loaded. # Session tool to be loaded.
from datetime import datetime, timedelta from datetime import datetime, timedelta
import re
from urllib import quote, unquote from urllib import quote, unquote
import cherrypy import cherrypy
@ -37,17 +36,27 @@ JWT_ALGORITHM = 'HS256'
JWT_COOKIE_NAME = 'tautulli_token_' JWT_COOKIE_NAME = 'tautulli_token_'
def user_login(username=None, password=None): def user_login(username=None, password=None, token=None):
if not username or not password: user_token = None
return None user_id = None
# Try to login to Plex.tv to check if the user has a vaild account # Try to login to Plex.tv to check if the user has a vaild account
plex_tv = PlexTV(username=username, password=password) if username and password:
plex_user = plex_tv.get_token() plex_tv = PlexTV(username=username, password=password)
if plex_user: plex_user = plex_tv.get_token()
user_token = plex_user['auth_token'] if plex_user:
user_id = plex_user['user_id'] user_token = plex_user['auth_token']
user_id = plex_user['user_id']
elif token:
plex_tv = PlexTV(token=token)
plex_user = plex_tv.get_plex_account_details()
if plex_user:
user_token = token
user_id = plex_user['user_id']
else:
return None
if user_token and user_id:
# Try to retrieve the user from the database. # Try to retrieve the user from the database.
# Also make sure guest access is enabled for the user and the user is not deleted. # Also make sure guest access is enabled for the user and the user is not deleted.
user_data = Users() user_data = Users()
@ -57,7 +66,7 @@ def user_login(username=None, password=None):
return None return None
elif plexpy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']: elif plexpy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']:
# Plex admin login # Plex admin login
return 'admin' return user_details, 'admin'
elif not user_details['allow_guest'] or user_details['deleted_user']: elif not user_details['allow_guest'] or user_details['deleted_user']:
# Guest access is disabled or the user is deleted. # Guest access is disabled or the user is deleted.
return None return None
@ -75,49 +84,64 @@ def user_login(username=None, password=None):
# Register the new user / update the access tokens. # Register the new user / update the access tokens.
monitor_db = MonitorDatabase() monitor_db = MonitorDatabase()
try: try:
logger.debug(u"Tautulli WebAuth :: Regestering tokens for user '%s' in the database." % username) logger.debug(u"Tautulli WebAuth :: Regestering tokens for user '%s' in the database."
result = monitor_db.action('UPDATE users SET user_token = ?, server_token = ? WHERE user_id = ?', % user_details['username'])
[user_token, server_token, user_id]) result = monitor_db.action('UPDATE users SET server_token = ? WHERE user_id = ?',
[server_token, user_details['user_id']])
if result: if result:
# Refresh the users list to make sure we have all the correct permissions. # Refresh the users list to make sure we have all the correct permissions.
refresh_users() refresh_users()
# Successful login # Successful login
return 'guest' return user_details, 'guest'
else: else:
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database." % username) logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database."
% user_details['username'])
return None return None
except Exception as e: except Exception as e:
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database: %s." % (username, e)) logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database: %s."
% (user_details['username'], e))
return None return None
else: else:
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv server token for user '%s'." % username) logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv server token for user '%s'."
% user_details['username'])
return None return None
else: elif username:
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for user '%s'." % username) logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for user '%s'." % username)
return None return None
return None elif token:
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for Plex OAuth.")
return None
def check_credentials(username, password, admin_login='0'): def check_credentials(username=None, password=None, token=None, admin_login='0'):
"""Verifies credentials for username and password. """Verifies credentials for username and password.
Returns True and the user group on success or False and no user group""" Returns True and the user group on success or False and no user group"""
if plexpy.CONFIG.HTTP_PASSWORD: if username and password:
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ if plexpy.CONFIG.HTTP_PASSWORD:
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD): user_details = {'user_id': None, 'username': username}
return True, 'tautulli admin'
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
return True, 'tautulli admin'
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS): if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
plex_login = user_login(username, password) username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
if plex_login is not None: return True, user_details, 'admin'
return True, plex_login elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
return True, user_details, 'admin'
return False, None if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
plex_login = user_login(username=username, password=password)
if plex_login is not None:
return True, plex_login[0], plex_login[1]
elif token:
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
plex_login = user_login(token=token)
if plex_login is not None:
return True, plex_login[0], plex_login[1]
return False, None, None
def check_jwt_token(): def check_jwt_token():
@ -279,41 +303,33 @@ class AuthController(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
def signin(self, username=None, password=None, remember_me='0', admin_login='0', *args, **kwargs): def signin(self, username=None, password=None, token=None, remember_me='0', admin_login='0', *args, **kwargs):
if cherrypy.request.method != 'POST': if cherrypy.request.method != 'POST':
cherrypy.response.status = 405 cherrypy.response.status = 405
return {'status': 'error', 'message': 'Sign in using POST.'} return {'status': 'error', 'message': 'Sign in using POST.'}
error_message = {'status': 'error', 'message': 'Incorrect username or password.'} error_message = {'status': 'error', 'message': 'Invalid credentials.'}
valid_login, user_group = check_credentials(username, password, admin_login) valid_login, user_details, user_group = check_credentials(username=username,
password=password,
token=token,
admin_login=admin_login)
if valid_login: if valid_login:
if user_group == 'tautulli admin':
user_group = 'admin'
user_id = None
else:
if re.match(r"[^@]+@[^@]+\.[^@]+", username):
user_details = Users().get_details(email=username)
else:
user_details = Users().get_details(user=username)
user_id = user_details['user_id']
time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60) time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60)
expiry = datetime.utcnow() + time_delta expiry = datetime.utcnow() + time_delta
payload = { payload = {
'user_id': user_id, 'user_id': user_details['user_id'],
'user': username, 'user': user_details['username'],
'user_group': user_group, 'user_group': user_group,
'exp': expiry 'exp': expiry
} }
jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM) jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM)
self.on_login(username=username, self.on_login(username=user_details['username'],
user_id=user_id, user_id=user_details['user_id'],
user_group=user_group, user_group=user_group,
success=1) success=1)
@ -326,14 +342,36 @@ class AuthController(object):
cherrypy.response.status = 200 cherrypy.response.status = 200
return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID} return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID}
elif admin_login == '1': elif admin_login == '1' and username:
self.on_login(username=username) self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username) logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username)
cherrypy.response.status = 401 cherrypy.response.status = 401
return error_message return error_message
else: elif username:
self.on_login(username=username) self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username) logger.debug(u"Tautulli WebAuth :: Invalid user login attempt from '%s'." % username)
cherrypy.response.status = 401 cherrypy.response.status = 401
return error_message return error_message
elif token:
logger.debug(u"Tautulli WebAuth :: Invalid Plex OAuth login attempt.")
cherrypy.response.status = 401
return error_message
@cherrypy.expose
@cherrypy.tools.json_out()
def get_plex_oauth_url(self, *args, **kwargs):
pin = PlexTV().get_pin()
oauth_url = 'https://app.plex.tv/auth/#!?clientID={}&code={}'.format(
plexpy.CONFIG.PMS_UUID, pin['code'])
return {'pin': pin['id'], 'url': oauth_url}
@cherrypy.expose
@cherrypy.tools.json_out()
def pin(self, pin=None, *args, **kwargs):
pin = PlexTV().get_pin(pin=pin)
if pin['token']:
return {'result': 'success', 'token': pin['token']}
else:
return {'result': 'polling'}