feat(auth): allow dedicated PKCE MCP client only on MCP endpoint
This commit is contained in:
parent
c41d242667
commit
478f90a230
@ -1,7 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canApproveAccess, hasAccess, realmRolesFromPayload, rolesFromPayload } from './auth.js'
|
||||
import { canApproveAccess, hasAccess, intendedForFieldLens, realmRolesFromPayload, rolesFromPayload } from './auth.js'
|
||||
|
||||
describe('FieldLens authorization isolation', () => {
|
||||
it('accepts the dedicated MCP client only at the MCP endpoint', () => {
|
||||
const token = { azp: 'fieldlens-mcp', aud: 'fieldlens-api' }
|
||||
expect(intendedForFieldLens(token, true)).toBe(true)
|
||||
expect(intendedForFieldLens(token, false)).toBe(false)
|
||||
expect(intendedForFieldLens({ azp: 'unrelated' }, true)).toBe(false)
|
||||
})
|
||||
it('extracts FieldLens client roles and ignores realm/platform roles for app access', () => {
|
||||
const payload = {
|
||||
resource_access: {
|
||||
|
||||
@ -33,6 +33,7 @@ export function rolesFromPayload(payload: JWTPayload) {
|
||||
return [...new Set([
|
||||
...strings(resourceAccess?.[config.audience]?.roles),
|
||||
...strings(resourceAccess?.[config.browserClientId]?.roles),
|
||||
...strings(resourceAccess?.[config.mcpClientId]?.roles),
|
||||
...config.desktopClientIds.flatMap((clientId) => strings(resourceAccess?.[clientId]?.roles)),
|
||||
...strings(resourceAccess?.[config.machineClientId]?.roles),
|
||||
])]
|
||||
@ -43,9 +44,10 @@ export function realmRolesFromPayload(payload: JWTPayload) {
|
||||
return [...new Set(strings(realmAccess?.roles))]
|
||||
}
|
||||
|
||||
function intendedForFieldLens(payload: JWTPayload) {
|
||||
export function intendedForFieldLens(payload: JWTPayload, mcpEndpoint = false) {
|
||||
const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : []
|
||||
const azp = typeof payload.azp === 'string' ? payload.azp : ''
|
||||
if (azp === config.mcpClientId) return mcpEndpoint
|
||||
return audience.includes(config.audience)
|
||||
|| azp === config.browserClientId
|
||||
|| config.desktopClientIds.includes(azp)
|
||||
@ -62,7 +64,7 @@ export async function authenticate(req: Request, res: Response, next: NextFuncti
|
||||
try {
|
||||
const token = header.slice('Bearer '.length)
|
||||
const { payload } = await jwtVerify(token, jwks, { issuer: config.issuer })
|
||||
if (!intendedForFieldLens(payload) || !payload.sub) {
|
||||
if (!intendedForFieldLens(payload, req.baseUrl === '/api/mcp') || !payload.sub) {
|
||||
res.status(401).json({ error: 'invalid_token_target' })
|
||||
return
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ export const config = {
|
||||
jwksUri: (process.env.KEYCLOAK_JWKS_URI ?? '').trim(),
|
||||
audience: process.env.KEYCLOAK_AUDIENCE?.trim() || 'fieldlens-api',
|
||||
browserClientId: process.env.KEYCLOAK_BROWSER_CLIENT_ID?.trim() || 'fieldlens',
|
||||
mcpClientId: process.env.KEYCLOAK_MCP_CLIENT_ID?.trim() || 'fieldlens-mcp',
|
||||
desktopClientId,
|
||||
desktopClientIds: list('KEYCLOAK_DESKTOP_CLIENT_IDS', [
|
||||
desktopClientId,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user