🔐 OIDC back-channel logout (#521)
* feat: oidc backchannel session storage * feat: oidc backchannel logout * ref: e2e test * 🛠️ Code review * ♻️ Do not mock internal auth token but call "/login" with oidc_token instead * ♻️ Refactoring for back-channel logout test * fix: feedback and e2e tests * feat: more e2e tests * ♻️ Added test to test multiple login requests flow * ref: jwt sid verifier and e2e tests --------- Co-authored-by: Monta <monta@HP-ProBook-445-14-inch-G9-Notebook-PC-505aadfc.localdomain> Co-authored-by: Anton SHEPILOV <ashepilov@linagora.com>
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
"mobile_googleplay": "https://play.google.com/store/apps/details?id=com.tdrive.tdrive&gl=FR"
|
||||
},
|
||||
"accounts": {
|
||||
"_type": "remote",
|
||||
"type": "internal",
|
||||
"internal": {
|
||||
"disable_account_creation": false
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"general": {
|
||||
"accounts": {
|
||||
"type": "remote"
|
||||
}
|
||||
},
|
||||
"database": {
|
||||
"secret": "",
|
||||
"mongodb": {
|
||||
|
||||
@@ -24,6 +24,7 @@ export default interface AuthServiceAPI extends TdriveServiceProvider {
|
||||
generateJWT(
|
||||
userId: uuid,
|
||||
email: string,
|
||||
session: string,
|
||||
options: {
|
||||
track: boolean;
|
||||
provider_id: string;
|
||||
|
||||
@@ -31,6 +31,7 @@ export class AuthService implements AuthServiceAPI {
|
||||
generateJWT(
|
||||
userId: uuid,
|
||||
email: string,
|
||||
session: string,
|
||||
options: {
|
||||
track: boolean;
|
||||
provider_id: string;
|
||||
@@ -54,6 +55,7 @@ export class AuthService implements AuthServiceAPI {
|
||||
iat: now - 60 * 10,
|
||||
nbf: now - 60 * 10,
|
||||
sub: userId,
|
||||
sid: session,
|
||||
email: email,
|
||||
track: !!options.track,
|
||||
provider_id: options.provider_id || "",
|
||||
@@ -66,6 +68,7 @@ export class AuthService implements AuthServiceAPI {
|
||||
iat: now - 60 * 10,
|
||||
nbf: now - 60 * 10,
|
||||
sub: userId,
|
||||
sid: session,
|
||||
email: email,
|
||||
track: !!options.track,
|
||||
provider_id: options.provider_id || "",
|
||||
|
||||
@@ -5,8 +5,9 @@ import fp from "fastify-plugin";
|
||||
import config from "../../../../config";
|
||||
import { JwtType } from "../../types";
|
||||
import { executionStorage } from "../../../framework/execution-storage";
|
||||
import gr from "../../../../../services/global-resolver";
|
||||
|
||||
const jwtPlugin: FastifyPluginCallback = (fastify, _opts, next) => {
|
||||
const jwtPlugin: FastifyPluginCallback = async (fastify, _opts, next) => {
|
||||
fastify.register(cookie);
|
||||
fastify.register(fastifyJwt, {
|
||||
secret: config.get("auth.jwt.secret"),
|
||||
@@ -18,6 +19,10 @@ const jwtPlugin: FastifyPluginCallback = (fastify, _opts, next) => {
|
||||
|
||||
const authenticate = async (request: FastifyRequest) => {
|
||||
const jwt: JwtType = await request.jwtVerify();
|
||||
|
||||
// Verify the SID exists and is valid
|
||||
await gr.services.console.getClient().verifyJwtSid(jwt.sid);
|
||||
|
||||
if (jwt.type === "refresh") {
|
||||
// TODO in the future we must invalidate the refresh token (because it should be single use)
|
||||
}
|
||||
@@ -25,6 +30,7 @@ const jwtPlugin: FastifyPluginCallback = (fastify, _opts, next) => {
|
||||
request.currentUser = {
|
||||
...{ email: jwt.email },
|
||||
...{ id: jwt.sub },
|
||||
...{ sid: jwt.sid },
|
||||
...{ identity_provider_id: jwt.provider_id },
|
||||
...{ application_id: jwt.application_id || null },
|
||||
...{ server_request: jwt.server_request || false },
|
||||
|
||||
@@ -107,8 +107,8 @@ export function toMongoDbOrderable(timeuuid?: string): string {
|
||||
/**
|
||||
* Check if filtering is necessary
|
||||
* @param {string} key
|
||||
* @returns {boolean} Returns true if key is "is_in_trash" or "scope", otherwise returns false.
|
||||
* @returns {boolean} Returns true if key is "is_in_trash", "scope" or "sub", otherwise returns false.
|
||||
*/
|
||||
export const filteringRequired = (key: string) => {
|
||||
return key === "is_in_trash" || key === "scope";
|
||||
return key === "is_in_trash" || key === "scope" || key === "sub";
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type JwtType = {
|
||||
type: "access" | "refresh";
|
||||
sub: string;
|
||||
sid: string;
|
||||
provider_id: string; //Console sub
|
||||
email: string;
|
||||
application_id?: string;
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ApplicationsApiController {
|
||||
|
||||
return {
|
||||
resource: {
|
||||
access_token: gr.platformServices.auth.generateJWT(request.body.id, null, {
|
||||
access_token: gr.platformServices.auth.generateJWT(request.body.id, null, "", {
|
||||
track: false,
|
||||
provider_id: "",
|
||||
application_id: request.body.id,
|
||||
|
||||
@@ -62,5 +62,11 @@ export interface ConsoleServiceClient {
|
||||
|
||||
getUserByAccessToken(idToken: string): Promise<ConsoleHookUser>;
|
||||
|
||||
updateUserSession(idToken: string): Promise<string>;
|
||||
|
||||
verifyJwtSid(_sid: string): Promise<void>;
|
||||
|
||||
backChannelLogout(logoutToken: string): Promise<void>;
|
||||
|
||||
resendVerificationEmail(email: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -100,8 +100,22 @@ export class ConsoleInternalClient implements ConsoleServiceClient {
|
||||
throw new Error("Method should not be implemented.");
|
||||
}
|
||||
|
||||
updateUserSession(_idToken: string): Promise<string> {
|
||||
logger.info("Internal: updateUserSession");
|
||||
throw new Error("Method should not be implemented.");
|
||||
}
|
||||
|
||||
resendVerificationEmail(_accessToken: string): Promise<void> {
|
||||
logger.info("Internal: resendVerificationEmail");
|
||||
throw new Error("Method should not be implemented.");
|
||||
}
|
||||
|
||||
backChannelLogout(_logoutToken: string): Promise<void> {
|
||||
logger.info("Internal: backChannelLogout");
|
||||
throw new Error("Method should not be implemented.");
|
||||
}
|
||||
|
||||
async verifyJwtSid(_sid: string): Promise<void> {
|
||||
logger.info("Internal: verifyJwtSid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,4 +97,12 @@ export class OidcJwtVerifier {
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
async verifyLogoutToken(logoutTokenString: string) {
|
||||
const jwt = await this.verifyAsPromise(logoutTokenString);
|
||||
// verifyAudience(expectedClientId, jwt.claims.aud);
|
||||
// verifyIssuer(this.issuer, jwt.claims.iss);
|
||||
|
||||
return jwt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ConsoleServiceImpl } from "../service";
|
||||
import coalesce from "../../../utils/coalesce";
|
||||
import config from "config";
|
||||
import { CompanyUserRole } from "src/services/user/web/types";
|
||||
import Session from "../entities/session";
|
||||
export class ConsoleRemoteClient implements ConsoleServiceClient {
|
||||
version: "1";
|
||||
|
||||
@@ -166,7 +167,6 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
|
||||
await gr.services.users.save(user);
|
||||
|
||||
//For now TDrive works with only one company as we don't get it from the SSO
|
||||
|
||||
let company = await gr.services.companies.getCompany({
|
||||
id: "00000000-0000-4000-0000-000000000000",
|
||||
});
|
||||
@@ -253,4 +253,66 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async updateUserSession(idToken: string): Promise<string> {
|
||||
const sessionInfo = (await this.verifier.verifyIdToken(idToken, this.infos.client_id))?.claims;
|
||||
// make sure sid claim is present in the token and not empty
|
||||
if (sessionInfo.sid) {
|
||||
const sessionRepository = gr.services.console.getSessionRepo();
|
||||
|
||||
// check for existing session
|
||||
const existingSession = await sessionRepository.findOne({
|
||||
sid: sessionInfo.sid,
|
||||
});
|
||||
if (existingSession) {
|
||||
return existingSession.sid;
|
||||
} else {
|
||||
const sessionBody = new Session();
|
||||
sessionBody.sub = sessionInfo.sub;
|
||||
sessionBody.sid = sessionInfo.sid;
|
||||
await sessionRepository.save(sessionBody);
|
||||
return sessionBody.sid;
|
||||
}
|
||||
} else {
|
||||
throw new CrudException("Missing sid claim", 400);
|
||||
}
|
||||
}
|
||||
|
||||
async backChannelLogout(logoutToken: string): Promise<void> {
|
||||
const payload = await this.verifier.verifyLogoutToken(logoutToken);
|
||||
|
||||
if (!payload.iss || !payload.aud || !payload.iat || !payload.jti || !payload.events) {
|
||||
throw new CrudException("Missing required claims", 400);
|
||||
}
|
||||
|
||||
if (payload.nonce) {
|
||||
throw new CrudException("Nonce claim is prohibited", 400);
|
||||
}
|
||||
|
||||
if (!payload.sub && !payload.sid) {
|
||||
throw new CrudException("Missing sub or sid claim", 400);
|
||||
}
|
||||
|
||||
const sessionRepository = gr.services.console.getSessionRepo();
|
||||
const session = await sessionRepository.findOne({ sid: payload.sid });
|
||||
if (session) {
|
||||
await sessionRepository.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyJwtSid(sid: string): Promise<void> {
|
||||
const sessionRepository = gr.services.console.getSessionRepo();
|
||||
if (sid) {
|
||||
const session = await sessionRepository.findOne({
|
||||
sid,
|
||||
});
|
||||
if (!session) {
|
||||
// fail for not matching session id
|
||||
throw new Error("Invalid session id");
|
||||
}
|
||||
} else {
|
||||
// fail for missing session id
|
||||
throw new Error("Missing session id");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { merge } from "lodash";
|
||||
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "session";
|
||||
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["company_id"], "sid"],
|
||||
globalIndexes: [["sid"]],
|
||||
type: TYPE,
|
||||
})
|
||||
export default class Session {
|
||||
@Column("company_id", "uuid")
|
||||
company_id: string;
|
||||
|
||||
@Column("sub", "string")
|
||||
sub: string;
|
||||
|
||||
@Column("sid", "string")
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export type UserSessionPrimaryKey = Pick<Session, "sid">;
|
||||
|
||||
export function getInstance(session: Partial<Session> & UserSessionPrimaryKey): Session {
|
||||
return merge(new Session(), session);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import gr from "../global-resolver";
|
||||
import { Configuration, TdriveServiceProvider } from "../../core/platform/framework";
|
||||
import assert from "assert";
|
||||
import { ExecutionContext } from "../../core/platform/framework/api/crud-service";
|
||||
import Repository from "src/core/platform/services/database/services/orm/repository/repository";
|
||||
import Session from "./entities/session";
|
||||
|
||||
export class ConsoleServiceImpl implements TdriveServiceProvider {
|
||||
version: "1";
|
||||
@@ -17,6 +19,7 @@ export class ConsoleServiceImpl implements TdriveServiceProvider {
|
||||
database: DatabaseServiceAPI;
|
||||
};
|
||||
private configuration: Configuration;
|
||||
private sessionRepository: Repository<Session>;
|
||||
|
||||
constructor(options?: ConsoleOptions) {
|
||||
this.consoleOptions = options;
|
||||
@@ -44,6 +47,8 @@ export class ConsoleServiceImpl implements TdriveServiceProvider {
|
||||
|
||||
this.consoleOptions.type = type;
|
||||
this.consoleType = type;
|
||||
this.sessionRepository =
|
||||
type === "remote" ? await gr.database.getRepository<Session>("session", Session) : null;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -52,6 +57,10 @@ export class ConsoleServiceImpl implements TdriveServiceProvider {
|
||||
return ConsoleClientFactory.create(this);
|
||||
}
|
||||
|
||||
getSessionRepo(): Repository<Session> {
|
||||
return this.sessionRepository;
|
||||
}
|
||||
|
||||
async processPendingUser(user: User, context?: ExecutionContext): Promise<void> {
|
||||
await gr.services.workspaces.processPendingUser(user, null, context);
|
||||
}
|
||||
|
||||
@@ -237,3 +237,7 @@ export interface AuthResponse {
|
||||
access_token?: AccessToken;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type ConsoleBackchannelLogoutRequest = {
|
||||
logout_token: string;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ConsoleHookResponse,
|
||||
ConsoleHookUser,
|
||||
ConsoleType,
|
||||
ConsoleBackchannelLogoutRequest,
|
||||
} from "../types";
|
||||
import Company from "../../user/entities/company";
|
||||
import { CrudException } from "../../../core/platform/framework/api/crud-service";
|
||||
@@ -132,6 +133,7 @@ export class ConsoleController {
|
||||
access_token: gr.platformServices.auth.generateJWT(
|
||||
request.currentUser.id,
|
||||
request.currentUser.email,
|
||||
request.currentUser.sid,
|
||||
{
|
||||
track: request.currentUser?.allow_tracking || false,
|
||||
provider_id: request.currentUser.identity_provider_id,
|
||||
@@ -287,7 +289,7 @@ export class ConsoleController {
|
||||
logger.warn("ERROR_NOTONPROD: YOU ARE RUNNING IN DEVELOPMENT MODE, AUTH IS DISABLED!!!");
|
||||
}
|
||||
|
||||
return gr.platformServices.auth.generateJWT(user.id, user.email_canonical, {
|
||||
return gr.platformServices.auth.generateJWT(user.id, user.email_canonical, "", {
|
||||
track: user?.preferences?.allow_tracking || false,
|
||||
provider_id: user.identity_provider_id,
|
||||
});
|
||||
@@ -297,12 +299,34 @@ export class ConsoleController {
|
||||
const client = gr.services.console.getClient();
|
||||
const userDTO = await client.getUserByAccessToken(idToken);
|
||||
const user = await client.updateLocalUserFromConsole(userDTO);
|
||||
|
||||
if (!user) {
|
||||
throw CrudException.notFound(`User details not found for access token ${idToken}`);
|
||||
}
|
||||
return gr.platformServices.auth.generateJWT(user.id, user.email_canonical, {
|
||||
|
||||
// update the user session
|
||||
const session = await client.updateUserSession(idToken);
|
||||
|
||||
return gr.platformServices.auth.generateJWT(user.id, user.email_canonical, session, {
|
||||
track: user?.preferences?.allow_tracking || false,
|
||||
provider_id: user.identity_provider_id,
|
||||
});
|
||||
}
|
||||
|
||||
async backChannelLogout(
|
||||
request: FastifyRequest<{ Body: ConsoleBackchannelLogoutRequest }>,
|
||||
reply: FastifyReply,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const logoutToken = request.body.logout_token;
|
||||
if (!logoutToken) {
|
||||
throw new CrudException("Missing logout_token", 400);
|
||||
}
|
||||
await gr.services.console.getClient().backChannelLogout(logoutToken);
|
||||
reply.status(200).send({ success: true });
|
||||
} catch (e) {
|
||||
logger.error(e.message);
|
||||
reply.status(400).send({ error: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next)
|
||||
handler: controller.resendVerificationEmail.bind(controller),
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: "POST",
|
||||
url: "/backchannel_logout",
|
||||
handler: controller.backChannelLogout.bind(controller),
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
@@ -554,11 +554,16 @@ export class DocumentsController {
|
||||
}
|
||||
await globalResolver.services.companies.setUserRole(document.item.company_id, user.id, "guest");
|
||||
|
||||
const token = globalResolver.platformServices.auth.generateJWT(user.id, user.email_canonical, {
|
||||
track: false,
|
||||
provider_id: "tdrive",
|
||||
public_token_document_id: req.body.document_id,
|
||||
});
|
||||
const token = globalResolver.platformServices.auth.generateJWT(
|
||||
user.id,
|
||||
user.email_canonical,
|
||||
"",
|
||||
{
|
||||
track: false,
|
||||
provider_id: "tdrive",
|
||||
public_token_document_id: req.body.document_id,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
access_token: token,
|
||||
|
||||
@@ -22,6 +22,9 @@ export interface User {
|
||||
identity_provider_id?: uuid;
|
||||
// user email
|
||||
email?: string;
|
||||
|
||||
// session id
|
||||
sid?: string;
|
||||
// server request
|
||||
server_request?: boolean; //Set to true if request if from the user, can be used to cancel any access restriction
|
||||
// application call
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-ignore
|
||||
import fs from "fs";
|
||||
import { Readable } from 'stream';
|
||||
import { Readable } from "stream";
|
||||
import { ResourceUpdateResponse, Workspace } from "../../../src/utils/types";
|
||||
import { File } from "../../../src/services/files/entities/file";
|
||||
import { deserialize } from "class-transformer";
|
||||
@@ -11,7 +11,7 @@ import { TestDbService } from "../utils.prepare.db";
|
||||
import { DriveFile } from "../../../src/services/documents/entities/drive-file";
|
||||
import { FileVersion } from "../../../src/services/documents/entities/file-version";
|
||||
import {
|
||||
AccessTokenMockClass, DriveFileMockClass,
|
||||
AccessTokenMockClass,
|
||||
DriveItemDetailsMockClass,
|
||||
SearchResultMockClass,
|
||||
UserQuotaMockClass
|
||||
@@ -21,6 +21,7 @@ import { expect } from "@jest/globals";
|
||||
import { publicAccessLevel } from "../../../src/services/documents/types";
|
||||
import { UserQuota } from "../../../src/services/user/web/types";
|
||||
import { Api } from "../utils.api";
|
||||
import { OidcJwtVerifier } from "../../../src/services/console/clients/remote-jwks-verifier";
|
||||
|
||||
export default class UserApi {
|
||||
|
||||
@@ -43,6 +44,7 @@ export default class UserApi {
|
||||
workspace: Workspace;
|
||||
jwt: string;
|
||||
api: Api;
|
||||
session: string;
|
||||
|
||||
private constructor(
|
||||
platform: TestPlatform
|
||||
@@ -54,12 +56,15 @@ export default class UserApi {
|
||||
this.dbService = await TestDbService.getInstance(this.platform, true);
|
||||
if (newUser) {
|
||||
this.workspace = this.platform.workspace;
|
||||
const workspacePK = { id: this.workspace.workspace_id, company_id: this.workspace.company_id };
|
||||
const workspacePK = {
|
||||
id: this.workspace.workspace_id,
|
||||
company_id: this.workspace.company_id,
|
||||
};
|
||||
this.user = await this.dbService.createUser([workspacePK], options, uuidv1());
|
||||
this.anonymous = await this.dbService.createUser([workspacePK],
|
||||
{
|
||||
...options,
|
||||
identity_provider: "anonymous"
|
||||
identity_provider: "anonymous",
|
||||
},
|
||||
uuidv1());
|
||||
} else {
|
||||
@@ -67,9 +72,72 @@ export default class UserApi {
|
||||
this.workspace = this.platform.workspace;
|
||||
}
|
||||
this.api = new Api(this.platform, this.user);
|
||||
this.jwt = this.getJWTTokenForUser(this.user.id);
|
||||
this.jwt = await this.doLogin();
|
||||
}
|
||||
|
||||
private async doLogin() {
|
||||
const loginResponse = await this.login();
|
||||
|
||||
expect(loginResponse).toBeDefined();
|
||||
expect(loginResponse.statusCode).toEqual(200);
|
||||
|
||||
const accessToken = deserialize<AccessTokenMockClass>(AccessTokenMockClass, loginResponse.body);
|
||||
if (!accessToken.access_token?.value)
|
||||
throw Error("Auth error: authentication token doesn't exists in response");
|
||||
return accessToken.access_token.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Just send the login requests without any validation and login response assertion
|
||||
*/
|
||||
public async login(session?: string) {
|
||||
if (session !== undefined) {
|
||||
this.session = session;
|
||||
} else {
|
||||
this.session = uuidv1();
|
||||
}
|
||||
const payload = {
|
||||
claims: {
|
||||
sub: this.user.id,
|
||||
first_name: this.user.first_name,
|
||||
sid: this.session,
|
||||
},
|
||||
};
|
||||
const verifierMock = jest.spyOn(OidcJwtVerifier.prototype, "verifyIdToken");
|
||||
verifierMock.mockImplementation(() => {
|
||||
return Promise.resolve(payload); // Return the predefined payload
|
||||
});
|
||||
return await this.api.post("/internal/services/console/v1/login", {
|
||||
oidc_id_token: "sample_oidc_token",
|
||||
});
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
const payload = {
|
||||
iss: "tdrive_lemonldap",
|
||||
sub: this.user.id,
|
||||
sid: this.session,
|
||||
aud: "your-audience",
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
jti: "jwt-id",
|
||||
events: {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {},
|
||||
},
|
||||
};
|
||||
const verifierMock = jest.spyOn(OidcJwtVerifier.prototype, "verifyLogoutToken");
|
||||
verifierMock.mockImplementation(() => {
|
||||
return Promise.resolve(payload); // Return the predefined payload
|
||||
});
|
||||
const logoutToken = "logout_token_rsa256";
|
||||
|
||||
const response = await this.api.post("/internal/services/console/v1/backchannel_logout", {
|
||||
logout_token: logoutToken,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
public static async getInstance(platform: TestPlatform, newUser = false, options?: {}): Promise<UserApi> {
|
||||
const helpers = new UserApi(platform);
|
||||
await helpers.init(newUser, options);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "@jest/globals";
|
||||
import { init, TestPlatform } from "../setup";
|
||||
import { TestDbService } from "../utils.prepare.db";
|
||||
import UserApi from "../common/user-api";
|
||||
|
||||
describe("The /backchannel_logout API", () => {
|
||||
const url = "/internal/services/console/v1/backchannel_logout";
|
||||
let platform: TestPlatform;
|
||||
let testDbService: TestDbService;
|
||||
let currentUser: UserApi;
|
||||
|
||||
beforeEach(async () => {
|
||||
platform = await init();
|
||||
currentUser = await UserApi.getInstance(platform);
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
platform = await init({
|
||||
services: [
|
||||
"database",
|
||||
"search",
|
||||
"message-queue",
|
||||
"websocket",
|
||||
"applications",
|
||||
"webserver",
|
||||
"user",
|
||||
"auth",
|
||||
"storage",
|
||||
"counter",
|
||||
"console",
|
||||
"workspaces",
|
||||
"statistics",
|
||||
"platform-services",
|
||||
],
|
||||
});
|
||||
testDbService = await TestDbService.getInstance(platform);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await platform.tearDown();
|
||||
platform = null;
|
||||
});
|
||||
|
||||
it("should 400 when logout_token is missing", async () => {
|
||||
const response = await platform.app.inject({
|
||||
method: "POST",
|
||||
url: url,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json()).toEqual({
|
||||
error: "Missing logout_token",
|
||||
});
|
||||
});
|
||||
|
||||
it("should 200 when valid logout_token is provided", async () => {
|
||||
const response = await currentUser.logout();
|
||||
expect(response.statusCode).toBeDefined();
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
// Verify the session is removed from the database
|
||||
const deletedSession = await currentUser.dbService.getSessionById(currentUser.session);
|
||||
expect(deletedSession).toBeNull();
|
||||
expect((await currentUser.dbService.getSessionsByUserId(currentUser.user.id)).length).toEqual(0);
|
||||
|
||||
});
|
||||
|
||||
it("should create a session on login", async () => {
|
||||
const session = currentUser.session;
|
||||
expect(session).not.toBeNull();
|
||||
expect(session).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should receive 401 after logout and trying to access with the same token", async () => {
|
||||
const myDriveId = "user_" + currentUser.user.id;
|
||||
|
||||
let response = await currentUser.getDocument(myDriveId);
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
await currentUser.logout();
|
||||
|
||||
response = await currentUser.getDocument(myDriveId);
|
||||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it("should be able to log-in several times by having multiple sessions", async () => {
|
||||
// Perform a second login
|
||||
const newUserSession = await UserApi.getInstance(platform);
|
||||
|
||||
//two sessions are different
|
||||
expect(newUserSession.session).not.toEqual(currentUser.session);
|
||||
|
||||
// Verify that the user has two sessions
|
||||
const oldSession = await testDbService.getSessionById(currentUser.session);
|
||||
expect(oldSession).not.toBeNull();
|
||||
expect(oldSession.sub).toBe(currentUser.user.id);
|
||||
|
||||
const newSession = await testDbService.getSessionById(newUserSession.session);
|
||||
expect(newSession).not.toBeNull();
|
||||
expect(newSession.sub).toBe(currentUser.user.id);
|
||||
|
||||
//check that we can send requests for both session
|
||||
expect((await currentUser.getDocument("user_" + currentUser.user.id)).statusCode).toEqual(200);
|
||||
expect((await newUserSession.getDocument("user_" + currentUser.user.id)).statusCode).toEqual(200);
|
||||
|
||||
});
|
||||
|
||||
it("should logout from one session and still be logged in another", async () => {
|
||||
// Perform a second login
|
||||
const newUserSession = await UserApi.getInstance(platform);
|
||||
|
||||
await currentUser.logout();
|
||||
|
||||
// Verify session1 is removed
|
||||
expect((await currentUser.getDocument("user_" + currentUser.user.id)).statusCode).toEqual(401);
|
||||
expect((await newUserSession.getDocument("user_" + currentUser.user.id)).statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
it("I want to be able to log-in/recieve access token several time with the same session id", async () => {
|
||||
await currentUser.login(currentUser.session);
|
||||
await currentUser.login(currentUser.session);
|
||||
|
||||
expect((await currentUser.getDocument("user_" + currentUser.user.id)).statusCode).toEqual(200);
|
||||
const sessions = await currentUser.dbService.getSessionsByUserId(currentUser.user.id);
|
||||
expect(sessions.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should fail to login with empty session id", async () => {
|
||||
const response = await currentUser.login("");
|
||||
expect(response.statusCode).toBeDefined();
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import Workspace, {
|
||||
getInstance as getWorkspaceInstance,
|
||||
WorkspacePrimaryKey,
|
||||
} from "./../../src/services/workspaces/entities/workspace";
|
||||
import Session from "../../src/services/console/entities/session";
|
||||
|
||||
import { v1 as uuidv1 } from "uuid";
|
||||
import CompanyUser from "../../src/services/user/entities/company_user";
|
||||
@@ -161,9 +162,9 @@ export class TestDbService {
|
||||
this.users.push(createdUser);
|
||||
if (workspacesPk && workspacesPk.length) {
|
||||
await gr.services.companies.setUserRole(
|
||||
this.company ? this.company.id : workspacesPk[0].company_id,
|
||||
createdUser.id,
|
||||
options.companyRole ? options.companyRole : "member",
|
||||
this.company ? this.company.id : workspacesPk[0].company_id,
|
||||
createdUser.id,
|
||||
options.companyRole ? options.companyRole : "member",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,8 +256,27 @@ export class TestDbService {
|
||||
return this;
|
||||
}
|
||||
|
||||
async createSession(sid: string, sub: string): Promise<Session> {
|
||||
const session = new Session();
|
||||
session.sid = sid;
|
||||
session.sub = sub;
|
||||
const sessionRepository = await this.database.getRepository<Session>("session", Session);
|
||||
await sessionRepository.save(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async getSessionById(sid: string): Promise<Session> {
|
||||
const sessionRepository = await this.database.getRepository<Session>("session", Session);
|
||||
return sessionRepository.findOne({ sid });
|
||||
}
|
||||
|
||||
async getSessionsByUserId(sub: string): Promise<Session[]> {
|
||||
const sessionRepository = await this.database.getRepository<Session>("session", Session);
|
||||
return (await sessionRepository.find({ sub })).getEntities();
|
||||
}
|
||||
|
||||
async cleanUp() {
|
||||
await this.database.getConnector().drop()
|
||||
await this.database.getConnector().drop();
|
||||
}
|
||||
|
||||
getRepository = (type, entity) => {
|
||||
|
||||
Reference in New Issue
Block a user