Skip to content

Commit ea99c22

Browse files
committed
Add utils.plexJWTAuth helper function
1 parent 06f4fba commit ea99c22

File tree

1 file changed

+67
-4
lines changed

1 file changed

+67
-4
lines changed

plexapi/utils.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import time
1111
import unicodedata
12+
import uuid
1213
import warnings
1314
import zipfile
1415
from collections import deque
@@ -537,13 +538,17 @@ def createMyPlexDevice(headers, account, timeout=10): # pragma: no cover
537538
538539
Parameters:
539540
headers (dict): Provide the X-Plex- headers for the new device.
540-
A unique X-Plex-Client-Identifier is required.
541+
A unique X-Plex-Client-Identifier is required or one will be generated if not provided.
541542
account (MyPlexAccount): The Plex account to create the device on.
542543
timeout (int): Timeout in seconds to wait for device login.
543544
"""
544545
from plexapi.myplex import MyPlexPinLogin
545546

546-
if 'X-Plex-Client-Identifier' not in headers:
547+
if not headers:
548+
client_identifier = generateUUID()
549+
headers = {'X-Plex-Client-Identifier': client_identifier}
550+
print(f'No X-Plex-Client-Identifier provided, generated: {client_identifier}')
551+
elif 'X-Plex-Client-Identifier' not in headers:
547552
raise BadRequest('The X-Plex-Client-Identifier header is required.')
548553

549554
clientIdentifier = headers['X-Plex-Client-Identifier']
@@ -561,13 +566,17 @@ def plexOAuth(headers, forwardUrl=None, timeout=120): # pragma: no cover
561566
562567
Parameters:
563568
headers (dict): Provide the X-Plex- headers for the new device.
564-
A unique X-Plex-Client-Identifier is required.
569+
A unique X-Plex-Client-Identifier is required or one will be generated if not provided.
565570
forwardUrl (str, optional): The url to redirect the client to after login.
566571
timeout (int, optional): Timeout in seconds to wait for device login. Default 120 seconds.
567572
"""
568573
from plexapi.myplex import MyPlexAccount, MyPlexPinLogin
569574

570-
if 'X-Plex-Client-Identifier' not in headers:
575+
if not headers:
576+
client_identifier = generateUUID()
577+
headers = {'X-Plex-Client-Identifier': client_identifier}
578+
print(f'No X-Plex-Client-Identifier provided, generated: {client_identifier}')
579+
elif 'X-Plex-Client-Identifier' not in headers:
571580
raise BadRequest('The X-Plex-Client-Identifier header is required.')
572581

573582
pinlogin = MyPlexPinLogin(headers=headers, oauth=True)
@@ -583,6 +592,56 @@ def plexOAuth(headers, forwardUrl=None, timeout=120): # pragma: no cover
583592
print('Login failed.')
584593

585594

595+
def plexJWTAuth(headers=None, token=None, jwtToken=None, keypair=(None, None), scope=None): # pragma: no cover
596+
""" Helper function for Plex JWT authentication using initial Plex OAuth.
597+
598+
Parameters:
599+
headers (dict, optional): Provide the X-Plex- headers for the new device.
600+
A unique X-Plex-Client-Identifier is required or one will be generated if not provided.
601+
token (str, optional): The Plex token to use for initial device registration.
602+
If not provided, Plex OAuth will be used to obtain a token.
603+
jwtToken (str, optional): The Plex JWT (JSON Web Token) to use for authentication.
604+
If provided, the JWT will be validated and refreshed if necessary.
605+
keypair (tuple, optional): A tuple of the ED25519 (privateKey, publicKey) to use for signing the JWT.
606+
If not provided, a new keypair will be generated and saved to 'private.key' and 'public.key'.
607+
scope (list[str], optional): List of scopes to request in the JWT.
608+
"""
609+
from plexapi.myplex import MyPlexAccount, MyPlexJWTAuth
610+
611+
if not headers:
612+
client_identifier = generateUUID()
613+
headers = {'X-Plex-Client-Identifier': client_identifier}
614+
print(f'No X-Plex-Client-Identifier provided, generated: {client_identifier}')
615+
elif 'X-Plex-Client-Identifier' not in headers:
616+
raise BadRequest('The X-Plex-Client-Identifier header is required.')
617+
618+
if not token and not jwtToken:
619+
account = plexOAuth(headers)
620+
token = account.authenticationToken
621+
622+
jwtauth = MyPlexJWTAuth(headers=headers, token=token, jwtToken=jwtToken, keypair=keypair)
623+
if jwtToken and (not keypair[0] or not keypair[1]):
624+
raise BadRequest('When providing a jwtToken, the corresponding ED25519 keypair is required.')
625+
626+
if not keypair[0] or not keypair[1]:
627+
jwtauth.generateKeypair(keyfiles=('private.key', 'public.key'))
628+
print('Generated new ED25519 keypair and saved to "private.key" and "public.key".')
629+
630+
if not jwtToken:
631+
jwtauth.registerDevice()
632+
jwtauth.refreshJWT(scope=scope)
633+
print('Registered new device and obtained JWT token.')
634+
elif not jwtauth.verifyJWT():
635+
jwtauth.refreshJWT(scope=scope)
636+
print('Refreshed expired/invalid JWT token.')
637+
638+
if jwtauth.jwtToken:
639+
print('JWT authentication successful!')
640+
return MyPlexAccount(token=jwtauth.jwtToken)
641+
else:
642+
print('JWT authentication failed.')
643+
644+
586645
def choose(msg, items, attr): # pragma: no cover
587646
""" Command line helper to display a list of choices, asking the
588647
user to choose one of the options.
@@ -735,3 +794,7 @@ def parseXMLString(s: str):
735794
except ElementTree.ParseError: # If it fails, clean the string and try again
736795
cleaned_s = cleanXMLString(s).encode('utf-8')
737796
return ElementTree.fromstring(cleaned_s) if cleaned_s.strip() else None
797+
798+
799+
def generateUUID() -> str:
800+
return str(uuid.uuid4())

0 commit comments

Comments
 (0)