Fix bug where authorisation token being truncated by sonos (#86)

This commit is contained in:
Simon J
2021-12-12 14:12:56 +11:00
committed by GitHub
parent 1c94654fb3
commit ddb26e11b8
4 changed files with 69 additions and 46 deletions

View File

@@ -39,7 +39,6 @@ import { axiosImageFetcher, ImageFetcher } from "./subsonic";
import { import {
JWTSmapiLoginTokens, JWTSmapiLoginTokens,
SmapiAuthTokens, SmapiAuthTokens,
smapiTokenFromString,
} from "./smapi_auth"; } from "./smapi_auth";
export const BONOB_ACCESS_TOKEN_HEADER = "bat"; export const BONOB_ACCESS_TOKEN_HEADER = "bat";
@@ -378,28 +377,23 @@ function server(
logger.info( logger.info(
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify( `${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
req.query req.query
)}, headers=${JSON.stringify({ ...req.headers, authorization: "*****" })}` )}, headers=${JSON.stringify({ ...req.headers, "bnbt": "*****", "bnbk": "*****" })}`
); );
const authHeader = E.fromNullable("Missing header");
const bearerToken = E.fromNullable("No Bearer token");
const serviceToken = pipe( const serviceToken = pipe(
authHeader(req.headers["authorization"] as string), E.fromNullable("Missing bnbt header")(req.headers["bnbt"] as string),
E.chain((authorization) => E.chain(token => pipe(
E.fromNullable("Missing bnbk header")(req.headers["bnbk"] as string),
E.map(key => ({ token, key }))
)),
E.chain((auth) =>
pipe( pipe(
authorization.match(/Bearer (?<token>.*)/), smapiAuthTokens.verify(auth),
bearerToken, E.mapLeft((_) => "Auth token failed to verify")
E.map((match) => match[1]!)
)
),
E.chain((bearerToken) =>
pipe(
smapiAuthTokens.verify(smapiTokenFromString(bearerToken)),
E.mapLeft((_) => "Bearer token failed to verify")
) )
), ),
E.getOrElseW(() => undefined) E.getOrElseW(() => undefined)
); )
if (!serviceToken) { if (!serviceToken) {
return res.status(401).send(); return res.status(401).send();

View File

@@ -32,7 +32,6 @@ import {
isExpiredTokenError, isExpiredTokenError,
MissingLoginTokenError, MissingLoginTokenError,
SmapiAuthTokens, SmapiAuthTokens,
smapiTokenAsString,
SMAPI_FAULT_LOGIN_UNAUTHORIZED, SMAPI_FAULT_LOGIN_UNAUTHORIZED,
ToSmapiFault, ToSmapiFault,
} from "./smapi_auth"; } from "./smapi_auth";
@@ -532,10 +531,14 @@ function bindSmapiSoapServiceToExpress(
httpHeaders: [ httpHeaders: [
{ {
httpHeader: { httpHeader: {
header: "Authorization", header: "bnbt",
value: `Bearer ${smapiTokenAsString( value: credentials.loginToken.token,
credentials.loginToken },
)}`, },
{
httpHeader: {
header: "bnbk",
value: credentials.loginToken.key,
}, },
}, },
], ],

View File

@@ -26,7 +26,7 @@ import i8n, { randomLang } from "../src/i8n";
import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi"; import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi";
import { Clock, SystemClock } from "../src/clock"; import { Clock, SystemClock } from "../src/clock";
import { formatForURL } from "../src/burn"; import { formatForURL } from "../src/burn";
import { ExpiredTokenError, SmapiAuthTokens, SmapiToken, smapiTokenAsString } from "../src/smapi_auth"; import { ExpiredTokenError, SmapiAuthTokens, SmapiToken } from "../src/smapi_auth";
describe("rangeFilterFor", () => { describe("rangeFilterFor", () => {
describe("invalid range header string", () => { describe("invalid range header string", () => {
@@ -754,7 +754,7 @@ describe("server", () => {
}); });
}); });
describe("when the Bearer token has expired", () => { describe("when the authorization has expired", () => {
it("should return a 401", async () => { it("should return a 401", async () => {
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken))) smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken)))
@@ -764,13 +764,15 @@ describe("server", () => {
pathname: `/stream/track/${trackId}` pathname: `/stream/track/${trackId}`
}) })
.path(), .path(),
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(401); expect(res.status).toEqual(401);
}); });
}); });
describe("when the Bearer token is valid", () => { describe("when the authorization token & key are valid", () => {
beforeEach(() => { beforeEach(() => {
smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken)); smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken));
}); });
@@ -795,7 +797,9 @@ describe("server", () => {
bonobUrl bonobUrl
.append({ pathname: `/stream/track/${trackId}`}) .append({ pathname: `/stream/track/${trackId}`})
.path() .path()
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(trackStream.status); expect(res.status).toEqual(trackStream.status);
expect(res.headers["content-type"]).toEqual( expect(res.headers["content-type"]).toEqual(
@@ -821,8 +825,10 @@ describe("server", () => {
.head(bonobUrl .head(bonobUrl
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
) )
.set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); .set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(404); expect(res.status).toEqual(404);
expect(res.body).toEqual({}); expect(res.body).toEqual({});
@@ -851,13 +857,15 @@ describe("server", () => {
bonobUrl bonobUrl
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(401); expect(res.status).toEqual(401);
}); });
}); });
describe("when the Bearer token is valid", () => { describe("when the authorization token & key is valid", () => {
beforeEach(() => { beforeEach(() => {
smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken)); smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken));
}); });
@@ -878,7 +886,9 @@ describe("server", () => {
bonobUrl bonobUrl
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(404); expect(res.status).toEqual(404);
@@ -910,7 +920,9 @@ describe("server", () => {
bonobUrl bonobUrl
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(stream.status); expect(res.status).toEqual(stream.status);
expect(res.headers["content-type"]).toEqual( expect(res.headers["content-type"]).toEqual(
@@ -950,7 +962,9 @@ describe("server", () => {
bonobUrl bonobUrl
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(stream.status); expect(res.status).toEqual(stream.status);
expect(res.headers["content-type"]).toEqual( expect(res.headers["content-type"]).toEqual(
@@ -988,7 +1002,9 @@ describe("server", () => {
bonobUrl bonobUrl
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(stream.status); expect(res.status).toEqual(stream.status);
expect(res.header["content-type"]).toEqual( expect(res.header["content-type"]).toEqual(
@@ -1027,7 +1043,9 @@ describe("server", () => {
bonobUrl bonobUrl
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); )
.set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key);
expect(res.status).toEqual(stream.status); expect(res.status).toEqual(stream.status);
expect(res.header["content-type"]).toEqual( expect(res.header["content-type"]).toEqual(
@@ -1072,7 +1090,8 @@ describe("server", () => {
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
) )
.set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`) .set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key)
.set("Range", requestedRange); .set("Range", requestedRange);
expect(res.status).toEqual(stream.status); expect(res.status).toEqual(stream.status);
@@ -1116,7 +1135,8 @@ describe("server", () => {
.append({ pathname: `/stream/track/${trackId}` }) .append({ pathname: `/stream/track/${trackId}` })
.path() .path()
) )
.set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`) .set('bnbt', smapiAuthToken.token)
.set('bnbk', smapiAuthToken.key)
.set("Range", "4000-5000"); .set("Range", "4000-5000");
expect(res.status).toEqual(stream.status); expect(res.status).toEqual(stream.status);

View File

@@ -58,7 +58,7 @@ import { iconForGenre } from "../src/icon";
import { formatForURL } from "../src/burn"; import { formatForURL } from "../src/burn";
import { range } from "underscore"; import { range } from "underscore";
import { FixedClock } from "../src/clock"; import { FixedClock } from "../src/clock";
import { ExpiredTokenError, InvalidTokenError, SmapiAuthTokens, SmapiToken, smapiTokenAsString, ToSmapiFault } from "../src/smapi_auth"; import { ExpiredTokenError, InvalidTokenError, SmapiAuthTokens, SmapiToken, ToSmapiFault } from "../src/smapi_auth";
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value); const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
@@ -3046,14 +3046,20 @@ describe("wsdl api", () => {
pathname: `/stream/track/${trackId}`, pathname: `/stream/track/${trackId}`,
}) })
.href(), .href(),
httpHeaders: { httpHeaders: [
httpHeader: [ {
{ httpHeader: [{
header: "Authorization", header: "bnbt",
value: `Bearer ${smapiTokenAsString(smapiAuthToken)}`, value: smapiAuthToken.token,
}, }],
], },
}, {
httpHeader: [{
header: "bnbk",
value: smapiAuthToken.key,
}],
}
],
}); });
expect(musicService.login).toHaveBeenCalledWith(serviceToken); expect(musicService.login).toHaveBeenCalledWith(serviceToken);