@tsed/oidc-provider-plugin-wildcard-redirect-uri 8.0.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,13 +2,14 @@
2
2
  "name": "@tsed/oidc-provider-plugin-wildcard-redirect-uri",
3
3
  "description": "A plugin to support wildcard in the redirect uri whitelist",
4
4
  "type": "module",
5
- "version": "8.0.0",
5
+ "version": "8.0.2",
6
6
  "source": "./src/index.ts",
7
7
  "main": "./lib/esm/index.js",
8
8
  "module": "./lib/esm/index.js",
9
9
  "typings": "./lib/types/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
+ "@tsed/source": "./src/index.ts",
12
13
  "types": "./lib/types/index.d.ts",
13
14
  "import": "./lib/esm/index.js",
14
15
  "default": "./lib/esm/index.js"
@@ -27,11 +28,11 @@
27
28
  "wildcard": "2.0.1"
28
29
  },
29
30
  "devDependencies": {
30
- "@tsed/barrels": "8.0.0",
31
- "@tsed/core": "8.0.0",
32
- "@tsed/di": "8.0.0",
33
- "@tsed/oidc-provider": "8.0.0",
34
- "@tsed/typescript": "8.0.0",
31
+ "@tsed/barrels": "8.0.2",
32
+ "@tsed/core": "8.0.2",
33
+ "@tsed/di": "8.0.2",
34
+ "@tsed/oidc-provider": "8.0.2",
35
+ "@tsed/typescript": "8.0.2",
35
36
  "@types/psl": "^1.1.3",
36
37
  "@types/uuid": "10.0.0",
37
38
  "eslint": "9.12.0",
@@ -39,10 +40,10 @@
39
40
  "vitest": "2.1.2"
40
41
  },
41
42
  "peerDependencies": {
42
- "@tsed/core": "8.0.0",
43
- "@tsed/di": "8.0.0",
43
+ "@tsed/core": "8.0.2",
44
+ "@tsed/di": "8.0.2",
44
45
  "@tsed/logger": ">=6.7.5",
45
- "@tsed/oidc-provider": "8.0.0",
46
+ "@tsed/oidc-provider": "8.0.2",
46
47
  "oidc-provider": ">=8.0.0"
47
48
  },
48
49
  "peerDependenciesMeta": {
@@ -0,0 +1,133 @@
1
+ import {catchError} from "@tsed/core";
2
+ import {PlatformTest} from "@tsed/platform-http/testing";
3
+ import {errors, KoaContextWithOIDC} from "oidc-provider";
4
+
5
+ import {OidcWildcardRedirectUriModule} from "./OidcWildcardRedirectUriModule.js";
6
+ import InvalidClientMetadata = errors.InvalidClientMetadata;
7
+
8
+ describe("WildcardRedirectUriAllowed", () => {
9
+ beforeEach(() =>
10
+ PlatformTest.create({
11
+ oidc: {
12
+ plugins: {
13
+ wildcard: {
14
+ enabled: true
15
+ }
16
+ }
17
+ }
18
+ })
19
+ );
20
+ afterEach(() => PlatformTest.reset());
21
+
22
+ describe("$onCreateOIDC()", () => {
23
+ it("should configure the oidc provider", async () => {
24
+ const originalRedirectUriAllowed = vi.fn();
25
+ const originalPostLogoutRedirectUriAllowed = vi.fn();
26
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
27
+ const provider: any = {
28
+ Client: {
29
+ prototype: {
30
+ redirectUriAllowed: originalRedirectUriAllowed,
31
+ postLogoutRedirectUriAllowed: originalPostLogoutRedirectUriAllowed
32
+ }
33
+ }
34
+ };
35
+
36
+ await service.$onCreateOIDC(provider as any);
37
+
38
+ expect(provider.Client.prototype.redirectUriAllowed).not.toEqual(originalRedirectUriAllowed);
39
+ expect(provider.Client.prototype.postLogoutRedirectUriAllowed).not.toEqual(originalPostLogoutRedirectUriAllowed);
40
+ });
41
+ });
42
+ describe("$alterOidcConfiguration()", () => {
43
+ it("should alter configuration", async () => {
44
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
45
+ const config: any = {};
46
+
47
+ await service.$alterOidcConfiguration(config);
48
+
49
+ expect(config.extraClientMetadata.properties).toEqual(["redirect_uris", "post_logout_redirect_uris"]);
50
+ expect(config.extraClientMetadata.validator).toBeInstanceOf(Function);
51
+ });
52
+ });
53
+ describe("validator()", () => {
54
+ describe("for redirect_uris", () => {
55
+ it("should do nothing if key is not redirect_uris", () => {
56
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
57
+ service.validator({} as KoaContextWithOIDC, "key", "value");
58
+ });
59
+ it("should throw an error if one of client redirect_uri contains more than one '*'", () => {
60
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
61
+
62
+ const error = catchError<any>(() =>
63
+ service.validator({} as KoaContextWithOIDC, "redirect_uris", ["https://*.test.com/callback/*"])
64
+ );
65
+
66
+ expect(error).toBeInstanceOf(InvalidClientMetadata);
67
+ expect(error.message).toEqual("invalid_redirect_uri");
68
+ expect(error.error_description).toEqual("redirect_uris with a wildcard may only contain a single one");
69
+ });
70
+ it("should throw an error if one of client redirect_uri contains one '*' but not in hostname", () => {
71
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
72
+
73
+ const error = catchError<any>(() =>
74
+ service.validator({} as KoaContextWithOIDC, "redirect_uris", ["https://redirect.test.com/callback/*"])
75
+ );
76
+
77
+ expect(error).toBeInstanceOf(InvalidClientMetadata);
78
+ expect(error.message).toEqual("invalid_redirect_uri");
79
+ expect(error.error_description).toEqual("redirect_uris may only have a wildcard in the hostname");
80
+ });
81
+ it("should throw an error if one of client redirect_uri contains one '*' as hostname prefix", () => {
82
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
83
+
84
+ const error = catchError<any>(() => service.validator({} as KoaContextWithOIDC, "redirect_uris", ["https://prefix.*/callback"]));
85
+
86
+ expect(error).toBeInstanceOf(InvalidClientMetadata);
87
+ expect(error.message).toEqual("invalid_redirect_uri");
88
+ expect(error.error_description).toEqual("redirect_uris with a wildcard must not match an eTLD+1 of a known public suffix domain");
89
+ });
90
+ });
91
+ describe("for post_logout_redirect_uris", () => {
92
+ it("should do nothing if key is not redirect_uris", () => {
93
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
94
+ service.validator({} as KoaContextWithOIDC, "key", "value");
95
+ });
96
+ it("should throw an error if one of client post_logout_redirect_uris contains more than one '*'", () => {
97
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
98
+
99
+ const error = catchError<any>(() =>
100
+ service.validator({} as KoaContextWithOIDC, "post_logout_redirect_uris", ["https://*.test.com/callback/*"])
101
+ );
102
+
103
+ expect(error).toBeInstanceOf(InvalidClientMetadata);
104
+ expect(error.message).toEqual("invalid_client_metadata");
105
+ expect(error.error_description).toEqual("post_logout_redirect_uris with a wildcard may only contain a single one");
106
+ });
107
+ it("should throw an error if one of client post_logout_redirect_uris contains one '*' but not in hostname", () => {
108
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
109
+
110
+ const error = catchError<any>(() =>
111
+ service.validator({} as KoaContextWithOIDC, "post_logout_redirect_uris", ["https://redirect.test.com/callback/*"])
112
+ );
113
+
114
+ expect(error).toBeInstanceOf(InvalidClientMetadata);
115
+ expect(error.message).toEqual("invalid_client_metadata");
116
+ expect(error.error_description).toEqual("post_logout_redirect_uris may only have a wildcard in the hostname");
117
+ });
118
+ it("should throw an error if one of client post_logout_redirect_uris contains one '*' as hostname prefix", () => {
119
+ const service = PlatformTest.get<OidcWildcardRedirectUriModule>(OidcWildcardRedirectUriModule);
120
+
121
+ const error = catchError<any>(() =>
122
+ service.validator({} as KoaContextWithOIDC, "post_logout_redirect_uris", ["https://prefix.*/callback"])
123
+ );
124
+
125
+ expect(error).toBeInstanceOf(InvalidClientMetadata);
126
+ expect(error.message).toEqual("invalid_client_metadata");
127
+ expect(error.error_description).toEqual(
128
+ "post_logout_redirect_uris with a wildcard must not match an eTLD+1 of a known public suffix domain"
129
+ );
130
+ });
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,92 @@
1
+ import {constant, logger, Module} from "@tsed/di";
2
+ import {OidcSettings} from "@tsed/oidc-provider";
3
+ import Provider, {errors, type KoaContextWithOIDC} from "oidc-provider";
4
+ // @ts-ignore
5
+ import psl from "psl";
6
+
7
+ import {wildcardRedirectUriAllowed} from "./utils/wildcardRedirectUriAllowed.js";
8
+
9
+ declare global {
10
+ namespace TsED {
11
+ interface OIDCPluginSettings {
12
+ wildcard: {
13
+ enabled?: boolean;
14
+ };
15
+ }
16
+ }
17
+ }
18
+
19
+ @Module()
20
+ export class OidcWildcardRedirectUriModule {
21
+ readonly enabled = constant<boolean>("oidc.plugins.wildcard.enabled", false);
22
+
23
+ $onCreateOIDC(provider: Provider) {
24
+ if (this.enabled) {
25
+ const {redirectUriAllowed, postLogoutRedirectUriAllowed} = provider.Client.prototype;
26
+ provider.Client.prototype.redirectUriAllowed = wildcardRedirectUriAllowed(redirectUriAllowed, "redirectUris");
27
+ provider.Client.prototype.postLogoutRedirectUriAllowed = wildcardRedirectUriAllowed(
28
+ postLogoutRedirectUriAllowed,
29
+ "postLogoutRedirectUris"
30
+ );
31
+
32
+ logger().warn("⚠️⚠️⚠️ OIDC Wildcard Uris plugin is ENABLED ⚠️⚠️⚠️");
33
+ }
34
+ }
35
+
36
+ $alterOidcConfiguration(config: OidcSettings): Promise<OidcSettings> {
37
+ if (this.enabled) {
38
+ const actualProperties = config?.extraClientMetadata?.properties || [];
39
+
40
+ config.extraClientMetadata = {
41
+ properties: [...actualProperties, "redirect_uris", "post_logout_redirect_uris"],
42
+ validator: this.validator.bind(this)
43
+ };
44
+ }
45
+
46
+ return Promise.resolve(config);
47
+ }
48
+
49
+ validator(ctx: KoaContextWithOIDC, key: string, value: any) {
50
+ if (key === "redirect_uris") {
51
+ for (const redirectUri of value) {
52
+ if (redirectUri.includes("*")) {
53
+ const {hostname, href} = new URL(redirectUri);
54
+
55
+ if (href.split("*").length !== 2) {
56
+ throw new errors.InvalidClientMetadata("redirect_uris with a wildcard may only contain a single one");
57
+ }
58
+
59
+ if (!hostname.includes("*")) {
60
+ throw new errors.InvalidClientMetadata("redirect_uris may only have a wildcard in the hostname");
61
+ }
62
+
63
+ if (!psl.get(hostname.split("*.")[1])) {
64
+ throw new errors.InvalidClientMetadata(
65
+ "redirect_uris with a wildcard must not match an eTLD+1 of a known public suffix domain"
66
+ );
67
+ }
68
+ }
69
+ }
70
+ } else if (key === "post_logout_redirect_uris") {
71
+ for (const postLogoutRedirectUri of value) {
72
+ if (postLogoutRedirectUri.includes("*")) {
73
+ const {hostname, href} = new URL(postLogoutRedirectUri);
74
+
75
+ if (href.split("*").length !== 2) {
76
+ throw new errors.InvalidClientMetadata("post_logout_redirect_uris with a wildcard may only contain a single one");
77
+ }
78
+
79
+ if (!hostname.includes("*")) {
80
+ throw new errors.InvalidClientMetadata("post_logout_redirect_uris may only have a wildcard in the hostname");
81
+ }
82
+
83
+ if (!psl.get(hostname.split("*.")[1])) {
84
+ throw new errors.InvalidClientMetadata(
85
+ "post_logout_redirect_uris with a wildcard must not match an eTLD+1 of a known public suffix domain"
86
+ );
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @file Automatically generated by @tsed/barrels.
3
+ */
4
+ export * from "./OidcWildcardRedirectUriModule.js";
5
+ export * from "./utils/wildcardRedirectUriAllowed.js";
@@ -0,0 +1,48 @@
1
+ import {wildcardRedirectUriAllowed} from "./wildcardRedirectUriAllowed.js";
2
+
3
+ describe("wildcardRedirectUriAllowed()", () => {
4
+ it("should call the original function if there is no wildcard in client redirect_uris", () => {
5
+ const originalRedirectUriAllowed = vi.fn();
6
+ wildcardRedirectUriAllowed(originalRedirectUriAllowed, "redirectUris").call(
7
+ {
8
+ redirectUris: ["https://redirect.test.com"]
9
+ },
10
+ "https://redirect.test.com"
11
+ );
12
+ expect(originalRedirectUriAllowed).toHaveBeenCalledWith("https://redirect.test.com");
13
+ });
14
+
15
+ it("should return original function result if client has wildcard uris but not used", () => {
16
+ const originalRedirectUriAllowed = vi.fn().mockReturnValue(true);
17
+ const result = wildcardRedirectUriAllowed(originalRedirectUriAllowed, "redirectUris").call(
18
+ {
19
+ redirectUris: ["https://test.com/titi", "https://*.test.com/toto"]
20
+ },
21
+ "https://test.com/titi"
22
+ );
23
+ expect(originalRedirectUriAllowed).toHaveBeenCalledWith("https://test.com/titi");
24
+ expect(result).toEqual(true);
25
+ });
26
+ it("should return true if the redirect_uri match client redirect_uri with wildcard", () => {
27
+ const originalRedirectUriAllowed = vi.fn();
28
+ const result = wildcardRedirectUriAllowed(originalRedirectUriAllowed, "redirectUris").call(
29
+ {
30
+ redirectUris: ["https://*.test.com/"]
31
+ },
32
+ "https://toto.test.com/"
33
+ );
34
+ expect(originalRedirectUriAllowed).not.toHaveBeenCalledWith();
35
+ expect(result).toEqual(true);
36
+ });
37
+ it("should return true if the post_logout_redirect_uri match client redirect_uri with wildcard", () => {
38
+ const originalRedirectUriAllowed = vi.fn();
39
+ const result = wildcardRedirectUriAllowed(originalRedirectUriAllowed, "postLogoutRedirectUris").call(
40
+ {
41
+ postLogoutRedirectUris: ["https://*.test.com/"]
42
+ },
43
+ "https://toto.test.com/"
44
+ );
45
+ expect(originalRedirectUriAllowed).not.toHaveBeenCalledWith();
46
+ expect(result).toEqual(true);
47
+ });
48
+ });
@@ -0,0 +1,22 @@
1
+ // @ts-ignore
2
+ import wildcard from "wildcard";
3
+
4
+ const hasWildcardHost = (redirectUri: string) => {
5
+ const {hostname} = new URL(redirectUri);
6
+ return hostname.includes("*");
7
+ };
8
+
9
+ const wildcardMatches = (redirectUri: string, wildcardUri: string) => !!wildcard(wildcardUri, redirectUri);
10
+
11
+ export function wildcardRedirectUriAllowed(
12
+ originalRedirectUriAllowed: any,
13
+ redirectUriOrPostLogoutRedirectUri: "redirectUris" | "postLogoutRedirectUris"
14
+ ) {
15
+ return function (redirectUri: string) {
16
+ if (this[redirectUriOrPostLogoutRedirectUri].some(hasWildcardHost)) {
17
+ const wildcardUris = this[redirectUriOrPostLogoutRedirectUri].filter(hasWildcardHost);
18
+ return wildcardUris.some(wildcardMatches.bind(undefined, redirectUri)) || originalRedirectUriAllowed.call(this, redirectUri);
19
+ }
20
+ return originalRedirectUriAllowed.call(this, redirectUri);
21
+ };
22
+ }