Refreshing bearer tokens when smapi token is refreshed (#85)

This commit is contained in:
Simon J
2021-12-09 14:41:52 +11:00
committed by GitHub
parent 7c0db619c9
commit 1c94654fb3
12 changed files with 606 additions and 637 deletions

View File

@@ -1,6 +1,8 @@
import { taskEither as TE } from "fp-ts";
import { pipe } from "fp-ts/lib/function";
import { InMemoryMusicService } from "./in_memory_music_service";
import {
AuthSuccess,
MusicLibrary,
artistToArtistSummary,
albumToAlbumSummary,
@@ -28,7 +30,10 @@ describe("InMemoryMusicService", () => {
service.hasUser(credentials);
const token = (await service.generateToken(credentials)) as AuthSuccess;
const token = await pipe(
service.generateToken(credentials),
TE.getOrElse(e => { throw e })
)();
expect(token.userId).toEqual(credentials.username);
expect(token.nickname).toEqual(credentials.username);
@@ -43,7 +48,10 @@ describe("InMemoryMusicService", () => {
service.hasUser(credentials);
const token = (await service.generateToken(credentials)) as AuthSuccess;
const token = await pipe(
service.generateToken(credentials),
TE.getOrElse(e => { throw e })
)();
service.clear();
@@ -62,7 +70,11 @@ describe("InMemoryMusicService", () => {
service.hasUser(user);
const token = (await service.generateToken(user)) as AuthSuccess;
const token = await pipe(
service.generateToken(user),
TE.getOrElse(e => { throw e })
)();
musicLibrary = (await service.login(token.serviceToken)) as MusicLibrary;
});

View File

@@ -1,4 +1,4 @@
import { option as O } from "fp-ts";
import { option as O, taskEither as TE } from "fp-ts";
import * as A from "fp-ts/Array";
import { fromEquals } from "fp-ts/lib/Eq";
import { pipe } from "fp-ts/lib/function";
@@ -34,23 +34,27 @@ export class InMemoryMusicService implements MusicService {
generateToken({
username,
password,
}: Credentials): Promise<AuthSuccess | AuthFailure> {
}: Credentials): TE.TaskEither<AuthFailure, AuthSuccess> {
if (
username != undefined &&
password != undefined &&
this.users[username] == password
) {
return Promise.resolve({
return TE.right({
serviceToken: b64Encode(JSON.stringify({ username, password })),
userId: username,
nickname: username,
type: "in-memory"
});
} else {
return Promise.resolve({ message: `Invalid user:${username}` });
return TE.left(new AuthFailure(`Invalid user:${username}`));
}
}
refreshToken(serviceToken: string): TE.TaskEither<AuthFailure, AuthSuccess> {
return this.generateToken(JSON.parse(b64Decode(serviceToken)))
}
login(serviceToken: string): Promise<MusicLibrary> {
const credentials = JSON.parse(b64Decode(serviceToken)) as Credentials;
if (this.users[credentials.username] != credentials.password)

View File

@@ -3,10 +3,10 @@ import dayjs from "dayjs";
import request from "supertest";
import Image from "image-js";
import fs from "fs";
import { either as E } from "fp-ts";
import { either as E, taskEither as TE } from "fp-ts";
import path from "path";
import { MusicService } from "../src/music_service";
import { AuthFailure, MusicService } from "../src/music_service";
import makeServer, {
BONOB_ACCESS_TOKEN_HEADER,
RangeBytesFromFilter,
@@ -637,7 +637,7 @@ describe("server", () => {
};
linkCodes.has.mockReturnValue(true);
musicService.generateToken.mockResolvedValue(authSuccess);
musicService.generateToken.mockReturnValue(TE.right(authSuccess))
linkCodes.associate.mockReturnValue(true);
const res = await request(server)
@@ -669,7 +669,7 @@ describe("server", () => {
const message = `Invalid user:${username}`;
linkCodes.has.mockReturnValue(true);
musicService.generateToken.mockResolvedValue({ message });
musicService.generateToken.mockReturnValue(TE.left(new AuthFailure(message)))
const res = await request(server)
.post(bonobUrl.append({ pathname: "/login" }).pathname())
@@ -683,27 +683,6 @@ describe("server", () => {
});
});
describe("when an unexpected failure occurs", () => {
it("should return 403 with message", async () => {
const username = "userDoesntExist";
const password = "password";
const linkCode = uuid();
linkCodes.has.mockReturnValue(true);
musicService.generateToken.mockRejectedValue("BOOOOOOM");
const res = await request(server)
.post(bonobUrl.append({ pathname: "/login" }).pathname())
.set("accept-language", acceptLanguage)
.type("form")
.send({ username, password, linkCode })
.expect(403);
expect(res.text).toContain(lang("loginFailed"));
expect(res.text).toContain('Unexpected error occured - BOOOOOOM');
});
});
describe("when linkCode is invalid", () => {
it("should return 400 with message", async () => {
const username = "jane";
@@ -777,7 +756,7 @@ describe("server", () => {
describe("when the Bearer token has expired", () => {
it("should return a 401", async () => {
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0)))
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken)))
const res = await request(server).head(
bonobUrl
@@ -865,7 +844,7 @@ describe("server", () => {
describe("when the Bearer token has expired", () => {
it("should return a 401", async () => {
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0)))
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken)))
const res = await request(server)
.get(

View File

@@ -2,7 +2,7 @@ import crypto from "crypto";
import request from "supertest";
import { Client, createClientAsync } from "soap";
import { v4 as uuid } from "uuid";
import { either as E } from "fp-ts";
import { either as E, taskEither as TE } from "fp-ts";
import { DOMParserImpl } from "xmldom-ts";
import * as xpath from "xpath-ts";
import { randomInt } from "crypto";
@@ -861,6 +861,7 @@ describe("defaultArtistArtURI", () => {
describe("wsdl api", () => {
const musicService = {
generateToken: jest.fn(),
refreshToken: jest.fn(),
login: jest.fn(),
};
const linkCodes = {
@@ -1079,10 +1080,11 @@ describe("wsdl api", () => {
describe("when token has expired", () => {
it("should return a refreshed auth token", async () => {
const oneDayAgo = clock.time.subtract(1, "d");
const refreshedServiceToken = `refreshedServiceToken-${uuid()}`
const newSmapiAuthToken = { token: `newToken-${uuid()}`, key: `newKey-${uuid()}` };
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, oneDayAgo.unix())));
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken)));
musicService.refreshToken.mockReturnValue(TE.right({ serviceToken: refreshedServiceToken }));
smapiAuthTokens.issue.mockReturnValue(newSmapiAuthToken);
const ws = await createClientAsync(`${service.uri}?wsdl`, {
@@ -1101,6 +1103,9 @@ describe("wsdl api", () => {
privateKey: newSmapiAuthToken.key,
},
});
expect(musicService.refreshToken).toHaveBeenCalledWith(serviceToken);
expect(smapiAuthTokens.issue).toHaveBeenCalledWith(refreshedServiceToken);
});
});
@@ -1128,8 +1133,11 @@ describe("wsdl api", () => {
describe("when existing auth token has not expired", () => {
it("should return a refreshed auth token", async () => {
const refreshedServiceToken = `refreshedServiceToken-${uuid()}`
const newSmapiAuthToken = { token: `newToken-${uuid()}`, key: `newKey-${uuid()}` };
smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken));
musicService.refreshToken.mockReturnValue(TE.right({ serviceToken: refreshedServiceToken }));
smapiAuthTokens.issue.mockReturnValue(newSmapiAuthToken);
const ws = await createClientAsync(`${service.uri}?wsdl`, {
@@ -1148,6 +1156,9 @@ describe("wsdl api", () => {
privateKey: newSmapiAuthToken.key
},
});
expect(musicService.refreshToken).toHaveBeenCalledWith(serviceToken);
expect(smapiAuthTokens.issue).toHaveBeenCalledWith(refreshedServiceToken);
});
});
});
@@ -1325,13 +1336,14 @@ describe("wsdl api", () => {
describe("when token has expired", () => {
it("should return a fault of Client.TokenRefreshRequired with a refreshAuthTokenResult", async () => {
const expiry = dayjs().subtract(1, "d");
const refreshedServiceToken = `refreshedServiceToken-${uuid()}`
const newToken = {
token: `newToken-${uuid()}`,
key: `newKey-${uuid()}`
};
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, expiry.unix())))
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken)))
musicService.refreshToken.mockReturnValue(TE.right({ serviceToken: refreshedServiceToken }))
smapiAuthTokens.issue.mockReturnValue(newToken)
musicService.login.mockRejectedValue(
"fail, should not call login!"
@@ -1360,7 +1372,8 @@ describe("wsdl api", () => {
});
expect(smapiAuthTokens.verify).toHaveBeenCalledWith(smapiAuthToken);
expect(smapiAuthTokens.issue).toHaveBeenCalledWith(serviceToken);
expect(musicService.refreshToken).toHaveBeenCalledWith(serviceToken);
expect(smapiAuthTokens.issue).toHaveBeenCalledWith(refreshedServiceToken);
});
});
}

View File

@@ -177,8 +177,7 @@ describe("auth", () => {
expect(result).toEqual(
E.left(
new ExpiredTokenError(
authToken,
tokenIssuedAt.add(30, "seconds").unix()
authToken
)
)
);

File diff suppressed because it is too large Load Diff