mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🔁 fix: Tighten Google admin refresh and limit social-login changes
Brutal-review findings on top of the upstream feature work.
socialLogin.js: the migrate-or-reject pattern from the previous commit
applied to every provider's chat-side verify callback, not just the admin
flow. Gate both branches on `options.existingUsersOnly` so the chat-side
googleLogin / facebookLogin / etc. keep their pre-existing email-fallback
behavior unchanged. Tests follow: restore the original `should fallback to
finding user by email` chat-side case and re-add the migration and
mismatch-reject cases as admin-only by passing `{ existingUsersOnly: true }`
to socialLogin in those tests.
googleRefresh.ts: add a defense-in-depth `isEmailAllowed(user)` dep that
the helper invokes before `canAccessAdmin`. Mirrors the
`isEmailDomainAllowed` check the initial Google admin login already runs,
so a deployment that removes a domain from `registration.allowedDomains`
after issuance can no longer mint fresh JWTs for that admin via refresh.
The route handler wires it up with `resolveAppConfigForUser` +
`isEmailDomainAllowed`, falling back to `baseOnly` config for users
without a tenantId.
googleRefresh.ts: drop the unreachable `?? ''` defensive coalescing in
`fetchGoogleTokenset`. The `GOOGLE_NOT_CONFIGURED` guard upstream already
narrows `clientId`/`clientSecret` to non-empty strings; the function
takes a narrowed `GoogleAdminRefreshConfiguredOptions` shape and
`applyGoogleAdminRefresh` constructs that shape after the guard.
This commit is contained in:
parent
21922eea78
commit
0e55d8a1df
5 changed files with 112 additions and 16 deletions
|
|
@ -69,6 +69,7 @@ describe('applyGoogleAdminRefresh', () => {
|
|||
findUsers: jest.fn(),
|
||||
getUserById: jest.fn(),
|
||||
canAccessAdmin: jest.fn(),
|
||||
isEmailAllowed: jest.fn().mockResolvedValue(true),
|
||||
mintToken: jest.fn(),
|
||||
};
|
||||
originalFetch = global.fetch;
|
||||
|
|
@ -247,6 +248,22 @@ describe('applyGoogleAdminRefresh', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('throws FORBIDDEN when isEmailAllowed rejects the refreshed identity', async () => {
|
||||
const user = makeUser();
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
makeOkJson({ access_token: 'new-access', id_token: makeIdToken() }),
|
||||
);
|
||||
deps.findUsers.mockResolvedValue([user]);
|
||||
(deps.isEmailAllowed as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
await expect(applyGoogleAdminRefresh(deps, baseOptions)).rejects.toMatchObject({
|
||||
code: 'FORBIDDEN',
|
||||
status: 403,
|
||||
message: expect.stringContaining('domain'),
|
||||
});
|
||||
expect(deps.canAccessAdmin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the rotated refresh_token when Google supplies one', async () => {
|
||||
const user = makeUser();
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ export interface GoogleAdminRefreshDeps {
|
|||
) => Promise<IUser[]>;
|
||||
getUserById: (id: string, projection: string) => Promise<IUser | null>;
|
||||
canAccessAdmin: (user: IUser) => Promise<boolean>;
|
||||
/**
|
||||
* Re-runs the deployment's `registration.allowedDomains` check against the
|
||||
* resolved user's email. Returns true to allow refresh, false to reject.
|
||||
* Mirrors the `isEmailDomainAllowed` call the initial OAuth login enforces
|
||||
* so a domain removed from the allowlist after issuance can't refresh.
|
||||
*/
|
||||
isEmailAllowed?: (user: IUser) => Promise<boolean>;
|
||||
mintToken: (user: IUser) => Promise<MintedGoogleAdminToken>;
|
||||
}
|
||||
|
||||
|
|
@ -80,15 +87,22 @@ async function resolveSubFromUserinfo(accessToken: string): Promise<string | und
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchGoogleTokenset(options: GoogleAdminRefreshOptions): Promise<GoogleTokenset> {
|
||||
interface GoogleAdminRefreshConfiguredOptions extends GoogleAdminRefreshOptions {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
async function fetchGoogleTokenset(
|
||||
options: GoogleAdminRefreshConfiguredOptions,
|
||||
): Promise<GoogleTokenset> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(GOOGLE_TOKEN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
client_id: options.clientId ?? '',
|
||||
client_secret: options.clientSecret ?? '',
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret,
|
||||
refresh_token: options.refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
}),
|
||||
|
|
@ -212,10 +226,24 @@ export async function applyGoogleAdminRefresh(
|
|||
);
|
||||
}
|
||||
|
||||
const tokenset = await fetchGoogleTokenset(options);
|
||||
const configured: GoogleAdminRefreshConfiguredOptions = {
|
||||
...options,
|
||||
clientId: options.clientId,
|
||||
clientSecret: options.clientSecret,
|
||||
};
|
||||
|
||||
const tokenset = await fetchGoogleTokenset(configured);
|
||||
const googleId = await resolveGoogleSub(tokenset);
|
||||
const user = await resolveAdminUser(googleId, deps, options);
|
||||
|
||||
if (deps.isEmailAllowed && !(await deps.isEmailAllowed(user))) {
|
||||
throw new AdminRefreshError(
|
||||
'FORBIDDEN',
|
||||
403,
|
||||
'User email domain is not on the deployment allowlist',
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await deps.canAccessAdmin(user))) {
|
||||
throw new AdminRefreshError('FORBIDDEN', 403, 'User does not have admin access');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue