112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import authOptions from "./options";
|
|
|
|
describe("NextAuth access token refresh", () => {
|
|
it("forces a Keycloak refresh for an agent credential request", async () => {
|
|
const previousEnv = {
|
|
issuer: process.env.KEYCLOAK_ISSUER,
|
|
clientId: process.env.KEYCLOAK_CLIENT_ID,
|
|
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
|
};
|
|
process.env.KEYCLOAK_ISSUER = "https://keycloak.example/realms/tjwater";
|
|
process.env.KEYCLOAK_CLIENT_ID = "frontend";
|
|
process.env.KEYCLOAK_CLIENT_SECRET = "secret";
|
|
const originalFetch = globalThis.fetch;
|
|
const fetchMock = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
access_token: "fresh-access-token",
|
|
expires_in: 900,
|
|
refresh_token: "rotated-refresh-token",
|
|
}),
|
|
});
|
|
globalThis.fetch = fetchMock as typeof fetch;
|
|
|
|
try {
|
|
const jwt = authOptions.callbacks?.jwt as (input: unknown) => Promise<{
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
}>;
|
|
const result = await jwt({
|
|
token: {
|
|
accessToken: "still-fresh-access-token",
|
|
accessTokenExpires: Date.now() + 600_000,
|
|
accessTokenIssuedAt: Date.now(),
|
|
refreshToken: "refresh-token",
|
|
},
|
|
trigger: "update",
|
|
session: { forceRefresh: true },
|
|
});
|
|
|
|
expect(result.accessToken).toBe("fresh-access-token");
|
|
expect(result.refreshToken).toBe("rotated-refresh-token");
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"https://keycloak.example/realms/tjwater/protocol/openid-connect/token",
|
|
expect.objectContaining({ method: "POST" }),
|
|
);
|
|
} finally {
|
|
if (originalFetch) globalThis.fetch = originalFetch;
|
|
else delete (globalThis as { fetch?: typeof fetch }).fetch;
|
|
restoreEnv("KEYCLOAK_ISSUER", previousEnv.issuer);
|
|
restoreEnv("KEYCLOAK_CLIENT_ID", previousEnv.clientId);
|
|
restoreEnv("KEYCLOAK_CLIENT_SECRET", previousEnv.clientSecret);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
["network failure", () => Promise.reject(new Error("connection refused"))],
|
|
[
|
|
"non-JSON response",
|
|
() =>
|
|
Promise.resolve({
|
|
ok: false,
|
|
json: async () => {
|
|
throw new SyntaxError("Unexpected token");
|
|
},
|
|
}),
|
|
],
|
|
[
|
|
"invalid token payload",
|
|
() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: async () => ({ access_token: " ", expires_in: 0 }),
|
|
}),
|
|
],
|
|
])("returns a session error for %s", async (_label, fetchResult) => {
|
|
const previousEnv = {
|
|
issuer: process.env.KEYCLOAK_ISSUER,
|
|
clientId: process.env.KEYCLOAK_CLIENT_ID,
|
|
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
|
};
|
|
process.env.KEYCLOAK_ISSUER = "https://keycloak.example/realms/tjwater";
|
|
process.env.KEYCLOAK_CLIENT_ID = "frontend";
|
|
process.env.KEYCLOAK_CLIENT_SECRET = "secret";
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = jest.fn(fetchResult) as unknown as typeof fetch;
|
|
|
|
try {
|
|
const jwt = authOptions.callbacks?.jwt as (input: unknown) => Promise<{
|
|
error?: string;
|
|
}>;
|
|
await expect(
|
|
jwt({
|
|
token: { refreshToken: "refresh-token" },
|
|
trigger: "update",
|
|
session: { forceRefresh: true },
|
|
}),
|
|
).resolves.toMatchObject({ error: "RefreshAccessTokenError" });
|
|
} finally {
|
|
if (originalFetch) globalThis.fetch = originalFetch;
|
|
else delete (globalThis as { fetch?: typeof fetch }).fetch;
|
|
restoreEnv("KEYCLOAK_ISSUER", previousEnv.issuer);
|
|
restoreEnv("KEYCLOAK_CLIENT_ID", previousEnv.clientId);
|
|
restoreEnv("KEYCLOAK_CLIENT_SECRET", previousEnv.clientSecret);
|
|
}
|
|
});
|
|
});
|
|
|
|
const restoreEnv = (key: string, value: string | undefined) => {
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
};
|