Skip to content

Commit a991136

Browse files
committed
feat: add authenticateIdentity to handle agents
1 parent 9af4a2e commit a991136

2 files changed

Lines changed: 167 additions & 67 deletions

File tree

packages/auth/src/payload-jwt/authenticateRequest.ts

Lines changed: 159 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ function createAuthError(message: string, statusCode: number): AuthError {
2222
export type { AuthError };
2323

2424
function getValidRole(permissions?: string[]): ValidRole {
25-
if (!permissions || permissions.length === 0) return 'user';
26-
for (const permission of permissions) {
27-
if (VALID_ROLES[permission]) {
28-
return VALID_ROLES[permission];
25+
if (!permissions || permissions.length === 0) return 'user';
26+
for (const permission of permissions) {
27+
if (VALID_ROLES[permission]) {
28+
return VALID_ROLES[permission];
29+
}
2930
}
30-
}
31-
return 'user';
31+
return 'user';
3232
}
3333

3434
interface User {
@@ -91,43 +91,43 @@ export async function authenticateRequest({ req, payload }: AuthenticateRequestO
9191

9292
if (!payload) throw createAuthError("Payload instance is required for Keycloak user normalisation", 500);
9393

94-
const payloadUser = (await payload.find({
95-
collection: 'users',
96-
depth: 1,
97-
limit: 1,
98-
draft: false,
99-
overrideAccess: true,
100-
where: { email: { equals: session.extra.email } }
94+
const payloadUser = (await payload.find({
95+
collection: 'users',
96+
depth: 1,
97+
limit: 1,
98+
draft: false,
99+
overrideAccess: true,
100+
where: { email: { equals: session.extra.email } }
101101
})).docs[0];
102102

103103
const role = getValidRole(permissions);
104104

105105
if (!payloadUser) {
106-
const newUser = await payload.create({
107-
collection: 'users',
108-
data: {
109-
email: session.extra.email,
110-
name: session.extra.name,
111-
role,
112-
enabled: true,
113-
accounts: [{ provider: 'keycloak', providerAccountId: session.sub, type: 'oidc' }],
114-
},
115-
draft: false,
116-
overrideAccess: true,
117-
});
118-
console.log("Created new Payload user for Keycloak user:", newUser.id, newUser.email);
119-
return { method: 'bearer', ...newUser };
106+
const newUser = await payload.create({
107+
collection: 'users',
108+
data: {
109+
email: session.extra.email,
110+
name: session.extra.name,
111+
role,
112+
enabled: true,
113+
accounts: [{ provider: 'keycloak', providerAccountId: session.sub, type: 'oidc' }],
114+
},
115+
draft: false,
116+
overrideAccess: true,
117+
});
118+
console.log("Created new Payload user for Keycloak user:", newUser.id, newUser.email);
119+
return { method: 'bearer', ...newUser };
120120
}
121121

122122
if (payloadUser.role !== role) {
123-
await payload.update({
124-
collection: 'users',
125-
id: payloadUser.id,
126-
data: { role },
127-
draft: false,
128-
overrideAccess: true,
129-
});
130-
console.log(`Updated Payload user role for ${payloadUser.email} to ${role}`);
123+
await payload.update({
124+
collection: 'users',
125+
id: payloadUser.id,
126+
data: { role },
127+
draft: false,
128+
overrideAccess: true,
129+
});
130+
console.log(`Updated Payload user role for ${payloadUser.email} to ${role}`);
131131
}
132132

133133
return { method: 'bearer', ...payloadUser };
@@ -138,52 +138,144 @@ export async function authenticateRequestHeaders({ headers, payload }: { headers
138138
if (!authHeader) return null;
139139

140140
const session = await verifyToken(authHeader.replace('Bearer ', ''));
141-
if (!session?.sub || !session.extra) throw createAuthError("No valid session found", 401);
141+
if (!session?.sub || !session.extra || !session.agent_id) throw createAuthError("No valid session found", 401);
142142

143143
const OAUTH_CLIENT_ID = process.env.OAUTH_CLIENT_ID!;
144144
const permissions = ((session.resource_access as Record<string, { roles?: string[] }>)?.[OAUTH_CLIENT_ID]?.roles as string[] | undefined);
145-
if (!permissions) throw createAuthError("User does not have permission to access this application.", 403);
145+
if (!permissions) throw createAuthError("User or Agent does not have permission to access this application.", 403);
146146

147147
if (!payload) throw createAuthError("Payload instance is required for Keycloak user normalisation", 500);
148148

149-
const payloadUser = (await payload.find({
150-
collection: 'users',
151-
depth: 1,
152-
limit: 1,
153-
draft: false,
154-
overrideAccess: true,
155-
where: { email: { equals: session.extra.email } }
149+
const payloadUser = (await payload.find({
150+
collection: 'users',
151+
depth: 1,
152+
limit: 1,
153+
draft: false,
154+
overrideAccess: true,
155+
where: { email: { equals: session.extra.email } }
156156
})).docs[0];
157157

158158
const role = getValidRole(permissions);
159159

160160
if (!payloadUser) {
161-
const newUser = await payload.create({
162-
collection: 'users',
163-
data: {
164-
email: session.extra.email,
165-
name: session.extra.name,
166-
role,
167-
enabled: true,
168-
accounts: [{ provider: 'keycloak', providerAccountId: session.sub, type: 'oidc' }],
169-
},
170-
draft: false,
171-
overrideAccess: true,
172-
});
173-
console.log("Created new Payload user for Keycloak user:", newUser.id, newUser.email);
174-
return { user: { ...newUser, collection: 'users' as const } };
161+
const newUser = await payload.create({
162+
collection: 'users',
163+
data: {
164+
email: session.extra.email,
165+
name: session.extra.name,
166+
role,
167+
enabled: true,
168+
accounts: [{ provider: 'keycloak', providerAccountId: session.sub, type: 'oidc' }],
169+
},
170+
draft: false,
171+
overrideAccess: true,
172+
});
173+
console.log("Created new Payload user for Keycloak user:", newUser.id, newUser.email);
174+
return { user: { ...newUser, collection: 'users' as const } };
175175
}
176176

177177
if (payloadUser.role !== role) {
178-
await payload.update({
179-
collection: 'users',
180-
id: payloadUser.id,
181-
data: { role },
182-
draft: false,
183-
overrideAccess: true,
184-
});
185-
console.log(`Updated Payload user role for ${payloadUser.email} to ${role}`);
178+
await payload.update({
179+
collection: 'users',
180+
id: payloadUser.id,
181+
data: { role },
182+
draft: false,
183+
overrideAccess: true,
184+
});
185+
console.log(`Updated Payload user role for ${payloadUser.email} to ${role}`);
186186
}
187187

188188
return { user: { ...payloadUser, collection: 'users' as const } };
189-
}
189+
}
190+
191+
export async function authenticateIdentity({
192+
headers,
193+
payload,
194+
}: {
195+
headers: Headers
196+
payload: Payload
197+
}) {
198+
const authHeader = headers.get('authorization')
199+
if (!authHeader) return null
200+
201+
const session = await verifyToken(authHeader.replace('Bearer ', ''))
202+
203+
if ((!session?.sub || !session.extra) && !session?.agent_id)
204+
throw createAuthError('No valid session found', 401)
205+
206+
const OAUTH_CLIENT_ID = process.env.OAUTH_CLIENT_ID!
207+
const permissions = (session.resource_access as Record<string, { roles?: string[] }>)?.[
208+
OAUTH_CLIENT_ID
209+
]?.roles as string[] | undefined
210+
if (!permissions)
211+
throw createAuthError('User does not have permission to access this application.', 403)
212+
213+
if (!payload)
214+
throw createAuthError('Payload instance is required for Keycloak user normalisation', 500)
215+
216+
let identity = null
217+
let collection = 'users'
218+
if (session.agent_id && 'identities' in payload.collections) { // this case is for agent authentication where we expect a preferred_username in the token in the format of service-account-{agentId} to link to an identity record with a keycloakUserId field matching the sub claim in the token
219+
const identities = await payload.find({
220+
collection: 'identities',
221+
limit: 1,
222+
where: {
223+
keycloakUserId: {
224+
equals: session.sub,
225+
},
226+
},
227+
overrideAccess: true,
228+
})
229+
230+
identity = identities.docs[0]
231+
collection = 'identities'
232+
if (!identity) {
233+
throw createAuthError('No identity found for authenticated user', 403)
234+
}
235+
} else if (session?.extra?.email) { // this case is for user authentication where we expect an email in the token to link to a Payload user record
236+
const payloadUser = (
237+
await payload.find({
238+
collection: 'users',
239+
depth: 1,
240+
limit: 1,
241+
draft: false,
242+
overrideAccess: true,
243+
where: { email: { equals: session.extra.email } },
244+
})
245+
).docs[0]
246+
const role = getValidRole(permissions);
247+
248+
if (!payloadUser) {
249+
const newUser = await payload.create({
250+
collection: 'users',
251+
data: {
252+
email: session.extra.email,
253+
name: session.extra.name,
254+
role,
255+
enabled: true,
256+
accounts: [{ provider: 'keycloak', providerAccountId: session.sub, type: 'oidc' }],
257+
},
258+
draft: false,
259+
overrideAccess: true,
260+
});
261+
console.log("Created new Payload user for Keycloak user:", newUser.id, newUser.email);
262+
identity = newUser;
263+
// return { user: { ...newUser, collection: 'users' as const } };
264+
}
265+
266+
if (payloadUser && payloadUser.role !== role) {
267+
await payload.update({
268+
collection: 'users',
269+
id: payloadUser.id,
270+
data: { role },
271+
draft: false,
272+
overrideAccess: true,
273+
});
274+
console.log(`Updated Payload user role for ${payloadUser.email} to ${role}`);
275+
identity = { ...payloadUser, role: role as ValidRole }
276+
}
277+
} else {
278+
throw createAuthError('Authenticated session does not contain an agent ID', 403)
279+
}
280+
return { user: { ...identity, collection } }
281+
}

packages/auth/src/payload-jwt/configuration.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,18 @@ const userCollectionDatabaseFields = [{
140140
},
141141
},
142142
{ name: 'enabled', type: 'checkbox', label: 'Enabled', defaultValue: true },
143+
{
144+
name: 'sessions', type: 'array', access: {
145+
read: payloadAcl.ownOnly
146+
},
147+
},
143148
{
144149
name: "accounts",
145150
type: "array",
146151
admin: { disabled: false }, // optional
152+
access: {
153+
read: payloadAcl.ownOnly
154+
},
147155
fields: [
148156
{ name: "provider", type: "text", required: true },
149157
{ name: "providerAccountId", type: "text", required: true },

0 commit comments

Comments
 (0)