TimotheeGerber / spotify-connect

Log on your Spotify Connect compatible devices through the terminal
MIT License
8 stars 1 forks source link

TokenType authorization_code #3

Open kingosticks opened 1 year ago

kingosticks commented 1 year ago

I randomly dumped the getInfo response from my Sonos Roam and noticed it was sending tokenType":"authorization_code" along with a clientID and all the usual stuff. I've not yet tried it with this tool to see what happens when I respond with an access token. Has anyone got this flow working?

TimotheeGerber commented 1 year ago

I don't have any official Spotify hardware. So I did not have the chance to test this tool on a device sending tokenType":"authorization_code". But keep in mind that 4 different authentication methods are proposed. You can switch the authentication mechanisms with the --auth-type flag followed by one of the 4 possibilities: reusable, password, default-token or access-token. Maybe one of them will let you connect on your Sonos Roam.

If it is not the case, you can try to intercept the traffic between the official client and your receiver. Or you can create a false receiver and try to connect with the official client to get the information it is sending. It would help to develop another authentication mechanisms if needed.

kingosticks commented 1 year ago

Yep, I'll give it a go. Although I suspect it'll need some extra code. Maybe we need to use the Hermes code endpoint (which librespot doesn't (yet) expose).

kingosticks commented 1 year ago

Unfortunately, not much progress. I couldn't get my Android client to do the addUser when using "tokenType": "authorization_code" unless I also use "clientID": "9b377073ea334637b1406f329ce005de" in my getInfo response. This is the client ID my Sonos Roam was returning in its getInfo response. I tried a bunch of other client IDs (keymaster, Android client, my own) but the Android client didn't seem to like them. Maybe there's a list of client IDs that are allowed to use this auth method.

When using the working clientID, the addUser request I got was:

{
  "action": "addUser",
  "blob": "AQC_0P91EOZDXSVldrLKtt5l1NoEecy8lvNvUF_wPXnZ8iPd22wq4lSA3xk4_WKxl4bmIsqz_-jYngzxoPwaSK.......Q3g",
  "clientKey": "",
  "deviceId": "f2eb0c419582c0b6690fa9ad4999b1b758ad38a3",
  "deviceName": "Pixel 6a",
  "loginId": "a969e0be850f......dc38f265a725",
  "tokenType": "authorization_code",
  "userName": "kingosticks4",
  "version": "2.7.1"
}

Maybe that's helpful to someone down the line who makes more progress.

kingosticks commented 1 year ago

I used mitm proxy to see what my desktop client was doing in response to "tokenType": "authorization_code". The client makes a POST request to https://accounts.spotify.com/api/token to exchange its (access?) token into an authorization_code. It sets a bunch of params, one of which is audience with the value taken from clientID in my getInfo response.

If I use the Sonos client ID, the client receives a valid response and it sends me the returned token in the blob field of its subsequent addUser request:

{
    "access_token": "AQC6oYY86uub...........................AGbcbx47Kvw",
    "expires_in": 600,
    "issued_token_type": "urn:spotify:params:oauth:authorization_code",
    "scope": "streaming",
    "token_type": "Bearer"
}

But if I use any other client ID, the client gets the following and gives up:

{
    "error": "invalid_target",
    "error_description": "audience: not supported"
}

For librespot, we could probably hardcode the Sonos client ID in getInfo responses for this token type. For the other side (something like this tool), we'd need to add support for converting credentials into an authorization_code. Either via HTTP (like my desktop client did) or maybe via Hermes (old way?) using the keymaster (hm://keymaster/code???). I'm not sure I care enough to do either but I had fun investigating and maybe useful for someone else.

TimotheeGerber commented 1 year ago

I also noticed that Spotify is usually making some checks on the clientID. It is not possible to send something gibberish or a known clientID. It would be refused.

Thanks for your research! Currently, I don't have time to code and test something. If you or anyone ever find the need for and the time, don't hesitate to make a PR!

kingosticks commented 1 year ago

No problem, I'll probably add some of this to the librespot docs at some point.

thlucas1 commented 4 months ago

Just curious if anyone has made progress on handling the tokentype=authorization_code type of requests?

It sounds like the client is exchanging an access token for an authorization_code token. If that is the case, isn't the access token bound to a specific clientId (Sonos constant of 9b377073ea334637b1406f329ce005de) and clientSecret (unknown / not public knowledge) value? You would need access to the access token that was generated for that clientid, which is stored on the client itself. In other words, I don't think you could generate an access token for that clientId without knowing the clientSecret value, no?

TimotheeGerber commented 3 months ago

No progress from my side. I don't have any official receiver, so I can't try anything. Sorry and good luck!

thlucas1 commented 3 months ago

@TimotheeGerber Thanks for replying. I currently don't have any Sonos devices either, but am in the process of getting a Sonos Symphonisk device. Will keep you posted.

kingosticks commented 3 months ago

So, reading again what I wrote last year, I think I must have been confused...

The client makes a POST request to https://accounts.spotify.com/api/token to exchange its (access?) token into an authorization_code

I'm not sure what I was thinking here. You cannot exchange a short-lived access token for an authorization code, that makes no sense. Surely the client was grabbing a new access token to then pass on to the sonos device for it to use. On top of this there is sanity checking of the audience parameter, for some reason?

So in theory the player should be able to use the access token with the Spotify Web API. In my efforts at https://github.com/librespot-org/librespot/pull/1098 I had lots of problems using that access token. However, I was trying to do something more complicated than simply use it for Web API requests, it might work just fine for that. I hope that makes sense.

thlucas1 commented 3 months ago

I guess my biggest question is what's in the Spotify Connect Zeroconf API addUser blob for tokentype=authorization_code?

I know the blob layout for the tokenType=default (seems to work for tokenType=accesstoken as well):

blob = bytearray()
write_int(0x49, blob)                           # 'I'
write_bytes(self.credentials.username, blob)    # username
write_int(0x50, blob)                           # 'P'
write_int(self.credentials.auth_type, blob)     # auth_type (0x00)
write_int(0x51, blob)                           # 'Q'
write_bytes(self.credentials.password, blob)    # password

One would think it would be some sort of serialized token, instead of a formatted blob structure?

My only goal at this point is to "wake" the Sonos device up, so that it will be listed in the Spotify Connect active device list. The fact that it is implementing the Spotify Connect Zeroconf API endpoints (addUser, resetUsers, and getInfo) tells me that it's possible.

kingosticks commented 3 months ago

I think the format is the same except password is replaced with access token. I vaguely remember dumping it out and the "access token" I got looked vaguely sensible. I think (I don't remember exactly) that's what I implemented in the PR I linked. I could try and resurrect this but time is more scarce these days.

thlucas1 commented 3 months ago

@kingosticks Thanks for the info.

Regarding the access token, can you recall if:

I would assume that it's a Spotify Webservices API access token, and am also assuming that it should have the streaming scope (or whatever is returned by the getInfo action response)?

I reviewed the PR changes, but did not see anything Sonos specific in that code. Note that I am not too familiar with librespot though, so no surprise there. I'm learning though!

kingosticks commented 3 months ago

It was a Spotify Web API token, my earlier post specifies the endpoint and the scope. Everything here is about Spotify, it just happens to be running on a Sonos device.

thlucas1 commented 2 months ago

@kingosticks Making some progress on this. I have traced the authorization flow (using Fiddler) of the Spotify desktop client. It performs the following steps:

Where I am stuck is the call to the Login5 endpoint. I need to find a way to use this endpoint with a Spotify userid and password (instead of a cached token), preferably in Python. The closest thing I could find is the spotify-login PHP code on GitHub, but not sure if that is what I need.

Am I even in the ballpark with this? Interested to hear your thoughts on it.

Thanks - Todd

kingosticks commented 2 months ago

I've actually been working on this again from a slightly different angle (desktop login) following Spotify's temporary breakage of user+password login, which we've since heard they intend to make permanent so librespot (and friends) need an alternative.

Spotify's desktop app login is currently as follows https://github.com/librespot-org/librespot/issues/1308#issuecomment-2258478905:

1. Oauth prompt opened in user's web browser at accounts.spotify.com for oauth flow, user completes and `code` is returned to app

2. Gets a `client-token` (can't remember how but we do this already)

3. Exchange `code` for short-lived access token with POST https://accounts.spotify.com/api/token (using `client-token` header),

4. Swap the temporary access token for stored credentials using Mercury `ClientResponseEncrypted` request of type `AUTHENTICATION_SPOTIFY_TOKEN` with our username and the `auth_data` is just the access token from step 3. Receives `APWelcome` response containing long-lived `reusable_auth_credentials`.

5. All future logins (Mercury and login5) are using reusable creds. e.g. POST https://login5.spotify.com/v3/login (using `client-token` header) sending `LoginRequest` using `stored_credential` `LoginRequest` method

And I've done a PR to support this oauth-style flow at https://github.com/librespot-org/librespot/pull/1309 which also documents some limitations I found when experimenting with session authentication using an access token.

User and password authentication with Login5 might still be possible in the future using the Android client-id and solving the hashcash challenges, that's not clear yet. That's what the Spotify Android app currently does but maybe they'll change that too.

So back to what we were trying to do here, I would expect that the Login5 call you see is using a "stored credentials" blob rather than an access token. I still think it's backwards to try and get an authorization code from an access token, so struggling to get my head around that but maybe, I've not looked again in detail at this flow again (yet).

He big take-away here is that user+password login is being deprecated by Spotify. So you'll need to factor that into your plans.

thlucas1 commented 2 months ago

@kingosticks Thanks for the info. I would be interested to learn how you get the client-token value as that is definitely in use, based on the Spotify desktop client trace I did. Here's the (redacted) trace if you're interested.

Traced via Fiddler

To re-produce the Spotify Connect authorization flow:

==================================================================================================================
** Spotify Login5 request
- I think this login is based on an existing token that is refreshed (e.g. header value "client-token" supplied).

--- Request --------------------------------------------------------------------
POST https://login5.spotify.com/v3/login HTTP/1.1
Host: login5.spotify.com
Connection: keep-alive
Content-Length: 338
Pragma: no-cache
Cache-Control: no-cache, no-store, max-age=0
Content-Type: application/x-protobuf
User-Agent: Spotify/124300420 Win32_x86_64/0 (PC desktop)
client-token: AADEtsyn61nyFc72pfD9QmqeeL7ktrrm+UuSqJ4VDl06aecKKf ... redacted ... MwYZw==
Origin: https://login5.spotify.com
Accept-Language: en-Latn-US,en-US;q=0.9,en-Latn;q=0.8,en;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br, zstd

K
 65b708073fc0480ea92a077233ca87bd'S-1-5-21-240303764-901663538-1355479652  
31l77y2a ... redacted ...  AQDwpox_h8mDuyQDk6bw8_jF0sgSjRpQjuRIS ... redacted ... 

--- Response ------------------------------------------------------------------
HTTP/1.1 200 OK
cache-control: private, max-age=0
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
date: Fri, 09 Aug 2024 16:32:39 GMT
server: envoy
Via: HTTP/2 edgeproxy, 1.1 google
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Length: 653

 
31l77y2a ... redacted ...  BQDkEds_dluzkCwIi8C4P8l8uRv00kAGYhjy5g2r ... redacted ... 

==================================================================================================================
** Spotify Connect Zeroconf "getInfo" request

--- Request --------------------------------------------------------------------
GET http://192.168.1.91:1400/spotifyzc?action=getInfo&version=2.7.1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
User-Agent: Spotify/124300420 Win32_x86_64/0 (PC desktop)
Host: 192.168.1.91:1400
Keep-Alive: 0
Accept-Encoding: gzip
Connection: keep-alive

--- Response ------------------------------------------------------------------
HTTP/1.1 200 OK
Content-length: 672
Content-type: application/json; charset=utf-8
Server: Linux UPnP/1.0 Sonos/79.1-54060 (ZPS33)
Connection: close

{
  "status":101,
  "statusString":"OK",
  "spotifyError":0,
  "version":"2.9.0",
  "deviceID":"f87342b3ad455375317d5af8aabde64a28bd49d0",
  "publicKey":"\/DJnNdfSnKIKZWqcJgvjUb1qeotdMIhSXy ... redacted ... ",
  "deviceType":"SPEAKER",
  "libraryVersion":"3.199.414-gea87b026",
  "resolverVersion":"0",
  "groupStatus":"NONE",
  "tokenType":"authorization_code",
  "clientID":"9b377073ea334637b1406f329ce005de",
  "productID":1233,
  "scope":"streaming",
  "availability":"",
  "supported_drm_media_formats":[{"drm":1,"formats":70}],
  "supported_capabilities":3,
  "modelDisplayName":"Bookshelf",
  "brandDisplayName":"Sonos",
  "remoteName":"Office"
}

==================================================================================================================
** token exchange request to https://accounts.spotify.com/api/token
- access token is exchanged for an authorization_code token.
- authorization_code token will be used as the blob parameter for the subsequent addUser request.
- where does the "client-token" header value come from?

--- Request --------------------------------------------------------------------
POST https://accounts.spotify.com/api/token HTTP/1.1
Host: accounts.spotify.com
Connection: keep-alive
Content-Length: 764
Content-Type: application/x-www-form-urlencoded
User-Agent: Spotify/124300420 Win32_x86_64/0 (PC desktop)
client-token: AADEtsyn61nyFc72pfD9QmqeeL7ktrrm+UuSqJ4VDl06aecKKf ... redacted ... MwYZw==
Origin: https://accounts.spotify.com
Accept-Language: en-Latn-US,en-US;q=0.9,en-Latn;q=0.8,en;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br, zstd

?audience=9b377073ea334637b1406f329ce005de
&client_id=65b708073fc0480ea92a077233ca87bd
&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&requested_token_type=urn%3Aspotify%3Aparams%3Aoauth%3Aauthorization_code
&resource=urn%3Aspotify%3Aresources%3Aconnect
&scope=streaming
&subject_token=BQBZuPAUW3M4WtjtAgyFIp3OvYQdgHQMbyvbtFPiP_7D ... redacted ... 
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token

--- Response ------------------------------------------------------------------
HTTP/1.1 200 OK
date: Fri, 09 Aug 2024 01:04:09 GMT
content-type: application/json
vary: Accept-Encoding
set-cookie: __Host-device_id=AQDo3-gBF9TVpq ... redacted ... ;Version=1;Path=/;Max-Age=2147483647;Secure;HttpOnly;SameSite=Lax
set-cookie: sp_tr=false;Version=1;Domain=accounts.spotify.com;Path=/;Secure;SameSite=Lax
access-control-allow-origin: https://accounts.spotify.com
access-control-allow-headers: User-Agent, Keep-Alive, Content-Type, Authorization, client-token, spotify-installation-id, dpop
access-control-allow-credentials: true
access-control-allow-methods: OPTIONS, GET, POST, DELETE, PUT
access-control-expose-headers: dpop-nonce
sp-trace-id: 079f5b692962fe83
x-envoy-upstream-service-time: 21
server: envoy
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
Via: HTTP/2 edgeproxy, 1.1 google
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Length: 290

{
  "access_token":"AQCTctoTVKQNifAaGgQoG1MG1mEN3vkE0UAnxCnT969G-jrOi ... redacted ... ",
  "expires_in":600,
  "issued_token_type":"urn:spotify:params:oauth:authorization_code",
  "scope":"streaming",
  "token_type":"Bearer",
}

==================================================================================================================
** Spotify Connect Zeroconf "addUser" request

--- Request --------------------------------------------------------------------

POST http://192.168.1.91:1400/spotifyzc HTTP/1.1
Content-Length: 370
Content-Type: application/x-www-form-urlencoded
User-Agent: Spotify/124300420 Win32_x86_64/0 (PC desktop)
Host: 192.168.1.91:1400
Keep-Alive: 0
Accept-Encoding: gzip
Connection: keep-alive

?action=addUser
&blob=AQCTctoTVKQNifAaGgQoG1MG1mEN3vkE0UAnxCnT969G-jrOi ... redacted ... 
&clientKey=
&deviceId=80da4987232671a83397682373f7ce21ea92f2e4
&deviceName=THL ... redacted ... 
&loginId=54119f7882aec70 ... redacted ... 
&tokenType=authorization_code
&userName=31l77y2a ... redacted ... 
&version=2.7.1

--- Response ------------------------------------------------------------------
HTTP/1.1 200 OK
Content-length: 69
Content-type: application/json; charset=utf-8
Server: Linux UPnP/1.0 Sonos/79.1-54060 (ZPS33)
Connection: close

{
  "status":101,
  "statusString":"OK",
  "spotifyError":0,
  "version":"2.9.0"
}

==================================================================================================================
kingosticks commented 2 months ago

https://github.com/librespot-org/librespot/blob/299b7dec20b45b9fa19a4a46252079e8a8b7a8ba/core/src/spclient.rs#L152

And here's an old example trace I have:

POST https://clienttoken.spotify.com/v1/clienttoken

[uint32]     1          1
[message]    2
[string]     2.1        1.2.14.1149.ga3ae422d
[string]     2.2        65b708073fc0480ea92a077233ca87bd
[message]    2.3
[message]    2.3.1
[message]    2.3.1.4
[uint32]     2.3.1.4.1  10
[uint32]     2.3.1.4.3  22621
[uint32]     2.3.1.4.4  2
[uint32]     2.3.1.4.6  9
[uint32]     2.3.1.4.7  332
[uint32]     2.3.1.4.8  34404
[string]     2.3.2      S-1-5-21-2654172357-4096885972-2531102750
TimotheeGerber commented 1 month ago

Thank you very much @kingosticks (and all the librespot community that helped you) for your PR about OAuth flow! As the username/password flow is broken (at least for me), I switched to the OAuth flow by default in this tool too. It works well with my versions of librespot at least. Don't know about officially supported hardware, I still don't have one.

@thlucas1 It seems almost everything is here to implement the authorization_code flow for Sonos. You can generate access token thanks to the new OAuth flow. Have a look to the AuthType::AccessToken here to see how to use the information from the device into your next request to get your authorization_code and send the correct addUser POST request. Good luck!

thlucas1 commented 1 month ago

@TimotheeGerber Thank you, I already had it working. There is an extra step for the user in my process to run a Python script to allow them to authorize the access request, but other than that it works great.
Thanks for reaching out.