|
1 | 1 | 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 |
3 | 4 | import type { JWT } from "next-auth/jwt"; |
4 | 5 | import github from "next-auth/providers/github"; |
5 | 6 | import keycloak from "next-auth/providers/keycloak"; |
6 | 7 | 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"; |
13 | 9 |
|
14 | 10 | declare module "next-auth" { |
15 | | - interface Profile { |
16 | | - roles?: string[]; |
17 | | - } |
18 | 11 | // 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 | + > {} |
20 | 24 | } |
21 | 25 |
|
22 | 26 | export const authConfig: NextAuthConfig = { |
23 | 27 | theme: { logo: "https://authjs.dev/img/logo-sm.png" }, |
24 | 28 | providers: [ |
25 | 29 | github({ |
26 | 30 | allowDangerousEmailAccountLinking: true, |
| 31 | + /** |
| 32 | + * Add additional fields to the user on first sign in |
| 33 | + */ |
27 | 34 | profile(profile) { |
28 | | - profile.roles = ["user"]; // Extend the profile |
29 | 35 | return { |
| 36 | + // Default fields (@see https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/github.ts#L176) |
30 | 37 | id: profile.id.toString(), |
31 | 38 | name: profile.name ?? profile.login, |
32 | | - email: profile.email, |
| 39 | + email: profile.email!, |
33 | 40 | 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()}`, |
35 | 49 | }; |
36 | 50 | }, |
37 | 51 | }), |
38 | 52 | keycloak({ |
39 | 53 | 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) { |
48 | 58 | return { |
| 59 | + // Default fields |
49 | 60 | id: profile.sub, |
50 | 61 | name: profile.name, |
51 | 62 | email: profile.email, |
52 | 63 | 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()}`, |
54 | 73 | }; |
55 | 74 | }, |
56 | 75 | }), |
57 | 76 | nodemailer({ |
58 | 77 | server: process.env.EMAIL_SERVER, |
59 | 78 | from: process.env.EMAIL_FROM, |
| 79 | + /* sendVerificationRequest: ({ url }) => { |
| 80 | + console.log("nodemailer:", url); |
| 81 | + }, */ |
60 | 82 | }), |
61 | 83 | ], |
62 | | - /* session: { |
| 84 | + session: { |
63 | 85 | strategy: "jwt", |
64 | | - }, */ |
| 86 | + }, |
65 | 87 | 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 | + */ |
68 | 94 | if (user) { |
69 | | - token.id = user.id; |
| 95 | + if (user.id) { |
| 96 | + token.id = user.id; |
| 97 | + } |
| 98 | + token.additionalUserDatabaseField = user.additionalUserDatabaseField; |
70 | 99 | } |
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)]; |
74 | 120 | } |
| 121 | + |
75 | 122 | return token; |
76 | 123 | }, |
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 | + */ |
79 | 130 | if (token) { |
80 | 131 | if (token.id) { |
81 | 132 | session.user.id = token.id; |
82 | 133 | } |
| 134 | + |
| 135 | + session.user.additionalUserDatabaseField = token.additionalUserDatabaseField; |
| 136 | + session.user.additionalUserVirtualField = token.additionalUserVirtualField; |
| 137 | + |
83 | 138 | session.user.roles = token.roles; |
84 | 139 | } |
85 | | - // session strategy: "database" |
86 | | - if (user) { |
87 | | - session.user.id = user.id; |
88 | | - } |
| 140 | + |
89 | 141 | return session; |
90 | 142 | }, |
91 | | - /* signIn: async () => { |
92 | | - console.log("signIn auth.ts"); |
93 | | - return true; |
94 | | - }, */ |
95 | 143 | authorized: ({ auth }) => { |
96 | 144 | // Logged in users are authenticated, otherwise redirect to login page |
97 | 145 | return !!auth; |
98 | 146 | }, |
99 | 147 | }, |
| 148 | + /* events: { |
| 149 | + signIn: () => { |
| 150 | + console.log("original events.signIn"); |
| 151 | + }, |
| 152 | + }, */ |
100 | 153 | }; |
0 commit comments