mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-31 23:19:15 +02:00
⚡️ Add support for custom claim paths (#1197)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
1d138cb2ab
commit
104d214d2e
7 changed files with 90 additions and 17 deletions
|
@ -7,12 +7,8 @@ description: How to use your own identity provider
|
||||||
<Info>Available in v3.4.0 and later.</Info>
|
<Info>Available in v3.4.0 and later.</Info>
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
#### Account Linking
|
Accounts using the same email are linked together. This assumes that you are
|
||||||
|
using a trusted identity provider that uses verified email addresses.
|
||||||
Accounts using the same email are linked together. This assumes
|
|
||||||
that you are using a trusted identity provider that uses verified email
|
|
||||||
addresses.
|
|
||||||
|
|
||||||
</Warning>
|
</Warning>
|
||||||
|
|
||||||
## OpenID Connect (OIDC)
|
## OpenID Connect (OIDC)
|
||||||
|
@ -29,7 +25,7 @@ Your OAuth 2.0 application needs to be configured with the following scopes:
|
||||||
|
|
||||||
### Callback URL / Redirect URI
|
### Callback URL / Redirect URI
|
||||||
|
|
||||||
Your identity provider will redirect the user back to the following URL:
|
Your identity provider should redirect the user back to the following URL:
|
||||||
|
|
||||||
```
|
```
|
||||||
{BASE_URL}/api/auth/callback/oidc
|
{BASE_URL}/api/auth/callback/oidc
|
||||||
|
@ -46,7 +42,7 @@ The following configuration options are available for OIDC.
|
||||||
All required fields must be set for OIDC to be enabled.
|
All required fields must be set for OIDC to be enabled.
|
||||||
|
|
||||||
<ParamField path="OIDC_NAME" default="OpenID Connect">
|
<ParamField path="OIDC_NAME" default="OpenID Connect">
|
||||||
The user-facing name of your provider as it will be shown on the login page
|
The display name of your provider as it will be shown on the login page
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="OIDC_DISCOVERY_URL" required>
|
<ParamField path="OIDC_DISCOVERY_URL" required>
|
||||||
|
@ -60,3 +56,17 @@ All required fields must be set for OIDC to be enabled.
|
||||||
<ParamField path="OIDC_CLIENT_SECRET" required>
|
<ParamField path="OIDC_CLIENT_SECRET" required>
|
||||||
The client secret of your OIDC application
|
The client secret of your OIDC application
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="OIDC_NAME_CLAIM_PATH" default="name">
|
||||||
|
The path to the claim that contains the user's name
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="OIDC_EMAIL_CLAIM_PATH" default="email">
|
||||||
|
The path to the claim that contains the user's email address
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="OIDC_PICTURE_CLAIM_PATH" default="picture">
|
||||||
|
The path to the claim that contains the user's profile picture
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<Info>Use dot notation in `_CLAIM_PATH` fields to access nested objects.</Info>
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@rallly/tsconfig/next.json",
|
|
||||||
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
|
|
||||||
"exclude": ["node_modules"],
|
|
||||||
}
|
|
|
@ -19,6 +19,9 @@ export const env = createEnv({
|
||||||
OIDC_DISCOVERY_URL: z.string().optional(),
|
OIDC_DISCOVERY_URL: z.string().optional(),
|
||||||
OIDC_CLIENT_ID: z.string().optional(),
|
OIDC_CLIENT_ID: z.string().optional(),
|
||||||
OIDC_CLIENT_SECRET: z.string().optional(),
|
OIDC_CLIENT_SECRET: z.string().optional(),
|
||||||
|
OIDC_EMAIL_CLAIM_PATH: z.string().default("email"),
|
||||||
|
OIDC_NAME_CLAIM_PATH: z.string().default("name"),
|
||||||
|
OIDC_PICTURE_CLAIM_PATH: z.string().default("picture"),
|
||||||
/**
|
/**
|
||||||
* Email Provider
|
* Email Provider
|
||||||
* Choose which service provider to use for sending emails.
|
* Choose which service provider to use for sending emails.
|
||||||
|
@ -70,6 +73,9 @@ export const env = createEnv({
|
||||||
OIDC_DISCOVERY_URL: process.env.OIDC_DISCOVERY_URL,
|
OIDC_DISCOVERY_URL: process.env.OIDC_DISCOVERY_URL,
|
||||||
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
|
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
|
||||||
OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET,
|
OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET,
|
||||||
|
OIDC_EMAIL_CLAIM_PATH: process.env.OIDC_EMAIL_CLAIM_PATH,
|
||||||
|
OIDC_NAME_CLAIM_PATH: process.env.OIDC_NAME_CLAIM_PATH,
|
||||||
|
OIDC_PICTURE_CLAIM_PATH: process.env.OIDC_PICTURE_CLAIM_PATH,
|
||||||
EMAIL_PROVIDER: process.env.EMAIL_PROVIDER,
|
EMAIL_PROVIDER: process.env.EMAIL_PROVIDER,
|
||||||
SMTP_HOST: process.env.SMTP_HOST,
|
SMTP_HOST: process.env.SMTP_HOST,
|
||||||
SMTP_USER: process.env.SMTP_USER,
|
SMTP_USER: process.env.SMTP_USER,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
NextApiRequest,
|
NextApiRequest,
|
||||||
NextApiResponse,
|
NextApiResponse,
|
||||||
} from "next";
|
} from "next";
|
||||||
import { NextAuthOptions } from "next-auth";
|
import { NextAuthOptions, User } from "next-auth";
|
||||||
import NextAuth, {
|
import NextAuth, {
|
||||||
getServerSession as getServerSessionWithOptions,
|
getServerSession as getServerSessionWithOptions,
|
||||||
} from "next-auth/next";
|
} from "next-auth/next";
|
||||||
|
@ -18,10 +18,12 @@ import GoogleProvider from "next-auth/providers/google";
|
||||||
import { Provider } from "next-auth/providers/index";
|
import { Provider } from "next-auth/providers/index";
|
||||||
|
|
||||||
import { posthog } from "@/app/posthog";
|
import { posthog } from "@/app/posthog";
|
||||||
|
import { env } from "@/env";
|
||||||
import { absoluteUrl } from "@/utils/absolute-url";
|
import { absoluteUrl } from "@/utils/absolute-url";
|
||||||
import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter";
|
import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter";
|
||||||
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
|
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
|
||||||
import { emailClient } from "@/utils/emails";
|
import { emailClient } from "@/utils/emails";
|
||||||
|
import { getValueByPath } from "@/utils/get-value-by-path";
|
||||||
|
|
||||||
const providers: Provider[] = [
|
const providers: Provider[] = [
|
||||||
// When a user registers, we don't want to go through the email verification process
|
// When a user registers, we don't want to go through the email verification process
|
||||||
|
@ -128,9 +130,10 @@ if (
|
||||||
profile(profile) {
|
profile(profile) {
|
||||||
return {
|
return {
|
||||||
id: profile.sub,
|
id: profile.sub,
|
||||||
name: profile.name,
|
name: getValueByPath(profile, env.OIDC_NAME_CLAIM_PATH),
|
||||||
email: profile.email,
|
email: getValueByPath(profile, env.OIDC_EMAIL_CLAIM_PATH),
|
||||||
};
|
image: getValueByPath(profile, env.OIDC_PICTURE_CLAIM_PATH),
|
||||||
|
} as User;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
41
apps/web/src/utils/get-value-by-path.test.ts
Normal file
41
apps/web/src/utils/get-value-by-path.test.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { getValueByPath } from "./get-value-by-path";
|
||||||
|
|
||||||
|
describe("getValueByPath", () => {
|
||||||
|
describe("when the path is not nested", () => {
|
||||||
|
it("should return the value of the key", () => {
|
||||||
|
const path = "key";
|
||||||
|
const obj = { key: "value" };
|
||||||
|
const value = getValueByPath(obj, path);
|
||||||
|
expect(value).toBe("value");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the path is nested", () => {
|
||||||
|
it("should return the value of the nested key", () => {
|
||||||
|
const path = "nested.key";
|
||||||
|
const obj = { nested: { key: "value" } };
|
||||||
|
const value = getValueByPath(obj, path);
|
||||||
|
expect(value).toBe("value");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the path is deeply nested", () => {
|
||||||
|
it("should return the value of the deeply nested key", () => {
|
||||||
|
const path = "deeply.nested.key";
|
||||||
|
const obj = { deeply: { nested: { key: "value" } } };
|
||||||
|
const value = getValueByPath(obj, path);
|
||||||
|
expect(value).toBe("value");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the path is not found", () => {
|
||||||
|
it("should return undefined", () => {
|
||||||
|
const path = "key";
|
||||||
|
const obj = {};
|
||||||
|
const value = getValueByPath(obj, path);
|
||||||
|
expect(value).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
15
apps/web/src/utils/get-value-by-path.ts
Normal file
15
apps/web/src/utils/get-value-by-path.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export function getValueByPath<O extends Record<string, unknown>>(
|
||||||
|
obj: O,
|
||||||
|
path: string,
|
||||||
|
): unknown {
|
||||||
|
const pathArray = path.split(".");
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let curr: any = obj;
|
||||||
|
for (const part of pathArray) {
|
||||||
|
if (curr[part] === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
curr = curr[part];
|
||||||
|
}
|
||||||
|
return curr;
|
||||||
|
}
|
|
@ -84,7 +84,10 @@
|
||||||
"OIDC_CLIENT_ID",
|
"OIDC_CLIENT_ID",
|
||||||
"OIDC_CLIENT_SECRET",
|
"OIDC_CLIENT_SECRET",
|
||||||
"OIDC_DISCOVERY_URL",
|
"OIDC_DISCOVERY_URL",
|
||||||
|
"OIDC_EMAIL_CLAIM_PATH",
|
||||||
|
"OIDC_NAME_CLAIM_PATH",
|
||||||
"OIDC_NAME",
|
"OIDC_NAME",
|
||||||
|
"OIDC_PICTURE_CLAIM_PATH",
|
||||||
"PADDLE_PUBLIC_KEY",
|
"PADDLE_PUBLIC_KEY",
|
||||||
"PORT",
|
"PORT",
|
||||||
"SECRET_PASSWORD",
|
"SECRET_PASSWORD",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue