Skip to content

Commit d20f863

Browse files
committed
feat: Add support for virtual fields (jwt session strategy)
1 parent 9052f38 commit d20f863

File tree

11 files changed

+380
-79
lines changed

11 files changed

+380
-79
lines changed

packages/dev/src/app/(app)/_components/AuthOverview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const AuthOverview = async () => {
1212
<div>
1313
<h3>Auth.js Session</h3>
1414
<p>{session?.user ? <SignOutButtonAuthjs /> : <SignInButtonAuthjs />}</p>
15-
<pre>{JSON.stringify(session?.user, null, 2)}</pre>
15+
<pre>{JSON.stringify(session ?? undefined, null, 2)}</pre>
1616
<br />
1717
<h3>Payload CMS User</h3>
1818
<p>{payloadUser && <SignOutButtonPayload />}</p>

packages/dev/src/auth.config.ts

Lines changed: 94 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,153 @@
11
import jwt from "jsonwebtoken";
2-
import type { NextAuthConfig, Profile } from "next-auth";
2+
import type { NextAuthConfig } from "next-auth";
3+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
34
import type { JWT } from "next-auth/jwt";
45
import github from "next-auth/providers/github";
56
import keycloak from "next-auth/providers/keycloak";
67
import nodemailer from "next-auth/providers/nodemailer";
7-
8-
declare module "next-auth/jwt" {
9-
interface JWT extends Pick<Profile, "roles"> {
10-
id?: string;
11-
}
12-
}
8+
import type { User as PayloadUser } from "payload/generated-types";
139

1410
declare module "next-auth" {
15-
interface Profile {
16-
roles?: string[];
17-
}
1811
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
19-
interface User extends Pick<JWT, "id" | "roles"> {}
12+
interface User
13+
extends Partial<Omit<PayloadUser, "accounts" | "sessions" | "verificationTokens">> {}
14+
}
15+
declare module "next-auth/jwt" {
16+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
17+
interface JWT
18+
extends Partial<
19+
Pick<
20+
PayloadUser,
21+
"id" | "additionalUserDatabaseField" | "additionalUserVirtualField" | "roles"
22+
>
23+
> {}
2024
}
2125

2226
export const authConfig: NextAuthConfig = {
2327
theme: { logo: "https://authjs.dev/img/logo-sm.png" },
2428
providers: [
2529
github({
2630
allowDangerousEmailAccountLinking: true,
31+
/**
32+
* Add additional fields to the user on first sign in
33+
*/
2734
profile(profile) {
28-
profile.roles = ["user"]; // Extend the profile
2935
return {
36+
// Default fields (@see https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/github.ts#L176)
3037
id: profile.id.toString(),
3138
name: profile.name ?? profile.login,
32-
email: profile.email,
39+
email: profile.email!,
3340
image: profile.avatar_url,
34-
roles: ["user"], // Extend the user
41+
// Custom fields
42+
additionalUserDatabaseField: `Create by github provider profile callback at ${new Date().toISOString()}`,
43+
};
44+
},
45+
account(tokens) {
46+
return {
47+
...tokens,
48+
additionalAccountDatabaseField: `Create by github provider profile callback at ${new Date().toISOString()}`,
3549
};
3650
},
3751
}),
3852
keycloak({
3953
allowDangerousEmailAccountLinking: true,
40-
profile(profile, tokens) {
41-
// Add roles to the profile
42-
if (tokens.access_token) {
43-
const decodedToken = jwt.decode(tokens.access_token);
44-
if (decodedToken && typeof decodedToken !== "string") {
45-
profile.roles = decodedToken.resource_access?.[process.env.AUTH_KEYCLOAK_ID!]?.roles; // Extend the profile
46-
}
47-
}
54+
/**
55+
* Add additional fields to the user on first sign in
56+
*/
57+
profile(profile) {
4858
return {
59+
// Default fields
4960
id: profile.sub,
5061
name: profile.name,
5162
email: profile.email,
5263
image: profile.picture,
53-
roles: profile.roles ?? [], // Extend the user
64+
// Custom fields
65+
locale: profile.locale,
66+
additionalUserDatabaseField: `Create by keycloak provider profile callback at ${new Date().toISOString()}`,
67+
};
68+
},
69+
account(tokens) {
70+
return {
71+
...tokens,
72+
additionalAccountDatabaseField: `Create by keycloak provider profile callback at ${new Date().toISOString()}`,
5473
};
5574
},
5675
}),
5776
nodemailer({
5877
server: process.env.EMAIL_SERVER,
5978
from: process.env.EMAIL_FROM,
79+
/* sendVerificationRequest: ({ url }) => {
80+
console.log("nodemailer:", url);
81+
}, */
6082
}),
6183
],
62-
/* session: {
84+
session: {
6385
strategy: "jwt",
64-
}, */
86+
},
6587
callbacks: {
66-
jwt: ({ token, user, profile }) => {
67-
// Include user id in the JWT token
88+
jwt: ({ token, user, account, trigger }) => {
89+
//console.log("callbacks.jwt", token, user, account);
90+
91+
/**
92+
* For jwt session strategy, we need to forward additional fields to the token
93+
*/
6894
if (user) {
69-
token.id = user.id;
95+
if (user.id) {
96+
token.id = user.id;
97+
}
98+
token.additionalUserDatabaseField = user.additionalUserDatabaseField;
7099
}
71-
// Include roles in the JWT token
72-
if (profile) {
73-
token.roles = profile.roles;
100+
101+
// Add virtual field to the token
102+
token.additionalUserVirtualField = `Create by jwt callback at ${new Date().toISOString()}`;
103+
104+
/**
105+
* Add roles to the token
106+
* - Extract roles from the token for keycloak provider
107+
* - otherwise use default roles ["user"]
108+
*/
109+
if (trigger === "signIn" || trigger === "signUp") {
110+
const roles: string[] = ["user"];
111+
if (account?.provider === "keycloak" && account.access_token) {
112+
const decodedToken = jwt.decode(account.access_token);
113+
if (decodedToken && typeof decodedToken !== "string") {
114+
roles.push(
115+
...(decodedToken.resource_access?.[process.env.AUTH_KEYCLOAK_ID!]?.roles ?? []),
116+
);
117+
}
118+
}
119+
token.roles = [...new Set(roles)];
74120
}
121+
75122
return token;
76123
},
77-
session: ({ session, user, token }) => {
78-
// session strategy: "jwt"
124+
session: ({ session, token }) => {
125+
//console.log("callbacks.session", session, user, token);
126+
127+
/**
128+
* For jwt session strategy, we need to forward additional fields to the session
129+
*/
79130
if (token) {
80131
if (token.id) {
81132
session.user.id = token.id;
82133
}
134+
135+
session.user.additionalUserDatabaseField = token.additionalUserDatabaseField;
136+
session.user.additionalUserVirtualField = token.additionalUserVirtualField;
137+
83138
session.user.roles = token.roles;
84139
}
85-
// session strategy: "database"
86-
if (user) {
87-
session.user.id = user.id;
88-
}
140+
89141
return session;
90142
},
91-
/* signIn: async () => {
92-
console.log("signIn auth.ts");
93-
return true;
94-
}, */
95143
authorized: ({ auth }) => {
96144
// Logged in users are authenticated, otherwise redirect to login page
97145
return !!auth;
98146
},
99147
},
148+
/* events: {
149+
signIn: () => {
150+
console.log("original events.signIn");
151+
},
152+
}, */
100153
};

packages/dev/src/payload-types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export interface User {
6767
emailVerified?: string | null;
6868
name?: string | null;
6969
image?: string | null;
70+
additionalUserDatabaseField: string;
71+
additionalUserVirtualField?: string | null;
72+
locale?: string | null;
7073
roles?: string[];
7174
accounts?:
7275
| {
@@ -75,13 +78,16 @@ export interface User {
7578
providerAccountId: string;
7679
type: string;
7780
access_token?: string | null;
81+
additionalAccountDatabaseField: string;
82+
createdAt: string;
7883
}[]
7984
| null;
8085
verificationTokens?:
8186
| {
8287
id?: string | null;
8388
token: string;
8489
expires: string;
90+
createdAt: string;
8591
}[]
8692
| null;
8793
updatedAt: string;
@@ -164,6 +170,9 @@ export interface UsersSelect<T extends boolean = true> {
164170
emailVerified?: T;
165171
name?: T;
166172
image?: T;
173+
additionalUserDatabaseField?: T;
174+
additionalUserVirtualField?: T;
175+
locale?: T;
167176
roles?: T;
168177
accounts?:
169178
| T
@@ -173,13 +182,16 @@ export interface UsersSelect<T extends boolean = true> {
173182
providerAccountId?: T;
174183
type?: T;
175184
access_token?: T;
185+
additionalAccountDatabaseField?: T;
186+
createdAt?: T;
176187
};
177188
verificationTokens?:
178189
| T
179190
| {
180191
id?: T;
181192
token?: T;
182193
expires?: T;
194+
createdAt?: T;
183195
};
184196
updatedAt?: T;
185197
createdAt?: T;

packages/dev/src/payload/collections/users.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CollectionConfig } from "payload";
2+
import { createdAtField } from "../fields/createdAt";
23

34
const Users: CollectionConfig = {
45
slug: "users",
@@ -33,12 +34,50 @@ const Users: CollectionConfig = {
3334
name: "access_token",
3435
type: "text",
3536
},
37+
{
38+
name: "additionalAccountDatabaseField",
39+
type: "text",
40+
required: true,
41+
},
42+
createdAtField,
3643
],
3744
},
45+
/* {
46+
name: "sessions",
47+
type: "array",
48+
fields: [createdAtField],
49+
}, */
50+
{
51+
name: "verificationTokens",
52+
type: "array",
53+
fields: [createdAtField],
54+
},
3855
// Add custom field
56+
{
57+
name: "additionalUserDatabaseField",
58+
type: "text",
59+
required: true,
60+
},
61+
{
62+
name: "additionalUserVirtualField",
63+
type: "text",
64+
virtual: true,
65+
admin: {
66+
hidden: true,
67+
},
68+
},
69+
{
70+
name: "locale",
71+
type: "text",
72+
},
73+
/**
74+
* Add roles field
75+
* This field will not be stored in the database
76+
*/
3977
{
4078
name: "roles",
4179
type: "json",
80+
virtual: true,
4281
typescriptSchema: [
4382
() => ({
4483
type: "array",
@@ -47,6 +86,9 @@ const Users: CollectionConfig = {
4786
},
4887
}),
4988
],
89+
admin: {
90+
hidden: true,
91+
},
5092
},
5193
],
5294
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { type Field } from "payload";
2+
3+
export const createdAtField: Field = {
4+
name: "createdAt",
5+
type: "date",
6+
required: true,
7+
defaultValue: () => new Date(),
8+
};

0 commit comments

Comments
 (0)