Skip to content

Commit a285fa9

Browse files
authored
Merge pull request #112 from kinde-oss/feat/marketing-properties
2 parents c09e57c + 5f88778 commit a285fa9

File tree

3 files changed

+184
-4
lines changed

3 files changed

+184
-4
lines changed

lib/types.ts

+46-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export enum PromptTypes {
1111
login = "login",
1212
}
1313

14-
export type LoginMethodParams = Partial<
14+
export type LoginMethodParams<T = Record<string, string>> = Partial<
1515
Pick<
16-
LoginOptions,
16+
LoginOptions<T>,
1717
| "audience"
1818
| "scope"
1919
| "isCreateOrg"
@@ -26,10 +26,49 @@ export type LoginMethodParams = Partial<
2626
| "redirectURL"
2727
| "hasSuccessPage"
2828
| "workflowDeploymentId"
29+
| "properties"
2930
>
3031
>;
3132

32-
export type LoginOptions = {
33+
export type KindeProperties = Partial<{
34+
// UTM tags
35+
utm_source: string;
36+
utm_medium: string;
37+
utm_campaign: string;
38+
utm_content: string;
39+
utm_term: string;
40+
41+
// Google Ads smart campaign tracking
42+
gclid: string;
43+
click_id: string;
44+
hsa_acc: string;
45+
hsa_cam: string;
46+
hsa_grp: string;
47+
hsa_ad: string;
48+
hsa_src: string;
49+
hsa_tgt: string;
50+
hsa_kw: string;
51+
hsa_mt: string;
52+
hsa_net: string;
53+
hsa_ver: string;
54+
55+
// Marketing category
56+
match_type: string;
57+
keyword: string;
58+
device: string;
59+
ad_group_id: string;
60+
campaign_id: string;
61+
creative: string;
62+
network: string;
63+
ad_position: string;
64+
fbclid: string;
65+
li_fat_id: string;
66+
msclkid: string;
67+
twclid: string;
68+
ttclid: string;
69+
}>;
70+
71+
export type LoginOptions<T = Record<string, string>> = {
3372
/** Audience to include in the token */
3473
audience?: string;
3574
/** Client ID of the application
@@ -122,6 +161,10 @@ export type LoginOptions = {
122161
* Workflow Deployment ID to trigger on authentication
123162
*/
124163
workflowDeploymentId?: string;
164+
/**
165+
* Properties to be passed
166+
*/
167+
properties?: T & KindeProperties;
125168
};
126169

127170
export enum IssuerRouteTypes {

lib/utils/generateAuthUrl.test.ts

+87-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, vi } from "vitest";
22
import { IssuerRouteTypes, LoginOptions, PromptTypes, Scopes } from "../types";
33
import { generateAuthUrl } from "./generateAuthUrl";
44
import { MemoryStorage, StorageKeys } from "../sessionManager";
@@ -228,4 +228,90 @@ describe("generateAuthUrl", () => {
228228
result.url.searchParams.delete("state");
229229
expect(result.url.toString()).toBe(expectedUrl);
230230
});
231+
232+
it("Properties are added when defined", async () => {
233+
const domain = "https://auth.example.com";
234+
const options: LoginOptions = {
235+
clientId: "client123",
236+
scope: [Scopes.openid, Scopes.profile, Scopes.offline_access],
237+
redirectURL: "https://example2.com",
238+
prompt: PromptTypes.create,
239+
properties: {
240+
utm_campaign: "test",
241+
},
242+
};
243+
const expectedUrl =
244+
"https://auth.example.com/oauth2/auth?client_id=client123&response_type=code&redirect_uri=https%3A%2F%2Fexample2.com&audience=&scope=openid+profile+offline&prompt=create&code_challenge_method=S256&utm_campaign=test";
245+
246+
const result = await generateAuthUrl(
247+
domain,
248+
IssuerRouteTypes.login,
249+
options,
250+
);
251+
const nonce = result.url.searchParams.get("nonce");
252+
expect(nonce).not.toBeNull();
253+
expect(nonce!.length).toBe(16);
254+
const state = result.url.searchParams.get("state");
255+
expect(state).not.toBeNull();
256+
expect(state!.length).toBe(32);
257+
const codeChallenge = result.url.searchParams.get("code_challenge");
258+
expect(codeChallenge!.length).toBeGreaterThanOrEqual(27);
259+
result.url.searchParams.delete("code_challenge");
260+
result.url.searchParams.delete("nonce");
261+
result.url.searchParams.delete("state");
262+
expect(result.url.toString()).toBe(expectedUrl);
263+
});
264+
265+
it("When non whitelisted properties are added when defined, warn for each one do not add to the url", async () => {
266+
const consoleWarnSpy = vi.spyOn(console, "warn");
267+
268+
const domain = "https://auth.example.com";
269+
const options: LoginOptions<{
270+
testProperty1: string;
271+
testProperty2: string;
272+
}> = {
273+
clientId: "client123",
274+
scope: [Scopes.openid, Scopes.profile, Scopes.offline_access],
275+
redirectURL: "https://example2.com",
276+
prompt: PromptTypes.create,
277+
properties: {
278+
utm_campaign: "test",
279+
testProperty1: "testValue1",
280+
testProperty2: "testValue2",
281+
},
282+
};
283+
const expectedUrl =
284+
"https://auth.example.com/oauth2/auth?client_id=client123&response_type=code&redirect_uri=https%3A%2F%2Fexample2.com&audience=&scope=openid+profile+offline&prompt=create&code_challenge_method=S256&utm_campaign=test";
285+
286+
const result = await generateAuthUrl(
287+
domain,
288+
IssuerRouteTypes.login,
289+
options,
290+
);
291+
const nonce = result.url.searchParams.get("nonce");
292+
expect(nonce).not.toBeNull();
293+
expect(nonce!.length).toBe(16);
294+
const state = result.url.searchParams.get("state");
295+
expect(state).not.toBeNull();
296+
expect(state!.length).toBe(32);
297+
const codeChallenge = result.url.searchParams.get("code_challenge");
298+
expect(codeChallenge!.length).toBeGreaterThanOrEqual(27);
299+
result.url.searchParams.delete("code_challenge");
300+
result.url.searchParams.delete("nonce");
301+
result.url.searchParams.delete("state");
302+
expect(result.url.toString()).toBe(expectedUrl);
303+
304+
expect(consoleWarnSpy).toHaveBeenCalledWith(
305+
"Unsupported Property for url generation: ",
306+
"testProperty1",
307+
);
308+
expect(consoleWarnSpy).toHaveBeenCalledWith(
309+
"Unsupported Property for url generation: ",
310+
"testProperty2",
311+
);
312+
expect(consoleWarnSpy).not.toHaveBeenCalledWith(
313+
"Unsupported Property for url generation: ",
314+
"utm_campaign",
315+
);
316+
});
231317
});

lib/utils/generateAuthUrl.ts

+51
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,44 @@ import { IssuerRouteTypes, LoginOptions, PromptTypes } from "../types";
33
import { generateRandomString } from "./generateRandomString";
44
import { mapLoginMethodParamsForUrl } from "./mapLoginMethodParamsForUrl";
55

6+
const whiteListedProperties = [
7+
// UTM tags
8+
"utm_source",
9+
"utm_medium",
10+
"utm_campaign",
11+
"utm_content",
12+
"utm_term",
13+
14+
// Google Ads smart campaign tracking
15+
"gclid",
16+
"click_id",
17+
"hsa_acc",
18+
"hsa_cam",
19+
"hsa_grp",
20+
"hsa_ad",
21+
"hsa_src",
22+
"hsa_tgt",
23+
"hsa_kw",
24+
"hsa_mt",
25+
"hsa_net",
26+
"hsa_ver",
27+
28+
// Marketing category
29+
"match_type",
30+
"keyword",
31+
"device",
32+
"ad_group_id",
33+
"campaign_id",
34+
"creative",
35+
"network",
36+
"ad_position",
37+
"fbclid",
38+
"li_fat_id",
39+
"msclkid",
40+
"twclid",
41+
"ttclid",
42+
];
43+
644
interface generateAuthUrlConfig {
745
disableUrlSanitization: boolean;
846
}
@@ -70,6 +108,19 @@ export const generateAuthUrl = async (
70108
searchParams["prompt"] = PromptTypes.create;
71109
}
72110

111+
if (loginOptions.properties) {
112+
Object.keys(loginOptions.properties).forEach((key) => {
113+
if (!whiteListedProperties.includes(key)) {
114+
console.warn("Unsupported Property for url generation: ", key);
115+
return;
116+
}
117+
const value = loginOptions.properties?.[key];
118+
if (value !== undefined) {
119+
searchParams[key] = value;
120+
}
121+
});
122+
}
123+
73124
const queryString = new URLSearchParams(searchParams).toString();
74125

75126
return {

0 commit comments

Comments
 (0)