I'm developing a "content service" for Sonos. I'm using OAuth as my Authentication Method in the Sonos sandbox.
I have implemented browser level authentication. The flow seems to work up to getMetadata
but at this point no credentials/authentication tokens are sent, so I can't lookup the correct user to return the metadata.
The first call is to getAppLink:
<s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<credentials
xmlns="http://www.sonos.com/Services/1.1">
<deviceId>00-00-00-00-00-00:0</deviceId>
<deviceProvider>Sonos</deviceProvider>
</credentials>
</s:Header>
<s:Body>
<getAppLink
xmlns="http://www.sonos.com/Services/1.1">
<householdId>Sonos_SNBSD7YPBwpgYAw5W6b8Gf4TGS_211454cc</householdId>
<hardware>Google:sunfish:Pixel 4a</hardware>
<osVersion>13</osVersion>
<sonosAppName>ACR_:Google:sunfish:Pixel 4a</sonosAppName>
<callbackPath>sonos-2://x-callback-url/addAccount?state=iid%3Dcom.exam.sonos%26hhid%3DSonos_SNBSD7YPBwpgYAw5W6b8Gf4TGS.EKgI4Te8ewA7RL2sGRBD%26callbackPath%3D%2FaddAccount</callbackPath>
</getAppLink>
</s:Body>
</s:Envelope>
I reply:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://www.sonos.com/Services/1.1">
<soap:Body>
<getAppLinkResponse
xmlns="http://www.sonos.com/Services/1.1">
<getAppLinkResult>
<authorizeAccount>
<appUrlStringId>AppLinkMessage</appUrlStringId>
<deviceLink>
<regUrl>https://sonos.exam.com/login?linkCode=6f7b278b-097e-49a1-a393-879c0409b61b</regUrl>
<linkCode>6f7b278b-097e-49a1-a393-879c0409b61b</linkCode>
<showLinkCode>false</showLinkCode>
</deviceLink>
</authorizeAccount>
</getAppLinkResult>
</getAppLinkResponse>
</soap:Body>
</soap:Envelope>
The HTTP log is:
"POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/79.1-54060"
In the app this then redirects me to the regUrl
and I log in successfully.
getDeviceAuthToken
is polled with:
<s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<credentials
xmlns="http://www.sonos.com/Services/1.1">
<deviceId>00-00-00-00-00-00:0</deviceId>
<deviceProvider>Sonos</deviceProvider>
</credentials>
</s:Header>
<s:Body>
<getDeviceAuthToken
xmlns="http://www.sonos.com/Services/1.1">
<householdId>Sonos_SNBSD7YPBwpgYAw5W6b8Gf4TGS_211454cc</householdId>
<linkCode>6f7b278b-097e-49a1-a393-879c0409b61b</linkCode>
</getDeviceAuthToken>
</s:Body>
</s:Envelope>
We reply:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://www.sonos.com/Services/1.1">
<soap:Body>
<getDeviceAuthTokenResponse
xmlns="http://www.sonos.com/Services/1.1">
<getDeviceAuthTokenResult>
<authToken>mytoken</authToken>
<privateKey>mykey</privateKey>
<userInfo>
<nickname>nickname</nickname>
<userIdHashCode>hashcode</userIdHashCode>
</userInfo>
</getDeviceAuthTokenResult>
</getDeviceAuthTokenResponse>
</soap:Body>
</soap:Envelope>
(I've shortened the responses above).
The HTTP log is:
"POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/79.1-54060"
We then receive (note the token
and key
is included inside loginToken
):
<s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<credentials
xmlns="http://www.sonos.com/Services/1.1">
<deviceId>00-00-00-00-00-00:0</deviceId>
<deviceProvider>Sonos</deviceProvider>
<loginToken>
<token>mytoken</token>
<key>mykey</key>
<householdId>Sonos_SNBSD7YPBwpgYAw5W6b8Gf4TGS_211454cc</householdId>
</loginToken>
</credentials>
</s:Header>
<s:Body>
<reportAccountAction
xmlns="http://www.sonos.com/Services/1.1">
<type>addAccount</type>
</reportAccountAction>
</s:Body>
</s:Envelope>
And we dutifully reply:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://www.sonos.com/Services/1.1">
<soap:Body>
<reportAccountActionResponse
xmlns="http://www.sonos.com/Services/1.1">
<reportAccountActionResult></reportAccountActionResult>
</reportAccountActionResponse>
</soap:Body>
</soap:Envelope>
Again, this is from Linux UPnP/1.0 Sonos/79.1-54060
This is where it goes wrong. We receive the getMetadata
call without the token
and key
inside loginToken
:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://www.sonos.com/Services/1.1">
<soap:Header>
<credentials
xmlns="http://www.sonos.com/Services/1.1">
<deviceProvider>Sonos</deviceProvider>
<loginToken>
<householdId>Sonos_SNBSD7YPBwpgYAw5W6b8Gf4TGS_211454cc</householdId>
</loginToken>
</credentials>
<ns:context>
<contentFiltering>
<explicit>false</explicit>
</contentFiltering>
<ns:timeZone>+01:00</ns:timeZone>
</ns:context>
</soap:Header>
<soap:Body>
<ns:getMetadata
xmlns="http://www.sonos.com/Services/1.1">
<id>root</id>
<index>0</index>
<count>100</count>
</ns:getMetadata>
</soap:Body>
</soap:Envelope>
And so we respond:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://www.sonos.com/Services/1.1">
<soap:Body>
<soap:Fault>
<faultcode>Client.LoginUnauthorized</faultcode>
<faultstring>Failed to authenticate, try Re-Authorising your account in the sonos app</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>
The interesting thing is this is coming from a different client:
"POST /ws/sonos HTTP/1.1" 200 - "-" "pdsw-app-passport-android/80.6.04 (Android 13; Pixel 4a; google; sunfish) (Sonos/Universal-Content-Service 1.1.678)"
If I use the Sonos web app we get the same call and response, this time with this user-agent:
"POST /ws/sonos HTTP/1.1" 200 - "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0 (Sonos/Universal-Content-Service 1.1.678)"
So I suppose I shouldn't be surprised - how would the other clients get the token to use? But I didn't read anything in the docs about how to handle this.
Following discussion with Sonos developer support, it turns out that the token
and key
arguments to loginToken
are only passed in the first post-authorization call (in this case, to reportAccountAction
).
Subsequently, the token only is passed in the Authorization
HTTP header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5SbGMzUkFaV3h6ZEdWdWMyOW1kSGRoY21VdVkyOXRJaXdpY0dGemMzZHZjbVFpT2lKWFNYUjJORVp6VlhWS1dFSjFZMlUyV0RGdk1pSjkiLCJpYXQiOjE3Mjc4ODI5MDYsImV4cCI6MTcyNzg4NjUwNn0.bb1pak6D3aoFeT4DEB-75w-hA-UHoE8K_nlyREc5ZU8
This means your SMAPI server must associate the token with the other information you require to successfully perform any authentication on your backend service.