Skip to content

Commit

Permalink
⚡️ Add support for custom claim paths (#1197)
Browse files Browse the repository at this point in the history
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
lukevella and coderabbitai[bot] authored Jul 5, 2024
1 parent 1d138cb commit 104d214
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 17 deletions.
26 changes: 18 additions & 8 deletions apps/docs/self-hosting/single-sign-on.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ description: How to use your own identity provider
<Info>Available in v3.4.0 and later.</Info>

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

## OpenID Connect (OIDC)
Expand All @@ -29,7 +25,7 @@ Your OAuth 2.0 application needs to be configured with the following scopes:

### 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
Expand All @@ -46,7 +42,7 @@ The following configuration options are available for OIDC.
All required fields must be set for OIDC to be enabled.

<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 path="OIDC_DISCOVERY_URL" required>
Expand All @@ -60,3 +56,17 @@ All required fields must be set for OIDC to be enabled.
<ParamField path="OIDC_CLIENT_SECRET" required>
The client secret of your OIDC application
</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>
5 changes: 0 additions & 5 deletions apps/docs/tsconfig.json

This file was deleted.

6 changes: 6 additions & 0 deletions apps/web/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export const env = createEnv({
OIDC_DISCOVERY_URL: z.string().optional(),
OIDC_CLIENT_ID: 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
* Choose which service provider to use for sending emails.
Expand Down Expand Up @@ -70,6 +73,9 @@ export const env = createEnv({
OIDC_DISCOVERY_URL: process.env.OIDC_DISCOVERY_URL,
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
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,
SMTP_HOST: process.env.SMTP_HOST,
SMTP_USER: process.env.SMTP_USER,
Expand Down
11 changes: 7 additions & 4 deletions apps/web/src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
NextApiRequest,
NextApiResponse,
} from "next";
import { NextAuthOptions } from "next-auth";
import { NextAuthOptions, User } from "next-auth";
import NextAuth, {
getServerSession as getServerSessionWithOptions,
} from "next-auth/next";
Expand All @@ -18,10 +18,12 @@ import GoogleProvider from "next-auth/providers/google";
import { Provider } from "next-auth/providers/index";

import { posthog } from "@/app/posthog";
import { env } from "@/env";
import { absoluteUrl } from "@/utils/absolute-url";
import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter";
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
import { emailClient } from "@/utils/emails";
import { getValueByPath } from "@/utils/get-value-by-path";

const providers: Provider[] = [
// When a user registers, we don't want to go through the email verification process
Expand Down Expand Up @@ -128,9 +130,10 @@ if (
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
};
name: getValueByPath(profile, env.OIDC_NAME_CLAIM_PATH),
email: getValueByPath(profile, env.OIDC_EMAIL_CLAIM_PATH),
image: getValueByPath(profile, env.OIDC_PICTURE_CLAIM_PATH),
} as User;
},
});
}
Expand Down
41 changes: 41 additions & 0 deletions apps/web/src/utils/get-value-by-path.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions apps/web/src/utils/get-value-by-path.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@
"OIDC_CLIENT_ID",
"OIDC_CLIENT_SECRET",
"OIDC_DISCOVERY_URL",
"OIDC_EMAIL_CLAIM_PATH",
"OIDC_NAME_CLAIM_PATH",
"OIDC_NAME",
"OIDC_PICTURE_CLAIM_PATH",
"PADDLE_PUBLIC_KEY",
"PORT",
"SECRET_PASSWORD",
Expand Down

0 comments on commit 104d214

Please sign in to comment.