️ Add support for custom claim paths (#1197)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Luke Vella 2024-07-05 09:03:10 +01:00 committed by GitHub
parent 1d138cb2ab
commit 104d214d2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 90 additions and 17 deletions

View file

@ -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>

View file

@ -1,5 +0,0 @@
{
"extends": "@rallly/tsconfig/next.json",
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
"exclude": ["node_modules"],
}

View file

@ -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,

View file

@ -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;
}, },
}); });
} }

View 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();
});
});
});

View 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;
}

View file

@ -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",