Skip to content

Commit afc993f

Browse files
committed
use login5 authentication instead of keymaster
fixes #306
1 parent 3a6ce32 commit afc993f

File tree

2 files changed

+72
-28
lines changed

2 files changed

+72
-28
lines changed

librespot/core.py

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from Cryptodome.PublicKey import RSA
3030
from Cryptodome.Signature import PKCS1_v1_5
3131

32-
from librespot import util, oauth
32+
from librespot import util
3333
from librespot import Version
3434
from librespot.audio import AudioKeyManager
3535
from librespot.audio import CdnManager
@@ -57,6 +57,8 @@
5757
from librespot.proto import Metadata_pb2 as Metadata
5858
from librespot.proto import Playlist4External_pb2 as Playlist4External
5959
from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate
60+
from librespot.proto.spotify.login5.v3 import Login5_pb2 as Login5
61+
from librespot.proto.spotify.login5.v3.credentials import Credentials_pb2 as Login5Credentials
6062
from librespot.structure import Closeable
6163
from librespot.structure import MessageListener
6264
from librespot.structure import RequestListener
@@ -908,6 +910,8 @@ class Session(Closeable, MessageListener, SubListener):
908910
__dealer_client: typing.Union[DealerClient, None] = None
909911
__event_service: typing.Union[EventService, None] = None
910912
__keys: DiffieHellman
913+
__login5_access_token: typing.Union[str, None] = None
914+
__login5_token_expiry: typing.Union[int, None] = None
911915
__mercury_client: MercuryClient
912916
__receiver: typing.Union[Receiver, None] = None
913917
__search: typing.Union[SearchManager, None]
@@ -968,6 +972,7 @@ def authenticate(self,
968972
969973
"""
970974
self.__authenticate_partial(credential, False)
975+
self.__authenticate_login5()
971976
with self.__auth_lock:
972977
self.__mercury_client = MercuryClient(self)
973978
self.__token_provider = TokenProvider(self)
@@ -1203,6 +1208,13 @@ def is_valid(self) -> bool:
12031208
self.__wait_auth_lock()
12041209
return self.__ap_welcome is not None and self.connection is not None
12051210

1211+
def login5(self) -> tuple[str, int]:
1212+
""" """
1213+
self.__wait_auth_lock()
1214+
if self.__login5_access_token is None or self.__login5_token_expiry is None:
1215+
raise RuntimeError("Session isn't authenticated!")
1216+
return self.__login5_access_token, self.__login5_token_expiry
1217+
12061218
def mercury(self) -> MercuryClient:
12071219
""" """
12081220
self.__wait_auth_lock()
@@ -1382,6 +1394,43 @@ def __authenticate_partial(self,
13821394
else:
13831395
raise RuntimeError("Unknown CMD 0x" + packet.cmd.hex())
13841396

1397+
def __authenticate_login5(self) -> None:
1398+
"""Authenticate using Login5 to get access token"""
1399+
login5_request = Login5.LoginRequest()
1400+
login5_request.client_info.client_id = MercuryRequests.keymaster_client_id
1401+
login5_request.client_info.device_id = self.__inner.device_id
1402+
1403+
# Set stored credential from APWelcome
1404+
if hasattr(self, '_Session__ap_welcome') and self.__ap_welcome:
1405+
stored_cred = Login5Credentials.StoredCredential()
1406+
stored_cred.username = self.__ap_welcome.canonical_username
1407+
stored_cred.data = self.__ap_welcome.reusable_auth_credentials
1408+
login5_request.stored_credential.CopyFrom(stored_cred)
1409+
1410+
response = requests.post(
1411+
"https://login5.spotify.com/v3/login",
1412+
data=login5_request.SerializeToString(),
1413+
headers={
1414+
"Content-Type": "application/x-protobuf",
1415+
"Accept": "application/x-protobuf"
1416+
}
1417+
)
1418+
1419+
if response.status_code == 200:
1420+
login5_response = Login5.LoginResponse()
1421+
login5_response.ParseFromString(response.content)
1422+
1423+
if login5_response.HasField('ok'):
1424+
self.__login5_access_token = login5_response.ok.access_token
1425+
self.__login5_token_expiry = int(time.time()) + login5_response.ok.access_token_expires_in
1426+
self.logger.info("Login5 authentication successful, got access token")
1427+
else:
1428+
self.logger.warning("Login5 authentication failed: {}".format(login5_response.error))
1429+
else:
1430+
self.logger.warning("Login5 request failed with status: {}".format(response.status_code))
1431+
else:
1432+
self.logger.error("Login5 authentication failed: No APWelcome found")
1433+
13851434
def __send_unchecked(self, cmd: bytes, payload: bytes) -> None:
13861435
self.cipher_pair.send_encoded(self.connection, cmd, payload)
13871436

@@ -2258,7 +2307,7 @@ class TokenProvider:
22582307
__tokens: typing.List[StoredToken] = []
22592308

22602309
def __init__(self, session: Session):
2261-
self._session = session
2310+
self.__session = session
22622311

22632312
def find_token_with_all_scopes(
22642313
self, scopes: typing.List[str]) -> typing.Union[StoredToken, None]:
@@ -2289,24 +2338,27 @@ def get_token(self, *scopes) -> StoredToken:
22892338
scopes = list(scopes)
22902339
if len(scopes) == 0:
22912340
raise RuntimeError("The token doesn't have any scope")
2341+
22922342
token = self.find_token_with_all_scopes(scopes)
22932343
if token is not None:
22942344
if token.expired():
22952345
self.__tokens.remove(token)
22962346
else:
22972347
return token
2298-
self.logger.debug(
2299-
"Token expired or not suitable, requesting again. scopes: {}, old_token: {}"
2300-
.format(scopes, token))
2301-
response = self._session.mercury().send_sync_json(
2302-
MercuryRequests.request_token(self._session.device_id(),
2303-
",".join(scopes)))
2304-
token = TokenProvider.StoredToken(response)
2305-
self.logger.debug(
2306-
"Updated token successfully! scopes: {}, new_token: {}".format(
2307-
scopes, token))
2308-
self.__tokens.append(token)
2309-
return token
2348+
2349+
login5_token = None
2350+
login5_access_token, login5_token_expiry = self.__session.login5()
2351+
if int(time.time()) < login5_token_expiry - 60: # 60 second buffer
2352+
login5_token = TokenProvider.StoredToken({
2353+
"expiresIn": login5_token_expiry - int(time.time()),
2354+
"accessToken": login5_access_token,
2355+
"scope": scopes
2356+
})
2357+
self.__tokens.append(login5_token)
2358+
self.logger.debug("Using Login5 access token for scopes: {}".format(scopes))
2359+
else:
2360+
self.logger.debug("Login5 token expired, need to re-authenticate")
2361+
return login5_token
23102362

23112363
class StoredToken:
23122364
""" """

librespot/proto/spotify/login5/v3/Login5_pb2.py

Lines changed: 6 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)