sonos

Calls to Sonos API don't include credential information after being authenticated


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.

getAppLink

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

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"

reportAccountAction

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

getMetadata

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.


Solution

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