Skip to content

Commit e79caf8

Browse files
committed
Prevent slash duplication in request URLs
Closes keycloak#44269 Signed-off-by: Jon Koops <jonkoops@gmail.com> (cherry picked from commit f7e4b78)
1 parent 5d67183 commit e79caf8

7 files changed

Lines changed: 65 additions & 23 deletions

File tree

js/libs/keycloak-admin-client/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
},
4141
"dependencies": {
4242
"camelize-ts": "^3.0.0",
43-
"url-join": "^5.0.0",
4443
"url-template": "^3.1.1"
4544
},
4645
"devDependencies": {

js/libs/keycloak-admin-client/src/resources/agent.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import urlJoin from "url-join";
21
import { parseTemplate } from "url-template";
32
import type { KeycloakAdminClient } from "../client.js";
43
import {
54
fetchWithError,
65
NetworkError,
76
parseResponse,
87
} from "../utils/fetchWithError.js";
8+
import { joinPath } from "../utils/joinPath.js";
99
import { stringifyQueryParams } from "../utils/stringifyQueryParams.js";
1010

1111
// constants
@@ -53,7 +53,7 @@ export class Agent {
5353
#client: KeycloakAdminClient;
5454
#basePath: string;
5555
#getBaseParams?: () => Record<string, any>;
56-
#getBaseUrl?: () => string;
56+
#getBaseUrl: () => string;
5757

5858
constructor({
5959
client,
@@ -199,12 +199,6 @@ export class Agent {
199199
returnResourceIdInLocationHeader?: { field: string };
200200
headers?: [string, string][] | Record<string, string> | Headers;
201201
}) {
202-
const newPath = urlJoin(this.#basePath, path);
203-
204-
// Parse template and replace with values from urlParams
205-
const pathTemplate = parseTemplate(newPath);
206-
const parsedPath = pathTemplate.expand(urlParams);
207-
const url = new URL(`${this.#getBaseUrl?.() ?? ""}${parsedPath}`);
208202
const requestOptions = { ...this.#client.getRequestOptions() };
209203
const requestHeaders = new Headers([
210204
...new Headers(requestOptions.headers).entries(),
@@ -243,6 +237,10 @@ export class Agent {
243237
Object.assign(searchParams, queryParams);
244238
}
245239

240+
const url = new URL(this.#getBaseUrl());
241+
const pathTemplate = parseTemplate(joinPath(this.#basePath, path));
242+
243+
url.pathname = joinPath(url.pathname, pathTemplate.expand(urlParams));
246244
url.search = stringifyQueryParams(searchParams);
247245

248246
try {

js/libs/keycloak-admin-client/src/utils/auth.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import camelize from "camelize-ts";
2+
import { parseTemplate } from "url-template";
23
import { defaultBaseUrl, defaultRealm } from "./constants.js";
34
import { fetchWithError } from "./fetchWithError.js";
5+
import { joinPath } from "./joinPath.js";
46
import { stringifyQueryParams } from "./stringifyQueryParams.js";
57

68
export type GrantTypes = "client_credentials" | "password" | "refresh_token";
@@ -68,10 +70,17 @@ const encodeFormURIComponent = (data: string) =>
6870
encodeRFC3986URIComponent(data).replaceAll("%20", "+");
6971

7072
export const getToken = async (settings: Settings): Promise<TokenResponse> => {
71-
// Construct URL
72-
const baseUrl = settings.baseUrl || defaultBaseUrl;
73-
const realmName = settings.realmName || defaultRealm;
74-
const url = `${baseUrl}/realms/${realmName}/protocol/openid-connect/token`;
73+
const url = new URL(settings.baseUrl ?? defaultBaseUrl);
74+
const pathTemplate = parseTemplate(
75+
"/realms/{realmName}/protocol/openid-connect/token",
76+
);
77+
78+
url.pathname = joinPath(
79+
url.pathname,
80+
pathTemplate.expand({
81+
realmName: settings.realmName ?? defaultRealm,
82+
}),
83+
);
7584

7685
// Prepare credentials for openid-connect token request
7786
// ref: http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const PATH_SEPARATOR = "/";
2+
3+
export function joinPath(...paths: string[]) {
4+
const normalizedPaths = paths.map((path, index) => {
5+
const isFirst = index === 0;
6+
const isLast = index === paths.length - 1;
7+
8+
// Strip out any leading slashes from the path.
9+
if (!isFirst && path.startsWith(PATH_SEPARATOR)) {
10+
path = path.slice(1);
11+
}
12+
13+
// Strip out any trailing slashes from the path.
14+
if (!isLast && path.endsWith(PATH_SEPARATOR)) {
15+
path = path.slice(0, -1);
16+
}
17+
18+
return path;
19+
}, []);
20+
21+
return normalizedPaths.join(PATH_SEPARATOR);
22+
}

js/libs/keycloak-admin-client/test/groups.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { KeycloakAdminClient } from "../src/client.js";
55
import type ClientRepresentation from "../src/defs/clientRepresentation.js";
66
import type GroupRepresentation from "../src/defs/groupRepresentation.js";
77
import type RoleRepresentation from "../src/defs/roleRepresentation.js";
8+
import type { SubGroupQuery } from "../src/resources/groups.js";
89
import { credentials } from "./constants.js";
9-
import { SubGroupQuery } from "../src/resources/groups.js";
1010

1111
const expect = chai.expect;
1212

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect } from "chai";
2+
import { joinPath } from "../lib/utils/joinPath.js";
3+
4+
describe("joinPath", () => {
5+
it("returns an empty string when no paths are provided", () => {
6+
expect(joinPath()).to.equal("");
7+
});
8+
9+
it("joins paths", () => {
10+
expect(joinPath("foo", "bar", "baz")).to.equal("foo/bar/baz");
11+
expect(joinPath("foo", "/bar", "baz")).to.equal("foo/bar/baz");
12+
expect(joinPath("foo", "bar/", "baz")).to.equal("foo/bar/baz");
13+
expect(joinPath("foo", "/bar/", "baz")).to.equal("foo/bar/baz");
14+
});
15+
16+
it("joins paths with leading slashes", () => {
17+
expect(joinPath("/foo", "bar", "baz")).to.equal("/foo/bar/baz");
18+
});
19+
20+
it("joins paths with trailing slashes", () => {
21+
expect(joinPath("foo", "bar", "baz/")).to.equal("foo/bar/baz/");
22+
});
23+
});

js/pnpm-lock.yaml

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)