mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
add scope validation
This commit is contained in:
parent
5232ee5822
commit
7b36e24079
3 changed files with 75 additions and 1 deletions
|
|
@ -499,4 +499,62 @@ describe('createRemoteAgentAuth', () => {
|
|||
expect(mockUpdateUser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('scope validation', () => {
|
||||
it('returns 401 when required scope is missing from token', async () => {
|
||||
setupOidcMocks({ sub: 'sub123', email: 'agent@test.com', scope: 'openid profile' });
|
||||
|
||||
const deps = makeDeps(makeConfig({ scope: 'remote_agent' }, { enabled: false }));
|
||||
const { res, status, json } = makeRes();
|
||||
|
||||
await createRemoteAgentAuth(deps)(
|
||||
makeReq({ authorization: `Bearer ${FAKE_TOKEN}` }) as Request,
|
||||
res,
|
||||
mockNext,
|
||||
);
|
||||
|
||||
expect(status).toHaveBeenCalledWith(401);
|
||||
expect(json).toHaveBeenCalledWith({ error: 'Unauthorized' });
|
||||
});
|
||||
|
||||
it('passes when required scope is present in token', async () => {
|
||||
setupOidcMocks({
|
||||
sub: 'sub123',
|
||||
email: 'agent@test.com',
|
||||
scope: 'openid profile remote_agent',
|
||||
});
|
||||
(findOpenIDUser as jest.Mock).mockResolvedValue({
|
||||
user: { ...mockUser },
|
||||
error: null,
|
||||
migration: false,
|
||||
});
|
||||
|
||||
const deps = makeDeps(makeConfig({ scope: 'remote_agent' }));
|
||||
await createRemoteAgentAuth(deps)(
|
||||
makeReq({ authorization: `Bearer ${FAKE_TOKEN}` }) as Request,
|
||||
makeRes().res,
|
||||
mockNext,
|
||||
);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes when scope is not configured (backward compat)', async () => {
|
||||
setupOidcMocks({ sub: 'sub123', email: 'agent@test.com' }); // no scope claim at all
|
||||
(findOpenIDUser as jest.Mock).mockResolvedValue({
|
||||
user: { ...mockUser },
|
||||
error: null,
|
||||
migration: false,
|
||||
});
|
||||
|
||||
const deps = makeDeps(makeConfig({ scope: undefined }));
|
||||
await createRemoteAgentAuth(deps)(
|
||||
makeReq({ authorization: `Bearer ${FAKE_TOKEN}` }) as Request,
|
||||
makeRes().res,
|
||||
mockNext,
|
||||
);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ async function resolveUser(
|
|||
|
||||
const updateData: Partial<IUser> = {};
|
||||
|
||||
if (migration) {
|
||||
if (migration && payload.sub != null) {
|
||||
updateData.provider = 'openid';
|
||||
updateData.openidId = payload.sub;
|
||||
}
|
||||
|
|
@ -195,6 +195,7 @@ async function resolveUser(
|
|||
* issuer: <issuer>
|
||||
* jwksUri: <jwksUri>
|
||||
* audience: <audience>
|
||||
* scope: <scope>
|
||||
* ```
|
||||
*/
|
||||
export function createRemoteAgentAuth({
|
||||
|
|
@ -223,6 +224,20 @@ export function createRemoteAgentAuth({
|
|||
|
||||
try {
|
||||
const payload = await verifyOidcBearer(token, authConfig.oidc);
|
||||
|
||||
if (authConfig.oidc.scope != null) {
|
||||
const rawScope = payload['scp'] ?? payload['scope'];
|
||||
const tokenScopes: string[] = Array.isArray(rawScope)
|
||||
? rawScope
|
||||
: ((rawScope as string | undefined)?.split(' ') ?? []);
|
||||
if (!tokenScopes.includes(authConfig.oidc.scope)) {
|
||||
logger.warn(`[remoteAgentAuth] Token missing required scope: ${authConfig.oidc.scope}`);
|
||||
if (apiKeyEnabled) return apiKeyMiddleware(req, res, next);
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await resolveUser(token, payload, findUser, updateUser);
|
||||
|
||||
if (user == null) {
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ const remoteApiAuthSchema = z.object({
|
|||
issuer: z.string(),
|
||||
audience: z.string().optional(),
|
||||
jwksUri: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue