diff --git a/src/server.ts b/src/server.ts index d128ef8..8b06337 100644 --- a/src/server.ts +++ b/src/server.ts @@ -36,7 +36,7 @@ import morgan from "morgan"; import { takeWithRepeats } from "./utils"; import { parse } from "./burn"; import { axiosImageFetcher, ImageFetcher } from "./subsonic"; -import { JWTSmapiLoginTokens, SmapiAuthTokens, SmapiToken } from "./smapi_auth"; +import { JWTSmapiLoginTokens, SmapiAuthTokens, smapiTokenFromString } from "./smapi_auth"; export const BONOB_ACCESS_TOKEN_HEADER = "bat"; @@ -371,7 +371,7 @@ function server( logger.info( `${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify( req.query - )}, headers=${JSON.stringify({ ...req.headers, "authorization": "***" })}` + )}, headers=${JSON.stringify({ ...req.headers, "authorization": "*****" })}` ); const authHeader = E.fromNullable("Missing header"); @@ -384,7 +384,7 @@ function server( E.map(match => match[1]!) )), E.chain(bearerToken => pipe( - smapiAuthTokens.verify(bearerToken as unknown as SmapiToken), + smapiAuthTokens.verify(smapiTokenFromString(bearerToken)), E.mapLeft(_ => "Bearer token failed to verify") )), E.getOrElseW(() => undefined) diff --git a/src/smapi.ts b/src/smapi.ts index c81f391..405f89b 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -459,30 +459,30 @@ function bindSmapiSoapServiceToExpress( }, }), refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders) => - pipe( - auth(soapyHeaders?.credentials), - E.map(({ serviceToken }) => smapiAuthTokens.issue(serviceToken)), - E.map((newToken) => ({ - authToken: newToken.token, - privateKey: newToken.key, - })), - E.orElse((fault) => - pipe( - fault.toSmapiFault(smapiAuthTokens), - E.fromPredicate(isSmapiRefreshTokenResultFault, (_) => fault), - E.map((it) => it.Fault.detail.refreshAuthTokenResult) - ) - ), - E.map((newToken) => ({ - refreshAuthTokenResult: { - authToken: newToken.authToken, - privateKey: newToken.privateKey, - }, - })), - E.getOrElseW((fault) => { - throw fault.toSmapiFault(smapiAuthTokens); - }) + pipe( + auth(soapyHeaders?.credentials), + E.map(({ serviceToken }) => smapiAuthTokens.issue(serviceToken)), + E.map((newToken) => ({ + authToken: newToken.token, + privateKey: newToken.key, + })), + E.orElse((fault) => + pipe( + fault.toSmapiFault(smapiAuthTokens), + E.fromPredicate(isSmapiRefreshTokenResultFault, (_) => fault), + E.map((it) => it.Fault.detail.refreshAuthTokenResult) + ) ), + E.map((newToken) => ({ + refreshAuthTokenResult: { + authToken: newToken.authToken, + privateKey: newToken.privateKey, + }, + })), + E.getOrElseW((fault) => { + throw fault.toSmapiFault(smapiAuthTokens); + }) + ), getMediaURI: async ( { id }: { id: string }, _, diff --git a/src/smapi_auth.ts b/src/smapi_auth.ts index e35df38..53f811a 100644 --- a/src/smapi_auth.ts +++ b/src/smapi_auth.ts @@ -57,17 +57,17 @@ export class InvalidTokenError extends Error implements ToSmapiFault { export class ExpiredTokenError extends Error implements ToSmapiFault { _tag = "ExpiredTokenError"; - authToken: string; + serviceToken: string; expiredAt: number; - constructor(authToken: string, expiredAt: number) { + constructor(serviceToken: string, expiredAt: number) { super("SMAPI token has expired"); - this.authToken = authToken; + this.serviceToken = serviceToken; this.expiredAt = expiredAt; } toSmapiFault = (smapiAuthTokens: SmapiAuthTokens) => { - const newToken = smapiAuthTokens.issue(this.authToken) + const newToken = smapiAuthTokens.issue(this.serviceToken) return { Fault: { faultcode: "Client.TokenRefreshRequired", @@ -142,8 +142,8 @@ export class JWTSmapiLoginTokens implements SmapiAuthTokens { return right((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key) as any).serviceToken); } catch (e) { if(isTokenExpiredError(e)) { - const x = ((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key, { ignoreExpiration: true })) as any).serviceToken; - return left(new ExpiredTokenError(x, e.expiredAt)) + const serviceToken = ((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key, { ignoreExpiration: true })) as any).serviceToken; + return left(new ExpiredTokenError(serviceToken, e.expiredAt)) } else if(isError(e)) return left(new InvalidTokenError(e.message)); else diff --git a/tests/server.test.ts b/tests/server.test.ts index 327a050..9284ff7 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -26,7 +26,7 @@ import i8n, { randomLang } from "../src/i8n"; import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi"; import { Clock, SystemClock } from "../src/clock"; import { formatForURL } from "../src/burn"; -import { ExpiredTokenError, SmapiAuthTokens } from "../src/smapi_auth"; +import { ExpiredTokenError, SmapiAuthTokens, SmapiToken, smapiTokenAsString } from "../src/smapi_auth"; describe("rangeFilterFor", () => { describe("invalid range header string", () => { @@ -750,9 +750,9 @@ describe("server", () => { } ); - const serviceToken = uuid(); - const trackId = uuid(); - const smapiAuthToken = `smapiAuthToken-${uuid()}`; + const serviceToken = `serviceToken-${uuid()}`; + const trackId = `t-${uuid()}`; + const smapiAuthToken: SmapiToken = { token: `token-${uuid()}`, key: `key-${uuid()}` }; const streamContent = (content: string) => ({ pipe: (_: Transform) => { @@ -777,7 +777,7 @@ describe("server", () => { describe("when the Bearer token has expired", () => { it("should return a 401", async () => { - smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(smapiAuthToken, 0))) + smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0))) const res = await request(server).head( bonobUrl @@ -785,7 +785,7 @@ describe("server", () => { pathname: `/stream/track/${trackId}` }) .path(), - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(401); }); @@ -816,7 +816,7 @@ describe("server", () => { bonobUrl .append({ pathname: `/stream/track/${trackId}`}) .path() - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(trackStream.status); expect(res.headers["content-type"]).toEqual( @@ -843,7 +843,7 @@ describe("server", () => { .append({ pathname: `/stream/track/${trackId}` }) .path() ) - .set('Authorization', `Bearer ${smapiAuthToken}`); + .set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(404); expect(res.body).toEqual({}); @@ -865,14 +865,14 @@ describe("server", () => { describe("when the Bearer token has expired", () => { it("should return a 401", async () => { - smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(smapiAuthToken, 0))) + smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0))) const res = await request(server) .get( bonobUrl .append({ pathname: `/stream/track/${trackId}` }) .path() - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(401); }); @@ -899,7 +899,7 @@ describe("server", () => { bonobUrl .append({ pathname: `/stream/track/${trackId}` }) .path() - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(404); @@ -931,7 +931,7 @@ describe("server", () => { bonobUrl .append({ pathname: `/stream/track/${trackId}` }) .path() - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(stream.status); expect(res.headers["content-type"]).toEqual( @@ -971,7 +971,7 @@ describe("server", () => { bonobUrl .append({ pathname: `/stream/track/${trackId}` }) .path() - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(stream.status); expect(res.headers["content-type"]).toEqual( @@ -1009,7 +1009,7 @@ describe("server", () => { bonobUrl .append({ pathname: `/stream/track/${trackId}` }) .path() - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(stream.status); expect(res.header["content-type"]).toEqual( @@ -1048,7 +1048,7 @@ describe("server", () => { bonobUrl .append({ pathname: `/stream/track/${trackId}` }) .path() - ).set('Authorization', `Bearer ${smapiAuthToken}`); + ).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`); expect(res.status).toEqual(stream.status); expect(res.header["content-type"]).toEqual( @@ -1093,7 +1093,7 @@ describe("server", () => { .append({ pathname: `/stream/track/${trackId}` }) .path() ) - .set('Authorization', `Bearer ${smapiAuthToken}`) + .set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`) .set("Range", requestedRange); expect(res.status).toEqual(stream.status); @@ -1137,7 +1137,7 @@ describe("server", () => { .append({ pathname: `/stream/track/${trackId}` }) .path() ) - .set('Authorization', `Bearer ${smapiAuthToken}`) + .set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`) .set("Range", "4000-5000"); expect(res.status).toEqual(stream.status);