mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-15 01:32:57 -07:00
Add Plex OAuth to login page
This commit is contained in:
parent
b49e500221
commit
3bd1b03faf
4 changed files with 273 additions and 63 deletions
|
@ -3470,6 +3470,9 @@ a.no-highlight:hover {
|
|||
max-width: 1170px;
|
||||
}
|
||||
}
|
||||
.login-body-container {
|
||||
margin: 50px 0;
|
||||
}
|
||||
.login-container {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
|
@ -3512,6 +3515,30 @@ a.no-highlight:hover {
|
|||
font-weight: 400;
|
||||
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 {
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
|
|
|
@ -32,20 +32,22 @@
|
|||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="body-container">
|
||||
<body style="margin: 0; overflow: auto;">
|
||||
<div class="login-body-container">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="login-container">
|
||||
<div class="login-logo">
|
||||
<img src="${http_root}images/logo-tautulli-100.png" height="100" alt="PlexPy">
|
||||
</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="col-sm-6 col-sm-offset-3">
|
||||
<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">
|
||||
<label for="username" class="control-label">
|
||||
Username
|
||||
|
@ -69,6 +71,19 @@
|
|||
</form>
|
||||
</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> Sign In with Plex</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -78,25 +93,91 @@
|
|||
<script>
|
||||
$('#login-form').submit(function(event) {
|
||||
event.preventDefault();
|
||||
$('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i> 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> Sign In with Plex');
|
||||
} else {
|
||||
$('#sign-in').html('<i class="fa fa-refresh fa-spin"></i> Sign In');
|
||||
}
|
||||
|
||||
const username = plex ? null : $('#username').val();
|
||||
const password = plex ? null : $('#password').val();
|
||||
const remember_me = $('#remember_me').is(':checked') ? '1' : '0';
|
||||
|
||||
$.ajax({
|
||||
url: '${http_root}auth/signin',
|
||||
type: 'POST',
|
||||
data: $(this).serialize(),
|
||||
data: {
|
||||
username: username,
|
||||
password: password,
|
||||
token: token,
|
||||
remember_me: remember_me
|
||||
},
|
||||
dataType: 'json',
|
||||
statusCode: {
|
||||
200: function() {
|
||||
window.location = "${redirect_uri or http_root}";
|
||||
},
|
||||
401: function() {
|
||||
$('#incorrect-login').show();
|
||||
$('#username').focus();
|
||||
if (plex) {
|
||||
$('#sign-in-plex-alert').text('Invalid Plex Login.').show();
|
||||
} else {
|
||||
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||
$('#username').focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i> Sign In');
|
||||
$('.login-container button').prop('disabled', false);
|
||||
if (plex) {
|
||||
$('#sign-in-plex').html('<i class="fa fa-sign-in"></i> Sign In with Plex');
|
||||
} else {
|
||||
$('#sign-in').html('<i class="fa fa-sign-in"></i> 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>
|
||||
</body>
|
||||
|
|
|
@ -226,6 +226,45 @@ class PlexTV(object):
|
|||
|
||||
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):
|
||||
plextv_response = self.get_plex_auth(output_format='dict')
|
||||
|
||||
|
@ -819,3 +858,28 @@ class PlexTV(object):
|
|||
return True
|
||||
else:
|
||||
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
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
# Session tool to be loaded.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
from urllib import quote, unquote
|
||||
|
||||
import cherrypy
|
||||
|
@ -37,17 +36,27 @@ JWT_ALGORITHM = 'HS256'
|
|||
JWT_COOKIE_NAME = 'tautulli_token_'
|
||||
|
||||
|
||||
def user_login(username=None, password=None):
|
||||
if not username or not password:
|
||||
return None
|
||||
def user_login(username=None, password=None, token=None):
|
||||
user_token = None
|
||||
user_id = None
|
||||
|
||||
# Try to login to Plex.tv to check if the user has a vaild account
|
||||
plex_tv = PlexTV(username=username, password=password)
|
||||
plex_user = plex_tv.get_token()
|
||||
if plex_user:
|
||||
user_token = plex_user['auth_token']
|
||||
user_id = plex_user['user_id']
|
||||
if username and password:
|
||||
plex_tv = PlexTV(username=username, password=password)
|
||||
plex_user = plex_tv.get_token()
|
||||
if plex_user:
|
||||
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.
|
||||
# Also make sure guest access is enabled for the user and the user is not deleted.
|
||||
user_data = Users()
|
||||
|
@ -57,7 +66,7 @@ def user_login(username=None, password=None):
|
|||
return None
|
||||
elif plexpy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']:
|
||||
# Plex admin login
|
||||
return 'admin'
|
||||
return user_details, 'admin'
|
||||
elif not user_details['allow_guest'] or user_details['deleted_user']:
|
||||
# Guest access is disabled or the user is deleted.
|
||||
return None
|
||||
|
@ -75,49 +84,64 @@ def user_login(username=None, password=None):
|
|||
# Register the new user / update the access tokens.
|
||||
monitor_db = MonitorDatabase()
|
||||
try:
|
||||
logger.debug(u"Tautulli WebAuth :: Regestering tokens for user '%s' in the database." % username)
|
||||
result = monitor_db.action('UPDATE users SET user_token = ?, server_token = ? WHERE user_id = ?',
|
||||
[user_token, server_token, user_id])
|
||||
logger.debug(u"Tautulli WebAuth :: Regestering tokens for user '%s' in the database."
|
||||
% user_details['username'])
|
||||
result = monitor_db.action('UPDATE users SET server_token = ? WHERE user_id = ?',
|
||||
[server_token, user_details['user_id']])
|
||||
|
||||
if result:
|
||||
# Refresh the users list to make sure we have all the correct permissions.
|
||||
refresh_users()
|
||||
# Successful login
|
||||
return 'guest'
|
||||
return user_details, 'guest'
|
||||
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
|
||||
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
|
||||
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
|
||||
else:
|
||||
elif username:
|
||||
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for user '%s'." % username)
|
||||
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.
|
||||
Returns True and the user group on success or False and no user group"""
|
||||
|
||||
if plexpy.CONFIG.HTTP_PASSWORD:
|
||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
||||
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 username and password:
|
||||
if plexpy.CONFIG.HTTP_PASSWORD:
|
||||
user_details = {'user_id': None, 'username': username}
|
||||
|
||||
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
|
||||
plex_login = user_login(username, password)
|
||||
if plex_login is not None:
|
||||
return True, plex_login
|
||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
||||
return True, user_details, 'admin'
|
||||
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():
|
||||
|
@ -279,41 +303,33 @@ class AuthController(object):
|
|||
|
||||
@cherrypy.expose
|
||||
@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':
|
||||
cherrypy.response.status = 405
|
||||
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 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)
|
||||
expiry = datetime.utcnow() + time_delta
|
||||
|
||||
payload = {
|
||||
'user_id': user_id,
|
||||
'user': username,
|
||||
'user_id': user_details['user_id'],
|
||||
'user': user_details['username'],
|
||||
'user_group': user_group,
|
||||
'exp': expiry
|
||||
}
|
||||
|
||||
jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
|
||||
self.on_login(username=username,
|
||||
user_id=user_id,
|
||||
self.on_login(username=user_details['username'],
|
||||
user_id=user_details['user_id'],
|
||||
user_group=user_group,
|
||||
success=1)
|
||||
|
||||
|
@ -326,14 +342,36 @@ class AuthController(object):
|
|||
cherrypy.response.status = 200
|
||||
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)
|
||||
logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username)
|
||||
cherrypy.response.status = 401
|
||||
return error_message
|
||||
|
||||
else:
|
||||
elif 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
|
||||
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'}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue