Fixed "Cannot read properties of undefined (reading 'id')" error that occurred when existing OIDC users attempted to log in. The issue was a case mismatch in the Sequelize association accessor. Changes: - Fixed case mismatch: identity.user -> identity.User (matches association alias) - Removed invalid username field that doesn't exist in User model - Fixed admin role assignment to use Role model instead of User model - Improved transaction error handling to prevent double rollbacks - Removed unused generateUsername function Tests: - Added comprehensive test suite with 12 tests covering all provisioning scenarios - All tests passing including the previously failing login scenario
275 lines
9.1 KiB
JavaScript
275 lines
9.1 KiB
JavaScript
const { sequelize, User, OIDCIdentity, Role } = require('../../../../models');
|
|
const provisioningService = require('../../../../modules/oidc/provisioningService');
|
|
const providerConfig = require('../../../../modules/oidc/providerConfig');
|
|
|
|
jest.mock('../../../../modules/oidc/providerConfig');
|
|
|
|
describe('OIDC Provisioning Service', () => {
|
|
beforeAll(async () => {
|
|
await sequelize.sync({ force: true });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await sequelize.close();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await OIDCIdentity.destroy({ where: {}, force: true });
|
|
await Role.destroy({ where: {}, force: true });
|
|
await User.destroy({ where: {}, force: true });
|
|
|
|
providerConfig.getProvider.mockReturnValue({
|
|
slug: 'test-provider',
|
|
name: 'Test Provider',
|
|
autoProvision: true,
|
|
adminEmailDomains: ['admin.com'],
|
|
});
|
|
});
|
|
|
|
describe('provisionUser', () => {
|
|
it('should return user object when existing identity logs in', async () => {
|
|
const existingUser = await User.create({
|
|
email: 'test@example.com',
|
|
username: 'testuser',
|
|
password_digest: 'hashed',
|
|
verified_email: true,
|
|
});
|
|
|
|
await OIDCIdentity.create({
|
|
user_id: existingUser.id,
|
|
provider_slug: 'test-provider',
|
|
subject: 'sub-123',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
first_login_at: new Date(),
|
|
last_login_at: new Date(),
|
|
});
|
|
|
|
const claims = {
|
|
sub: 'sub-123',
|
|
email: 'test@example.com',
|
|
name: 'Test User Updated',
|
|
};
|
|
|
|
const result = await provisioningService.provisionUser(
|
|
'test-provider',
|
|
claims,
|
|
{}
|
|
);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.user).toBeDefined();
|
|
expect(result.user.id).toBe(existingUser.id);
|
|
expect(result.user.email).toBe('test@example.com');
|
|
expect(result.isNewUser).toBe(false);
|
|
});
|
|
|
|
it('should create new user when auto-provision is enabled and identity does not exist', async () => {
|
|
const claims = {
|
|
sub: 'sub-456',
|
|
email: 'newuser@example.com',
|
|
name: 'New User',
|
|
given_name: 'New',
|
|
family_name: 'User',
|
|
};
|
|
|
|
const result = await provisioningService.provisionUser(
|
|
'test-provider',
|
|
claims,
|
|
{}
|
|
);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.user).toBeDefined();
|
|
expect(result.user.email).toBe('newuser@example.com');
|
|
expect(result.isNewUser).toBe(true);
|
|
|
|
const identity = await OIDCIdentity.findOne({
|
|
where: { subject: 'sub-456' },
|
|
});
|
|
expect(identity).toBeDefined();
|
|
expect(identity.user_id).toBe(result.user.id);
|
|
});
|
|
|
|
it('should link existing user by email when creating new identity', async () => {
|
|
const existingUser = await User.create({
|
|
email: 'existing@example.com',
|
|
username: 'existing',
|
|
password_digest: 'hashed',
|
|
verified_email: true,
|
|
});
|
|
|
|
const claims = {
|
|
sub: 'sub-789',
|
|
email: 'existing@example.com',
|
|
name: 'Existing User',
|
|
};
|
|
|
|
const result = await provisioningService.provisionUser(
|
|
'test-provider',
|
|
claims,
|
|
{}
|
|
);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.user).toBeDefined();
|
|
expect(result.user.id).toBe(existingUser.id);
|
|
expect(result.isNewUser).toBe(false);
|
|
|
|
const identity = await OIDCIdentity.findOne({
|
|
where: { subject: 'sub-789' },
|
|
});
|
|
expect(identity).toBeDefined();
|
|
expect(identity.user_id).toBe(existingUser.id);
|
|
});
|
|
|
|
it('should throw error when auto-provision is disabled and user does not exist', async () => {
|
|
providerConfig.getProvider.mockReturnValue({
|
|
slug: 'test-provider',
|
|
name: 'Test Provider',
|
|
autoProvision: false,
|
|
});
|
|
|
|
const claims = {
|
|
sub: 'sub-999',
|
|
email: 'notallowed@example.com',
|
|
name: 'Not Allowed',
|
|
};
|
|
|
|
await expect(
|
|
provisioningService.provisionUser('test-provider', claims, {})
|
|
).rejects.toThrow('Auto-provisioning is disabled');
|
|
});
|
|
|
|
it('should throw error when email claim is missing', async () => {
|
|
const claims = {
|
|
sub: 'sub-000',
|
|
name: 'No Email User',
|
|
};
|
|
|
|
await expect(
|
|
provisioningService.provisionUser('test-provider', claims, {})
|
|
).rejects.toThrow('Email claim is required');
|
|
});
|
|
|
|
it('should set admin flag when email domain matches admin domains', async () => {
|
|
const claims = {
|
|
sub: 'sub-admin',
|
|
email: 'admin@admin.com',
|
|
name: 'Admin User',
|
|
};
|
|
|
|
const result = await provisioningService.provisionUser(
|
|
'test-provider',
|
|
claims,
|
|
{}
|
|
);
|
|
|
|
const role = await Role.findOne({
|
|
where: { user_id: result.user.id },
|
|
});
|
|
expect(role).toBeDefined();
|
|
expect(role.is_admin).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('linkIdentityToUser', () => {
|
|
it('should link new identity to existing user', async () => {
|
|
const user = await User.create({
|
|
email: 'link@example.com',
|
|
username: 'linkuser',
|
|
password_digest: 'hashed',
|
|
verified_email: true,
|
|
});
|
|
|
|
const claims = {
|
|
sub: 'sub-link-123',
|
|
email: 'link@example.com',
|
|
name: 'Link User',
|
|
};
|
|
|
|
const identity = await provisioningService.linkIdentityToUser(
|
|
user.id,
|
|
'test-provider',
|
|
claims
|
|
);
|
|
|
|
expect(identity).toBeDefined();
|
|
expect(identity.user_id).toBe(user.id);
|
|
expect(identity.subject).toBe('sub-link-123');
|
|
});
|
|
|
|
it('should throw error when identity is already linked to another user', async () => {
|
|
const user1 = await User.create({
|
|
email: 'user1@example.com',
|
|
username: 'user1',
|
|
password_digest: 'hashed',
|
|
verified_email: true,
|
|
});
|
|
|
|
const user2 = await User.create({
|
|
email: 'user2@example.com',
|
|
username: 'user2',
|
|
password_digest: 'hashed',
|
|
verified_email: true,
|
|
});
|
|
|
|
await OIDCIdentity.create({
|
|
user_id: user1.id,
|
|
provider_slug: 'test-provider',
|
|
subject: 'sub-existing',
|
|
email: 'user1@example.com',
|
|
name: 'User 1',
|
|
first_login_at: new Date(),
|
|
last_login_at: new Date(),
|
|
});
|
|
|
|
const claims = {
|
|
sub: 'sub-existing',
|
|
email: 'user2@example.com',
|
|
name: 'User 2',
|
|
};
|
|
|
|
await expect(
|
|
provisioningService.linkIdentityToUser(
|
|
user2.id,
|
|
'test-provider',
|
|
claims
|
|
)
|
|
).rejects.toThrow('already linked to another user');
|
|
});
|
|
});
|
|
|
|
describe('shouldBeAdmin', () => {
|
|
it('should return true when email domain matches admin domains', () => {
|
|
const config = { adminEmailDomains: ['admin.com', 'company.com'] };
|
|
expect(
|
|
provisioningService.shouldBeAdmin(config, 'user@admin.com')
|
|
).toBe(true);
|
|
expect(
|
|
provisioningService.shouldBeAdmin(config, 'user@company.com')
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should return false when email domain does not match', () => {
|
|
const config = { adminEmailDomains: ['admin.com'] };
|
|
expect(
|
|
provisioningService.shouldBeAdmin(config, 'user@other.com')
|
|
).toBe(false);
|
|
});
|
|
|
|
it('should return false when adminEmailDomains is empty', () => {
|
|
const config = { adminEmailDomains: [] };
|
|
expect(
|
|
provisioningService.shouldBeAdmin(config, 'user@admin.com')
|
|
).toBe(false);
|
|
});
|
|
|
|
it('should return false when adminEmailDomains is not set', () => {
|
|
const config = {};
|
|
expect(
|
|
provisioningService.shouldBeAdmin(config, 'user@admin.com')
|
|
).toBe(false);
|
|
});
|
|
});
|
|
});
|