diff --git a/src/classes/insightsConnection.ts b/src/classes/insightsConnection.ts index 43673040..1f281f48 100644 --- a/src/classes/insightsConnection.ts +++ b/src/classes/insightsConnection.ts @@ -48,6 +48,7 @@ export class InsightsConnection { await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ).then(async (token) => { this.connected = token ? true : false; if (token) { @@ -77,6 +78,7 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { @@ -108,6 +110,7 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { tokenUndefinedError(this.connLabel); @@ -213,6 +216,7 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { @@ -289,6 +293,7 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { tokenUndefinedError(this.connLabel); @@ -357,6 +362,7 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index 4ed1cfb2..2f6ad1fa 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -83,6 +83,7 @@ export async function addInsightsConnection(insightsData: InsightDetails) { auth: true, alias: insightsData.alias, server: insightsData.server!, + realm: insightsData.realm, }, }; } else { @@ -90,6 +91,7 @@ export async function addInsightsConnection(insightsData: InsightDetails) { auth: true, alias: insightsData.alias, server: insightsData.server!, + realm: insightsData.realm, }; } diff --git a/src/models/insights.ts b/src/models/insights.ts index 4b13b0e1..bccf6667 100644 --- a/src/models/insights.ts +++ b/src/models/insights.ts @@ -15,6 +15,7 @@ export interface InsightDetails { alias: string; server: string; auth: boolean; + realm?: string; } export interface Insights { diff --git a/src/services/kdbInsights/codeFlowLogin.ts b/src/services/kdbInsights/codeFlowLogin.ts index d0bb688e..33c344b1 100644 --- a/src/services/kdbInsights/codeFlowLogin.ts +++ b/src/services/kdbInsights/codeFlowLogin.ts @@ -28,6 +28,27 @@ interface IDeferred { reject: (reason: any) => void; } +function getAuthUrl(insightsUrl: string, realm: string) { + return new url.URL( + `auth/realms/${realm}/protocol/openid-connect/auth`, + insightsUrl, + ); +} + +function getTokenUrl(insightsUrl: string, realm: string) { + return new url.URL( + `auth/realms/${realm}/protocol/openid-connect/token`, + insightsUrl, + ); +} + +function getRevokeUrl(insightsUrl: string, realm: string) { + return new url.URL( + `auth/realms/${realm}/protocol/openid-connect/revoke`, + insightsUrl, + ); +} + export interface IToken { accessToken: string; accessTokenExpirationDate: Date; @@ -41,7 +62,7 @@ const commonRequestParams = { client_id: "insights-app", }; -export async function signIn(insightsUrl: string) { +export async function signIn(insightsUrl: string, realm: string) { const { server, codePromise } = createServer(); try { @@ -54,17 +75,14 @@ export async function signIn(insightsUrl: string) { state: crypto.randomBytes(20).toString("hex"), }; - const authorizationUrl = new url.URL( - ext.insightsAuthUrls.authURL, - insightsUrl, - ); + const authorizationUrl = getAuthUrl(insightsUrl, realm); authorizationUrl.search = queryString(authParams); await env.openExternal(Uri.parse(authorizationUrl.toString())); const code = await codePromise; - return await getToken(insightsUrl, code); + return await getToken(insightsUrl, realm, code); } finally { setImmediate(() => server.close()); } @@ -72,6 +90,7 @@ export async function signIn(insightsUrl: string) { export async function signOut( insightsUrl: string, + realm: string, token: string, ): Promise { const queryParams = queryString({ @@ -84,7 +103,7 @@ export async function signOut( const headers = { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }; - const requestUrl = new url.URL(ext.insightsAuthUrls.revoke, insightsUrl); + const requestUrl = getRevokeUrl(insightsUrl, realm); await axios.post(requestUrl.toString(), body, headers).then((res) => { return res.data; @@ -93,17 +112,20 @@ export async function signOut( export async function refreshToken( insightsUrl: string, + realm: string, token: string, ): Promise { - return await tokenRequest(insightsUrl, { + return await tokenRequest(insightsUrl, realm, { grant_type: ext.insightsGrantType.refreshToken, refresh_token: token, }); } +/* istanbul ignore next */ export async function getCurrentToken( serverName: string, serverAlias: string, + realm: string, ): Promise { if (serverName === "" || serverAlias === "") { return undefined; @@ -115,9 +137,9 @@ export async function getCurrentToken( if (existingToken !== undefined) { const storedToken: IToken = JSON.parse(existingToken); if (new Date(storedToken.accessTokenExpirationDate) < new Date()) { - token = await refreshToken(serverName, storedToken.refreshToken); + token = await refreshToken(serverName, realm, storedToken.refreshToken); if (token === undefined) { - token = await signIn(serverName); + token = await signIn(serverName, realm); ext.context.secrets.store(serverAlias, JSON.stringify(token)); } ext.context.secrets.store(serverAlias, JSON.stringify(token)); @@ -126,17 +148,19 @@ export async function getCurrentToken( return storedToken; } } else { - token = await signIn(serverName); + token = await signIn(serverName, realm); ext.context.secrets.store(serverAlias, JSON.stringify(token)); } return token; } +/* istanbul ignore next */ async function getToken( insightsUrl: string, + realm: string, code: string, ): Promise { - return await tokenRequest(insightsUrl, { + return await tokenRequest(insightsUrl, realm, { code, grant_type: ext.insightsGrantType.authorizationCode, }); @@ -144,6 +168,7 @@ async function getToken( async function tokenRequest( insightsUrl: string, + realm: string, params: any, ): Promise { const queryParams = queryString(params); @@ -155,7 +180,7 @@ async function tokenRequest( signal: AbortSignal.timeout(closeTimeout), }; - const requestUrl = new url.URL(ext.insightsAuthUrls.tokenURL, insightsUrl); + const requestUrl = getTokenUrl(insightsUrl, realm); let response; if (params.grant_type === "refresh_token") { diff --git a/src/webview/components/kdbNewConnectionView.ts b/src/webview/components/kdbNewConnectionView.ts index a106e398..63455338 100644 --- a/src/webview/components/kdbNewConnectionView.ts +++ b/src/webview/components/kdbNewConnectionView.ts @@ -52,6 +52,7 @@ export class KdbNewConnectionView extends LitElement { alias: "", server: "", auth: true, + realm: "", }; this.bundledServer = { serverName: "127.0.0.1", @@ -220,6 +221,29 @@ export class KdbNewConnectionView extends LitElement { `; } + renderRealm() { + return html` +
+ Define Realm (optional) +
+
+ Specify the Keycloak realm for authentication. Use this field to connect + to a specific realm as configured on your server. +
+ `; + } + tabClickAction(tabNumber: number) { const config = this.tabConfig[tabNumber as keyof typeof this.tabConfig] || @@ -381,6 +405,14 @@ export class KdbNewConnectionView extends LitElement { ${this.renderConnAddress(ServerType.INSIGHTS)} +
+
+
+ Advanced + ${this.renderRealm()} +
+
+
diff --git a/test/suite/services.test.ts b/test/suite/services.test.ts index 39e3d649..7f00f7a7 100644 --- a/test/suite/services.test.ts +++ b/test/suite/services.test.ts @@ -560,13 +560,13 @@ describe("Code flow login service tests", () => { it("Should return a correct login", async () => { sinon.stub(codeFlow, "signIn").returns(token); - const result = await signIn("http://localhost"); + const result = await signIn("http://localhost", "insights"); assert.strictEqual(result, token, "Invalid token returned"); }); it("Should execute a correct logout", async () => { sinon.stub(axios, "post").resolves(Promise.resolve({ data: token })); - const result = await signOut("http://localhost", "token"); + const result = await signOut("http://localhost", "insights", "token"); assert.strictEqual(result, undefined, "Invalid response from logout"); }); @@ -574,6 +574,7 @@ describe("Code flow login service tests", () => { sinon.stub(axios, "post").resolves(Promise.resolve({ data: token })); const result = await refreshToken( "http://localhost", + "insights", JSON.stringify(token), ); assert.strictEqual( @@ -584,7 +585,7 @@ describe("Code flow login service tests", () => { }); it("Should not return token from secret store", async () => { - const result = await getCurrentToken("", "testalias"); + const result = await getCurrentToken("", "testalias", "insights"); assert.strictEqual( result, undefined, @@ -593,7 +594,7 @@ describe("Code flow login service tests", () => { }); it("Should not return token from secret store", async () => { - const result = await getCurrentToken("testserver", ""); + const result = await getCurrentToken("testserver", "", "insights"); assert.strictEqual( result, undefined, @@ -601,9 +602,11 @@ describe("Code flow login service tests", () => { ); }); - it.skip("Should not sign in if link is not opened", async () => { - sinon.stub(env, "openExternal").value(async () => false); - await assert.rejects(() => signIn("http://127.0.0.1")); + it("Should continue sign in if link is copied", async () => { + sinon.stub(env, "openExternal").value(async () => { + throw new Error(); + }); + await assert.rejects(() => signIn("http://127.0.0.1", "insights")); }); }); diff --git a/test/suite/webview.test.ts b/test/suite/webview.test.ts index b888ab4e..740d8948 100644 --- a/test/suite/webview.test.ts +++ b/test/suite/webview.test.ts @@ -479,6 +479,7 @@ describe("KdbNewConnectionView", () => { alias: "", server: "", auth: true, + realm: "", }; const data = view["data"]; assert.deepEqual(data, expectedData);