feat: add ldap and oidc sso (#500)
* wip: sso * feat: add ldap client and provider * feat: implement login form * feat: finish sso * fix: lint and format issue * chore: address pull request feedback * fix: build not working * fix: oidc is redirected to internal docker container hostname * fix: build not working * refactor: migrate to ldapts * fix: format and frozen lock file * fix: deepsource issues * fix: unit tests for ldap authorization not working * refactor: remove unnecessary args from dockerfile * chore: address pull request feedback * fix: use console instead of logger in auth env.mjs * fix: default value for auth provider of wrong type * fix: broken lock file * fix: format issue
This commit is contained in:
97
packages/auth/providers/test/basic-authorization.spec.ts
Normal file
97
packages/auth/providers/test/basic-authorization.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createId } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
||||
import { authorizeWithBasicCredentialsAsync } from "../credentials/authorization/basic-authorization";
|
||||
|
||||
const defaultUserId = createId();
|
||||
|
||||
describe("authorizeWithBasicCredentials", () => {
|
||||
test("should authorize user with correct credentials", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const salt = await createSaltAsync();
|
||||
await db.insert(users).values({
|
||||
id: defaultUserId,
|
||||
name: "test",
|
||||
salt,
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithBasicCredentialsAsync(db, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "basic",
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({ id: defaultUserId, name: "test" });
|
||||
});
|
||||
|
||||
test("should not authorize user with incorrect credentials", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const salt = await createSaltAsync();
|
||||
await db.insert(users).values({
|
||||
id: defaultUserId,
|
||||
name: "test",
|
||||
salt,
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithBasicCredentialsAsync(db, {
|
||||
name: "test",
|
||||
password: "wrong",
|
||||
credentialType: "basic",
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should not authorize user with incorrect username", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const salt = await createSaltAsync();
|
||||
await db.insert(users).values({
|
||||
id: defaultUserId,
|
||||
name: "test",
|
||||
salt,
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithBasicCredentialsAsync(db, {
|
||||
name: "wrong",
|
||||
password: "test",
|
||||
credentialType: "basic",
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should not authorize user when password is not set", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
await db.insert(users).values({
|
||||
id: defaultUserId,
|
||||
name: "test",
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithBasicCredentialsAsync(db, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "basic",
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createId } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
||||
import { createCredentialsConfiguration } from "../credentials";
|
||||
|
||||
describe("Credentials authorization", () => {
|
||||
it("should authorize user with correct credentials", async () => {
|
||||
const db = createDb();
|
||||
const userId = createId();
|
||||
const salt = await createSaltAsync();
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
name: "test",
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
salt,
|
||||
});
|
||||
const result = await createCredentialsConfiguration(db).authorize({
|
||||
name: "test",
|
||||
password: "test",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ id: userId, name: "test" });
|
||||
});
|
||||
|
||||
const passwordsThatShouldNotAuthorize = ["wrong", "Test", "test ", " test", " test "];
|
||||
|
||||
passwordsThatShouldNotAuthorize.forEach((password) => {
|
||||
it(`should not authorize user with incorrect credentials (${password})`, async () => {
|
||||
const db = createDb();
|
||||
const userId = createId();
|
||||
const salt = await createSaltAsync();
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
name: "test",
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
salt,
|
||||
});
|
||||
const result = await createCredentialsConfiguration(db).authorize({
|
||||
name: "test",
|
||||
password,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not authorize user for not existing user", async () => {
|
||||
const db = createDb();
|
||||
const result = await createCredentialsConfiguration(db).authorize({
|
||||
name: "test",
|
||||
password: "test",
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
204
packages/auth/providers/test/ldap-authorization.spec.ts
Normal file
204
packages/auth/providers/test/ldap-authorization.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { Adapter } from "@auth/core/adapters";
|
||||
import { CredentialsSignin } from "@auth/core/errors";
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
||||
import { authorizeWithLdapCredentialsAsync } from "../credentials/authorization/ldap-authorization";
|
||||
import * as ldapClient from "../credentials/ldap-client";
|
||||
|
||||
vi.mock("../../env.mjs", () => ({
|
||||
env: {
|
||||
AUTH_LDAP_BIND_DN: "bind_dn",
|
||||
AUTH_LDAP_BIND_PASSWORD: "bind_password",
|
||||
AUTH_LDAP_USER_MAIL_ATTRIBUTE: "mail",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("authorizeWithLdapCredentials", () => {
|
||||
test("should fail when wrong ldap base credentials", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.reject(new Error("bindAsync"))),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow(CredentialsSignin);
|
||||
});
|
||||
|
||||
test("should fail when user not found", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() => Promise.resolve([])),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow(CredentialsSignin);
|
||||
});
|
||||
|
||||
test("should fail when user has invalid email", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test",
|
||||
},
|
||||
]),
|
||||
),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow(CredentialsSignin);
|
||||
});
|
||||
|
||||
test("should fail when user password is incorrect", async () => {
|
||||
// Arrange
|
||||
const searchSpy = vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn((props: ldapClient.BindOptions) =>
|
||||
props.distinguishedName === "test" ? Promise.reject(new Error("bindAsync")) : Promise.resolve(),
|
||||
),
|
||||
searchAsync: searchSpy,
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
authorizeWithLdapCredentialsAsync(null as unknown as Adapter, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow(CredentialsSignin);
|
||||
expect(searchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should authorize user with correct credentials and create user", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const adapter = DrizzleAdapter(db);
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithLdapCredentialsAsync(adapter, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result.name).toBe("test");
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
where: eq(users.name, "test"),
|
||||
});
|
||||
expect(dbUser).toBeDefined();
|
||||
expect(dbUser?.id).toBe(result.id);
|
||||
expect(dbUser?.email).toBe("test@gmail.com");
|
||||
expect(dbUser?.emailVerified).not.toBeNull();
|
||||
});
|
||||
|
||||
test("should authorize user with correct credentials and update name", async () => {
|
||||
// Arrange
|
||||
const userId = createId();
|
||||
const db = createDb();
|
||||
const adapter = DrizzleAdapter(db);
|
||||
const salt = await createSaltAsync();
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
name: "test-old",
|
||||
salt,
|
||||
password: await hashPasswordAsync("test", salt),
|
||||
email: "test@gmail.com",
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithLdapCredentialsAsync(adapter, {
|
||||
name: "test",
|
||||
password: "test",
|
||||
credentialType: "ldap",
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({ id: userId, name: "test" });
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, userId),
|
||||
});
|
||||
|
||||
expect(dbUser).toBeDefined();
|
||||
expect(dbUser?.id).toBe(userId);
|
||||
expect(dbUser?.name).toBe("test");
|
||||
expect(dbUser?.email).toBe("test@gmail.com");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user