🔐 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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user