ircu2/tools/iauthd-ts/tests/auth/keycloak.test.ts

416 lines
12 KiB
TypeScript

/**
* Tests for KeycloakAuthProvider
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { KeycloakAuthProvider } from '../../src/auth/providers/keycloak.js';
// Mock fetch globally
const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);
describe('KeycloakAuthProvider', () => {
beforeEach(() => {
mockFetch.mockReset();
});
afterEach(() => {
vi.clearAllMocks();
});
/**
* Create a mock JWT access token
*/
function createMockToken(payload: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signature = 'mock-signature';
return `${header}.${body}.${signature}`;
}
describe('initialization', () => {
it('should throw if url is missing', async () => {
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: '',
realm: 'test',
clientid: 'myclient',
});
await expect(provider.initialize()).rejects.toThrow('requires url');
});
it('should throw if realm is missing', async () => {
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: '',
clientid: 'myclient',
});
await expect(provider.initialize()).rejects.toThrow('requires realm');
});
it('should throw if clientid is missing', async () => {
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: '',
});
await expect(provider.initialize()).rejects.toThrow('requires clientid');
});
it('should initialize successfully with valid config', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ issuer: 'https://keycloak.example.com/realms/test' }),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
expect(provider.isHealthy()).toBe(true);
});
it('should use default priority of 100', () => {
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
expect(provider.priority).toBe(100);
});
it('should use custom priority', () => {
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
priority: 50,
});
expect(provider.priority).toBe(50);
});
});
describe('authentication', () => {
it('should authenticate valid user', async () => {
const accessToken = createMockToken({
sub: 'user-uuid',
preferred_username: 'testuser',
email: 'test@example.com',
});
// Mock well-known endpoint
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({}),
});
// Mock token endpoint
mockFetch.mockResolvedValueOnce({
ok: true,
text: async () =>
JSON.stringify({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 300,
scope: 'openid profile email',
}),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
const result = await provider.authenticate('testuser', 'password123');
expect(result.success).toBe(true);
expect(result.account).toBe('testuser');
});
it('should use clientsecret for confidential clients', async () => {
const accessToken = createMockToken({ preferred_username: 'testuser' });
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify({ access_token: accessToken }),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
clientsecret: 'secret123',
});
await provider.initialize();
await provider.authenticate('testuser', 'password123');
// Check that client_secret was included in the request
const tokenCall = mockFetch.mock.calls[1];
expect(tokenCall[1].body).toContain('client_secret=secret123');
});
it('should reject invalid credentials', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
text: async () =>
JSON.stringify({
error: 'invalid_grant',
error_description: 'Invalid user credentials',
}),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
const result = await provider.authenticate('testuser', 'wrongpass');
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid credentials');
});
it('should handle disabled account', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: async () =>
JSON.stringify({
error: 'invalid_grant',
error_description: 'Account disabled',
}),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
const result = await provider.authenticate('disableduser', 'password');
expect(result.success).toBe(false);
expect(result.error).toBe('Account disabled');
});
it('should handle client misconfiguration', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
text: async () =>
JSON.stringify({
error: 'unauthorized_client',
error_description: 'Client not allowed for direct access grants',
}),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'misconfigured-client',
});
await provider.initialize();
const result = await provider.authenticate('testuser', 'password');
expect(result.success).toBe(false);
expect(result.error).toContain('Client error');
});
it('should extract account from custom attribute', async () => {
const accessToken = createMockToken({
sub: 'user-uuid',
preferred_username: 'testuser',
irc_account: 'CustomNick',
});
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify({ access_token: accessToken }),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
accountattr: 'irc_account',
});
await provider.initialize();
const result = await provider.authenticate('testuser', 'password');
expect(result.success).toBe(true);
expect(result.account).toBe('CustomNick');
});
it('should fall back to sub if preferred_username not available', async () => {
const accessToken = createMockToken({
sub: 'user-uuid-12345',
});
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify({ access_token: accessToken }),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
const result = await provider.authenticate('testuser', 'password');
expect(result.success).toBe(true);
expect(result.account).toBe('user-uuid-12345');
});
});
describe('token endpoint URL', () => {
it('should build correct token endpoint URL', async () => {
const accessToken = createMockToken({ preferred_username: 'testuser' });
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify({ access_token: accessToken }),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'myrealm',
clientid: 'myclient',
});
await provider.initialize();
await provider.authenticate('testuser', 'password');
// Check the token endpoint URL
const tokenCall = mockFetch.mock.calls[1];
expect(tokenCall[0]).toBe(
'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token'
);
});
it('should handle trailing slash in URL', async () => {
const accessToken = createMockToken({ preferred_username: 'testuser' });
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
mockFetch.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify({ access_token: accessToken }),
});
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com/', // Trailing slash
realm: 'myrealm',
clientid: 'myclient',
});
await provider.initialize();
await provider.authenticate('testuser', 'password');
const tokenCall = mockFetch.mock.calls[1];
expect(tokenCall[0]).toBe(
'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token'
);
});
});
describe('health status', () => {
it('should mark as unhealthy on connection failure', async () => {
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
expect(provider.isHealthy()).toBe(false);
});
it('should mark as healthy after successful auth', async () => {
const accessToken = createMockToken({ preferred_username: 'testuser' });
// Initialize fails
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
expect(provider.isHealthy()).toBe(false);
// Auth succeeds
mockFetch.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify({ access_token: accessToken }),
});
const result = await provider.authenticate('testuser', 'password');
expect(result.success).toBe(true);
expect(provider.isHealthy()).toBe(true);
});
});
describe('shutdown', () => {
it('should mark provider as unhealthy after shutdown', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
const provider = new KeycloakAuthProvider({
provider: 'keycloak',
url: 'https://keycloak.example.com',
realm: 'test',
clientid: 'myclient',
});
await provider.initialize();
expect(provider.isHealthy()).toBe(true);
await provider.shutdown();
expect(provider.isHealthy()).toBe(false);
});
});
});