import { v4 as uuid } from "uuid"; import jwt from "jsonwebtoken"; import { ExpiredTokenError, InvalidTokenError, isSmapiRefreshTokenResultFault, JWTSmapiLoginTokens, smapiTokenAsString, smapiTokenFromString, SMAPI_TOKEN_VERSION, } from "../src/smapi_auth"; import { either as E } from "fp-ts"; import { FixedClock } from "../src/clock"; import dayjs from "dayjs"; import { b64Encode } from "../src/b64"; describe("smapiTokenAsString", () => { it("can round trip token to and from string", () => { const smapiToken = { token: uuid(), key: uuid(), someOtherStuff: 'this needs to be explicitly ignored' }; const asString = smapiTokenAsString(smapiToken) expect(asString).toEqual(b64Encode(JSON.stringify({ token: smapiToken.token, key: smapiToken.key, }))); expect(smapiTokenFromString(asString)).toEqual({ token: smapiToken.token, key: smapiToken.key }); }); }); describe("isSmapiRefreshTokenResultFault", () => { it("should return true for a refreshAuthTokenResult fault", () => { const faultWithRefreshAuthToken = { Fault: { faultcode: "", faultstring: "", detail: { refreshAuthTokenResult: { authToken: "x", privateKey: "x", }, }, }, }; expect(isSmapiRefreshTokenResultFault(faultWithRefreshAuthToken)).toEqual( true ); }); it("should return false when is not a refreshAuthTokenResult", () => { expect(isSmapiRefreshTokenResultFault({ Fault: { faultcode: "", faultstring:" " }})).toEqual( false ); }); }); describe("auth", () => { describe("JWTSmapiLoginTokens", () => { const clock = new FixedClock(dayjs()); const expiresIn = "1h"; const secret = `secret-${uuid()}`; const key = uuid(); const smapiLoginTokens = new JWTSmapiLoginTokens(clock, secret, expiresIn, () => key); describe("issuing a new token", () => { it("should issue a token that can then be verified", () => { const serviceToken = `service-token-${uuid()}`; const smapiToken = smapiLoginTokens.issue(serviceToken); const expected = jwt.sign( { serviceToken, iat: clock.now().unix(), }, secret + SMAPI_TOKEN_VERSION + key, { expiresIn } ); expect(smapiToken.token).toEqual(expected); expect(smapiToken.token).not.toContain(serviceToken); expect(smapiToken.token).not.toContain(secret); expect(smapiToken.token).not.toContain(":"); const roundTripped = smapiLoginTokens.verify(smapiToken); expect(roundTripped).toEqual(E.right(serviceToken)); }); }); describe("when verifying the token fails", () => { describe("due to the version changing", () => { it("should return an error", () => { const authToken = uuid(); const vXSmapiTokens = new JWTSmapiLoginTokens( clock, secret, expiresIn, uuid, SMAPI_TOKEN_VERSION ); const vXPlus1SmapiTokens = new JWTSmapiLoginTokens( clock, secret, expiresIn, () => uuid(), SMAPI_TOKEN_VERSION + 1 ); const v1Token = vXSmapiTokens.issue(authToken); expect(vXSmapiTokens.verify(v1Token)).toEqual(E.right(authToken)); const result = vXPlus1SmapiTokens.verify(v1Token); expect(result).toEqual( E.left(new InvalidTokenError("invalid signature")) ); }); }); describe("due to secret changing", () => { it("should return an error", () => { const authToken = uuid(); const smapiToken = new JWTSmapiLoginTokens( clock, "A different secret", expiresIn ).issue(authToken); const result = smapiLoginTokens.verify(smapiToken); expect(result).toEqual( E.left(new InvalidTokenError("invalid signature")) ); }); }); describe("due to key changing", () => { it("should return an error", () => { const authToken = uuid(); const smapiToken = smapiLoginTokens.issue(authToken); const result = smapiLoginTokens.verify({ ...smapiToken, key: "some other key", }); expect(result).toEqual( E.left(new InvalidTokenError("invalid signature")) ); }); }); }); describe("when the token has expired", () => { it("should return an ExpiredTokenError, with the authToken", () => { const authToken = uuid(); const now = dayjs(); const tokenIssuedAt = now.subtract(31, "seconds"); const tokensWith30SecondExpiry = new JWTSmapiLoginTokens( clock, uuid(), "30s" ); clock.time = tokenIssuedAt; const expiredToken = tokensWith30SecondExpiry.issue(authToken); clock.time = now; const result = tokensWith30SecondExpiry.verify(expiredToken); expect(result).toEqual( E.left( new ExpiredTokenError( authToken ) ) ); }); }); }); });