Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TokenType authorization_code #3

Open
kingosticks opened this issue Jan 18, 2023 · 20 comments
Open

TokenType authorization_code #3

kingosticks opened this issue Jan 18, 2023 · 20 comments

Comments

@kingosticks
Copy link

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
Copy link
Owner

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
Copy link
Author

kingosticks commented Jan 18, 2023

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
Copy link
Author

kingosticks commented Jan 20, 2023

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
Copy link
Author

kingosticks commented Jan 20, 2023

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
Copy link
Owner

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
Copy link
Author

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

@thlucas1
Copy link

thlucas1 commented Jul 4, 2024

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
Copy link
Owner

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

@thlucas1
Copy link

thlucas1 commented Jul 9, 2024

@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
Copy link
Author

kingosticks commented Jul 9, 2024

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 librespot-org/librespot#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
Copy link

thlucas1 commented Jul 9, 2024

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
Copy link
Author

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
Copy link

thlucas1 commented Jul 9, 2024

@kingosticks
Thanks for the info.

Regarding the access token, can you recall if:

  • was it for a Spotify Webservices API access token? If so, there should have been a request made to https://accounts.spotify.com/api/token to request / renew a Spotify authorization token.
  • was it for a Sonos Control API access token?

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
Copy link
Author

kingosticks commented Jul 10, 2024

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
Copy link

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

  • POST https://login5.spotify.com/v3/login
    This appears to login to Spotify using a cached access token. I need to find a way to use this endpoint with a Spotify userid and password (instead of a cached token).
  • GET http://192.168.1.91:1400/spotifyzc?action=getInfo
    Does a Spotify Connect Zeroconf getInfo request to retrieve Spotify Connect device details.
  • POST https://accounts.spotify.com/api/token
    Does a token exchange of the Login5 access-token (subject_token), and returns an authorization_code token type (requested_token_type). You weren't confused from what you wrote last year; it does appear to be converting the access token to an authorization_code token type. Note the audience argument that contains the Sonos app id (9b377073ea334637b1406f329ce005de) - I believe this is what allows the Sonos device to do what it needs to do to access Spotify resources on behalf of the user. The client_id must also use the Spotify desktop app id value (65b708073fc0480ea92a077233ca87bd), otherwise it returns a client_id not allowed error for the exchange request.
?audience=9b377073ea334637b1406f329ce005de
&client_id=65b708073fc0480ea92a077233ca87bd
&grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:spotify:params:oauth:authorization_code
&resource=urn:spotify:resources:connect
&scope=streaming
&subject_token=BQBZuPAUW ...redacted
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
  • POST http://192.168.1.91:1400/spotifyzc
    Does a Spotify Connect Zeroconf addUser request to login the user to Spotify Connect on the Sonos device. The blob parameter contains the authorization_code token returned by the previous token-exchange.

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
Copy link
Author

kingosticks commented Aug 10, 2024

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 librespot-org/librespot#1308 (comment):

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 librespot-org/librespot#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
Copy link

@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:

  • ensure Spotify Desktop Client App is shutdown.
  • open Fiddler and start tracing.
  • issue "resetUsers" request to Sonos device via PostMan (kills the Spotify Connect session).
  • open Spotify Desktop Client App, and select the device to connect to (Sonos "Office" in this example).
  • wait for the music to start playing on the speaker.
  • stop the Fiddler trace.
  • "pause" play of track in Spotify desktop app.
  • analyze the trace:
    • search for last "?action=getInfo" call (getInfo request) - analyze this.
    • getInfo should be followed by the https://accounts.spotify.com/api/token - analyze this.
    • search for "addUser" call (addUser request) - analyze this.
==================================================================================================================
** 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
Copy link
Author

kingosticks commented Aug 10, 2024

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
Copy link
Owner

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
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants